@punks/cli 1.0.7 → 2.0.0-beta.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.
Files changed (42) hide show
  1. package/AGENTS.md +4 -5
  2. package/README.md +2 -2
  3. package/dist/data/catalog/hooks.ts +7 -0
  4. package/dist/data/catalog/lint.ts +7 -21
  5. package/dist/data/catalog/packs.ts +263 -21
  6. package/dist/data/catalog/skills.ts +352 -38
  7. package/dist/data/hooks/require-tests-for-pr.mjs +206 -0
  8. package/dist/data/hooks.test.ts +29 -0
  9. package/dist/data/scripts/sync-subagents.mjs +64 -6
  10. package/dist/data/subagents/manifest.mjs +15 -49
  11. package/dist/index.js +14368 -3445
  12. package/dist/skills/agnostic/debug/debugging-phase/SKILL.md +87 -0
  13. package/dist/skills/agnostic/docs/docs-ingest-phase/SKILL.md +87 -0
  14. package/dist/skills/agnostic/docs/docs-ingest-phase/agents/openai.yaml +4 -0
  15. package/dist/skills/agnostic/docs/{docs-maintenance → docs-ingest-phase}/references/concept-pages.md +1 -1
  16. package/dist/skills/agnostic/docs/{docs-maintenance → docs-ingest-phase}/references/flow-pages.md +1 -1
  17. package/dist/skills/agnostic/docs/docs-ingest-phase/references/fumadocs-routing.md +88 -0
  18. package/dist/skills/agnostic/docs/docs-ingest-phase/references/repo-docs.md +38 -0
  19. package/dist/skills/agnostic/docs/docs-ingest-phase/references/wiki-ingest.md +131 -0
  20. package/dist/skills/agnostic/planning/create-plan/SKILL.md +11 -9
  21. package/dist/skills/agnostic/planning/create-spec/SKILL.md +20 -18
  22. package/dist/skills/agnostic/planning/delivery-phase/SKILL.md +82 -0
  23. package/dist/skills/agnostic/planning/goalify/EXAMPLES.md +72 -0
  24. package/dist/skills/agnostic/planning/goalify/SKILL.md +97 -0
  25. package/dist/skills/agnostic/planning/implement-spec/SKILL.md +3 -3
  26. package/dist/skills/agnostic/planning/implement-spec/assets/IMPLEMENTATION-NOTES-TEMPLATE.md +6 -0
  27. package/dist/skills/agnostic/planning/implement-spec/references/lifecycle.md +23 -2
  28. package/dist/skills/agnostic/planning/resolve-debt-phase/SKILL.md +87 -0
  29. package/dist/skills/agnostic/requirements/requirements-grill/SKILL.md +4 -3
  30. package/dist/skills/agnostic/requirements/requirements-grill/references/artifact-output.md +56 -2
  31. package/dist/skills/agnostic/requirements/requirements-grill/references/grilling-flow.md +16 -4
  32. package/dist/skills/agnostic/requirements/requirements-grill/references/wiki-output.md +6 -2
  33. package/dist/skills/agnostic/requirements/requirements-phase/SKILL.md +67 -0
  34. package/dist/skills/agnostic/research/review-phase/SKILL.md +99 -0
  35. package/package.json +17 -7
  36. package/dist/skills/agnostic/docs/docs-maintenance/SKILL.md +0 -193
  37. package/dist/skills/agnostic/docs/docs-maintenance/agents/openai.yaml +0 -4
  38. package/docs/README.md +0 -35
  39. package/docs/harness-intelligence-grill-log.md +0 -39
  40. package/docs/harness-intelligence-grill-status.md +0 -25
  41. package/docs/reference/dp-requirements.md +0 -225
  42. package/docs/runbooks/dp-cli-scaffolding.md +0 -261
@@ -12,44 +12,358 @@ export interface SkillCatalogEntry {
12
12
  }
13
13
 
14
14
  export const skillCatalog = [
15
- { id: "agent-browser", sourceDirectory: "skills/agnostic/frontend/agent-browser", tier: "framework", language: null, framework: "browser-automation", requiresTools: ["agent-browser"] },
16
- { id: "backend-domain-structure", sourceDirectory: "skills/agnostic/backend/backend-domain-structure", tier: "agnostic", language: null, framework: null, requiresTools: [] },
17
- { id: "backend-recoverable-actions", sourceDirectory: "skills/agnostic/backend/backend-recoverable-actions", tier: "agnostic", language: null, framework: null, requiresTools: [] },
18
- { id: "logging-best-practices", sourceDirectory: "skills/agnostic/backend/logging-best-practices", tier: "agnostic", language: null, framework: null, requiresTools: [] },
19
- { id: "better-auth-best-practices", sourceDirectory: "skills/frameworks/better-auth/better-auth-best-practices", tier: "framework", language: "typescript", framework: "better-auth", requiresTools: [] },
20
- { id: "create-plan", sourceDirectory: "skills/agnostic/planning/create-plan", tier: "agnostic", language: null, framework: null, requiresTools: [] },
21
- { id: "create-spec", sourceDirectory: "skills/agnostic/planning/create-spec", tier: "agnostic", language: null, framework: null, requiresTools: [] },
22
- { id: "debug-agent", sourceDirectory: "skills/agnostic/debug/debug-agent", tier: "agnostic", language: null, framework: null, requiresTools: ["debug-agent"] },
23
- { id: "docs-maintenance", sourceDirectory: "skills/agnostic/docs/docs-maintenance", tier: "agnostic", language: null, framework: null, requiresTools: [] },
24
- { id: "effect-authoring", sourceDirectory: "skills/frameworks/effect/effect-authoring", tier: "framework", language: "typescript", framework: "effect", requiresTools: ["opensrc"] },
25
- { id: "effect-backend-structure", sourceDirectory: "skills/frameworks/effect/effect-backend-structure", tier: "framework", language: "typescript", framework: "effect", requiresTools: ["opensrc"] },
26
- { id: "effect-best-practices", sourceDirectory: "skills/frameworks/effect/effect-best-practices", tier: "framework", language: "typescript", framework: "effect", requiresTools: [] },
27
- { id: "effect-recoverable-actions", sourceDirectory: "skills/frameworks/effect/effect-recoverable-actions", tier: "framework", language: "typescript", framework: "effect", requiresTools: ["opensrc"] },
28
- { id: "elysiajs", sourceDirectory: "skills/frameworks/elysia/elysiajs", tier: "framework", language: "typescript", framework: "elysiajs", requiresTools: [] },
29
- { id: "design-taste-frontend", sourceDirectory: "skills/agnostic/frontend/design-taste-frontend", tier: "agnostic", language: null, framework: null, requiresTools: [] },
30
- { id: "frontend-domain-structure", sourceDirectory: "skills/agnostic/frontend/frontend-domain-structure", tier: "agnostic", language: null, framework: null, requiresTools: [] },
31
- { id: "grill-me", sourceDirectory: "skills/agnostic/planning/grill-me", tier: "agnostic", language: null, framework: null, requiresTools: [] },
32
- { id: "improve-codebase-architecture", sourceDirectory: "skills/agnostic/research/improve-codebase-architecture", tier: "agnostic", language: null, framework: null, requiresTools: [] },
33
- { id: "implement-spec", sourceDirectory: "skills/agnostic/planning/implement-spec", tier: "agnostic", language: null, framework: null, requiresTools: [] },
34
- { id: "next-best-practices", sourceDirectory: "skills/frameworks/nextjs/next-best-practices", tier: "framework", language: "typescript", framework: "nextjs", requiresTools: [] },
35
- { id: "next-cache-components", sourceDirectory: "skills/frameworks/nextjs/next-cache-components", tier: "framework", language: "typescript", framework: "nextjs", requiresTools: [] },
36
- { id: "parallel-research", sourceDirectory: "skills/agnostic/research/parallel-research", tier: "agnostic", language: null, framework: null, requiresTools: [] },
37
- { id: "async-python-patterns", sourceDirectory: "skills/languages/python/async-python-patterns", tier: "language", language: "python", framework: null, requiresTools: [] },
38
- { id: "python-code-style", sourceDirectory: "skills/languages/python/python-code-style", tier: "language", language: "python", framework: null, requiresTools: [] },
39
- { id: "python-design-patterns", sourceDirectory: "skills/languages/python/python-design-patterns", tier: "language", language: "python", framework: null, requiresTools: [] },
40
- { id: "python-project-structure", sourceDirectory: "skills/languages/python/python-project-structure", tier: "language", language: "python", framework: null, requiresTools: [] },
41
- { id: "python-testing-patterns", sourceDirectory: "skills/languages/python/python-testing-patterns", tier: "language", language: "python", framework: null, requiresTools: [] },
42
- { id: "quality-types", sourceDirectory: "skills/languages/typescript/quality-types", tier: "language", language: "typescript", framework: null, requiresTools: [] },
43
- { id: "async-react-patterns", sourceDirectory: "skills/frameworks/react/async-react-patterns", tier: "framework", language: "typescript", framework: "react", requiresTools: [] },
44
- { id: "requirements-grill", sourceDirectory: "skills/agnostic/requirements/requirements-grill", tier: "agnostic", language: null, framework: null, requiresTools: [] },
45
- { id: "simplify", sourceDirectory: "skills/agnostic/quality/simplify", tier: "agnostic", language: null, framework: null, requiresTools: [] },
46
- { id: "swarm-planner", sourceDirectory: "skills/agnostic/subagents/swarm-planner", tier: "agnostic", language: null, framework: null, requiresTools: [] },
47
- { id: "tanstack-query", sourceDirectory: "skills/frameworks/tanstack-query/tanstack-query", tier: "framework", language: "typescript", framework: "tanstack-query", requiresTools: ["opensrc"] },
48
- { id: "tdd", sourceDirectory: "skills/agnostic/quality/tdd", tier: "agnostic", language: null, framework: null, requiresTools: [] },
49
- { id: "turborepo", sourceDirectory: "skills/frameworks/turborepo/turborepo", tier: "framework", language: null, framework: "turborepo", requiresTools: [] },
50
- { id: "vercel-composition-patterns", sourceDirectory: "skills/frameworks/react/vercel-composition-patterns", tier: "framework", language: "typescript", framework: "react", requiresTools: [] },
51
- { id: "vercel-react-best-practices", sourceDirectory: "skills/frameworks/react/vercel-react-best-practices", tier: "framework", language: "typescript", framework: "react", requiresTools: [] },
52
- { id: "write-backlog", sourceDirectory: "skills/agnostic/requirements/write-backlog", tier: "agnostic", language: null, framework: null, requiresTools: [] },
15
+ {
16
+ id: "agent-browser",
17
+ sourceDirectory: "skills/agnostic/frontend/agent-browser",
18
+ tier: "framework",
19
+ language: null,
20
+ framework: "browser-automation",
21
+ requiresTools: ["agent-browser"],
22
+ },
23
+ {
24
+ id: "backend-domain-structure",
25
+ sourceDirectory: "skills/agnostic/backend/backend-domain-structure",
26
+ tier: "agnostic",
27
+ language: null,
28
+ framework: null,
29
+ requiresTools: [],
30
+ },
31
+ {
32
+ id: "backend-recoverable-actions",
33
+ sourceDirectory: "skills/agnostic/backend/backend-recoverable-actions",
34
+ tier: "agnostic",
35
+ language: null,
36
+ framework: null,
37
+ requiresTools: [],
38
+ },
39
+ {
40
+ id: "logging-best-practices",
41
+ sourceDirectory: "skills/agnostic/backend/logging-best-practices",
42
+ tier: "agnostic",
43
+ language: null,
44
+ framework: null,
45
+ requiresTools: [],
46
+ },
47
+ {
48
+ id: "better-auth-best-practices",
49
+ sourceDirectory: "skills/frameworks/better-auth/better-auth-best-practices",
50
+ tier: "framework",
51
+ language: "typescript",
52
+ framework: "better-auth",
53
+ requiresTools: [],
54
+ },
55
+ {
56
+ id: "create-plan",
57
+ sourceDirectory: "skills/agnostic/planning/create-plan",
58
+ tier: "agnostic",
59
+ language: null,
60
+ framework: null,
61
+ requiresTools: [],
62
+ },
63
+ {
64
+ id: "create-spec",
65
+ sourceDirectory: "skills/agnostic/planning/create-spec",
66
+ tier: "agnostic",
67
+ language: null,
68
+ framework: null,
69
+ requiresTools: [],
70
+ },
71
+ {
72
+ id: "delivery-phase",
73
+ sourceDirectory: "skills/agnostic/planning/delivery-phase",
74
+ tier: "agnostic",
75
+ language: null,
76
+ framework: null,
77
+ requiresTools: [],
78
+ },
79
+ {
80
+ id: "debug-agent",
81
+ sourceDirectory: "skills/agnostic/debug/debug-agent",
82
+ tier: "agnostic",
83
+ language: null,
84
+ framework: null,
85
+ requiresTools: ["debug-agent"],
86
+ },
87
+ {
88
+ id: "debugging-phase",
89
+ sourceDirectory: "skills/agnostic/debug/debugging-phase",
90
+ tier: "agnostic",
91
+ language: null,
92
+ framework: null,
93
+ requiresTools: ["debug-agent"],
94
+ },
95
+ {
96
+ id: "docs-ingest-phase",
97
+ sourceDirectory: "skills/agnostic/docs/docs-ingest-phase",
98
+ tier: "agnostic",
99
+ language: null,
100
+ framework: null,
101
+ requiresTools: [],
102
+ },
103
+ {
104
+ id: "effect-authoring",
105
+ sourceDirectory: "skills/frameworks/effect/effect-authoring",
106
+ tier: "framework",
107
+ language: "typescript",
108
+ framework: "effect",
109
+ requiresTools: ["opensrc"],
110
+ },
111
+ {
112
+ id: "effect-backend-structure",
113
+ sourceDirectory: "skills/frameworks/effect/effect-backend-structure",
114
+ tier: "framework",
115
+ language: "typescript",
116
+ framework: "effect",
117
+ requiresTools: ["opensrc"],
118
+ },
119
+ {
120
+ id: "effect-best-practices",
121
+ sourceDirectory: "skills/frameworks/effect/effect-best-practices",
122
+ tier: "framework",
123
+ language: "typescript",
124
+ framework: "effect",
125
+ requiresTools: [],
126
+ },
127
+ {
128
+ id: "effect-recoverable-actions",
129
+ sourceDirectory: "skills/frameworks/effect/effect-recoverable-actions",
130
+ tier: "framework",
131
+ language: "typescript",
132
+ framework: "effect",
133
+ requiresTools: ["opensrc"],
134
+ },
135
+ {
136
+ id: "elysiajs",
137
+ sourceDirectory: "skills/frameworks/elysia/elysiajs",
138
+ tier: "framework",
139
+ language: "typescript",
140
+ framework: "elysiajs",
141
+ requiresTools: [],
142
+ },
143
+ {
144
+ id: "design-taste-frontend",
145
+ sourceDirectory: "skills/agnostic/frontend/design-taste-frontend",
146
+ tier: "agnostic",
147
+ language: null,
148
+ framework: null,
149
+ requiresTools: [],
150
+ },
151
+ {
152
+ id: "frontend-domain-structure",
153
+ sourceDirectory: "skills/agnostic/frontend/frontend-domain-structure",
154
+ tier: "agnostic",
155
+ language: null,
156
+ framework: null,
157
+ requiresTools: [],
158
+ },
159
+ {
160
+ id: "goalify",
161
+ sourceDirectory: "skills/agnostic/planning/goalify",
162
+ tier: "agnostic",
163
+ language: null,
164
+ framework: null,
165
+ requiresTools: [],
166
+ },
167
+ {
168
+ id: "grill-me",
169
+ sourceDirectory: "skills/agnostic/planning/grill-me",
170
+ tier: "agnostic",
171
+ language: null,
172
+ framework: null,
173
+ requiresTools: [],
174
+ },
175
+ {
176
+ id: "improve-codebase-architecture",
177
+ sourceDirectory: "skills/agnostic/research/improve-codebase-architecture",
178
+ tier: "agnostic",
179
+ language: null,
180
+ framework: null,
181
+ requiresTools: [],
182
+ },
183
+ {
184
+ id: "implement-spec",
185
+ sourceDirectory: "skills/agnostic/planning/implement-spec",
186
+ tier: "agnostic",
187
+ language: null,
188
+ framework: null,
189
+ requiresTools: [],
190
+ },
191
+ {
192
+ id: "next-best-practices",
193
+ sourceDirectory: "skills/frameworks/nextjs/next-best-practices",
194
+ tier: "framework",
195
+ language: "typescript",
196
+ framework: "nextjs",
197
+ requiresTools: [],
198
+ },
199
+ {
200
+ id: "next-cache-components",
201
+ sourceDirectory: "skills/frameworks/nextjs/next-cache-components",
202
+ tier: "framework",
203
+ language: "typescript",
204
+ framework: "nextjs",
205
+ requiresTools: [],
206
+ },
207
+ {
208
+ id: "parallel-research",
209
+ sourceDirectory: "skills/agnostic/research/parallel-research",
210
+ tier: "agnostic",
211
+ language: null,
212
+ framework: null,
213
+ requiresTools: [],
214
+ },
215
+ {
216
+ id: "async-python-patterns",
217
+ sourceDirectory: "skills/languages/python/async-python-patterns",
218
+ tier: "language",
219
+ language: "python",
220
+ framework: null,
221
+ requiresTools: [],
222
+ },
223
+ {
224
+ id: "python-code-style",
225
+ sourceDirectory: "skills/languages/python/python-code-style",
226
+ tier: "language",
227
+ language: "python",
228
+ framework: null,
229
+ requiresTools: [],
230
+ },
231
+ {
232
+ id: "python-design-patterns",
233
+ sourceDirectory: "skills/languages/python/python-design-patterns",
234
+ tier: "language",
235
+ language: "python",
236
+ framework: null,
237
+ requiresTools: [],
238
+ },
239
+ {
240
+ id: "python-project-structure",
241
+ sourceDirectory: "skills/languages/python/python-project-structure",
242
+ tier: "language",
243
+ language: "python",
244
+ framework: null,
245
+ requiresTools: [],
246
+ },
247
+ {
248
+ id: "python-testing-patterns",
249
+ sourceDirectory: "skills/languages/python/python-testing-patterns",
250
+ tier: "language",
251
+ language: "python",
252
+ framework: null,
253
+ requiresTools: [],
254
+ },
255
+ {
256
+ id: "quality-types",
257
+ sourceDirectory: "skills/languages/typescript/quality-types",
258
+ tier: "language",
259
+ language: "typescript",
260
+ framework: null,
261
+ requiresTools: [],
262
+ },
263
+ {
264
+ id: "async-react-patterns",
265
+ sourceDirectory: "skills/frameworks/react/async-react-patterns",
266
+ tier: "framework",
267
+ language: "typescript",
268
+ framework: "react",
269
+ requiresTools: [],
270
+ },
271
+ {
272
+ id: "requirements-grill",
273
+ sourceDirectory: "skills/agnostic/requirements/requirements-grill",
274
+ tier: "agnostic",
275
+ language: null,
276
+ framework: null,
277
+ requiresTools: [],
278
+ },
279
+ {
280
+ id: "requirements-phase",
281
+ sourceDirectory: "skills/agnostic/requirements/requirements-phase",
282
+ tier: "agnostic",
283
+ language: null,
284
+ framework: null,
285
+ requiresTools: [],
286
+ },
287
+ {
288
+ id: "resolve-debt-phase",
289
+ sourceDirectory: "skills/agnostic/planning/resolve-debt-phase",
290
+ tier: "agnostic",
291
+ language: null,
292
+ framework: null,
293
+ requiresTools: [],
294
+ },
295
+ {
296
+ id: "review-phase",
297
+ sourceDirectory: "skills/agnostic/research/review-phase",
298
+ tier: "agnostic",
299
+ language: null,
300
+ framework: null,
301
+ requiresTools: [],
302
+ },
303
+ {
304
+ id: "simplify",
305
+ sourceDirectory: "skills/agnostic/quality/simplify",
306
+ tier: "agnostic",
307
+ language: null,
308
+ framework: null,
309
+ requiresTools: [],
310
+ },
311
+ {
312
+ id: "swarm-planner",
313
+ sourceDirectory: "skills/agnostic/subagents/swarm-planner",
314
+ tier: "agnostic",
315
+ language: null,
316
+ framework: null,
317
+ requiresTools: [],
318
+ },
319
+ {
320
+ id: "tanstack-query",
321
+ sourceDirectory: "skills/frameworks/tanstack-query/tanstack-query",
322
+ tier: "framework",
323
+ language: "typescript",
324
+ framework: "tanstack-query",
325
+ requiresTools: ["opensrc"],
326
+ },
327
+ {
328
+ id: "tdd",
329
+ sourceDirectory: "skills/agnostic/quality/tdd",
330
+ tier: "agnostic",
331
+ language: null,
332
+ framework: null,
333
+ requiresTools: [],
334
+ },
335
+ {
336
+ id: "turborepo",
337
+ sourceDirectory: "skills/frameworks/turborepo/turborepo",
338
+ tier: "framework",
339
+ language: null,
340
+ framework: "turborepo",
341
+ requiresTools: [],
342
+ },
343
+ {
344
+ id: "vercel-composition-patterns",
345
+ sourceDirectory: "skills/frameworks/react/vercel-composition-patterns",
346
+ tier: "framework",
347
+ language: "typescript",
348
+ framework: "react",
349
+ requiresTools: [],
350
+ },
351
+ {
352
+ id: "vercel-react-best-practices",
353
+ sourceDirectory: "skills/frameworks/react/vercel-react-best-practices",
354
+ tier: "framework",
355
+ language: "typescript",
356
+ framework: "react",
357
+ requiresTools: [],
358
+ },
359
+ {
360
+ id: "write-backlog",
361
+ sourceDirectory: "skills/agnostic/requirements/write-backlog",
362
+ tier: "agnostic",
363
+ language: null,
364
+ framework: null,
365
+ requiresTools: [],
366
+ },
53
367
  ] as const satisfies ReadonlyArray<SkillCatalogEntry>;
54
368
 
55
369
  export type SkillId = string;
@@ -0,0 +1,206 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawnSync } from "node:child_process";
4
+ import { existsSync, readFileSync } from "node:fs";
5
+ import path from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ const denyMessage = "Tests are failing. Fix all test failures before creating a PR.";
9
+ const prCommandPattern = /\bgh\s+pr\s+create\b/i;
10
+ const prToolPattern = /(^|[._-])create[_-]?pull[_-]?request([._-]|$)/i;
11
+ const packageManagers = new Set(["bun", "pnpm", "npm", "yarn"]);
12
+
13
+ function readStdinJson() {
14
+ try {
15
+ return JSON.parse(readFileSync(0, "utf8"));
16
+ } catch {
17
+ return {};
18
+ }
19
+ }
20
+
21
+ function runGit(args, cwd) {
22
+ const result = spawnSync("git", ["-C", cwd, ...args], {
23
+ encoding: "utf8",
24
+ stdio: ["ignore", "pipe", "ignore"],
25
+ });
26
+
27
+ return result.status === 0 ? result.stdout : null;
28
+ }
29
+
30
+ function repoRoot(cwd) {
31
+ return runGit(["rev-parse", "--show-toplevel"], cwd)?.trim() ?? "";
32
+ }
33
+
34
+ function collectStrings(value, values = []) {
35
+ if (typeof value === "string") {
36
+ const trimmed = value.trim();
37
+ if (trimmed) {
38
+ values.push(trimmed);
39
+ }
40
+ return values;
41
+ }
42
+
43
+ if (Array.isArray(value)) {
44
+ for (const entry of value) {
45
+ collectStrings(entry, values);
46
+ }
47
+ return values;
48
+ }
49
+
50
+ if (!value || typeof value !== "object") {
51
+ return values;
52
+ }
53
+
54
+ for (const entry of Object.values(value)) {
55
+ collectStrings(entry, values);
56
+ }
57
+
58
+ return values;
59
+ }
60
+
61
+ function matchesPrAction(...values) {
62
+ const strings = [...new Set(values.flatMap((value) => collectStrings(value)))];
63
+
64
+ return strings.some((value) => prCommandPattern.test(value) || prToolPattern.test(value));
65
+ }
66
+
67
+ function readPackageJson(root) {
68
+ try {
69
+ return JSON.parse(readFileSync(path.join(root, "package.json"), "utf8"));
70
+ } catch {
71
+ return {};
72
+ }
73
+ }
74
+
75
+ function packageManagerFromPackageJson(root) {
76
+ const packageManager = readPackageJson(root)?.packageManager;
77
+
78
+ if (typeof packageManager !== "string") {
79
+ return null;
80
+ }
81
+
82
+ const name = packageManager.split("@")[0];
83
+ return packageManagers.has(name) ? name : null;
84
+ }
85
+
86
+ function packageManagerFromLockfile(root) {
87
+ if (existsSync(path.join(root, "bun.lock")) || existsSync(path.join(root, "bun.lockb"))) {
88
+ return "bun";
89
+ }
90
+
91
+ if (
92
+ existsSync(path.join(root, "pnpm-lock.yaml")) ||
93
+ existsSync(path.join(root, "pnpm-workspace.yaml"))
94
+ ) {
95
+ return "pnpm";
96
+ }
97
+
98
+ if (existsSync(path.join(root, "yarn.lock"))) {
99
+ return "yarn";
100
+ }
101
+
102
+ if (existsSync(path.join(root, "package-lock.json"))) {
103
+ return "npm";
104
+ }
105
+
106
+ return null;
107
+ }
108
+
109
+ function testCommand(root) {
110
+ switch (packageManagerFromPackageJson(root) ?? packageManagerFromLockfile(root) ?? "npm") {
111
+ case "bun":
112
+ return ["bun", "run", "test"];
113
+ case "pnpm":
114
+ return ["pnpm", "--silent", "test"];
115
+ case "yarn":
116
+ return ["yarn", "--silent", "test"];
117
+ case "npm":
118
+ default:
119
+ return ["npm", "--silent", "test"];
120
+ }
121
+ }
122
+
123
+ function runTests(root) {
124
+ const command = testCommand(root);
125
+ const result = spawnSync(command[0], command.slice(1), {
126
+ cwd: root,
127
+ stdio: "ignore",
128
+ });
129
+
130
+ return result.status === 0;
131
+ }
132
+
133
+ function emitCodexOrClaudeBlock(message) {
134
+ process.stdout.write(
135
+ JSON.stringify({
136
+ hookSpecificOutput: {
137
+ hookEventName: "PreToolUse",
138
+ permissionDecision: "deny",
139
+ permissionDecisionReason: message,
140
+ },
141
+ systemMessage: message,
142
+ }),
143
+ );
144
+ }
145
+
146
+ function emitCursorBlock(message) {
147
+ process.stdout.write(
148
+ JSON.stringify({
149
+ continue: false,
150
+ permission: "deny",
151
+ agent_message: message,
152
+ user_message: message,
153
+ }),
154
+ );
155
+ }
156
+
157
+ function runCommandHook(style) {
158
+ const payload = readStdinJson();
159
+ const root = repoRoot(String(payload.cwd ?? process.cwd()));
160
+
161
+ if (!root || !matchesPrAction(payload) || runTests(root)) {
162
+ return;
163
+ }
164
+
165
+ if (style === "cursor") {
166
+ emitCursorBlock(denyMessage);
167
+ return;
168
+ }
169
+
170
+ emitCodexOrClaudeBlock(denyMessage);
171
+ }
172
+
173
+ export const RequireTestsForPrPlugin = async ({ worktree }) => {
174
+ return {
175
+ "tool.execute.before": async (input, output) => {
176
+ if (!matchesPrAction(input?.tool, output?.args?.command)) {
177
+ return;
178
+ }
179
+
180
+ if (runTests(worktree)) {
181
+ return;
182
+ }
183
+
184
+ throw new Error(denyMessage);
185
+ },
186
+ };
187
+ };
188
+
189
+ export const testHooks = {
190
+ testCommand,
191
+ };
192
+
193
+ function main() {
194
+ const mode = process.argv[2] ?? "";
195
+
196
+ if (mode === "claude" || mode === "codex" || mode === "cursor") {
197
+ runCommandHook(mode);
198
+ }
199
+ }
200
+
201
+ const invokedPath = process.argv[1] ? path.resolve(process.argv[1]) : "";
202
+ const modulePath = fileURLToPath(import.meta.url);
203
+
204
+ if (invokedPath && path.basename(invokedPath) === path.basename(modulePath)) {
205
+ main();
206
+ }
@@ -31,6 +31,35 @@ const tempRepo = (files: Record<string, string>) => {
31
31
  };
32
32
 
33
33
  describe("scaffolded hooks", () => {
34
+ it("runs the PR test gate with the repo package manager", async () => {
35
+ const { testHooks } = await loadHookModule("require-tests-for-pr.mjs");
36
+
37
+ expect(
38
+ testHooks.testCommand?.(
39
+ tempRepo({
40
+ "package.json": JSON.stringify({ packageManager: "bun@1.3.5" }),
41
+ }),
42
+ ),
43
+ ).toEqual(["bun", "run", "test"]);
44
+
45
+ expect(
46
+ testHooks.testCommand?.(
47
+ tempRepo({
48
+ "package.json": JSON.stringify({ packageManager: "pnpm@10.0.0" }),
49
+ }),
50
+ ),
51
+ ).toEqual(["pnpm", "--silent", "test"]);
52
+
53
+ expect(
54
+ testHooks.testCommand?.(
55
+ tempRepo({
56
+ "package.json": "{}",
57
+ "package-lock.json": "",
58
+ }),
59
+ ),
60
+ ).toEqual(["npm", "--silent", "test"]);
61
+ });
62
+
34
63
  it("executes formatter tools with the repo package manager", async () => {
35
64
  const { testHooks } = await loadHookModule("format-edited-file.mjs");
36
65