@punks/cli 1.0.5 → 1.0.7
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/README.md +1 -1
- package/dist/data/catalog/hooks.ts +0 -8
- package/dist/data/catalog/packs.ts +2 -2
- package/dist/data/hooks/format-edited-file.mjs +52 -0
- package/dist/data/hooks.test.ts +58 -0
- package/dist/data/scripts/sync-subagents.mjs +4 -55
- package/dist/index.js +79 -91
- package/dist/skills/agnostic/docs/docs-maintenance/SKILL.md +1 -1
- package/dist/skills/agnostic/planning/create-plan/REFERENCE.md +1 -1
- package/dist/skills/agnostic/planning/create-plan/SKILL.md +6 -5
- package/dist/skills/agnostic/planning/create-plan/references/grill-phase.md +2 -1
- package/dist/skills/agnostic/planning/create-plan/references/plan-schema.md +4 -0
- package/dist/skills/agnostic/planning/create-plan/references/planner-phase.md +5 -2
- package/dist/skills/agnostic/planning/create-spec/SKILL.md +17 -11
- package/dist/skills/agnostic/planning/create-spec/assets/SPEC-TEMPLATE.md +1 -1
- package/dist/skills/agnostic/planning/create-spec/references/backlog-sync.md +46 -0
- package/dist/skills/agnostic/planning/create-spec/references/grill-phase.md +47 -0
- package/dist/skills/agnostic/planning/create-spec/references/questioning.md +12 -2
- package/docs/README.md +9 -3
- package/docs/harness-intelligence-grill-log.md +39 -0
- package/docs/harness-intelligence-grill-status.md +25 -0
- package/docs/runbooks/dp-cli-scaffolding.md +4 -2
- package/package.json +1 -1
- package/dist/data/hooks/format-edited-file.py +0 -157
- package/dist/data/hooks/require-tests-for-pr.mjs +0 -144
package/README.md
CHANGED
|
@@ -67,7 +67,7 @@ Current scaffold-managed global tools:
|
|
|
67
67
|
- `portless`
|
|
68
68
|
- `skills`
|
|
69
69
|
|
|
70
|
-
On startup, `punks`
|
|
70
|
+
On startup, `punks` may start a detached best-effort worker for a newer CLI version and the presence of the `dp-cli` operator skill. The requested command renders immediately; startup checks are advisory, run at most once per 12 hours by default, and never install/update packages or skills while another CLI command is starting. Set `DP_NO_UPDATE_CHECK=1` or `DP_NO_SKILL_UPDATE_CHECK=1` to skip those checks, or `DP_STARTUP_CHECK_INTERVAL_MS=0` to force the worker during local testing.
|
|
71
71
|
|
|
72
72
|
## Publishing
|
|
73
73
|
|
|
@@ -15,12 +15,4 @@ export const hookCatalog = [
|
|
|
15
15
|
"Shared hook that auto-formats edited JS/TS/JSON files and lint-checks product files after tool use.",
|
|
16
16
|
sourcePath: "hooks/format-edited-file.mjs",
|
|
17
17
|
},
|
|
18
|
-
{
|
|
19
|
-
id: "require-tests-for-pr",
|
|
20
|
-
harness: "shared",
|
|
21
|
-
outputPath: ".agents/hooks/require-tests-for-pr.mjs",
|
|
22
|
-
description:
|
|
23
|
-
"Shared hook that blocks PR creation while tests are failing.",
|
|
24
|
-
sourcePath: "hooks/require-tests-for-pr.mjs",
|
|
25
|
-
},
|
|
26
18
|
] as const satisfies ReadonlyArray<HookDefinition>;
|
|
@@ -2,7 +2,7 @@ import type { PackCategory, PackId } from "../../core/models";
|
|
|
2
2
|
import type { LintAssetId } from "./lint";
|
|
3
3
|
import type { SkillId } from "./skills";
|
|
4
4
|
|
|
5
|
-
export type PackHookId = "format-edited-file"
|
|
5
|
+
export type PackHookId = "format-edited-file";
|
|
6
6
|
export type PackPromptSurfaceId =
|
|
7
7
|
| "shared-agents"
|
|
8
8
|
| "root-prompt-spec"
|
|
@@ -42,5 +42,5 @@ export const packCatalog = [
|
|
|
42
42
|
{ id: "trpc", category: "detected", triggerPackages: ["@trpc/"], description: "tRPC guidance pack.", skills: [], lintAssets: [], hooks: [], promptSurfaces: ["shared-agents", "root-prompt-spec", "workspace-prompt-spec"], promptDetails: ["shared contract-first baseline", "workspace client/server boundaries"] },
|
|
43
43
|
{ id: "turborepo", category: "detected", triggerPackages: ["turbo"], description: "Turborepo guidance pack.", skills: ["turborepo"], lintAssets: [], hooks: [], promptSurfaces: ["shared-agents", "root-prompt-spec", "workspace-prompt-spec"], promptDetails: ["shared monorepo baseline", "workspace task boundary rules"] },
|
|
44
44
|
{ id: "python", category: "language", triggerPackages: [], description: "Shared Python language pack for async, style, structure, testing, and design guidance.", skills: ["async-python-patterns", "python-code-style", "python-design-patterns", "python-project-structure", "python-testing-patterns"], lintAssets: [], hooks: [], promptSurfaces: [], promptDetails: [] },
|
|
45
|
-
{ id: "typescript", category: "language", triggerPackages: ["typescript"], description: "Shared JavaScript / TypeScript language pack for hook and language-level scaffold assets.", skills: ["quality-types"], lintAssets: [], hooks: ["format-edited-file"
|
|
45
|
+
{ id: "typescript", category: "language", triggerPackages: ["typescript"], description: "Shared JavaScript / TypeScript language pack for hook and language-level scaffold assets.", skills: ["quality-types"], lintAssets: [], hooks: ["format-edited-file"], promptSurfaces: [], promptDetails: [] },
|
|
46
46
|
] as const satisfies ReadonlyArray<PackCatalogEntry>;
|
|
@@ -11,6 +11,7 @@ const formattableSuffixes = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs
|
|
|
11
11
|
const lintableSuffixes = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
12
12
|
const ignoredParts = new Set([".git", "node_modules", ".next", "dist"]);
|
|
13
13
|
const productRoots = new Set(["apps", "packages"]);
|
|
14
|
+
const packageManagers = new Set(["bun", "pnpm", "npm", "yarn"]);
|
|
14
15
|
|
|
15
16
|
function readStdinJson() {
|
|
16
17
|
try {
|
|
@@ -93,7 +94,54 @@ function commandExists(command) {
|
|
|
93
94
|
);
|
|
94
95
|
}
|
|
95
96
|
|
|
97
|
+
function packageManagerFromPackageJson(root) {
|
|
98
|
+
const packageManager = loadJson(path.join(root, "package.json"))?.packageManager;
|
|
99
|
+
|
|
100
|
+
if (typeof packageManager !== "string") {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const name = packageManager.split("@")[0];
|
|
105
|
+
return packageManagers.has(name) ? name : null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function packageManagerFromLockfile(root) {
|
|
109
|
+
if (existsSync(path.join(root, "bun.lock")) || existsSync(path.join(root, "bun.lockb"))) {
|
|
110
|
+
return "bun";
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (
|
|
114
|
+
existsSync(path.join(root, "pnpm-lock.yaml")) ||
|
|
115
|
+
existsSync(path.join(root, "pnpm-workspace.yaml"))
|
|
116
|
+
) {
|
|
117
|
+
return "pnpm";
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (existsSync(path.join(root, "yarn.lock"))) {
|
|
121
|
+
return "yarn";
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (existsSync(path.join(root, "package-lock.json"))) {
|
|
125
|
+
return "npm";
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
96
131
|
function toolCommand(root, tool, args) {
|
|
132
|
+
switch (packageManagerFromPackageJson(root) ?? packageManagerFromLockfile(root)) {
|
|
133
|
+
case "bun":
|
|
134
|
+
return ["bunx", tool, ...args];
|
|
135
|
+
case "pnpm":
|
|
136
|
+
return ["pnpm", "exec", tool, ...args];
|
|
137
|
+
case "yarn":
|
|
138
|
+
return ["yarn", "exec", tool, ...args];
|
|
139
|
+
case "npm":
|
|
140
|
+
return ["npm", "exec", "--", tool, ...args];
|
|
141
|
+
default:
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
|
|
97
145
|
if (commandExists("bunx")) {
|
|
98
146
|
return ["bunx", tool, ...args];
|
|
99
147
|
}
|
|
@@ -524,6 +572,10 @@ export const FormatAndLintPlugin = async ({ client, worktree }) => {
|
|
|
524
572
|
};
|
|
525
573
|
};
|
|
526
574
|
|
|
575
|
+
export const testHooks = {
|
|
576
|
+
toolCommand,
|
|
577
|
+
};
|
|
578
|
+
|
|
527
579
|
function main() {
|
|
528
580
|
const mode = process.argv[2] ?? "";
|
|
529
581
|
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { mkdtempSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { describe, expect, it } from "@effect/vitest";
|
|
6
|
+
|
|
7
|
+
type HookModule = {
|
|
8
|
+
readonly testHooks: {
|
|
9
|
+
readonly testCommand?: (root: string) => ReadonlyArray<string>;
|
|
10
|
+
readonly toolCommand?: (
|
|
11
|
+
root: string,
|
|
12
|
+
tool: string,
|
|
13
|
+
args: ReadonlyArray<string>,
|
|
14
|
+
) => ReadonlyArray<string>;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const repoRoot = path.resolve(import.meta.dirname, "../..");
|
|
19
|
+
|
|
20
|
+
const loadHookModule = (relativePath: string) =>
|
|
21
|
+
import(path.join(repoRoot, "src/data/hooks", relativePath)) as Promise<HookModule>;
|
|
22
|
+
|
|
23
|
+
const tempRepo = (files: Record<string, string>) => {
|
|
24
|
+
const root = mkdtempSync(path.join(tmpdir(), "dp-hook-test-"));
|
|
25
|
+
|
|
26
|
+
for (const [filePath, content] of Object.entries(files)) {
|
|
27
|
+
writeFileSync(path.join(root, filePath), content);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return root;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
describe("scaffolded hooks", () => {
|
|
34
|
+
it("executes formatter tools with the repo package manager", async () => {
|
|
35
|
+
const { testHooks } = await loadHookModule("format-edited-file.mjs");
|
|
36
|
+
|
|
37
|
+
expect(
|
|
38
|
+
testHooks.toolCommand?.(
|
|
39
|
+
tempRepo({
|
|
40
|
+
"package.json": JSON.stringify({ packageManager: "bun@1.3.5" }),
|
|
41
|
+
}),
|
|
42
|
+
"oxfmt",
|
|
43
|
+
["--write", "src/index.ts"],
|
|
44
|
+
),
|
|
45
|
+
).toEqual(["bunx", "oxfmt", "--write", "src/index.ts"]);
|
|
46
|
+
|
|
47
|
+
expect(
|
|
48
|
+
testHooks.toolCommand?.(
|
|
49
|
+
tempRepo({
|
|
50
|
+
"package.json": "{}",
|
|
51
|
+
"pnpm-lock.yaml": "",
|
|
52
|
+
}),
|
|
53
|
+
"oxlint",
|
|
54
|
+
["-c", ".oxlintrc.json", "src/index.ts"],
|
|
55
|
+
),
|
|
56
|
+
).toEqual(["pnpm", "exec", "oxlint", "-c", ".oxlintrc.json", "src/index.ts"]);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -52,7 +52,7 @@ const cursorManagedComment =
|
|
|
52
52
|
const opencodeManagedComment =
|
|
53
53
|
"<!-- Generated by scripts/sync-subagents.mjs. Edit .agents/subagents/manifest.mjs instead. -->";
|
|
54
54
|
|
|
55
|
-
function buildCodexAgentConfig({ hasFormatHook
|
|
55
|
+
function buildCodexAgentConfig({ hasFormatHook }) {
|
|
56
56
|
const lines = [
|
|
57
57
|
"[agents]",
|
|
58
58
|
"max_threads = 6",
|
|
@@ -74,16 +74,6 @@ function buildCodexAgentConfig({ hasFormatHook, hasTestHook }) {
|
|
|
74
74
|
);
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
if (hasTestHook) {
|
|
78
|
-
lines.push(
|
|
79
|
-
"PreToolUse = [",
|
|
80
|
-
' { matcher = "Bash|.*[Pp]ull.?[Rr]equest.*|.*[Pp][Rr].?[Cc]reate.*|.*create[_-]?pull[_-]?request.*", hooks = [',
|
|
81
|
-
' { type = "command", command = "node \\"$(git rev-parse --show-toplevel)/.codex/hooks/require-tests-for-pr.mjs\\" codex", statusMessage = "Checking tests before PR creation", timeout = 600 },',
|
|
82
|
-
" ] },",
|
|
83
|
-
"]",
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
77
|
if (hasFormatHook) {
|
|
88
78
|
lines.push(
|
|
89
79
|
"PostToolUse = [",
|
|
@@ -98,35 +88,9 @@ function buildCodexAgentConfig({ hasFormatHook, hasTestHook }) {
|
|
|
98
88
|
return lines.join("\n");
|
|
99
89
|
}
|
|
100
90
|
|
|
101
|
-
function buildClaudeSettingsConfig({ hasFormatHook
|
|
91
|
+
function buildClaudeSettingsConfig({ hasFormatHook }) {
|
|
102
92
|
return {
|
|
103
93
|
hooks: {
|
|
104
|
-
...(hasTestHook
|
|
105
|
-
? {
|
|
106
|
-
PreToolUse: [
|
|
107
|
-
{
|
|
108
|
-
matcher: "mcp__github__create_pull_request",
|
|
109
|
-
hooks: [
|
|
110
|
-
{
|
|
111
|
-
type: "command",
|
|
112
|
-
command: 'node "$CLAUDE_PROJECT_DIR"/.claude/hooks/require-tests-for-pr.mjs claude',
|
|
113
|
-
timeout: 600,
|
|
114
|
-
},
|
|
115
|
-
],
|
|
116
|
-
},
|
|
117
|
-
{
|
|
118
|
-
matcher: "Bash",
|
|
119
|
-
hooks: [
|
|
120
|
-
{
|
|
121
|
-
type: "command",
|
|
122
|
-
command: 'node "$CLAUDE_PROJECT_DIR"/.claude/hooks/require-tests-for-pr.mjs claude',
|
|
123
|
-
timeout: 600,
|
|
124
|
-
},
|
|
125
|
-
],
|
|
126
|
-
},
|
|
127
|
-
],
|
|
128
|
-
}
|
|
129
|
-
: {}),
|
|
130
94
|
...(hasFormatHook
|
|
131
95
|
? {
|
|
132
96
|
PostToolUse: [
|
|
@@ -147,24 +111,10 @@ function buildClaudeSettingsConfig({ hasFormatHook, hasTestHook }) {
|
|
|
147
111
|
};
|
|
148
112
|
}
|
|
149
113
|
|
|
150
|
-
function buildCursorHooksConfig({ hasFormatHook
|
|
114
|
+
function buildCursorHooksConfig({ hasFormatHook }) {
|
|
151
115
|
return {
|
|
152
116
|
version: 1,
|
|
153
117
|
hooks: {
|
|
154
|
-
...(hasTestHook
|
|
155
|
-
? {
|
|
156
|
-
beforeMCPExecution: [
|
|
157
|
-
{
|
|
158
|
-
command: "node .cursor/hooks/require-tests-for-pr.mjs cursor",
|
|
159
|
-
},
|
|
160
|
-
],
|
|
161
|
-
beforeShellExecution: [
|
|
162
|
-
{
|
|
163
|
-
command: "node .cursor/hooks/require-tests-for-pr.mjs cursor",
|
|
164
|
-
},
|
|
165
|
-
],
|
|
166
|
-
}
|
|
167
|
-
: {}),
|
|
168
118
|
...(hasFormatHook
|
|
169
119
|
? {
|
|
170
120
|
afterFileEdit: [
|
|
@@ -391,7 +341,7 @@ async function syncHookSurface() {
|
|
|
391
341
|
await mkdir(cursorHooksDir, { recursive: true });
|
|
392
342
|
await mkdir(opencodePluginsDir, { recursive: true });
|
|
393
343
|
|
|
394
|
-
for (const hookName of ["format-edited-file.mjs"
|
|
344
|
+
for (const hookName of ["format-edited-file.mjs"]) {
|
|
395
345
|
const sourcePath = path.join(sharedHooksPath, hookName);
|
|
396
346
|
const targetPaths = [
|
|
397
347
|
path.join(codexHooksDir, hookName),
|
|
@@ -445,7 +395,6 @@ async function main() {
|
|
|
445
395
|
await mkdir(path.dirname(cursorHooksJsonPath), { recursive: true });
|
|
446
396
|
const availableHooks = {
|
|
447
397
|
hasFormatHook: await pathExists(path.join(sharedHooksPath, "format-edited-file.mjs")),
|
|
448
|
-
hasTestHook: await pathExists(path.join(sharedHooksPath, "require-tests-for-pr.mjs")),
|
|
449
398
|
};
|
|
450
399
|
await writeFile(codexConfigPath, buildCodexAgentConfig(availableHooks), "utf8");
|
|
451
400
|
await rm(path.join(codexDir, "hooks.json"), { force: true });
|
package/dist/index.js
CHANGED
|
@@ -10884,8 +10884,15 @@ var require_public_api = __commonJS((exports) => {
|
|
|
10884
10884
|
});
|
|
10885
10885
|
|
|
10886
10886
|
// src/index.ts
|
|
10887
|
-
import {
|
|
10888
|
-
import {
|
|
10887
|
+
import { spawn as spawn2 } from "child_process";
|
|
10888
|
+
import {
|
|
10889
|
+
existsSync as existsSync7,
|
|
10890
|
+
mkdirSync as mkdirSync6,
|
|
10891
|
+
readFileSync as readFileSync7,
|
|
10892
|
+
writeFileSync as writeFileSync5
|
|
10893
|
+
} from "fs";
|
|
10894
|
+
import { homedir as homedir2 } from "os";
|
|
10895
|
+
import path14 from "path";
|
|
10889
10896
|
|
|
10890
10897
|
// node_modules/.pnpm/effect@3.19.19/node_modules/effect/dist/esm/Array.js
|
|
10891
10898
|
var exports_Array = {};
|
|
@@ -41903,13 +41910,6 @@ var hookCatalog = [
|
|
|
41903
41910
|
outputPath: ".agents/hooks/format-edited-file.mjs",
|
|
41904
41911
|
description: "Shared hook that auto-formats edited JS/TS/JSON files and lint-checks product files after tool use.",
|
|
41905
41912
|
sourcePath: "hooks/format-edited-file.mjs"
|
|
41906
|
-
},
|
|
41907
|
-
{
|
|
41908
|
-
id: "require-tests-for-pr",
|
|
41909
|
-
harness: "shared",
|
|
41910
|
-
outputPath: ".agents/hooks/require-tests-for-pr.mjs",
|
|
41911
|
-
description: "Shared hook that blocks PR creation while tests are failing.",
|
|
41912
|
-
sourcePath: "hooks/require-tests-for-pr.mjs"
|
|
41913
41913
|
}
|
|
41914
41914
|
];
|
|
41915
41915
|
|
|
@@ -42208,7 +42208,7 @@ var packCatalog = [
|
|
|
42208
42208
|
{ id: "trpc", category: "detected", triggerPackages: ["@trpc/"], description: "tRPC guidance pack.", skills: [], lintAssets: [], hooks: [], promptSurfaces: ["shared-agents", "root-prompt-spec", "workspace-prompt-spec"], promptDetails: ["shared contract-first baseline", "workspace client/server boundaries"] },
|
|
42209
42209
|
{ id: "turborepo", category: "detected", triggerPackages: ["turbo"], description: "Turborepo guidance pack.", skills: ["turborepo"], lintAssets: [], hooks: [], promptSurfaces: ["shared-agents", "root-prompt-spec", "workspace-prompt-spec"], promptDetails: ["shared monorepo baseline", "workspace task boundary rules"] },
|
|
42210
42210
|
{ id: "python", category: "language", triggerPackages: [], description: "Shared Python language pack for async, style, structure, testing, and design guidance.", skills: ["async-python-patterns", "python-code-style", "python-design-patterns", "python-project-structure", "python-testing-patterns"], lintAssets: [], hooks: [], promptSurfaces: [], promptDetails: [] },
|
|
42211
|
-
{ id: "typescript", category: "language", triggerPackages: ["typescript"], description: "Shared JavaScript / TypeScript language pack for hook and language-level scaffold assets.", skills: ["quality-types"], lintAssets: [], hooks: ["format-edited-file"
|
|
42211
|
+
{ id: "typescript", category: "language", triggerPackages: ["typescript"], description: "Shared JavaScript / TypeScript language pack for hook and language-level scaffold assets.", skills: ["quality-types"], lintAssets: [], hooks: ["format-edited-file"], promptSurfaces: [], promptDetails: [] }
|
|
42212
42212
|
];
|
|
42213
42213
|
|
|
42214
42214
|
// src/data/catalog/skills.ts
|
|
@@ -42289,7 +42289,7 @@ var toolCatalog = [
|
|
|
42289
42289
|
|
|
42290
42290
|
// package.json
|
|
42291
42291
|
var name = "@punks/cli";
|
|
42292
|
-
var version = "1.0.
|
|
42292
|
+
var version = "1.0.7";
|
|
42293
42293
|
// src/baseline/bundled.ts
|
|
42294
42294
|
var bundledBaseline = {
|
|
42295
42295
|
summary: {
|
|
@@ -46687,6 +46687,9 @@ var runUpdate = ({
|
|
|
46687
46687
|
relativePath: change.path
|
|
46688
46688
|
});
|
|
46689
46689
|
}
|
|
46690
|
+
for (const staleFile of staleFiles) {
|
|
46691
|
+
rmSync3(path12.join(rootDirectory, staleFile), { recursive: true, force: true });
|
|
46692
|
+
}
|
|
46690
46693
|
}
|
|
46691
46694
|
if (check2 && needsAttention) {
|
|
46692
46695
|
process.exitCode = 1;
|
|
@@ -46967,7 +46970,6 @@ var listSkills = (scope5) => {
|
|
|
46967
46970
|
}
|
|
46968
46971
|
return parseSkillList(result.stdout);
|
|
46969
46972
|
};
|
|
46970
|
-
var skillUpdateCommand = (scope5) => scope5 === "global" ? ["skills", "update", "dp-cli", "--global", "--yes"] : ["skills", "update", "dp-cli", "--project", "--yes"];
|
|
46971
46973
|
var skillInstallCommand = () => [
|
|
46972
46974
|
"skills",
|
|
46973
46975
|
"add",
|
|
@@ -46977,18 +46979,6 @@ var skillInstallCommand = () => [
|
|
|
46977
46979
|
"dp-cli",
|
|
46978
46980
|
"--yes"
|
|
46979
46981
|
];
|
|
46980
|
-
var runSkillsCommand = (command) => {
|
|
46981
|
-
const result = spawnSync(command[0], command.slice(1), {
|
|
46982
|
-
stdio: "ignore",
|
|
46983
|
-
shell: false,
|
|
46984
|
-
timeout: 30000
|
|
46985
|
-
});
|
|
46986
|
-
return result.status === 0;
|
|
46987
|
-
};
|
|
46988
|
-
var updateDpCliSkill = (scope5) => runSkillsCommand(skillUpdateCommand(scope5));
|
|
46989
|
-
var installDpCliSkillGlobal = () => {
|
|
46990
|
-
return runSkillsCommand(skillInstallCommand());
|
|
46991
|
-
};
|
|
46992
46982
|
var checkDpCliSkillPresence = () => {
|
|
46993
46983
|
if (!commandExists2("skills")) {
|
|
46994
46984
|
return {
|
|
@@ -47009,37 +46999,13 @@ var checkDpCliSkillPresence = () => {
|
|
|
47009
46999
|
global
|
|
47010
47000
|
};
|
|
47011
47001
|
};
|
|
47012
|
-
var maintainDpCliSkill = () => {
|
|
47013
|
-
const presence = checkDpCliSkillPresence();
|
|
47014
|
-
if (!presence.skillsCliAvailable) {
|
|
47015
|
-
return {
|
|
47016
|
-
...presence,
|
|
47017
|
-
installedGlobal: false,
|
|
47018
|
-
updatedProject: false,
|
|
47019
|
-
updatedGlobal: false
|
|
47020
|
-
};
|
|
47021
|
-
}
|
|
47022
|
-
if (!presence.detected) {
|
|
47023
|
-
const installedGlobal = installDpCliSkillGlobal();
|
|
47024
|
-
return {
|
|
47025
|
-
skillsCliAvailable: true,
|
|
47026
|
-
detected: installedGlobal,
|
|
47027
|
-
project: false,
|
|
47028
|
-
global: installedGlobal,
|
|
47029
|
-
installedGlobal,
|
|
47030
|
-
updatedProject: false,
|
|
47031
|
-
updatedGlobal: installedGlobal ? updateDpCliSkill("global") : false
|
|
47032
|
-
};
|
|
47033
|
-
}
|
|
47034
|
-
return {
|
|
47035
|
-
...presence,
|
|
47036
|
-
installedGlobal: false,
|
|
47037
|
-
updatedProject: presence.project ? updateDpCliSkill("project") : false,
|
|
47038
|
-
updatedGlobal: presence.global ? updateDpCliSkill("global") : false
|
|
47039
|
-
};
|
|
47040
|
-
};
|
|
47041
47002
|
|
|
47042
47003
|
// src/index.ts
|
|
47004
|
+
var startupCheckWorkerEnv = "DP_STARTUP_CHECK_WORKER";
|
|
47005
|
+
var startupCheckCliEnv = "DP_STARTUP_CHECK_CLI";
|
|
47006
|
+
var startupCheckSkillEnv = "DP_STARTUP_CHECK_SKILL";
|
|
47007
|
+
var startupCheckIntervalMs = Number.parseInt(process.env.DP_STARTUP_CHECK_INTERVAL_MS ?? String(12 * 60 * 60 * 1000), 10);
|
|
47008
|
+
var startupCheckCacheFile = path14.join(homedir2(), ".cache", "punks", "startup-check.json");
|
|
47043
47009
|
var app = exports_Command.make("punks", {}, () => exports_Effect.sync(() => console.log(renderCommandGuide()))).pipe(exports_Command.withDescription("Devpunks AI scaffolding CLI."), exports_Command.withSubcommands([scaffoldCommand, updateCommand]));
|
|
47044
47010
|
var cli = exports_Command.run(app, {
|
|
47045
47011
|
name: "punks",
|
|
@@ -47061,54 +47027,76 @@ var runCliUpdateCheck = async () => {
|
|
|
47061
47027
|
command: result.command
|
|
47062
47028
|
}));
|
|
47063
47029
|
}
|
|
47064
|
-
if (result.status !== "available" || process.stdin.isTTY !== true) {
|
|
47065
|
-
return;
|
|
47066
|
-
}
|
|
47067
|
-
const prompt4 = createInterface3({
|
|
47068
|
-
input: process.stdin,
|
|
47069
|
-
output: process.stderr
|
|
47070
|
-
});
|
|
47071
|
-
try {
|
|
47072
|
-
const answer = await prompt4.question("Update punks CLI now? [y/N]: ");
|
|
47073
|
-
if (!["y", "yes"].includes(answer.trim().toLowerCase())) {
|
|
47074
|
-
return;
|
|
47075
|
-
}
|
|
47076
|
-
console.error(`punks startup checks: running \`${result.command.join(" ")}\``);
|
|
47077
|
-
const update5 = spawnSync2(result.command[0], result.command.slice(1), {
|
|
47078
|
-
stdio: "inherit",
|
|
47079
|
-
shell: false
|
|
47080
|
-
});
|
|
47081
|
-
if (update5.status !== 0) {
|
|
47082
|
-
console.error("punks startup checks: CLI update failed.");
|
|
47083
|
-
}
|
|
47084
|
-
} finally {
|
|
47085
|
-
prompt4.close();
|
|
47086
|
-
}
|
|
47087
47030
|
};
|
|
47088
47031
|
var runDpCliSkillCheck = () => {
|
|
47089
|
-
const skillResult =
|
|
47032
|
+
const skillResult = checkDpCliSkillPresence();
|
|
47090
47033
|
if (!skillResult.skillsCliAvailable) {
|
|
47091
47034
|
return;
|
|
47092
47035
|
}
|
|
47093
|
-
if (skillResult.
|
|
47094
|
-
console.error(
|
|
47036
|
+
if (!skillResult.detected) {
|
|
47037
|
+
console.error(`punks startup checks: \`dp-cli\` skill not found. Install it with \`${skillInstallCommand().join(" ")}\`.`);
|
|
47038
|
+
}
|
|
47039
|
+
};
|
|
47040
|
+
var runStartupChecks = async ({
|
|
47041
|
+
checkCli,
|
|
47042
|
+
checkSkill
|
|
47043
|
+
}) => {
|
|
47044
|
+
if (checkCli) {
|
|
47045
|
+
await runCliUpdateCheck();
|
|
47046
|
+
}
|
|
47047
|
+
if (checkSkill) {
|
|
47048
|
+
runDpCliSkillCheck();
|
|
47095
47049
|
}
|
|
47096
|
-
|
|
47097
|
-
|
|
47050
|
+
};
|
|
47051
|
+
var hasFreshStartupCheck = () => {
|
|
47052
|
+
if (!Number.isFinite(startupCheckIntervalMs) || startupCheckIntervalMs <= 0) {
|
|
47053
|
+
return false;
|
|
47054
|
+
}
|
|
47055
|
+
try {
|
|
47056
|
+
const parsed = JSON.parse(readFileSync7(startupCheckCacheFile, "utf8"));
|
|
47057
|
+
return typeof parsed.checkedAt === "number" && Date.now() - parsed.checkedAt < startupCheckIntervalMs;
|
|
47058
|
+
} catch {
|
|
47059
|
+
return false;
|
|
47098
47060
|
}
|
|
47099
47061
|
};
|
|
47100
|
-
var
|
|
47062
|
+
var markStartupCheckStarted = () => {
|
|
47063
|
+
mkdirSync6(path14.dirname(startupCheckCacheFile), { recursive: true });
|
|
47064
|
+
writeFileSync5(startupCheckCacheFile, JSON.stringify({ checkedAt: Date.now() }, null, 2));
|
|
47065
|
+
};
|
|
47066
|
+
var startBackgroundStartupChecks = () => {
|
|
47101
47067
|
const checkCli = shouldCheckSelfUpdate();
|
|
47102
47068
|
const checkSkill = shouldCheckDpCliSkillUpdate();
|
|
47103
|
-
if (!checkCli && !checkSkill) {
|
|
47069
|
+
if (!checkCli && !checkSkill || hasFreshStartupCheck()) {
|
|
47104
47070
|
return;
|
|
47105
47071
|
}
|
|
47106
|
-
|
|
47107
|
-
|
|
47108
|
-
|
|
47109
|
-
if (checkCli) {
|
|
47110
|
-
await runCliUpdateCheck();
|
|
47072
|
+
const entrypoint = process.argv[1];
|
|
47073
|
+
if (entrypoint === undefined || !existsSync7(entrypoint)) {
|
|
47074
|
+
return;
|
|
47111
47075
|
}
|
|
47112
|
-
|
|
47113
|
-
|
|
47114
|
-
|
|
47076
|
+
markStartupCheckStarted();
|
|
47077
|
+
const worker = spawn2(process.execPath, [entrypoint], {
|
|
47078
|
+
cwd: process.cwd(),
|
|
47079
|
+
detached: true,
|
|
47080
|
+
env: {
|
|
47081
|
+
...process.env,
|
|
47082
|
+
[startupCheckWorkerEnv]: "1",
|
|
47083
|
+
[startupCheckCliEnv]: checkCli ? "1" : "0",
|
|
47084
|
+
[startupCheckSkillEnv]: checkSkill ? "1" : "0"
|
|
47085
|
+
},
|
|
47086
|
+
stdio: ["ignore", "ignore", "inherit"]
|
|
47087
|
+
});
|
|
47088
|
+
worker.unref();
|
|
47089
|
+
};
|
|
47090
|
+
if (process.env[startupCheckWorkerEnv] === "1") {
|
|
47091
|
+
runStartupChecks({
|
|
47092
|
+
checkCli: process.env[startupCheckCliEnv] === "1",
|
|
47093
|
+
checkSkill: process.env[startupCheckSkillEnv] === "1"
|
|
47094
|
+
}).finally(() => {
|
|
47095
|
+
process.exit(0);
|
|
47096
|
+
});
|
|
47097
|
+
} else {
|
|
47098
|
+
try {
|
|
47099
|
+
startBackgroundStartupChecks();
|
|
47100
|
+
} catch {}
|
|
47101
|
+
cli(process.argv).pipe(exports_Effect.provide(exports_Layer.mergeAll(exports_BunContext.layer)), exports_BunRuntime.runMain);
|
|
47102
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: docs-maintenance
|
|
3
|
-
description: Ingest a spec folder into the wiki domain layer by extracting and writing flow pages first, then concept pages, then syncing ingest metadata. Secondary: update docs/ when code changes alter architecture, setup, contracts, or operator workflow. Use when a spec is ready to be captured as domain knowledge after review or implementation, or when a code task changes non-obvious behavior that docs/ should reflect.
|
|
3
|
+
description: "Ingest a spec folder into the wiki domain layer by extracting and writing flow pages first, then concept pages, then syncing ingest metadata. Secondary: update docs/ when code changes alter architecture, setup, contracts, or operator workflow. Use when a spec is ready to be captured as domain knowledge after review or implementation, or when a code task changes non-obvious behavior that docs/ should reflect."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Docs Maintenance
|
|
@@ -33,5 +33,5 @@
|
|
|
33
33
|
- Derive repo ownership and hosting from git state instead of hardcoding assumptions.
|
|
34
34
|
- If a required tool is unavailable, stop clearly and report the missing dependency.
|
|
35
35
|
- Keep the canonical backlog model aligned with [../write-backlog/assets/concepts/backlog-model.md](../write-backlog/assets/concepts/backlog-model.md).
|
|
36
|
-
- For every planned task, locate the relevant scoped `AGENTS.md` chain and assign the existing skills that the executor must load before editing.
|
|
36
|
+
- For every planned task, locate the relevant scoped `AGENTS.md` chain, load the relevant skill guidance during planning, and assign the existing skills that the executor must load again before editing.
|
|
37
37
|
- Never start implementation from this skill.
|
|
@@ -31,7 +31,7 @@ Create a plan first. Never implement code in this skill.
|
|
|
31
31
|
4. Update a running decision ledger after every answer so the user never has to reconstruct state from memory.
|
|
32
32
|
5. Insert a synthesis checkpoint before the thread gets noisy, then continue only if more ambiguity reduction is still needed.
|
|
33
33
|
6. Research with `opensrc path <package>` or `opensrc path <owner>/<repo>` plus primary-source web docs when current behavior matters.
|
|
34
|
-
7. Locate scoped `AGENTS.md` files for every planned task path
|
|
34
|
+
7. Locate scoped `AGENTS.md` files for every planned task path, extract `Primary skills here` lists, and load the relevant skill guidance before finalizing task design.
|
|
35
35
|
8. Read `references/planner-phase.md` and run `$swarm-planner` as an explicit inner phase.
|
|
36
36
|
9. Read `references/tdd-phase.md` and run `$tdd` as an explicit inner phase.
|
|
37
37
|
10. Read `references/backlog-sync.md` and sync backlog at epic/story level, not one item per plan task.
|
|
@@ -46,10 +46,11 @@ Create a plan first. Never implement code in this skill.
|
|
|
46
46
|
3. Every plan-shaping question must use the exact block: `Decision`, `Recommendation`, `Question`, `Why it matters`.
|
|
47
47
|
4. Keep `$grill-me`, `$swarm-planner`, and `$tdd` as visible required inner phases of one planning run.
|
|
48
48
|
5. Resolve each task's scoped guidance from root `AGENTS.md` down to the nearest `AGENTS.md` for its `location`.
|
|
49
|
-
6.
|
|
50
|
-
7.
|
|
51
|
-
8.
|
|
52
|
-
9.
|
|
49
|
+
6. Use each task's required skill guidance while shaping its scope, dependencies, validation, RED target, and review mode; do not merely list skills after the plan is written.
|
|
50
|
+
7. Assign each task the exact existing skills required by those scoped `Primary skills here` lists, merging all scopes for cross-directory tasks.
|
|
51
|
+
8. Normalize every task with stable ids, `depends_on`, `location`, `description`, `validation`, `status`, `log`, `files edited/created`, owning-story backlog references, `assigned_skills`, `tdd_target`, and `review_mode`.
|
|
52
|
+
9. Keep the saved plan standalone: include situation, issue, solution shape, assumptions, findings, research, dependency graph, testing strategy, skill-routing notes, risks, validation gates, unresolved questions, and a resolved decision ledger.
|
|
53
|
+
10. Stop after plan creation and backlog sync. Do not implement code or spawn implementation workers.
|
|
53
54
|
|
|
54
55
|
### Review modes
|
|
55
56
|
|
|
@@ -57,7 +57,8 @@ Preserve `$grill-me` behavior:
|
|
|
57
57
|
- provide a recommended answer with each question
|
|
58
58
|
- if a question can be answered from the codebase, answer it by inspecting instead
|
|
59
59
|
- keep grilling until every plan-shaping branch is resolved enough to plan safely
|
|
60
|
-
-
|
|
60
|
+
- before recording an unresolved question in the plan, ask the user whether to resolve it now or defer it
|
|
61
|
+
- when the user defers a branch, record the assumption or unresolved question explicitly in the plan with its planning impact
|
|
61
62
|
|
|
62
63
|
## Decision ledger
|
|
63
64
|
|
|
@@ -22,6 +22,8 @@ Include:
|
|
|
22
22
|
- validation gates per phase when phases exist
|
|
23
23
|
- unresolved questions
|
|
24
24
|
|
|
25
|
+
`unresolved questions` is not a hiding place for skipped planning. Before saving a plan with unresolved questions, prompt the user to resolve each plan-shaping question that they can reasonably answer now. Keep only deferred, externally blocked, or non-blocking questions, and state why each remains open.
|
|
26
|
+
|
|
25
27
|
## Task contract
|
|
26
28
|
|
|
27
29
|
Every task must include:
|
|
@@ -47,6 +49,8 @@ Multiple tasks may point to the same story when one story needs several executio
|
|
|
47
49
|
|
|
48
50
|
Do not create a new backlog item only because a task boundary exists in the plan.
|
|
49
51
|
|
|
52
|
+
`assigned_skills` must list the skills used to shape the task during planning, not only skills expected during implementation. Skill guidance should be reflected in the task's boundary, validation, `tdd_target`, and `review_mode`.
|
|
53
|
+
|
|
50
54
|
```md
|
|
51
55
|
### T3: Example task
|
|
52
56
|
|
|
@@ -18,10 +18,13 @@ Before finalizing task boundaries:
|
|
|
18
18
|
2. For each location, inspect the `AGENTS.md` chain from repo root to the nearest scoped file.
|
|
19
19
|
3. Extract every `Primary skills here` entry from applicable scoped files.
|
|
20
20
|
4. Verify each named skill exists in `.agents/skills/` or an installed skill source visible to the agent.
|
|
21
|
-
5.
|
|
21
|
+
5. Load the relevant skill instructions before finalizing the task's boundary, validation, RED target, and review mode.
|
|
22
|
+
6. Add the merged, deduplicated list to the task as `assigned_skills`.
|
|
22
23
|
|
|
23
24
|
If a task spans multiple scopes, include all required skills from all touched scopes. If a scope names a missing skill, keep the task planned but record the missing skill in risks and unresolved questions.
|
|
24
25
|
|
|
26
|
+
`assigned_skills` is both planning input and executor handoff. Do not design the task first and attach skills afterward. Use the skill guidance to decide what a correct task slice, dependency, validation, and test target should look like.
|
|
27
|
+
|
|
25
28
|
## Planner behavior
|
|
26
29
|
|
|
27
30
|
Produce exactly one named `PLAN.md` in the target spec folder.
|
|
@@ -33,7 +36,7 @@ Preserve `$swarm-planner` behavior:
|
|
|
33
36
|
- validations per task
|
|
34
37
|
- parallel execution waves
|
|
35
38
|
- risks and mitigations
|
|
36
|
-
- explicit `assigned_skills` per task from scoped `AGENTS.md
|
|
39
|
+
- explicit `assigned_skills` per task from scoped `AGENTS.md`, with task design shaped by those skills
|
|
37
40
|
- a final subagent review for missing deps, ordering issues, edge cases, and holes before yielding
|
|
38
41
|
|
|
39
42
|
Do not stop between the grill and planner phases unless a true blocking ambiguity remains.
|
|
@@ -10,7 +10,7 @@ description: Create a SPEC.md file for a new feature, product, or system using t
|
|
|
10
10
|
- **Role:** higher-order spec authoring skill
|
|
11
11
|
- **Entrypoint type:** public entrypoint
|
|
12
12
|
- **Upstream:** new idea, feature request, epic/capability issue, or problem statement
|
|
13
|
-
- **Delegates to:**
|
|
13
|
+
- **Delegates to:** `$requirements-grill` when discovery leaves meaningful spec-affecting unknowns; `$write-backlog` when grill outcomes change epic/story scope
|
|
14
14
|
- **Downstream:** reviewed `SPEC.md`, then usually `create-plan` or `implement-spec`
|
|
15
15
|
- **Entry conditions:** wiki domain can be resolved, or the user creates one first with `create-wiki-domain`
|
|
16
16
|
- **Stop conditions:** `SPEC.md`, wiki index, and wiki log are updated, then wait for user review
|
|
@@ -27,12 +27,14 @@ The output lives at `apps/wiki/specs/<domain>/<folder-name>/SPEC.md`.
|
|
|
27
27
|
2. Read `references/discovery.md` and orient yourself in the right wiki domain before asking questions.
|
|
28
28
|
3. If backlog context exists, read the parent epic and every child story before asking questions.
|
|
29
29
|
4. If the user did not provide a concrete request, ask for a rough description first.
|
|
30
|
-
5. Read `references/questioning.md` and ask only the clarifying questions needed to
|
|
31
|
-
6.
|
|
32
|
-
7.
|
|
33
|
-
8. Read `references/
|
|
34
|
-
9. Read `
|
|
35
|
-
10. Read `references/
|
|
30
|
+
5. Read `references/questioning.md` and ask only the lightweight clarifying questions needed to identify whether a grill phase is required.
|
|
31
|
+
6. If draft `Open Questions` would affect spec trust, read `references/grill-phase.md` and run a bounded `$requirements-grill` phase before writing.
|
|
32
|
+
7. If the grill changes accepted scope, child stories, deferred scope, or story order, read `references/backlog-sync.md` and run `$write-backlog` to sync the backlog automatically.
|
|
33
|
+
8. Read `references/folder-naming.md` to resolve the domain and spec folder path.
|
|
34
|
+
9. Read `assets/SPEC-TEMPLATE.md` and write the spec.
|
|
35
|
+
10. Read `references/spec-quality-bar.md` before saving.
|
|
36
|
+
11. Read `references/wiki-bookkeeping.md` to update `index.md`, `<domain>-specs.md`, and `log.md`.
|
|
37
|
+
12. Read `references/handoff.md` to choose the next-step recommendation and stop after user review.
|
|
36
38
|
|
|
37
39
|
## Workflow
|
|
38
40
|
|
|
@@ -41,15 +43,19 @@ The output lives at `apps/wiki/specs/<domain>/<folder-name>/SPEC.md`.
|
|
|
41
43
|
1. Build orientation first; do not jump straight into writing.
|
|
42
44
|
2. Ask only enough to make the spec crisp, testable, and bounded.
|
|
43
45
|
3. When an epic has child stories, harvest and preserve each story's requirements before drafting.
|
|
44
|
-
4.
|
|
45
|
-
5.
|
|
46
|
-
6.
|
|
47
|
-
7.
|
|
46
|
+
4. Use `$requirements-grill` for meaningful spec-affecting unknowns; do not replace that phase with ad hoc `Open Questions` prompts.
|
|
47
|
+
5. After a grill phase, use `$write-backlog` automatically when accepted decisions imply backlog changes.
|
|
48
|
+
6. Keep the spec free of implementation detail.
|
|
49
|
+
7. Use the template structure exactly, then remove all template scaffolding.
|
|
50
|
+
8. Update wiki bookkeeping in the same run.
|
|
51
|
+
9. Stop after presenting the spec and the recommended next step.
|
|
48
52
|
|
|
49
53
|
## Advanced features
|
|
50
54
|
|
|
51
55
|
- Discovery and repo orientation: see [references/discovery.md](references/discovery.md)
|
|
52
56
|
- Clarifying-question strategy: see [references/questioning.md](references/questioning.md)
|
|
57
|
+
- Requirements grill phase: see [references/grill-phase.md](references/grill-phase.md)
|
|
58
|
+
- Backlog sync after grilling: see [references/backlog-sync.md](references/backlog-sync.md)
|
|
53
59
|
- Domain and folder naming rules: see [references/folder-naming.md](references/folder-naming.md)
|
|
54
60
|
- Acceptance-criteria and quality bar: see [references/spec-quality-bar.md](references/spec-quality-bar.md)
|
|
55
61
|
- Wiki index and log updates: see [references/wiki-bookkeeping.md](references/wiki-bookkeeping.md)
|
|
@@ -74,7 +74,7 @@ _Optional. Technical discoveries, known system constraints, or early implementat
|
|
|
74
74
|
|
|
75
75
|
## Open Questions
|
|
76
76
|
|
|
77
|
-
_Unresolved questions that could affect implementation._
|
|
77
|
+
_Unresolved questions that could affect implementation. Before saving, route meaningful spec-affecting unknowns through the requirements-grill phase. Only keep questions here when the user explicitly defers them, the answer requires external validation, or the issue is non-blocking for spec review._
|
|
78
78
|
|
|
79
79
|
| # | Question | Affects | Owner | Status |
|
|
80
80
|
|---|----------|---------|-------|--------|
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Backlog Sync
|
|
2
|
+
|
|
3
|
+
Use after `$requirements-grill` and before final spec drafting.
|
|
4
|
+
|
|
5
|
+
## Trigger
|
|
6
|
+
|
|
7
|
+
Run `$write-backlog` automatically when grill outcomes change:
|
|
8
|
+
|
|
9
|
+
- the epic/capability boundary
|
|
10
|
+
- child-story acceptance signals, scope, or canonical terms
|
|
11
|
+
- missing, parked, moved, or future-scope stories
|
|
12
|
+
- story ordering, blockers, or parent/child relationships
|
|
13
|
+
|
|
14
|
+
Skip only for wording clarifications that do not change backlog scope, story meaning, or ordering.
|
|
15
|
+
|
|
16
|
+
## Load
|
|
17
|
+
|
|
18
|
+
Follow:
|
|
19
|
+
|
|
20
|
+
- `../../../requirements/write-backlog/SKILL.md`
|
|
21
|
+
- `../../../requirements/write-backlog/REFERENCE.md`
|
|
22
|
+
- `../../../requirements/write-backlog/assets/concepts/backlog-model.md`
|
|
23
|
+
|
|
24
|
+
If grill artifacts exist, read:
|
|
25
|
+
|
|
26
|
+
1. `docs/<topic>-grill-status.md`
|
|
27
|
+
2. `docs/<topic>-grill-log.md`
|
|
28
|
+
|
|
29
|
+
## Rules
|
|
30
|
+
|
|
31
|
+
- The parent epic remains the spec anchor.
|
|
32
|
+
- Child stories remain product-facing slices beneath that epic.
|
|
33
|
+
- Derive backlog changes only from accepted decisions and locked direction.
|
|
34
|
+
- Preserve parked branches as deferred scope, follow-up epic/story candidates, or backlog notes.
|
|
35
|
+
- Keep unresolved still-open items out of committed story scope unless explicitly marked.
|
|
36
|
+
- Use native parent/child and `blockedBy` / `blocks` relations when the provider supports them.
|
|
37
|
+
- Do not add implementation details, file paths, TDD targets, validation commands, or worker handoffs to backlog bodies.
|
|
38
|
+
|
|
39
|
+
## Handoff
|
|
40
|
+
|
|
41
|
+
- reread the updated epic and child stories before drafting
|
|
42
|
+
- incorporate all child-story requirements into the spec
|
|
43
|
+
- add backlog item ids/URLs to spec links when available
|
|
44
|
+
- mention deferred backlog items only as non-goals, future scope, or `Open Questions`
|
|
45
|
+
|
|
46
|
+
Do not finalize a backlog-backed spec from stale pre-grill story text.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Grill Phase
|
|
2
|
+
|
|
3
|
+
Use after discovery/questioning when unresolved spec questions would affect review trust.
|
|
4
|
+
|
|
5
|
+
## Trigger
|
|
6
|
+
|
|
7
|
+
Run bounded `$requirements-grill` when:
|
|
8
|
+
|
|
9
|
+
- multiple child stories conflict or leave cross-story behavior unclear
|
|
10
|
+
- the target actor, outcome, boundary, non-goal, or acceptance signal is ambiguous
|
|
11
|
+
- candidate `Open Questions` would materially change scope, trust, or acceptance criteria
|
|
12
|
+
|
|
13
|
+
Use lightweight direct clarification instead when only one small naming or wording detail is missing.
|
|
14
|
+
|
|
15
|
+
## Load
|
|
16
|
+
|
|
17
|
+
Follow:
|
|
18
|
+
|
|
19
|
+
- `../../../requirements/requirements-grill/references/grilling-flow.md`
|
|
20
|
+
- `../../../requirements/requirements-grill/references/artifact-output.md` for serious grilling sessions
|
|
21
|
+
|
|
22
|
+
## Rules
|
|
23
|
+
|
|
24
|
+
- Ask one question at a time by default.
|
|
25
|
+
- Include a recommended answer and why it is preferred.
|
|
26
|
+
- Inspect repo/docs/backlog first when the answer can be found locally.
|
|
27
|
+
- Force precise choices when multiple interpretations exist.
|
|
28
|
+
- Close, park, or explicitly defer each branch.
|
|
29
|
+
|
|
30
|
+
## Artifacts
|
|
31
|
+
|
|
32
|
+
For serious sessions, create or reuse:
|
|
33
|
+
|
|
34
|
+
- `docs/<topic>-grill-log.md`
|
|
35
|
+
- `docs/<topic>-grill-status.md`
|
|
36
|
+
|
|
37
|
+
Tiny clarification-only runs do not need durable grill artifacts.
|
|
38
|
+
|
|
39
|
+
## Handoff
|
|
40
|
+
|
|
41
|
+
- accepted answers become requirements, constraints, non-goals, acceptance criteria, or decisions
|
|
42
|
+
- parked branches outside the epic become non-goals or future scope
|
|
43
|
+
- explicitly deferred branches become `Open Questions`
|
|
44
|
+
- external-validation branches become `Open Questions` with owner/status
|
|
45
|
+
- accepted scope changes must flow through `backlog-sync.md` before final spec drafting
|
|
46
|
+
|
|
47
|
+
Do not write the spec until each discovered branch is closed, parked, or explicitly deferred.
|
|
@@ -4,7 +4,9 @@ Use this reference after discovery and before writing.
|
|
|
4
4
|
|
|
5
5
|
## Goal
|
|
6
6
|
|
|
7
|
-
Ask only enough to
|
|
7
|
+
Ask only enough to decide whether the spec is ready to draft or needs a bounded `$requirements-grill` phase. A vague spec creates false confidence.
|
|
8
|
+
|
|
9
|
+
Before writing `Open Questions`, route meaningful spec-affecting unknowns through `grill-phase.md`. Do not silently invent an open-question table as a substitute for requirements work.
|
|
8
10
|
|
|
9
11
|
## Priority topics
|
|
10
12
|
|
|
@@ -27,6 +29,8 @@ Surface these as needed:
|
|
|
27
29
|
- If a fact is ambiguous and matters to the spec, ask directly.
|
|
28
30
|
- Prefer concrete examples over abstract wording.
|
|
29
31
|
- When backlog context exists, ask about cross-story interactions only after reading all child stories first.
|
|
32
|
+
- If multiple or material unknowns remain, stop lightweight questioning and run the grill phase.
|
|
33
|
+
- If a tiny unknown would become an `Open Question`, ask whether the user can resolve it now or wants to defer it.
|
|
30
34
|
|
|
31
35
|
## Stop asking when
|
|
32
36
|
|
|
@@ -38,4 +42,10 @@ Surface these as needed:
|
|
|
38
42
|
- major functional requirements are identifiable
|
|
39
43
|
- key non-goals are explicit
|
|
40
44
|
|
|
41
|
-
|
|
45
|
+
Only leave `Open Questions` in the spec when one of these is true:
|
|
46
|
+
|
|
47
|
+
- the user explicitly chose to defer the question
|
|
48
|
+
- the answer requires external validation outside the current spec session
|
|
49
|
+
- the question is non-blocking and the spec can still be reviewed honestly
|
|
50
|
+
|
|
51
|
+
If a branch remains open after the grill phase or lightweight prompt, capture it as an open question or assumption inside the spec rather than pretending certainty. Include the prompt/grill outcome so reviewers know why it remains unresolved.
|
package/docs/README.md
CHANGED
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
- [Requirements](./reference/dp-requirements.md)
|
|
4
4
|
- [Scaffolding runbook](./runbooks/dp-cli-scaffolding.md)
|
|
5
5
|
|
|
6
|
+
Domain knowledge:
|
|
7
|
+
|
|
8
|
+
- [Wiki index](../wiki/index.md)
|
|
9
|
+
|
|
10
|
+
The wiki is the durable domain layer for specs, raw inputs, flows, and concepts. Keep operator runbooks and implementation reference in `docs/`; keep stable product/domain knowledge in `wiki/`.
|
|
11
|
+
|
|
6
12
|
Implementation notes:
|
|
7
13
|
|
|
8
14
|
- canonical bundled scaffold metadata and shared assets live in `src/data/`
|
|
@@ -11,12 +17,12 @@ Implementation notes:
|
|
|
11
17
|
- distributed skill assets live in `skills/`
|
|
12
18
|
- runtime projection/writing logic lives in `src/scaffold/`
|
|
13
19
|
- `punks update` refreshes scaffold-managed assets from `.devpunks/scaffold-manifest.json`
|
|
14
|
-
- CLI startup checks npm's `latest` dist-tag and
|
|
20
|
+
- CLI startup checks run in a detached advisory worker at most once per 12 hours by default. They check npm's `latest` dist-tag and whether the named `dp-cli` skill is present, but startup never installs or updates packages/skills while another CLI command is starting. Set `DP_NO_UPDATE_CHECK=1` or `DP_NO_SKILL_UPDATE_CHECK=1` to skip those checks, `DP_UPDATE_TAG=next` to compare against another dist-tag, and `DP_STARTUP_CHECK_INTERVAL_MS=0` to force the worker during local testing.
|
|
15
21
|
- baseline releases use `baseline/stable/*` GitHub release tags, separate from npm executable tags such as `v1.0.1`
|
|
16
|
-
- shared neutral hook and sync assets live in `src/data/hooks/` and `src/data/scripts
|
|
22
|
+
- shared neutral hook and sync assets live in `src/data/hooks/` and `src/data/scripts/`; hook commands infer the target repo package manager from `packageManager` and lockfiles
|
|
17
23
|
- scaffolded required tools always include `portless` and `skills` so generated guidance can standardize local dev origins and keep skill entrypoints up to date
|
|
18
24
|
- `punks scaffold setup` checks the base required tools (`portless`, `skills`) before repo detection and checks selected-pack tools after pack confirmation.
|
|
19
|
-
- Oxlint specs/starter config are scaffolded only when scanned manifests already declare `oxlint`; the auto format/lint hook is scaffolded only when manifests declare `oxfmt`. Other lint/format stacks are intentionally left untouched for now.
|
|
25
|
+
- Oxlint specs/starter config are scaffolded only when scanned manifests already declare `oxlint`; the auto format/lint hook is scaffolded only when manifests declare `oxfmt`. PR creation is not gated by a scaffolded test-suite hook. Other lint/format stacks are intentionally left untouched for now.
|
|
20
26
|
- the default debug pack scaffolds the local `debug-agent` skill and installs/verifies the `debug-agent` CLI without running its agent-install wizard
|
|
21
27
|
- scaffolded repos keep project-local skills in `.agents/skills/`; only `.claude/skills` is a compatibility symlink mirror
|
|
22
28
|
- React scaffold surfaces include `async-react-patterns` alongside the existing React composition, structure, and Vercel guidance so agents avoid outdated manual async state patterns.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Harness Intelligence Grill Log
|
|
2
|
+
|
|
3
|
+
This log records locked requirements decisions for rethinking this repo as the Harness Intelligence project.
|
|
4
|
+
|
|
5
|
+
## Initial Situation
|
|
6
|
+
|
|
7
|
+
The repo currently provides real value as the `punks`/`dp` scaffolding CLI plus canonical scaffold baseline data. It has battle-tested workflow assets, repo-aware setup behavior, baseline publishing, and a local wiki-shaped knowledge tree.
|
|
8
|
+
|
|
9
|
+
The repo does not yet provide the full product/distribution surface needed for wider colleague adoption or public company communication:
|
|
10
|
+
|
|
11
|
+
- no Turborepo application layout
|
|
12
|
+
- no public Fumadocs wiki/docs app
|
|
13
|
+
- no public landing page
|
|
14
|
+
- no Effect backend for scaffold distribution, validation, or future orchestration
|
|
15
|
+
- no settled CLI-vs-backend boundary
|
|
16
|
+
- no durable explanation of harness theory, criteria, trust model, and sales/marketing narrative
|
|
17
|
+
|
|
18
|
+
## Branches
|
|
19
|
+
|
|
20
|
+
### Branch A: Product Boundary
|
|
21
|
+
|
|
22
|
+
Purpose: define what "Harness Intelligence" is as a product/system, and what this repo must own.
|
|
23
|
+
|
|
24
|
+
### Branch B: Distribution Architecture
|
|
25
|
+
|
|
26
|
+
Purpose: define Turborepo shape, app/package boundaries, backend role, CLI role, baseline/data flow, and public surfaces.
|
|
27
|
+
|
|
28
|
+
### Branch C: Wiki and Theory
|
|
29
|
+
|
|
30
|
+
Purpose: define the docs knowledge model, Fumadocs information architecture, and the harness engineering criteria currently living in the user's head.
|
|
31
|
+
|
|
32
|
+
### Branch D: Trust and Adoption
|
|
33
|
+
|
|
34
|
+
Purpose: define what colleagues need to trust the AI harness setup, how validation works, and what proof material must exist.
|
|
35
|
+
|
|
36
|
+
### Branch E: Company Communication
|
|
37
|
+
|
|
38
|
+
Purpose: define landing-page and sales narrative for how Devpunks engineers are AI-superpowered without becoming vague marketing copy.
|
|
39
|
+
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Harness Intelligence Grill Status
|
|
2
|
+
|
|
3
|
+
Current grill artifacts:
|
|
4
|
+
|
|
5
|
+
- Log: `docs/harness-intelligence-grill-log.md`
|
|
6
|
+
- Status: `docs/harness-intelligence-grill-status.md`
|
|
7
|
+
|
|
8
|
+
## Branch Dashboard
|
|
9
|
+
|
|
10
|
+
| Branch | Completion | Locked direction | Still open |
|
|
11
|
+
| --- | ---: | --- | --- |
|
|
12
|
+
| Product Boundary | 10% | Repo is being reconsidered as the Harness Intelligence project, not only a CLI package. | Canonical product definition, audience split, repo ownership boundary. |
|
|
13
|
+
| Distribution Architecture | 10% | Target mentions Turborepo, public docs app, landing page, Effect backend, and CLI. | App/package boundaries, backend-vs-CLI responsibilities, data/source-of-truth flow. |
|
|
14
|
+
| Wiki and Theory | 10% | Existing wiki shape exists locally; missing public Fumadocs app and full theory/criteria content. | IA, criteria taxonomy, private vs public knowledge split, docs ownership. |
|
|
15
|
+
| Trust and Adoption | 10% | Colleague recognition/validation is a primary requirement. | Trust evidence, onboarding path, validation gates, adoption metrics. |
|
|
16
|
+
| Company Communication | 10% | Public explanation must support sales/marketing around AI-superpowered developers. | Landing page promise, proof points, tone, relationship to devpunks.com. |
|
|
17
|
+
|
|
18
|
+
## Parked Branches
|
|
19
|
+
|
|
20
|
+
- None yet.
|
|
21
|
+
|
|
22
|
+
## Current Question
|
|
23
|
+
|
|
24
|
+
Q1: Product Boundary.
|
|
25
|
+
|
|
@@ -95,6 +95,7 @@ Current scope:
|
|
|
95
95
|
- check the base required tools (`portless`, `skills`) at the start of setup before repo detection, then check selected-pack tools after pack confirmation
|
|
96
96
|
- include `debug-agent` through the default debug pack and install/verify the `debug-agent` CLI without running `debug-agent init`, because the CLI already scaffolds the project-local skill
|
|
97
97
|
- scaffold Oxlint specs/starter config only when scanned package manifests declare `oxlint`, and scaffold the `format-edited-file` Oxfmt/Oxlint hook only when manifests declare `oxfmt`; repos without those tools keep their existing lint/format setup untouched
|
|
98
|
+
- scaffolded hooks infer the target repo package manager from `packageManager` first, then lockfiles, so Oxfmt/Oxlint execution does not hardcode the CLI repo's package manager
|
|
98
99
|
- select language packs separately from framework packs; TypeScript is selected from a `typescript` package dependency or nested `.ts` / `.tsx` files, and Python is selected from nested `.py` files, while ignoring root config files plus vendor, virtualenv, scaffold, docs, examples, scripts, `opensrc`, cache, and build output
|
|
99
100
|
- seed Python subagent templates that combine the Python language skills into `python-app`, `python-async`, and `python-testing` specialists
|
|
100
101
|
- seed a read-only `code-review` subagent template that uses `simplify` for changed-code cleanup review and `improve-codebase-architecture` for grounded architecture-friction findings
|
|
@@ -217,14 +218,15 @@ The npm account must have publish access to `@punks/cli`; otherwise npm may repo
|
|
|
217
218
|
|
|
218
219
|
## CLI Self-Update Detection
|
|
219
220
|
|
|
220
|
-
The CLI
|
|
221
|
+
The CLI may start best-effort startup checks on normal command startup. The requested command must render immediately; checks run in a detached worker and are rate-limited by a local cache marker to at most once per 12 hours by default. The worker checks the npm package version for `@punks/cli`, and it checks the `dp-cli` skill through the `skills` CLI. It only prints advisory install/update commands. Startup must never install or update packages/skills while another CLI command is starting, and it must never run or suggest plain root `skills update`. These checks are separate from `punks update`, which updates scaffold-managed repo assets.
|
|
221
222
|
|
|
222
223
|
- checks `https://registry.npmjs.org/%40punks%2Fcli/latest`
|
|
223
224
|
- compares that dist-tag version with the bundled CLI version
|
|
224
|
-
-
|
|
225
|
+
- prints the inferred package-manager update command only when the registry version is newer
|
|
225
226
|
- silently skips the notice when npm is unreachable, the registry response is invalid, or the command is `--help` / `--version`
|
|
226
227
|
- skips in CI and when `DP_NO_UPDATE_CHECK=1`
|
|
227
228
|
- supports `DP_UPDATE_TAG=next` for canary/operator testing against another dist-tag
|
|
229
|
+
- supports `DP_STARTUP_CHECK_INTERVAL_MS=0` to force the detached worker during local testing
|
|
228
230
|
|
|
229
231
|
To test the built command locally without preparing another repo first, use the committed fixtures:
|
|
230
232
|
|
package/package.json
CHANGED
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import hashlib
|
|
6
|
-
import json
|
|
7
|
-
import os
|
|
8
|
-
import subprocess
|
|
9
|
-
import sys
|
|
10
|
-
from pathlib import Path
|
|
11
|
-
|
|
12
|
-
SUPPORTED_SUFFIXES = (".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".json")
|
|
13
|
-
IGNORED_PARTS = {".git", "node_modules", ".next", "dist"}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def read_input() -> dict[str, object]:
|
|
17
|
-
try:
|
|
18
|
-
return json.load(sys.stdin)
|
|
19
|
-
except json.JSONDecodeError:
|
|
20
|
-
return {}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def run_git(args: list[str], cwd: str) -> str:
|
|
24
|
-
result = subprocess.run(
|
|
25
|
-
["git", "-C", cwd, *args],
|
|
26
|
-
check=True,
|
|
27
|
-
capture_output=True,
|
|
28
|
-
text=True,
|
|
29
|
-
)
|
|
30
|
-
return result.stdout
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def repo_root(cwd: str) -> str:
|
|
34
|
-
try:
|
|
35
|
-
return run_git(["rev-parse", "--show-toplevel"], cwd).strip()
|
|
36
|
-
except subprocess.CalledProcessError:
|
|
37
|
-
return ""
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def dirty_files(root: str) -> list[str]:
|
|
41
|
-
try:
|
|
42
|
-
output = run_git(["ls-files", "-m", "-o", "--exclude-standard"], root)
|
|
43
|
-
except subprocess.CalledProcessError:
|
|
44
|
-
return []
|
|
45
|
-
|
|
46
|
-
files: list[str] = []
|
|
47
|
-
for raw_path in output.splitlines():
|
|
48
|
-
path = raw_path.strip()
|
|
49
|
-
if not path:
|
|
50
|
-
continue
|
|
51
|
-
if not path.endswith(SUPPORTED_SUFFIXES):
|
|
52
|
-
continue
|
|
53
|
-
if any(part in IGNORED_PARTS for part in Path(path).parts):
|
|
54
|
-
continue
|
|
55
|
-
files.append(path)
|
|
56
|
-
|
|
57
|
-
return sorted(set(files))
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
def file_hash(path: Path) -> str:
|
|
61
|
-
digest = hashlib.sha256()
|
|
62
|
-
with path.open("rb") as handle:
|
|
63
|
-
for chunk in iter(lambda: handle.read(65536), b""):
|
|
64
|
-
digest.update(chunk)
|
|
65
|
-
return digest.hexdigest()
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
def snapshot_path(root: str, session_id: str, tool_use_id: str) -> Path:
|
|
69
|
-
repo_name = Path(root).name or "repo"
|
|
70
|
-
safe_session_id = session_id.replace("/", "_")
|
|
71
|
-
safe_tool_use_id = tool_use_id.replace("/", "_")
|
|
72
|
-
state_dir = Path(os.environ.get("TMPDIR", "/tmp")) / "codex-hooks" / repo_name / safe_session_id
|
|
73
|
-
state_dir.mkdir(parents=True, exist_ok=True)
|
|
74
|
-
return state_dir / f"{safe_tool_use_id}.json"
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
def load_snapshot(path: Path) -> dict[str, str]:
|
|
78
|
-
if not path.exists():
|
|
79
|
-
return {}
|
|
80
|
-
try:
|
|
81
|
-
return json.loads(path.read_text(encoding="utf-8"))
|
|
82
|
-
except (OSError, json.JSONDecodeError):
|
|
83
|
-
return {}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
def fingerprints(root: str) -> dict[str, str]:
|
|
87
|
-
snapshot: dict[str, str] = {}
|
|
88
|
-
for relative_path in dirty_files(root):
|
|
89
|
-
absolute_path = Path(root) / relative_path
|
|
90
|
-
if absolute_path.exists():
|
|
91
|
-
snapshot[relative_path] = file_hash(absolute_path)
|
|
92
|
-
return snapshot
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def format_files(root: str, files: list[str]) -> list[str]:
|
|
96
|
-
failed: list[str] = []
|
|
97
|
-
for relative_path in files:
|
|
98
|
-
absolute_path = Path(root) / relative_path
|
|
99
|
-
result = subprocess.run(
|
|
100
|
-
["npx", "oxfmt", "--write", str(absolute_path)],
|
|
101
|
-
cwd=root,
|
|
102
|
-
stdout=subprocess.DEVNULL,
|
|
103
|
-
stderr=subprocess.DEVNULL,
|
|
104
|
-
check=False,
|
|
105
|
-
)
|
|
106
|
-
if result.returncode != 0:
|
|
107
|
-
failed.append(relative_path)
|
|
108
|
-
return failed
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
def emit_failure(files: list[str]) -> None:
|
|
112
|
-
message = "Auto-format failed for " + ", ".join(files) + ". Review the file and format it manually if needed."
|
|
113
|
-
print(json.dumps({"systemMessage": message}))
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
def main() -> int:
|
|
117
|
-
mode = sys.argv[1] if len(sys.argv) > 1 else ""
|
|
118
|
-
payload = read_input()
|
|
119
|
-
|
|
120
|
-
cwd = str(payload.get("cwd") or os.getcwd())
|
|
121
|
-
session_id = str(payload.get("session_id") or "")
|
|
122
|
-
tool_use_id = str(payload.get("tool_use_id") or "")
|
|
123
|
-
root = repo_root(cwd)
|
|
124
|
-
|
|
125
|
-
if not root or not session_id or not tool_use_id:
|
|
126
|
-
return 0
|
|
127
|
-
|
|
128
|
-
state_file = snapshot_path(root, session_id, tool_use_id)
|
|
129
|
-
|
|
130
|
-
if mode == "pre":
|
|
131
|
-
state_file.write_text(json.dumps(fingerprints(root), sort_keys=True), encoding="utf-8")
|
|
132
|
-
return 0
|
|
133
|
-
|
|
134
|
-
if mode != "post":
|
|
135
|
-
return 0
|
|
136
|
-
|
|
137
|
-
previous = load_snapshot(state_file)
|
|
138
|
-
current = fingerprints(root)
|
|
139
|
-
|
|
140
|
-
try:
|
|
141
|
-
state_file.unlink()
|
|
142
|
-
except OSError:
|
|
143
|
-
pass
|
|
144
|
-
|
|
145
|
-
touched = [path for path, digest in current.items() if previous.get(path) != digest]
|
|
146
|
-
if not touched:
|
|
147
|
-
return 0
|
|
148
|
-
|
|
149
|
-
failed = format_files(root, touched)
|
|
150
|
-
if failed:
|
|
151
|
-
emit_failure(failed)
|
|
152
|
-
|
|
153
|
-
return 0
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
if __name__ == "__main__":
|
|
157
|
-
raise SystemExit(main())
|
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { spawnSync } from "node:child_process";
|
|
4
|
-
import { 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
|
-
|
|
12
|
-
function readStdinJson() {
|
|
13
|
-
try {
|
|
14
|
-
return JSON.parse(readFileSync(0, "utf8"));
|
|
15
|
-
} catch {
|
|
16
|
-
return {};
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function runGit(args, cwd) {
|
|
21
|
-
const result = spawnSync("git", ["-C", cwd, ...args], {
|
|
22
|
-
encoding: "utf8",
|
|
23
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
return result.status === 0 ? result.stdout : null;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function repoRoot(cwd) {
|
|
30
|
-
return runGit(["rev-parse", "--show-toplevel"], cwd)?.trim() ?? "";
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function collectStrings(value, values = []) {
|
|
34
|
-
if (typeof value === "string") {
|
|
35
|
-
const trimmed = value.trim();
|
|
36
|
-
if (trimmed) {
|
|
37
|
-
values.push(trimmed);
|
|
38
|
-
}
|
|
39
|
-
return values;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
if (Array.isArray(value)) {
|
|
43
|
-
for (const entry of value) {
|
|
44
|
-
collectStrings(entry, values);
|
|
45
|
-
}
|
|
46
|
-
return values;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (!value || typeof value !== "object") {
|
|
50
|
-
return values;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
for (const entry of Object.values(value)) {
|
|
54
|
-
collectStrings(entry, values);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return values;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function matchesPrAction(...values) {
|
|
61
|
-
const strings = [...new Set(values.flatMap((value) => collectStrings(value)))];
|
|
62
|
-
|
|
63
|
-
return strings.some((value) => prCommandPattern.test(value) || prToolPattern.test(value));
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function runTests(root) {
|
|
67
|
-
const result = spawnSync("pnpm", ["--silent", "test"], {
|
|
68
|
-
cwd: root,
|
|
69
|
-
stdio: "ignore",
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
return result.status === 0;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function emitCodexOrClaudeBlock(message) {
|
|
76
|
-
process.stdout.write(
|
|
77
|
-
JSON.stringify({
|
|
78
|
-
hookSpecificOutput: {
|
|
79
|
-
hookEventName: "PreToolUse",
|
|
80
|
-
permissionDecision: "deny",
|
|
81
|
-
permissionDecisionReason: message,
|
|
82
|
-
},
|
|
83
|
-
systemMessage: message,
|
|
84
|
-
}),
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function emitCursorBlock(message) {
|
|
89
|
-
process.stdout.write(
|
|
90
|
-
JSON.stringify({
|
|
91
|
-
continue: false,
|
|
92
|
-
permission: "deny",
|
|
93
|
-
agent_message: message,
|
|
94
|
-
user_message: message,
|
|
95
|
-
}),
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function runCommandHook(style) {
|
|
100
|
-
const payload = readStdinJson();
|
|
101
|
-
const root = repoRoot(String(payload.cwd ?? process.cwd()));
|
|
102
|
-
|
|
103
|
-
if (!root || !matchesPrAction(payload) || runTests(root)) {
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
if (style === "cursor") {
|
|
108
|
-
emitCursorBlock(denyMessage);
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
emitCodexOrClaudeBlock(denyMessage);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export const RequireTestsForPrPlugin = async ({ worktree }) => {
|
|
116
|
-
return {
|
|
117
|
-
"tool.execute.before": async (input, output) => {
|
|
118
|
-
if (!matchesPrAction(input?.tool, output?.args?.command)) {
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
if (runTests(worktree)) {
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
throw new Error(denyMessage);
|
|
127
|
-
},
|
|
128
|
-
};
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
function main() {
|
|
132
|
-
const mode = process.argv[2] ?? "";
|
|
133
|
-
|
|
134
|
-
if (mode === "claude" || mode === "codex" || mode === "cursor") {
|
|
135
|
-
runCommandHook(mode);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const invokedPath = process.argv[1] ? path.resolve(process.argv[1]) : "";
|
|
140
|
-
const modulePath = fileURLToPath(import.meta.url);
|
|
141
|
-
|
|
142
|
-
if (invokedPath && path.basename(invokedPath) === path.basename(modulePath)) {
|
|
143
|
-
main();
|
|
144
|
-
}
|