@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.
Files changed (151) hide show
  1. package/README.md +23 -0
  2. package/bin/m3l-groundwork.mjs +10 -0
  3. package/dist/assets.d.ts +20 -0
  4. package/dist/assets.js +79 -0
  5. package/dist/caps.d.ts +25 -0
  6. package/dist/caps.js +69 -0
  7. package/dist/conflicts.d.ts +12 -0
  8. package/dist/conflicts.js +77 -0
  9. package/dist/emit.d.ts +7 -0
  10. package/dist/emit.js +42 -0
  11. package/dist/git.d.ts +3 -0
  12. package/dist/git.js +9 -0
  13. package/dist/harness/conformance.d.ts +20 -0
  14. package/dist/harness/conformance.js +18 -0
  15. package/dist/harness/frontmatter.d.ts +38 -0
  16. package/dist/harness/frontmatter.js +204 -0
  17. package/dist/harness/grade.d.ts +4 -0
  18. package/dist/harness/grade.js +105 -0
  19. package/dist/harness/rules.d.ts +55 -0
  20. package/dist/harness/rules.js +580 -0
  21. package/dist/harness/types.d.ts +32 -0
  22. package/dist/harness/types.js +9 -0
  23. package/dist/inventory.d.ts +63 -0
  24. package/dist/inventory.js +66 -0
  25. package/dist/jsonc.d.ts +14 -0
  26. package/dist/jsonc.js +83 -0
  27. package/dist/main.d.ts +24 -0
  28. package/dist/main.js +297 -0
  29. package/dist/merge-json.d.ts +74 -0
  30. package/dist/merge-json.js +135 -0
  31. package/dist/mode.d.ts +19 -0
  32. package/dist/mode.js +53 -0
  33. package/dist/packs.d.ts +61 -0
  34. package/dist/packs.js +186 -0
  35. package/dist/plugin.d.ts +23 -0
  36. package/dist/plugin.js +79 -0
  37. package/dist/report.d.ts +4 -0
  38. package/dist/report.js +323 -0
  39. package/dist/survey/fs-walk.d.ts +14 -0
  40. package/dist/survey/fs-walk.js +60 -0
  41. package/dist/survey/survey-docs.d.ts +4 -0
  42. package/dist/survey/survey-docs.js +69 -0
  43. package/dist/survey/survey-harness.d.ts +4 -0
  44. package/dist/survey/survey-harness.js +121 -0
  45. package/dist/survey/survey-shape.d.ts +4 -0
  46. package/dist/survey/survey-shape.js +182 -0
  47. package/dist/survey/survey-toolchain.d.ts +4 -0
  48. package/dist/survey/survey-toolchain.js +217 -0
  49. package/dist/survey/survey.d.ts +5 -0
  50. package/dist/survey/survey.js +21 -0
  51. package/dist/survey/types.d.ts +117 -0
  52. package/dist/survey/types.js +8 -0
  53. package/dist/tokens.d.ts +13 -0
  54. package/dist/tokens.js +13 -0
  55. package/dist/toolchain/conformance.d.ts +20 -0
  56. package/dist/toolchain/conformance.js +30 -0
  57. package/dist/toolchain/grade.d.ts +4 -0
  58. package/dist/toolchain/grade.js +244 -0
  59. package/dist/toolchain/rules.d.ts +118 -0
  60. package/dist/toolchain/rules.js +706 -0
  61. package/dist/toolchain/tsconfig-chain.d.ts +36 -0
  62. package/dist/toolchain/tsconfig-chain.js +116 -0
  63. package/dist/toolchain/types.d.ts +27 -0
  64. package/dist/toolchain/types.js +9 -0
  65. package/package.json +59 -0
  66. package/plugin/skills/customize/SKILL.md +305 -0
  67. package/plugin/src/domain-map.ts +134 -0
  68. package/plugin/src/index.ts +4 -0
  69. package/plugin/src/kind-facet-map.ts +174 -0
  70. package/plugin/src/pack-map.ts +65 -0
  71. package/templates/core/.claude/agents/Explore.md +43 -0
  72. package/templates/core/.claude/agents/code-implementer.md +258 -0
  73. package/templates/core/.claude/agents/code-reviewer.md +163 -0
  74. package/templates/core/.claude/agents/silent-failure-hunter.md +191 -0
  75. package/templates/core/.claude/agents/test-author.md +211 -0
  76. package/templates/core/.claude/hooks/guard-branch-isolation.mjs +123 -0
  77. package/templates/core/.claude/hooks/guard-double-background.mjs +113 -0
  78. package/templates/core/.claude/hooks/guard-git-push-signed.mjs +90 -0
  79. package/templates/core/.claude/hooks/guard-hub-src-writes.mjs +88 -0
  80. package/templates/core/.claude/hooks/guard-js-extension.mjs +66 -0
  81. package/templates/core/.claude/hooks/guard-no-commonjs.mjs +105 -0
  82. package/templates/core/.claude/hooks/guard-protected-paths.mjs +45 -0
  83. package/templates/core/.claude/hooks/guard-secret-writes.mjs +183 -0
  84. package/templates/core/.claude/hooks/inject-decision-gate.mjs +119 -0
  85. package/templates/core/.claude/hooks/post-edit-verify.mjs +150 -0
  86. package/templates/core/.claude/rules/agent-dispatch.md +121 -0
  87. package/templates/core/.claude/rules/refactoring.md +52 -0
  88. package/templates/core/.claude/rules/src.md +114 -0
  89. package/templates/core/.claude/rules/tests.md +129 -0
  90. package/templates/core/.claude/settings.json +111 -0
  91. package/templates/core/.claude/skills/creating-prs/SKILL.md +132 -0
  92. package/templates/core/.claude/skills/finishing-work/SKILL.md +117 -0
  93. package/templates/core/.claude/skills/harness-guidance/SKILL.md +140 -0
  94. package/templates/core/.claude/skills/harness-guidance/references/official-sources.md +58 -0
  95. package/templates/core/.claude/skills/starting-work/SKILL.md +94 -0
  96. package/templates/core/.claude/skills/triaging-ci/SKILL.md +111 -0
  97. package/templates/core/.claude/skills/typescript-guidance/SKILL.md +143 -0
  98. package/templates/core/.claude/skills/typescript-guidance/references/typescript-sources.md +102 -0
  99. package/templates/core/.claude/skills/writing-commits/SKILL.md +248 -0
  100. package/templates/core/.github/workflows/ci.yml +123 -0
  101. package/templates/core/.github/workflows/dependency-review.yml +26 -0
  102. package/templates/core/.github/workflows/security-audit.yml +54 -0
  103. package/templates/core/.node-version +1 -0
  104. package/templates/core/.prettierignore +5 -0
  105. package/templates/core/.prettierrc.json +4 -0
  106. package/templates/core/CLAUDE.md +127 -0
  107. package/templates/core/README.md +24 -0
  108. package/templates/core/_gitignore +19 -0
  109. package/templates/core/_npmrc +1 -0
  110. package/templates/core/bin/check-exports.mjs +92 -0
  111. package/templates/core/bin/check-harness.mjs +27 -0
  112. package/templates/core/bin/check-node-version.mjs +51 -0
  113. package/templates/core/bin/check-toolchain.mjs +20 -0
  114. package/templates/core/bin/lib/agent-roster.mjs +8 -0
  115. package/templates/core/bin/lib/frontmatter.mjs +210 -0
  116. package/templates/core/bin/lib/harness-rules.mjs +916 -0
  117. package/templates/core/bin/lib/protected-paths.mjs +23 -0
  118. package/templates/core/bin/lib/report.mjs +56 -0
  119. package/templates/core/bin/lib/signed-range.mjs +178 -0
  120. package/templates/core/bin/lib/toolchain-rules.mjs +1264 -0
  121. package/templates/core/bin/lib/verify-steps.mjs +131 -0
  122. package/templates/core/bin/lib/verify-steps.packs.json +1 -0
  123. package/templates/core/bin/lint-commit.mjs +50 -0
  124. package/templates/core/bin/strip-claude-trailers.mjs +25 -0
  125. package/templates/core/bin/verify.mjs +64 -0
  126. package/templates/core/commitlint.config.js +11 -0
  127. package/templates/core/docs/research/harness-refresh.md +27 -0
  128. package/templates/core/docs/research/typescript-refresh.md +32 -0
  129. package/templates/core/eslint.config.js +105 -0
  130. package/templates/core/knip.json +6 -0
  131. package/templates/core/lefthook.yml +39 -0
  132. package/templates/core/package.json +58 -0
  133. package/templates/core/pnpm-workspace.yaml +13 -0
  134. package/templates/core/src/index.ts +12 -0
  135. package/templates/core/tests/index.test.ts +8 -0
  136. package/templates/core/tsconfig.base.json +36 -0
  137. package/templates/core/tsconfig.build.json +10 -0
  138. package/templates/core/tsconfig.json +11 -0
  139. package/templates/core/vitest.config.ts +32 -0
  140. package/templates/packs/README.md +81 -0
  141. package/templates/packs/harness-extras/files/.claude/agents/type-design-analyzer.md +188 -0
  142. package/templates/packs/harness-extras/files/.claude/hooks/guard-readonly-bash.mjs +324 -0
  143. package/templates/packs/harness-extras/files/.claude/hooks/reinject-compact-handoff.mjs +197 -0
  144. package/templates/packs/harness-extras/files/.claude/hooks/write-compact-handoff.mjs +180 -0
  145. package/templates/packs/harness-extras/files/bin/check-file-budget.mjs +407 -0
  146. package/templates/packs/harness-extras/files/bin/file-budget-baseline.json +1 -0
  147. package/templates/packs/harness-extras/pack.json +65 -0
  148. package/templates/packs/statusline/files/.claude/hooks/statusline-layout.mjs +365 -0
  149. package/templates/packs/statusline/files/.claude/hooks/statusline.mjs +996 -0
  150. package/templates/packs/statusline/files/.claude/hooks/subagent-statusline.mjs +203 -0
  151. 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.