@monte3l/groundwork 0.0.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/README.md +23 -0
- package/bin/m3l-groundwork.mjs +10 -0
- package/dist/assets.d.ts +20 -0
- package/dist/assets.js +79 -0
- package/dist/caps.d.ts +25 -0
- package/dist/caps.js +69 -0
- package/dist/conflicts.d.ts +12 -0
- package/dist/conflicts.js +77 -0
- package/dist/emit.d.ts +7 -0
- package/dist/emit.js +42 -0
- package/dist/git.d.ts +3 -0
- package/dist/git.js +9 -0
- package/dist/harness/conformance.d.ts +20 -0
- package/dist/harness/conformance.js +18 -0
- package/dist/harness/frontmatter.d.ts +38 -0
- package/dist/harness/frontmatter.js +204 -0
- package/dist/harness/grade.d.ts +4 -0
- package/dist/harness/grade.js +105 -0
- package/dist/harness/rules.d.ts +55 -0
- package/dist/harness/rules.js +580 -0
- package/dist/harness/types.d.ts +32 -0
- package/dist/harness/types.js +9 -0
- package/dist/inventory.d.ts +63 -0
- package/dist/inventory.js +66 -0
- package/dist/jsonc.d.ts +14 -0
- package/dist/jsonc.js +83 -0
- package/dist/main.d.ts +24 -0
- package/dist/main.js +297 -0
- package/dist/merge-json.d.ts +74 -0
- package/dist/merge-json.js +135 -0
- package/dist/mode.d.ts +19 -0
- package/dist/mode.js +53 -0
- package/dist/packs.d.ts +61 -0
- package/dist/packs.js +186 -0
- package/dist/plugin.d.ts +23 -0
- package/dist/plugin.js +79 -0
- package/dist/report.d.ts +4 -0
- package/dist/report.js +323 -0
- package/dist/survey/fs-walk.d.ts +14 -0
- package/dist/survey/fs-walk.js +60 -0
- package/dist/survey/survey-docs.d.ts +4 -0
- package/dist/survey/survey-docs.js +69 -0
- package/dist/survey/survey-harness.d.ts +4 -0
- package/dist/survey/survey-harness.js +121 -0
- package/dist/survey/survey-shape.d.ts +4 -0
- package/dist/survey/survey-shape.js +182 -0
- package/dist/survey/survey-toolchain.d.ts +4 -0
- package/dist/survey/survey-toolchain.js +217 -0
- package/dist/survey/survey.d.ts +5 -0
- package/dist/survey/survey.js +21 -0
- package/dist/survey/types.d.ts +117 -0
- package/dist/survey/types.js +8 -0
- package/dist/tokens.d.ts +13 -0
- package/dist/tokens.js +13 -0
- package/dist/toolchain/conformance.d.ts +20 -0
- package/dist/toolchain/conformance.js +30 -0
- package/dist/toolchain/grade.d.ts +4 -0
- package/dist/toolchain/grade.js +244 -0
- package/dist/toolchain/rules.d.ts +118 -0
- package/dist/toolchain/rules.js +706 -0
- package/dist/toolchain/tsconfig-chain.d.ts +36 -0
- package/dist/toolchain/tsconfig-chain.js +116 -0
- package/dist/toolchain/types.d.ts +27 -0
- package/dist/toolchain/types.js +9 -0
- package/package.json +59 -0
- package/plugin/skills/customize/SKILL.md +305 -0
- package/plugin/src/domain-map.ts +134 -0
- package/plugin/src/index.ts +4 -0
- package/plugin/src/kind-facet-map.ts +174 -0
- package/plugin/src/pack-map.ts +65 -0
- package/templates/core/.claude/agents/Explore.md +43 -0
- package/templates/core/.claude/agents/code-implementer.md +258 -0
- package/templates/core/.claude/agents/code-reviewer.md +163 -0
- package/templates/core/.claude/agents/silent-failure-hunter.md +191 -0
- package/templates/core/.claude/agents/test-author.md +211 -0
- package/templates/core/.claude/hooks/guard-branch-isolation.mjs +123 -0
- package/templates/core/.claude/hooks/guard-double-background.mjs +113 -0
- package/templates/core/.claude/hooks/guard-git-push-signed.mjs +90 -0
- package/templates/core/.claude/hooks/guard-hub-src-writes.mjs +88 -0
- package/templates/core/.claude/hooks/guard-js-extension.mjs +66 -0
- package/templates/core/.claude/hooks/guard-no-commonjs.mjs +105 -0
- package/templates/core/.claude/hooks/guard-protected-paths.mjs +45 -0
- package/templates/core/.claude/hooks/guard-secret-writes.mjs +183 -0
- package/templates/core/.claude/hooks/inject-decision-gate.mjs +119 -0
- package/templates/core/.claude/hooks/post-edit-verify.mjs +150 -0
- package/templates/core/.claude/rules/agent-dispatch.md +121 -0
- package/templates/core/.claude/rules/refactoring.md +52 -0
- package/templates/core/.claude/rules/src.md +114 -0
- package/templates/core/.claude/rules/tests.md +129 -0
- package/templates/core/.claude/settings.json +111 -0
- package/templates/core/.claude/skills/creating-prs/SKILL.md +132 -0
- package/templates/core/.claude/skills/finishing-work/SKILL.md +117 -0
- package/templates/core/.claude/skills/harness-guidance/SKILL.md +140 -0
- package/templates/core/.claude/skills/harness-guidance/references/official-sources.md +58 -0
- package/templates/core/.claude/skills/starting-work/SKILL.md +94 -0
- package/templates/core/.claude/skills/triaging-ci/SKILL.md +111 -0
- package/templates/core/.claude/skills/typescript-guidance/SKILL.md +143 -0
- package/templates/core/.claude/skills/typescript-guidance/references/typescript-sources.md +102 -0
- package/templates/core/.claude/skills/writing-commits/SKILL.md +248 -0
- package/templates/core/.github/workflows/ci.yml +123 -0
- package/templates/core/.github/workflows/dependency-review.yml +26 -0
- package/templates/core/.github/workflows/security-audit.yml +54 -0
- package/templates/core/.node-version +1 -0
- package/templates/core/.prettierignore +5 -0
- package/templates/core/.prettierrc.json +4 -0
- package/templates/core/CLAUDE.md +127 -0
- package/templates/core/README.md +24 -0
- package/templates/core/_gitignore +19 -0
- package/templates/core/_npmrc +1 -0
- package/templates/core/bin/check-exports.mjs +92 -0
- package/templates/core/bin/check-harness.mjs +27 -0
- package/templates/core/bin/check-node-version.mjs +51 -0
- package/templates/core/bin/check-toolchain.mjs +20 -0
- package/templates/core/bin/lib/agent-roster.mjs +8 -0
- package/templates/core/bin/lib/frontmatter.mjs +210 -0
- package/templates/core/bin/lib/harness-rules.mjs +916 -0
- package/templates/core/bin/lib/protected-paths.mjs +23 -0
- package/templates/core/bin/lib/report.mjs +56 -0
- package/templates/core/bin/lib/signed-range.mjs +178 -0
- package/templates/core/bin/lib/toolchain-rules.mjs +1264 -0
- package/templates/core/bin/lib/verify-steps.mjs +131 -0
- package/templates/core/bin/lib/verify-steps.packs.json +1 -0
- package/templates/core/bin/lint-commit.mjs +50 -0
- package/templates/core/bin/strip-claude-trailers.mjs +25 -0
- package/templates/core/bin/verify.mjs +64 -0
- package/templates/core/commitlint.config.js +11 -0
- package/templates/core/docs/research/harness-refresh.md +27 -0
- package/templates/core/docs/research/typescript-refresh.md +32 -0
- package/templates/core/eslint.config.js +105 -0
- package/templates/core/knip.json +6 -0
- package/templates/core/lefthook.yml +39 -0
- package/templates/core/package.json +58 -0
- package/templates/core/pnpm-workspace.yaml +13 -0
- package/templates/core/src/index.ts +12 -0
- package/templates/core/tests/index.test.ts +8 -0
- package/templates/core/tsconfig.base.json +36 -0
- package/templates/core/tsconfig.build.json +10 -0
- package/templates/core/tsconfig.json +11 -0
- package/templates/core/vitest.config.ts +32 -0
- package/templates/packs/README.md +81 -0
- package/templates/packs/harness-extras/files/.claude/agents/type-design-analyzer.md +188 -0
- package/templates/packs/harness-extras/files/.claude/hooks/guard-readonly-bash.mjs +324 -0
- package/templates/packs/harness-extras/files/.claude/hooks/reinject-compact-handoff.mjs +197 -0
- package/templates/packs/harness-extras/files/.claude/hooks/write-compact-handoff.mjs +180 -0
- package/templates/packs/harness-extras/files/bin/check-file-budget.mjs +407 -0
- package/templates/packs/harness-extras/files/bin/file-budget-baseline.json +1 -0
- package/templates/packs/harness-extras/pack.json +65 -0
- package/templates/packs/statusline/files/.claude/hooks/statusline-layout.mjs +365 -0
- package/templates/packs/statusline/files/.claude/hooks/statusline.mjs +996 -0
- package/templates/packs/statusline/files/.claude/hooks/subagent-statusline.mjs +203 -0
- package/templates/packs/statusline/pack.json +31 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* UserPromptSubmit: when a prompt looks like change-work, inject a short
|
|
4
|
+
* decision-gate reminder as additional context.
|
|
5
|
+
*
|
|
6
|
+
* This is the only UserPromptSubmit hook in the baseline and the only one
|
|
7
|
+
* that *injects* context (every other hook communicates via stderr + exit
|
|
8
|
+
* code). It surfaces the decisions the `starting-work` skill formalizes --
|
|
9
|
+
* branch and PR target -- up front, so isolation is chosen deliberately
|
|
10
|
+
* instead of being discovered when `guard-branch-isolation.mjs` blocks a
|
|
11
|
+
* src/test write on `main`.
|
|
12
|
+
*
|
|
13
|
+
* It is advisory: it emits `additionalContext` and exits 0. It never blocks.
|
|
14
|
+
* The heuristic is deliberately loose (inject on any change-intent verb,
|
|
15
|
+
* stay quiet on obvious reads) -- a spurious reminder is cheap; a missed one
|
|
16
|
+
* costs a mid-task branch scramble.
|
|
17
|
+
*/
|
|
18
|
+
import process from "node:process";
|
|
19
|
+
import { realpathSync } from "node:fs";
|
|
20
|
+
import { fileURLToPath } from "node:url";
|
|
21
|
+
import { execFileSync } from "node:child_process";
|
|
22
|
+
|
|
23
|
+
// Verbs that signal the user is about to change the tree, not just ask about it.
|
|
24
|
+
const CHANGE_INTENT =
|
|
25
|
+
/\b(implement|build|add|create|write|fix|refactor|scaffold|rename|migrate|wire up|delete|remove|update|change|edit|generate)\b/i;
|
|
26
|
+
// Strong read-only openers; if the prompt is one of these and carries no change
|
|
27
|
+
// verb, stay quiet.
|
|
28
|
+
const READ_ONLY_OPENER =
|
|
29
|
+
/^\s*(what|why|how|when|where|which|who|explain|describe|show|list|read|find|search|look|review|audit|is|are|does|can|should|could)\b/i;
|
|
30
|
+
|
|
31
|
+
/** Current branch, or "" if git isn't available. "HEAD" means detached. */
|
|
32
|
+
function currentBranch() {
|
|
33
|
+
try {
|
|
34
|
+
return execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
35
|
+
encoding: "utf8",
|
|
36
|
+
}).trim();
|
|
37
|
+
} catch {
|
|
38
|
+
return "";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Heuristic: does this prompt look like it will change the tree (vs. a read)?
|
|
44
|
+
*
|
|
45
|
+
* @param {string} prompt
|
|
46
|
+
* @returns {boolean}
|
|
47
|
+
*/
|
|
48
|
+
export function looksLikeChangeWork(prompt) {
|
|
49
|
+
if (typeof prompt !== "string" || prompt.trim().length === 0) return false;
|
|
50
|
+
if (!CHANGE_INTENT.test(prompt)) return false;
|
|
51
|
+
if (
|
|
52
|
+
READ_ONLY_OPENER.test(prompt) &&
|
|
53
|
+
!/^\s*\S+\s+(the|this|a|an)\b/i.test(prompt)
|
|
54
|
+
)
|
|
55
|
+
return false;
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Build the decision-gate reminder text for the current branch.
|
|
61
|
+
*
|
|
62
|
+
* @param {string} branch current branch ("main", "HEAD" for detached, "" for no repo)
|
|
63
|
+
* @returns {string}
|
|
64
|
+
*/
|
|
65
|
+
export function buildContext(branch) {
|
|
66
|
+
const onMain = branch === "main" || branch === "HEAD" || branch === "";
|
|
67
|
+
const branchLine =
|
|
68
|
+
branch === ""
|
|
69
|
+
? "not a git repo"
|
|
70
|
+
: branch === "HEAD"
|
|
71
|
+
? "detached HEAD"
|
|
72
|
+
: `on \`${branch}\``;
|
|
73
|
+
return [
|
|
74
|
+
"Decision gate (before editing code/tests) -- currently " +
|
|
75
|
+
`${branchLine}. Settle these first, ideally via the \`starting-work\` skill:`,
|
|
76
|
+
" • Branch -- `feat/<slug>` or `fix/<slug>` off `main` (never `main`" +
|
|
77
|
+
(onMain ? ", and you appear to be on/at `main` now" : "") +
|
|
78
|
+
").",
|
|
79
|
+
" • PR -- any `src/`/`tests/` change lands via PR, never a direct commit to `main`.",
|
|
80
|
+
" • Push -- `origin <branch>`, not `origin main`.",
|
|
81
|
+
"guard-branch-isolation.mjs will block src/test writes on `main`, so branch first.",
|
|
82
|
+
].join("\n");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Deliberately inlined in every hook rather than shared: caps.ts counts
|
|
86
|
+
// .claude/hooks/*.mjs files against a hard limit, so a helper module would
|
|
87
|
+
// cost a hook slot. `import.meta.url` is symlink-resolved but `process.argv[1]`
|
|
88
|
+
// is not, so comparing them directly is false under any symlinked path and the
|
|
89
|
+
// guard body would never run -- exit 0, i.e. fail open.
|
|
90
|
+
function isEntryPoint() {
|
|
91
|
+
try {
|
|
92
|
+
return realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
93
|
+
} catch {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Only run when invoked directly, not when imported for testing.
|
|
99
|
+
if (isEntryPoint()) {
|
|
100
|
+
const chunks = [];
|
|
101
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
102
|
+
let input;
|
|
103
|
+
try {
|
|
104
|
+
input = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
105
|
+
} catch {
|
|
106
|
+
process.exit(0);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!looksLikeChangeWork(input.prompt ?? "")) process.exit(0);
|
|
110
|
+
|
|
111
|
+
const output = {
|
|
112
|
+
hookSpecificOutput: {
|
|
113
|
+
hookEventName: "UserPromptSubmit",
|
|
114
|
+
additionalContext: buildContext(currentBranch()),
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
process.stdout.write(JSON.stringify(output));
|
|
118
|
+
process.exit(0);
|
|
119
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* PostToolUse verify (Write|Edit): fast in-loop feedback on TypeScript edits.
|
|
4
|
+
*
|
|
5
|
+
* Lefthook + CI already gate at commit/push time, but that feedback arrives
|
|
6
|
+
* late. After an edit to a `.ts`/`.mts`/`.cts` file under `src/` or `tests/`
|
|
7
|
+
* (at the project root, or nested under any `packages/<pkg>/`), this runs
|
|
8
|
+
* checks scoped to the owning package so the signal is immediate without
|
|
9
|
+
* paying the whole-project cost:
|
|
10
|
+
*
|
|
11
|
+
* 1. prettier --write (auto-format the edited file)
|
|
12
|
+
* 2. eslint (lint the edited file; flat config from repo root)
|
|
13
|
+
* 3. package typecheck (`pnpm -C <pkg> typecheck`)
|
|
14
|
+
* 4. vitest related (only the tests that import the edited file)
|
|
15
|
+
* 5. eslint tests/ (only when a src/ file is edited -- catches stale
|
|
16
|
+
* eslint-disable directives that went unused after
|
|
17
|
+
* the implementation landed; skipped silently if
|
|
18
|
+
* tests/ doesn't exist)
|
|
19
|
+
*
|
|
20
|
+
* eslint runs in-loop (not just at the hub's `pnpm lint` gate) so eslint-only
|
|
21
|
+
* failures surface here, not a round later.
|
|
22
|
+
*
|
|
23
|
+
* On any failure it exits 2 with a concise stderr summary, which Claude Code
|
|
24
|
+
* surfaces back to the model as advisory feedback. The edit has already been
|
|
25
|
+
* applied -- this is a nudge, not a hard gate.
|
|
26
|
+
*/
|
|
27
|
+
import process from "node:process";
|
|
28
|
+
import path from "node:path";
|
|
29
|
+
import fs from "node:fs";
|
|
30
|
+
import { spawnSync } from "node:child_process";
|
|
31
|
+
import { isProtectedPath } from "../../bin/lib/protected-paths.mjs";
|
|
32
|
+
|
|
33
|
+
async function readStdin() {
|
|
34
|
+
const chunks = [];
|
|
35
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
36
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const projectDir = process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
|
|
40
|
+
|
|
41
|
+
const raw = await readStdin();
|
|
42
|
+
let input;
|
|
43
|
+
try {
|
|
44
|
+
input = JSON.parse(raw);
|
|
45
|
+
} catch {
|
|
46
|
+
process.exit(0);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const filePath = input.tool_input?.file_path;
|
|
50
|
+
if (typeof filePath !== "string" || filePath.length === 0) process.exit(0);
|
|
51
|
+
|
|
52
|
+
const abs = path.isAbsolute(filePath)
|
|
53
|
+
? filePath
|
|
54
|
+
: path.resolve(projectDir, filePath);
|
|
55
|
+
const rel = path.relative(projectDir, abs).split(path.sep).join("/");
|
|
56
|
+
|
|
57
|
+
// Only TypeScript sources; never generated declarations or the hooks themselves.
|
|
58
|
+
if (!/\.(ts|mts|cts)$/.test(rel) || /\.d\.ts$/.test(rel)) process.exit(0);
|
|
59
|
+
if (
|
|
60
|
+
rel.startsWith("..") ||
|
|
61
|
+
rel.includes("node_modules/") ||
|
|
62
|
+
/(^|\/)dist\//.test(rel) ||
|
|
63
|
+
rel.startsWith(".claude/")
|
|
64
|
+
) {
|
|
65
|
+
process.exit(0);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!isProtectedPath(rel)) process.exit(0);
|
|
69
|
+
|
|
70
|
+
// Walk up to the nearest package.json = the owning package root (the
|
|
71
|
+
// project root itself in a flat single-package layout).
|
|
72
|
+
function findPackageDir(startDir) {
|
|
73
|
+
let dir = startDir;
|
|
74
|
+
while (dir.startsWith(projectDir)) {
|
|
75
|
+
if (fs.existsSync(path.join(dir, "package.json"))) return dir;
|
|
76
|
+
const parent = path.dirname(dir);
|
|
77
|
+
if (parent === dir) break;
|
|
78
|
+
dir = parent;
|
|
79
|
+
}
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const pkgDir = findPackageDir(path.dirname(abs));
|
|
84
|
+
if (pkgDir === undefined) process.exit(0);
|
|
85
|
+
|
|
86
|
+
function run(cmd, args) {
|
|
87
|
+
const res = spawnSync(cmd, args, {
|
|
88
|
+
cwd: projectDir,
|
|
89
|
+
encoding: "utf8",
|
|
90
|
+
env: process.env,
|
|
91
|
+
});
|
|
92
|
+
// If the tool can't even be spawned (e.g. pnpm missing), skip silently
|
|
93
|
+
// rather than emit a misleading failure.
|
|
94
|
+
if (res.error) return undefined;
|
|
95
|
+
return res;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const failures = [];
|
|
99
|
+
|
|
100
|
+
// 1. Format the edited file (best-effort; a parse error is itself signal).
|
|
101
|
+
const fmt = run("pnpm", ["exec", "prettier", "--write", abs]);
|
|
102
|
+
if (fmt && fmt.status !== 0) {
|
|
103
|
+
failures.push(`prettier:\n${(fmt.stderr || fmt.stdout || "").trim()}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 2. Lint the edited file (single file; flat config resolves from repo root
|
|
107
|
+
// and honours its own `ignores`, so no per-package wrapper is needed).
|
|
108
|
+
// Report-only (no --fix) so the root cause is addressed, not masked.
|
|
109
|
+
const lint = run("pnpm", ["exec", "eslint", abs]);
|
|
110
|
+
if (lint && lint.status !== 0) {
|
|
111
|
+
failures.push(`eslint:\n${(lint.stdout || lint.stderr || "").trim()}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// 3. Type-check the owning package.
|
|
115
|
+
const tc = run("pnpm", ["-C", pkgDir, "typecheck"]);
|
|
116
|
+
if (tc && tc.status !== 0) {
|
|
117
|
+
failures.push(`typecheck:\n${(tc.stdout || tc.stderr || "").trim()}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 4. Run only the tests related to the edited file.
|
|
121
|
+
const vt = run("pnpm", ["exec", "vitest", "related", abs, "--run"]);
|
|
122
|
+
if (vt && vt.status !== 0) {
|
|
123
|
+
failures.push(`vitest related:\n${(vt.stdout || vt.stderr || "").trim()}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 5. When a src/ file is implemented/updated, also lint the package's tests/
|
|
127
|
+
// directory to surface stale eslint-disable directives that became unused
|
|
128
|
+
// once the implementation landed.
|
|
129
|
+
if (/(^|\/)src\//.test(rel)) {
|
|
130
|
+
const testsDir = path.join(pkgDir, "tests");
|
|
131
|
+
if (fs.existsSync(testsDir)) {
|
|
132
|
+
const testLint = run("pnpm", ["exec", "eslint", testsDir]);
|
|
133
|
+
if (testLint && testLint.status !== 0) {
|
|
134
|
+
failures.push(
|
|
135
|
+
`eslint (tests/ -- scanned because a src/ file was edited; fix the file(s) listed below):\n${(testLint.stdout || testLint.stderr || "").trim()}`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (failures.length > 0) {
|
|
142
|
+
process.stderr.write(
|
|
143
|
+
`post-edit-verify found issues in \`${rel}\` (package: ` +
|
|
144
|
+
`${path.relative(projectDir, pkgDir) || "."}). Address these before ` +
|
|
145
|
+
`moving on:\n\n${failures.join("\n\n")}\n`,
|
|
146
|
+
);
|
|
147
|
+
process.exit(2);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
process.exit(0);
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- ".claude/skills/**"
|
|
4
|
+
- ".claude/agents/**"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Subagent dispatch rules (hub-and-spoke TDD loop, truncation prevention & recovery)
|
|
8
|
+
|
|
9
|
+
> The terse checklist consulted when dispatching or resuming a spoke. No
|
|
10
|
+
> natural path-glob covers "dispatching a subagent," so this rule is also
|
|
11
|
+
> referenced from CLAUDE.md's Agent Operating Model section.
|
|
12
|
+
|
|
13
|
+
## The loop
|
|
14
|
+
|
|
15
|
+
Hub-and-spoke: the hub plans and dispatches, and never writes `src/`/test
|
|
16
|
+
code itself — enforced by `guard-hub-src-writes.mjs` and
|
|
17
|
+
`disallowedTools: Agent` on every spoke. For a piece of work with a clear
|
|
18
|
+
contract (a documented behavior, an interface to satisfy):
|
|
19
|
+
|
|
20
|
+
1. `test-author` writes failing tests from the contract (RED) and confirms
|
|
21
|
+
they fail for the right reason.
|
|
22
|
+
2. `code-implementer` makes them pass with the minimal correct implementation,
|
|
23
|
+
then refactors while green (GREEN).
|
|
24
|
+
3. Read-only reviewers (`code-reviewer` always; `silent-failure-hunter` when
|
|
25
|
+
the change has try/catch, async/await, or retry/poll logic) run in
|
|
26
|
+
parallel over the diff. Must-fix findings route back to
|
|
27
|
+
`code-implementer` and the loop repeats until clean.
|
|
28
|
+
|
|
29
|
+
## Dispatch sizing
|
|
30
|
+
|
|
31
|
+
- **Decompose before you dispatch.** Scale the dispatch to task complexity —
|
|
32
|
+
a module spanning many files gets split into bounded sub-dispatches up
|
|
33
|
+
front, not handed to one spoke as an indivisible turn. A single-file test
|
|
34
|
+
suite over ~40 tests, or a fix round over ~5 findings, splits into
|
|
35
|
+
checkpointed batches the same way. Hand a spoke an explicit file list
|
|
36
|
+
**and** a byte/file-count budget, measured before dispatch; a spoke told
|
|
37
|
+
the ceiling shrinks the file instead of ratcheting the baseline.
|
|
38
|
+
- **Size a FIX round by file, not by finding count.** Regroup findings by
|
|
39
|
+
file — one spoke per file (or tight file group), every finding for that
|
|
40
|
+
file in one prompt — so each spoke loads one file's context. This also
|
|
41
|
+
removes write conflicts, so spokes can run concurrently.
|
|
42
|
+
- **Bound review-spoke INPUT scope too, not just output.** Give each review
|
|
43
|
+
spoke a tight per-spoke file list (2–5 files) and split a review dispatch
|
|
44
|
+
by concern once the diff exceeds ~3–4 files or a few hundred lines. Every
|
|
45
|
+
review-spoke prompt also carries a **converge and report** instruction —
|
|
46
|
+
stop once its checklist is answered rather than re-verifying indefinitely.
|
|
47
|
+
- **Pre-resolve the facts a writer would otherwise discover, not just its
|
|
48
|
+
output scope.** Discovery, not writing, is what exhausts a turn budget.
|
|
49
|
+
Resolve the exact fixture contents, a collaborator's return shape, and the
|
|
50
|
+
precise `file:line` anchors yourself before dispatch, so the spoke's first
|
|
51
|
+
tool call is a write, not a search.
|
|
52
|
+
- **Two independent review lenses landing on the same line is signal, not
|
|
53
|
+
redundancy.** Treat a convergent finding as confirmed and fix it — never
|
|
54
|
+
discount the second report as a duplicate of the first.
|
|
55
|
+
- **Re-review every substantive fix round, bounded.** Must-fix fixes are new
|
|
56
|
+
writer code with no reviewer between them and the commit. Dispatch a
|
|
57
|
+
focused confirmation pass — the reviewer(s) whose findings drove the fixes,
|
|
58
|
+
scoped to the changed files only — before declaring the review loop
|
|
59
|
+
closed.
|
|
60
|
+
- **Don't raise the turn-limit as the fix.** More context/turns is not free
|
|
61
|
+
— accuracy degrades as token count grows. Scoping, journaling, and pacing
|
|
62
|
+
are the preferred levers.
|
|
63
|
+
|
|
64
|
+
## Journaling & recovery
|
|
65
|
+
|
|
66
|
+
- **Hand writer spokes (`test-author`, `code-implementer`) an explicit
|
|
67
|
+
journal path** in the dispatch prompt.
|
|
68
|
+
- **Never trust a "final" report at face value.** A mid-thought fragment is
|
|
69
|
+
the signature of a truncated turn, not a benign quirk — verify on-disk
|
|
70
|
+
state yourself (the spoke's journal, `git status`/`git diff`, re-run
|
|
71
|
+
typecheck/lint/test/coverage) before deciding what's actually done.
|
|
72
|
+
- **A spoke's scratchpad journal doesn't survive a session-level restart,
|
|
73
|
+
but its git-worktree edits do.** After a harness/process restart
|
|
74
|
+
mid-dispatch, check `git status`/`git diff` **in the worktree the spoke
|
|
75
|
+
ran in**, not this repo's own root — the edits may already be there even
|
|
76
|
+
with no journal left to read.
|
|
77
|
+
- **A coherent-looking report can still be wrong — a separate failure mode
|
|
78
|
+
from truncation.** Re-verify a fix round's completion (re-read the diff,
|
|
79
|
+
re-run the gates) regardless of how confident the report reads.
|
|
80
|
+
- **Resume the SAME spoke via a follow-up message**, never a fresh
|
|
81
|
+
dispatch — a fresh agent has no memory of the prior exploration and
|
|
82
|
+
restarts the whole budget from zero. Hand it a punch-list, not a recap.
|
|
83
|
+
- **Verification can conclude "no resume needed."** A truncated return whose
|
|
84
|
+
artifacts are already on disk (files written, gates green when you run
|
|
85
|
+
them yourself) needs no resume at all — re-running the verification
|
|
86
|
+
battery from the hub is cheaper. Reserve resumes for truncations where the
|
|
87
|
+
work itself is genuinely unfinished.
|
|
88
|
+
|
|
89
|
+
## Boundaries
|
|
90
|
+
|
|
91
|
+
- **Review spokes return a bounded digest**, not an open-ended report — the
|
|
92
|
+
full report travels back inline in the response, capped at roughly 8,000
|
|
93
|
+
characters (~2,000 tokens). No review spoke writes a scratchpad file.
|
|
94
|
+
- **Plan mode propagates its read-only restriction to every subagent it
|
|
95
|
+
dispatches** — not just the ones already read-only by design. A writer
|
|
96
|
+
spoke dispatched while plan mode is active loses write access too. If a
|
|
97
|
+
design depends on a subagent writing a file, verify with `git status`/`ls`
|
|
98
|
+
after the dispatch rather than trusting the return value's success claim.
|
|
99
|
+
- **A `SubagentStop`/`PreToolUse` hook flagging a dispatch is a prompt to
|
|
100
|
+
verify, not a replacement for verifying.** Treat its stderr reminder as a
|
|
101
|
+
signal to check state yourself.
|
|
102
|
+
- **A templated dispatch prompt needs a per-target assumption check, not
|
|
103
|
+
just a per-target file-list check.** Verify the template's implicit
|
|
104
|
+
assumptions against each target's own docs/tests before dispatch, not
|
|
105
|
+
just the file scope — a defect here can be semantically wrong but
|
|
106
|
+
syntactically valid, passing typecheck/lint/build clean.
|
|
107
|
+
- **Make barrel/index wiring its own numbered, separately-verified step in
|
|
108
|
+
any multi-file dispatch.** It's the step most often left for last, so it's
|
|
109
|
+
the step truncation most often lands on — a missing re-export line passes
|
|
110
|
+
the suite green while nothing in the new module is actually reachable.
|
|
111
|
+
|
|
112
|
+
## Before wiring a new hook or workflow script
|
|
113
|
+
|
|
114
|
+
- **Run it against known-good input before wiring it, not only against the
|
|
115
|
+
failure cases it was built from.** An advisory hook that fires on every
|
|
116
|
+
event of a type must be proven **quiet** on that type's normal output; one
|
|
117
|
+
that cries wolf trains the reader to ignore it, which is worse than not
|
|
118
|
+
shipping it.
|
|
119
|
+
- **A live end-to-end run on a small real input is the acceptance test for a
|
|
120
|
+
script — static gates and review passes cannot see runtime behavior.**
|
|
121
|
+
Review reads a hook or workflow script; nothing else runs it.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "src/**"
|
|
4
|
+
- "**/tests/**"
|
|
5
|
+
- "**/*.test.ts"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Refactoring rules (source & tests)
|
|
9
|
+
|
|
10
|
+
> This file is the terse checklist that auto-loads when you change existing code.
|
|
11
|
+
|
|
12
|
+
Refactoring changes internal structure **without changing observable behavior**.
|
|
13
|
+
It is not feature, performance, or behavior work — those are separate commits.
|
|
14
|
+
|
|
15
|
+
- **Test safety net first.** A passing suite must exist before you refactor; if the
|
|
16
|
+
area lacks tests, **add characterization tests** capturing current behavior first.
|
|
17
|
+
- **State the goal.** Name the problem you are removing (duplication, complexity,
|
|
18
|
+
naming, weak types). No identified problem → no refactor.
|
|
19
|
+
- **Small isolated steps**, each one focused operation, **committed individually**
|
|
20
|
+
with a `refactor:` commit. Rerun the full suite after each
|
|
21
|
+
step; a failure is a regression — revert before continuing.
|
|
22
|
+
- **Opportunistic / Boy-Scout:** leave touched code better than you found it, and
|
|
23
|
+
do a preparatory refactor first when it makes the change you came to do simpler —
|
|
24
|
+
but **keep it bounded** (don't chase one cleanup into a rewrite) and in its own
|
|
25
|
+
commit, separate from the feature/fix.
|
|
26
|
+
- **A refactor MUST NOT** add features, change observable behavior, add a
|
|
27
|
+
dependency, or change a public interface unless that is the explicit purpose.
|
|
28
|
+
- **Semver hazard:** changing an exported signature or the `exports` map is a
|
|
29
|
+
breaking change, not a free refactor — keep the public surface stable or plan
|
|
30
|
+
the major bump. New capability surfaces through the existing barrel/entry
|
|
31
|
+
point, never a new subpath added casually.
|
|
32
|
+
- **Tests are production code:** rename a test when its behavior is renamed, delete
|
|
33
|
+
tests that no longer assert a contract, refactor a shared fixture once (not every
|
|
34
|
+
caller), and update a mock target the moment the impl's I/O primitive changes (a
|
|
35
|
+
stale mock silently intercepts nothing).
|
|
36
|
+
- **Moving code out from under a test can leave it vacuous with no gate
|
|
37
|
+
catching it.** Extracting or relocating the behavior a test exercises,
|
|
38
|
+
without re-deriving what the test still actually reaches, can leave it
|
|
39
|
+
green while asserting nothing real — and a fixture edited to match the new
|
|
40
|
+
code is not automatically a test of the behavior that changed; confirm it
|
|
41
|
+
still fails when the new behavior is wrong, not just that it passes when
|
|
42
|
+
the code is right.
|
|
43
|
+
- **A full test-file rewrite must name what it must NOT touch.** A general
|
|
44
|
+
instruction ("don't drop tests for unchanged functions") is easy to satisfy
|
|
45
|
+
partially in a large rewrite without anyone noticing which specific cases got
|
|
46
|
+
lost — enumerate the functions whose existing coverage must survive
|
|
47
|
+
untouched, not just restate the rule.
|
|
48
|
+
- **Removing a lint suppression cannot be its own commit.** `reportUnusedDisableDirectives:
|
|
49
|
+
"error"` (`eslint.config.js`) makes a stale `eslint-disable-next-line` a lint error
|
|
50
|
+
the instant the finding it suppressed no longer fires — so the change that
|
|
51
|
+
clears the underlying issue and the deletion of its suppression comment must
|
|
52
|
+
land in the same commit, never split across two.
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "src/**"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Source rules (`src/**`)
|
|
7
|
+
|
|
8
|
+
> This file is the terse checklist that auto-loads when you edit source.
|
|
9
|
+
> Base standards — TypeScript strictness, ESM `.js` imports, named exports,
|
|
10
|
+
> exporting a type next to its value, `readonly`/`const`, `interface` vs
|
|
11
|
+
> `type`, exhaustive `switch`, TSDoc on every export — live in CLAUDE.md and
|
|
12
|
+
> `eslint.config.js`; this file adds what those don't already say.
|
|
13
|
+
|
|
14
|
+
- **Don't pass `undefined` to an optional property.** Under
|
|
15
|
+
`exactOptionalPropertyTypes`, an optional target field (`default?: number`)
|
|
16
|
+
rejects an explicit `undefined` (TS2379). When forwarding optional caller
|
|
17
|
+
options into a strict target, **omit the key** with a conditional spread —
|
|
18
|
+
`...(v !== undefined ? { k: v } : {})` — never `{ k: someValue | undefined }`.
|
|
19
|
+
- **Guard reads and writes together when mutating a property on an object you
|
|
20
|
+
don't own — verify success by reading it back, never by the absence of a
|
|
21
|
+
throw.** The property can be an accessor whose getter itself throws, the
|
|
22
|
+
object can be frozen/sealed/non-extensible, or a setter can silently no-op.
|
|
23
|
+
Wrap the read AND the write in one `try`/`catch`, and after a non-throwing
|
|
24
|
+
assignment compare `object.property === value` before reporting success.
|
|
25
|
+
- **Never export error-constructor options interfaces.** Callers _catch_
|
|
26
|
+
errors, they don't construct them.
|
|
27
|
+
- **Discriminate a swallow by error `code`, not class.** When one error class
|
|
28
|
+
carries several `code`s, `catch (e) { if (e instanceof X) skip }` drops the
|
|
29
|
+
very failures the codes distinguish. Narrow the skip to the specific benign
|
|
30
|
+
`code` and **re-throw** the rest.
|
|
31
|
+
- **Guard the parse step, not just the read and the validation around it.**
|
|
32
|
+
A read-then-`JSON.parse`-then-validate sequence needs the same typed-error
|
|
33
|
+
treatment on all three steps. Do **not** chain a raw `SyntaxError` as
|
|
34
|
+
`cause` when the file may hold sensitive content — Node embeds a snippet of
|
|
35
|
+
the malformed content in the message, and the cause chain carries it
|
|
36
|
+
forward to a log/stderr sink.
|
|
37
|
+
- **Fail loud on caller/config errors; stay lenient only on external data.**
|
|
38
|
+
Validate caller- and config-supplied input at the public boundary and throw
|
|
39
|
+
a typed error on violation. Reserve tolerant handling (skip / default /
|
|
40
|
+
warn) for data you don't control (file contents, network payloads).
|
|
41
|
+
- **Narrow a `try`/`catch` to just the fallible call, never the
|
|
42
|
+
post-processing.** Wrapping response-mapping inside the same `try` as an
|
|
43
|
+
async SDK/IO call mislabels a future local bug in the mapping as an upstream
|
|
44
|
+
failure. Assign the awaited result inside `try`/`catch`, build the return
|
|
45
|
+
value after the `catch` block resolves.
|
|
46
|
+
- **Co-locate by a shared value, not by shared code.** When two independent
|
|
47
|
+
mechanisms must agree on a derived path/id/name, give ONE owner the raw
|
|
48
|
+
value and have both derive the result through a single shared helper —
|
|
49
|
+
never let each capture its own copy and re-derive independently, which
|
|
50
|
+
drifts silently.
|
|
51
|
+
- **Never put a bare URL in TSDoc.** Reference sibling modules with `{@link
|
|
52
|
+
Symbol}` or a backticked relative path. Nothing validates link targets, so an
|
|
53
|
+
invented host survives review by eye — grep new source for `http` before
|
|
54
|
+
committing.
|
|
55
|
+
- **`Object.hasOwn(record, field)`, not `record[field] !== undefined`, when
|
|
56
|
+
reading a field off untrusted or partially-trusted input.** Bracket access
|
|
57
|
+
walks the prototype chain, so a record with no own `field` (e.g. one
|
|
58
|
+
literally named `"__proto__"`) can silently resolve an inherited — or,
|
|
59
|
+
under prototype pollution, attacker-controlled — value instead of the
|
|
60
|
+
"absent" the caller expects.
|
|
61
|
+
- **Validate a local copy, never the property expression — `Object.hasOwn`
|
|
62
|
+
guards _presence_, not _stability_.** Each mention of `x.f` is a **separate
|
|
63
|
+
read**, and an accessor may answer differently every time:
|
|
64
|
+
```ts
|
|
65
|
+
// BAD — three reads; the value returned is not the value validated
|
|
66
|
+
const bad = typeof e.n === "number" && Number.isFinite(e.n) ? e.n : null;
|
|
67
|
+
// GOOD — one read
|
|
68
|
+
const raw: unknown = e.n;
|
|
69
|
+
const good = typeof raw === "number" && Number.isFinite(raw) ? raw : null;
|
|
70
|
+
```
|
|
71
|
+
- **A cast across a serialization boundary hides the PROTOTYPE, not just the
|
|
72
|
+
shape.** A stream that rebuilds nodes with a null prototype can answer
|
|
73
|
+
`Array.isArray === true` while the array has no `.slice`. Re-hydrate with
|
|
74
|
+
`structuredClone` first, never `JSON.parse(JSON.stringify(...))`, which
|
|
75
|
+
also turns `-0` into `0` behind the narrowing layer.
|
|
76
|
+
- **Allowlist, never denylist, for a redaction or sanitization boundary.**
|
|
77
|
+
Enumerate the fields you keep; drop everything else. A pattern that tries
|
|
78
|
+
to _recognize_ what is unsafe (a regex over URLs, key-name heuristics) is a
|
|
79
|
+
denylist against unbounded input and does not converge. Where the input is
|
|
80
|
+
genuinely free text, say "best effort" in the TSDoc and reclassify the
|
|
81
|
+
artifact instead of promising a guarantee.
|
|
82
|
+
- **Every caller-supplied value crossing the public boundary is validated
|
|
83
|
+
once, at the boundary — including depth and recursion bounds — and that
|
|
84
|
+
validation's guarantee must hold all the way to where the value is used.**
|
|
85
|
+
Two hazards: **(1) never validate a caller value and then let something
|
|
86
|
+
else re-read it** — a non-idempotent getter, a non-enumerable own `toJSON`
|
|
87
|
+
invisible to `Object.keys` but applied by the serializer. Do the traversal
|
|
88
|
+
**once**: validate and project into a fresh structure, then derive the
|
|
89
|
+
downstream artifact from the projection, never the original. **(2) an
|
|
90
|
+
unbounded recursion or loop over caller-supplied structure is itself
|
|
91
|
+
unvalidated input**, even when every individual read is guarded — depth
|
|
92
|
+
and iteration count need their own explicit ceiling, checked before
|
|
93
|
+
recursing, not discovered as a bare `RangeError`/stack overflow at runtime.
|
|
94
|
+
- **`JSON.stringify` is typed `string` but returns `undefined`** — for a bare
|
|
95
|
+
`undefined`, a function, a symbol, or an object whose `toJSON()` returns
|
|
96
|
+
one. A template literal launders that into the text `"undefined"`, which
|
|
97
|
+
writes and hashes cleanly and parses as nothing. Assert it is a string
|
|
98
|
+
before measuring, writing, or digesting it.
|
|
99
|
+
- **A TSDoc sentence asserting a security or correctness property is a claim
|
|
100
|
+
to verify, not prose to write.** After the last contract change of a task,
|
|
101
|
+
re-read every guarantee sentence against the code, not against the plan. A
|
|
102
|
+
"never surfaced to the caller"-style claim needs a **per-channel** audit: a
|
|
103
|
+
resolved value and a thrown error's `cause`/`message` are separate
|
|
104
|
+
observable channels, and a claim proven true for one can still be false
|
|
105
|
+
for the other.
|
|
106
|
+
- **"Additive" is about construction, not just consumption.** Before calling
|
|
107
|
+
an added field on an options/context type additive, grep the whole repo —
|
|
108
|
+
tests included — for hand-construction of that type. A **required** field
|
|
109
|
+
added to any type that a caller or a test fake _constructs_ is
|
|
110
|
+
source-breaking, even when production code only ever _receives_ it.
|
|
111
|
+
- **Trust the CLI gate over the IDE/LSP.** Editor diagnostics lag and
|
|
112
|
+
misreport against the project `tsconfig`. A passing `pnpm typecheck` /
|
|
113
|
+
`pnpm lint` is the source of truth — don't chase a red squiggle the CLI
|
|
114
|
+
says is clean.
|