@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,131 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * The single source of truth for what `pnpm verify` runs locally, what the
4
+ * lefthook `pre-push` lanes run, and what each `.github/workflows/ci.yml`
5
+ * job runs. Both YAML files name a *group*, never a step id: groups are a
6
+ * closed, five-member set (see GROUPS below); steps are not. That's what
7
+ * lets a new step -- core or pack-contributed -- join `pnpm verify` without
8
+ * ever touching either YAML file, and what keeps this the one list to keep
9
+ * in sync instead of three that agree by hand.
10
+ *
11
+ * Pack-installed steps live in verify-steps.packs.json, a plain JSON array
12
+ * the bootstrapper CLI appends to when installing a pack -- it has no JS
13
+ * parser and never gains one. A missing file means no packs are installed.
14
+ * A present-but-malformed file is a hard failure, never a silent fallback
15
+ * to core-only: a typo'd `group` would otherwise mean a step that runs in
16
+ * `pnpm verify` and in no gate at all, which is exactly the drift this file
17
+ * exists to prevent.
18
+ *
19
+ * Each step's `cmd` is the argv array to run (via `node:child_process`
20
+ * `spawnSync`, stdio inherited so failures are visible directly).
21
+ */
22
+ import { readFileSync } from "node:fs";
23
+ import { fileURLToPath } from "node:url";
24
+
25
+ export const GROUPS = ["format", "lint", "typecheck", "build", "test"];
26
+
27
+ /**
28
+ * The gate steps this baseline ships, before any pack's are appended.
29
+ * @public Exported for the parity test that compares this file with its TypeScript twin in
30
+ * m3l-groundwork; nothing else in a bootstrapped project imports it.
31
+ */
32
+ export const CORE_STEPS = [
33
+ {
34
+ id: "format",
35
+ group: "format",
36
+ name: "Format check",
37
+ cmd: ["pnpm", "format:check"],
38
+ },
39
+ { id: "lint", group: "lint", name: "Lint", cmd: ["pnpm", "lint"] },
40
+ {
41
+ id: "harness",
42
+ group: "lint",
43
+ name: "Check Claude Code harness",
44
+ cmd: ["node", "bin/check-harness.mjs"],
45
+ },
46
+ {
47
+ id: "toolchain",
48
+ group: "lint",
49
+ name: "Check TypeScript toolchain",
50
+ cmd: ["node", "bin/check-toolchain.mjs"],
51
+ },
52
+ {
53
+ id: "knip",
54
+ group: "lint",
55
+ name: "Check unused code and dependencies",
56
+ cmd: ["pnpm", "knip"],
57
+ },
58
+ {
59
+ id: "typecheck",
60
+ group: "typecheck",
61
+ name: "Typecheck",
62
+ cmd: ["pnpm", "typecheck"],
63
+ },
64
+ { id: "build", group: "build", name: "Build", cmd: ["pnpm", "build"] },
65
+ {
66
+ id: "test",
67
+ group: "test",
68
+ name: "Test (coverage)",
69
+ cmd: ["pnpm", "test:coverage"],
70
+ },
71
+ {
72
+ id: "exports",
73
+ group: "build",
74
+ name: "Check package exports",
75
+ cmd: ["node", "bin/check-exports.mjs"],
76
+ },
77
+ {
78
+ id: "node-version",
79
+ group: "build",
80
+ name: "Check Node version pin",
81
+ cmd: ["node", "bin/check-node-version.mjs"],
82
+ },
83
+ ];
84
+
85
+ /** Reads and validates verify-steps.packs.json. Absence is fine (no packs installed). */
86
+ function readPackSteps() {
87
+ const path = fileURLToPath(
88
+ new URL("./verify-steps.packs.json", import.meta.url),
89
+ );
90
+ let raw;
91
+ try {
92
+ raw = readFileSync(path, "utf8");
93
+ } catch {
94
+ return [];
95
+ }
96
+
97
+ const parsed = JSON.parse(raw);
98
+ if (!Array.isArray(parsed)) {
99
+ throw new Error("verify-steps.packs.json must be a JSON array of steps");
100
+ }
101
+ for (const step of parsed) {
102
+ for (const field of ["id", "group", "name"]) {
103
+ if (typeof step?.[field] !== "string") {
104
+ throw new Error(
105
+ `verify-steps.packs.json: a step is missing "${field}"`,
106
+ );
107
+ }
108
+ }
109
+ if (!Array.isArray(step.cmd) || step.cmd.length === 0) {
110
+ throw new Error(`verify-steps.packs.json: step "${step.id}" has no cmd`);
111
+ }
112
+ if (!GROUPS.includes(step.group)) {
113
+ throw new Error(
114
+ `verify-steps.packs.json: step "${step.id}" has unknown group "${step.group}" -- known: ${GROUPS.join(", ")}`,
115
+ );
116
+ }
117
+ }
118
+ return parsed;
119
+ }
120
+
121
+ export const VERIFY_STEPS = [...CORE_STEPS, ...readPackSteps()];
122
+
123
+ /** Look up one step by id, or `undefined` if no step has that id. */
124
+ export function findStep(id) {
125
+ return VERIFY_STEPS.find((step) => step.id === id);
126
+ }
127
+
128
+ /** All steps belonging to one group, in VERIFY_STEPS order. */
129
+ export function stepsInGroup(group) {
130
+ return VERIFY_STEPS.filter((step) => step.group === group);
131
+ }
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Wraps `@commitlint/lint` + `@commitlint/load` directly (no `@commitlint/cli`
4
+ * dependency needed) against this project's `commitlint.config.js`. Also
5
+ * validates that no forbidden `Claude-*` trailer survived into the message
6
+ * (a backstop -- `strip-claude-trailers.mjs` runs first in the `commit-msg`
7
+ * hook and should have already removed any harness-injected one;
8
+ * `Co-Authored-By:` is never forbidden).
9
+ *
10
+ * Usage: `lint-commit.mjs --edit <path-to-commit-msg-file>` (lefthook's
11
+ * `commit-msg` hook contract) or `lint-commit.mjs <message>` directly.
12
+ */
13
+ import process from "node:process";
14
+ import { readFileSync } from "node:fs";
15
+ import load from "@commitlint/load";
16
+ import lint from "@commitlint/lint";
17
+
18
+ const args = process.argv.slice(2);
19
+ const editIndex = args.indexOf("--edit");
20
+ const message =
21
+ editIndex === -1 ? args.join(" ") : readFileSync(args[editIndex + 1], "utf8");
22
+
23
+ const FORBIDDEN_TRAILER = /^Claude-(?!Session:)[A-Za-z-]*:/m;
24
+
25
+ function validateForbiddenTrailers(text) {
26
+ return !FORBIDDEN_TRAILER.test(text) && !/^Claude-Session:/m.test(text);
27
+ }
28
+
29
+ const config = await load({}, { file: "commitlint.config.js" });
30
+ const result = await lint(
31
+ message,
32
+ config.rules,
33
+ config.parserPreset ? { parserOpts: config.parserPreset.parserOpts } : {},
34
+ );
35
+
36
+ if (!validateForbiddenTrailers(message)) {
37
+ console.error(
38
+ "commit message carries a forbidden Claude-* trailer (only Co-Authored-By: is allowed)",
39
+ );
40
+ process.exit(1);
41
+ }
42
+
43
+ if (!result.valid) {
44
+ for (const problem of result.errors) {
45
+ console.error(`✗ ${problem.message}`);
46
+ }
47
+ process.exit(1);
48
+ }
49
+
50
+ console.log("✓ commit message is a valid Conventional Commit");
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `commit-msg`: strips any harness-injected `Claude-*` trailer line from the
4
+ * commit message file in place before `lint-commit.mjs` validates it.
5
+ * `Co-Authored-By:` is never touched -- it is the one trailer this repo
6
+ * wants to keep.
7
+ */
8
+ import process from "node:process";
9
+ import { readFileSync, writeFileSync } from "node:fs";
10
+
11
+ const path = process.argv[2];
12
+ if (!path) {
13
+ console.error("strip-claude-trailers: expected a commit-message file path");
14
+ process.exit(1);
15
+ }
16
+
17
+ const text = readFileSync(path, "utf8");
18
+ const stripped = text
19
+ .split("\n")
20
+ .filter((line) => !/^Claude-[A-Za-z-]*:/.test(line))
21
+ .join("\n");
22
+
23
+ if (stripped !== text) {
24
+ writeFileSync(path, stripped);
25
+ }
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `pnpm verify` -- runs every step in VERIFY_STEPS (bin/lib/verify-steps.mjs)
4
+ * sequentially and reports pass/fail for each. `--step <id>` runs a single
5
+ * named step; `--group <name>` runs every step in one of the five fixed
6
+ * groups (format/lint/typecheck/build/test). `.github/workflows/ci.yml` and
7
+ * `lefthook.yml` each invoke `--group <name>`, never individual step ids, so
8
+ * a new step -- core or pack-contributed -- is picked up by both without
9
+ * either file changing.
10
+ */
11
+ import process from "node:process";
12
+ import { spawnSync } from "node:child_process";
13
+ import {
14
+ VERIFY_STEPS,
15
+ GROUPS,
16
+ findStep,
17
+ stepsInGroup,
18
+ } from "./lib/verify-steps.mjs";
19
+
20
+ const args = process.argv.slice(2);
21
+ const stepIndex = args.indexOf("--step");
22
+ const requestedId = stepIndex === -1 ? null : args[stepIndex + 1];
23
+ const groupIndex = args.indexOf("--group");
24
+ const requestedGroup = groupIndex === -1 ? null : args[groupIndex + 1];
25
+
26
+ if (requestedGroup && !GROUPS.includes(requestedGroup)) {
27
+ console.error(
28
+ `verify: unknown group "${requestedGroup}" -- known groups: ${GROUPS.join(", ")}`,
29
+ );
30
+ process.exit(1);
31
+ }
32
+
33
+ const steps = requestedId
34
+ ? [findStep(requestedId)].filter((step) => step !== undefined)
35
+ : requestedGroup
36
+ ? stepsInGroup(requestedGroup)
37
+ : VERIFY_STEPS;
38
+
39
+ if (requestedId && steps.length === 0) {
40
+ console.error(
41
+ `verify: unknown step "${requestedId}" -- known ids: ${VERIFY_STEPS.map((s) => s.id).join(", ")}`,
42
+ );
43
+ process.exit(1);
44
+ }
45
+
46
+ let failed = false;
47
+ for (const step of steps) {
48
+ console.log(`\n▶ ${step.name} (${step.id})`);
49
+ const [cmd, ...cmdArgs] = step.cmd;
50
+ const result = spawnSync(cmd, cmdArgs, { stdio: "inherit" });
51
+ if (result.status !== 0) {
52
+ failed = true;
53
+ console.error(`✗ ${step.name} failed`);
54
+ if (!requestedId && !requestedGroup) {
55
+ // Local `pnpm verify` (no --step/--group): keep going so a single
56
+ // early failure doesn't hide every other failing step in the same run.
57
+ continue;
58
+ }
59
+ break;
60
+ }
61
+ console.log(`✓ ${step.name}`);
62
+ }
63
+
64
+ process.exit(failed ? 1 : 0);
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Conventional Commits, enforced on `commit-msg` via lefthook.
3
+ *
4
+ * `feat:` -> minor, `fix:` -> patch, `feat!:` / `BREAKING CHANGE:` -> major.
5
+ * Other types (`docs`, `refactor`, `test`, `chore`, ...) do not release.
6
+ *
7
+ * @type {import("@commitlint/types").UserConfig}
8
+ */
9
+ export default {
10
+ extends: ["@commitlint/config-conventional"],
11
+ };
@@ -0,0 +1,27 @@
1
+ # Harness refresh tracker
2
+
3
+ <!-- harness-refresh: last-verified=unset claude-code-version=unset -->
4
+
5
+ Living record of `harness-guidance` (refresh mode) sweeps — the per-facet,
6
+ per-source state a run diffs against, so each sweep reports what
7
+ **changed** since the last one instead of rediscovering the whole `.claude/`
8
+ surface from scratch. Updated **in place** on every run, never as a new
9
+ dated file.
10
+
11
+ This tracker has not had its first sweep yet (`last-verified=unset`). Run
12
+ `harness-guidance` in refresh mode — or let `/customize`'s guidance pass
13
+ seed it — to populate the sections below.
14
+
15
+ ## Outstanding drift
16
+
17
+ _(none recorded yet — first sweep populates this section)_
18
+
19
+ ## Facet state
20
+
21
+ | Facet | Last verified | Notes |
22
+ | ---------------------------- | ------------- | ----- |
23
+ | `models-tiering` | unset | |
24
+ | `cc-features-settings` | unset | |
25
+ | `agent-subagent-design` | unset | |
26
+ | `skills-context-engineering` | unset | |
27
+ | `hooks-lifecycle` | unset | |
@@ -0,0 +1,32 @@
1
+ # TypeScript refresh tracker
2
+
3
+ <!-- typescript-refresh: last-verified=unset typescript-version=unset -->
4
+
5
+ Living record of `typescript-guidance` (refresh mode) sweeps — the
6
+ per-facet, per-source state a run diffs against, so each sweep reports what
7
+ **changed** since the last one instead of rediscovering the whole toolchain
8
+ from scratch. Updated **in place** on every run, never as a new dated file.
9
+
10
+ `typescript-version` in the header records the newest **upstream**
11
+ TypeScript release the last sweep verified against — deliberately distinct
12
+ from `package.json`'s own `typescript` devDependency pin, which this
13
+ tracker exists to check against, not restate. The two numbers disagreeing
14
+ is not a bug in the tracker; it's the finding.
15
+
16
+ This tracker has not had its first sweep yet (`last-verified=unset`). Run
17
+ `typescript-guidance` in refresh mode — or let `/customize`'s guidance pass
18
+ seed it — to populate the sections below.
19
+
20
+ ## Outstanding drift
21
+
22
+ _(none recorded yet — first sweep populates this section)_
23
+
24
+ ## Facet state
25
+
26
+ | Facet | Last verified | Notes |
27
+ | ---------------------------- | ------------- | ----- |
28
+ | `compiler-config-flags` | unset | |
29
+ | `modules-esm-node-interop` | unset | |
30
+ | `packaging-declaration-emit` | unset | |
31
+ | `lint-typing-rules` | unset | |
32
+ | `testing-language-features` | unset | |
@@ -0,0 +1,105 @@
1
+ // @ts-check
2
+ import js from "@eslint/js";
3
+ import { defineConfig } from "eslint/config";
4
+ import { configs } from "typescript-eslint";
5
+ import { importX } from "eslint-plugin-import-x";
6
+ import { createTypeScriptImportResolver } from "eslint-import-resolver-typescript";
7
+ import tsdoc from "eslint-plugin-tsdoc";
8
+ import globals from "globals";
9
+
10
+ const CJS_DIRNAME = "__dir" + "name";
11
+ const CJS_FILENAME = "__file" + "name";
12
+
13
+ export default defineConfig(
14
+ {
15
+ ignores: [
16
+ "**/dist/**",
17
+ "**/node_modules/**",
18
+ "**/coverage/**",
19
+ // .claude/agents|skills|rules contain only docs (prompts, reference
20
+ // data a skill reads, not code this project executes); hooks are the
21
+ // only code under .claude/ and stay linted below.
22
+ ".claude/agents/**",
23
+ ".claude/skills/**",
24
+ ".claude/rules/**",
25
+ ],
26
+ },
27
+ js.configs.recommended,
28
+ ...configs.recommendedTypeChecked,
29
+ importX.flatConfigs.recommended,
30
+ importX.flatConfigs.typescript,
31
+ {
32
+ files: ["**/*.ts"],
33
+ linterOptions: {
34
+ // Stale eslint-disable directives are always a bug: they either never
35
+ // suppressed anything or the underlying finding was fixed, leaving
36
+ // noise that misleads reviewers.
37
+ reportUnusedDisableDirectives: "error",
38
+ },
39
+ languageOptions: {
40
+ parserOptions: {
41
+ projectService: {
42
+ allowDefaultProject: ["vitest.config.ts"],
43
+ },
44
+ tsconfigRootDir: import.meta.dirname,
45
+ },
46
+ },
47
+ settings: {
48
+ "import-x/resolver-next": [createTypeScriptImportResolver()],
49
+ },
50
+ rules: {
51
+ // --- ESM correctness: the #1 documented gotcha ---------------------
52
+ "import-x/extensions": [
53
+ "error",
54
+ "ignorePackages",
55
+ { js: "always", ts: "never" },
56
+ ],
57
+
58
+ // --- Strictness: no `any` in the public API -------------------------
59
+ "@typescript-eslint/no-explicit-any": "error",
60
+ "@typescript-eslint/no-non-null-assertion": "error",
61
+ "@typescript-eslint/no-floating-promises": "error",
62
+ "@typescript-eslint/switch-exhaustiveness-check": "error",
63
+ "@typescript-eslint/consistent-type-imports": "error",
64
+ "@typescript-eslint/no-unused-vars": [
65
+ "error",
66
+ { argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
67
+ ],
68
+
69
+ // --- Style / design --------------------------------------------------
70
+ "prefer-const": "error",
71
+ "no-var": "error",
72
+ eqeqeq: ["error", "always", { null: "ignore" }],
73
+
74
+ // --- ESM only: ban CommonJS constructs -------------------------------
75
+ "no-restricted-globals": [
76
+ "error",
77
+ { name: CJS_DIRNAME, message: "CommonJS only; this package is ESM." },
78
+ { name: CJS_FILENAME, message: "CommonJS only; this package is ESM." },
79
+ { name: "require", message: "CommonJS only; this package is ESM." },
80
+ ],
81
+ "import-x/no-commonjs": "error",
82
+ },
83
+ },
84
+ {
85
+ files: ["src/**/*.ts"],
86
+ plugins: { tsdoc },
87
+ rules: {
88
+ "tsdoc/syntax": "warn",
89
+ "import-x/no-default-export": "error",
90
+ },
91
+ },
92
+ {
93
+ files: ["**/*.mjs", "**/*.js"],
94
+ ...configs.disableTypeChecked,
95
+ },
96
+ {
97
+ files: ["bin/**/*.mjs", ".claude/hooks/**/*.mjs"],
98
+ languageOptions: {
99
+ globals: { ...globals.node },
100
+ },
101
+ rules: {
102
+ "import-x/no-unresolved": "off",
103
+ },
104
+ },
105
+ );
@@ -0,0 +1,6 @@
1
+ {
2
+ "$schema": "https://unpkg.com/knip@6/schema.json",
3
+ "entry": ["src/index.ts", "bin/*.mjs", ".claude/hooks/*.mjs"],
4
+ "project": ["src/**/*.ts", "bin/**/*.mjs", ".claude/hooks/**/*.mjs"],
5
+ "ignoreDependencies": ["@commitlint/config-conventional", "@commitlint/types"]
6
+ }
@@ -0,0 +1,39 @@
1
+ # Git hooks, managed by Lefthook.
2
+ # Install/refresh with `pnpm exec lefthook install` (wired into `prepare`).
3
+ # yaml-language-server: $schema=https://json.schemastore.org/lefthook.json
4
+
5
+ # Fail closed when no lefthook binary resolves at all. Without this, a
6
+ # generated shim whose final else-branch only echoes "Can't find lefthook in
7
+ # PATH" falls through with exit 0, silently skipping every gate below.
8
+ assert_lefthook_installed: true
9
+
10
+ pre-commit:
11
+ parallel: true
12
+ commands:
13
+ lint:
14
+ glob: "**/*.ts"
15
+ run: pnpm exec eslint --fix {staged_files}
16
+ stage_fixed: true
17
+ format:
18
+ glob: "**/*.{ts,json,md,yml,yaml}"
19
+ run: pnpm exec prettier --write {staged_files}
20
+ stage_fixed: true
21
+
22
+ commit-msg:
23
+ commands:
24
+ commitlint:
25
+ run: node bin/strip-claude-trailers.mjs {1} && node bin/lint-commit.mjs --edit {1}
26
+
27
+ pre-push:
28
+ parallel: true
29
+ commands:
30
+ format:
31
+ run: node bin/verify.mjs --group format
32
+ lint:
33
+ run: node bin/verify.mjs --group lint
34
+ typecheck:
35
+ run: node bin/verify.mjs --group typecheck
36
+ build:
37
+ run: node bin/verify.mjs --group build
38
+ test:
39
+ run: node bin/verify.mjs --group test
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "__PROJECT_NAME__",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "description": "Bootstrapped by m3l-groundwork.",
7
+ "packageManager": "pnpm@12.4.0",
8
+ "engines": {
9
+ "node": ">=24"
10
+ },
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js"
15
+ }
16
+ },
17
+ "main": "./dist/index.js",
18
+ "types": "./dist/index.d.ts",
19
+ "files": [
20
+ "dist"
21
+ ],
22
+ "scripts": {
23
+ "build": "tsc -b tsconfig.build.json",
24
+ "typecheck": "tsc -b --force",
25
+ "lint": "eslint .",
26
+ "format": "prettier --write . --cache --cache-strategy content",
27
+ "format:check": "prettier --check . --cache --cache-strategy content",
28
+ "test": "vitest run",
29
+ "test:coverage": "vitest run --coverage",
30
+ "knip": "knip",
31
+ "check:exports": "node bin/check-exports.mjs",
32
+ "check:node-version": "node bin/check-node-version.mjs",
33
+ "verify": "node bin/verify.mjs",
34
+ "prepare": "lefthook install"
35
+ },
36
+ "devDependencies": {
37
+ "@arethetypeswrong/cli": "^0.18.5",
38
+ "@commitlint/config-conventional": "^21.2.2",
39
+ "@commitlint/lint": "^21.2.2",
40
+ "@commitlint/load": "^21.2.2",
41
+ "@commitlint/types": "^21.2.0",
42
+ "@eslint/js": "^10.0.1",
43
+ "@types/node": "^24.13.3",
44
+ "@vitest/coverage-v8": "^4.1.11",
45
+ "eslint": "^10.9.1",
46
+ "eslint-import-resolver-typescript": "^4.4.5",
47
+ "eslint-plugin-import-x": "^4.17.1",
48
+ "eslint-plugin-tsdoc": "^0.5.2",
49
+ "globals": "^17.12.0",
50
+ "knip": "^6.34.0",
51
+ "lefthook": "^2.1.12",
52
+ "prettier": "^3.9.6",
53
+ "publint": "^0.3.24",
54
+ "typescript": "^6.0.3",
55
+ "typescript-eslint": "^8.69.0",
56
+ "vitest": "^4.1.11"
57
+ }
58
+ }
@@ -0,0 +1,13 @@
1
+ # A single-package project still needs this file: pnpm 10+ moved
2
+ # install-script approval here (the `pnpm` field in package.json is no
3
+ # longer read).
4
+ allowBuilds:
5
+ # lefthook's postinstall fetches its platform binary; needed for the git
6
+ # hooks to actually work.
7
+ lefthook: true
8
+ # unrs-resolver is a native binary transitive dep of
9
+ # eslint-import-resolver-typescript; it works without its optional native
10
+ # acceleration, so this is explicitly false (not omitted) so pnpm skips
11
+ # the build with a notice instead of erroring when no compiler toolchain
12
+ # is available.
13
+ unrs-resolver: false
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Placeholder entry point — replace with your project's real public API.
3
+ *
4
+ * @example
5
+ * ```ts
6
+ * import { placeholder } from "./index.js";
7
+ * placeholder(); // "hello from this project"
8
+ * ```
9
+ */
10
+ export function placeholder(): string {
11
+ return "hello from this project";
12
+ }
@@ -0,0 +1,8 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { placeholder } from "../src/index.js";
3
+
4
+ describe("placeholder", () => {
5
+ it("returns a non-empty greeting", () => {
6
+ expect(placeholder()).toContain("hello");
7
+ });
8
+ });
@@ -0,0 +1,36 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/tsconfig",
3
+ "compilerOptions": {
4
+ "target": "es2023",
5
+ "lib": ["es2023"],
6
+ "types": ["node"],
7
+ "module": "nodenext",
8
+ "moduleResolution": "nodenext",
9
+ "strict": true,
10
+ "noUncheckedIndexedAccess": true,
11
+ "noImplicitOverride": true,
12
+ "noFallthroughCasesInSwitch": true,
13
+ "exactOptionalPropertyTypes": true,
14
+ "verbatimModuleSyntax": true,
15
+ "isolatedModules": true,
16
+ "noUncheckedSideEffectImports": true,
17
+ "noPropertyAccessFromIndexSignature": true,
18
+ "noImplicitReturns": true,
19
+ "allowUnreachableCode": false,
20
+ // isolatedDeclarations stays out of the base config deliberately: the
21
+ // build project sets it individually (see tsconfig.build.json), but the
22
+ // tooling project (tsconfig.json) sets declaration: false and includes
23
+ // tests/, which isolatedDeclarations does not tolerate.
24
+ //
25
+ // noUnusedLocals / noUnusedParameters are deliberately NOT set:
26
+ // @typescript-eslint/no-unused-vars already covers both at error level
27
+ // (eslint.config.js) with an `^_`-prefix escape hatch this codebase
28
+ // relies on; tsc's flags honor no such pattern for locals.
29
+ "declaration": true,
30
+ "declarationMap": true,
31
+ "sourceMap": true,
32
+ "composite": true,
33
+ "skipLibCheck": true,
34
+ "forceConsistentCasingInFileNames": true
35
+ }
36
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "./tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "isolatedDeclarations": true,
5
+ "outDir": "./dist",
6
+ "rootDir": "./src",
7
+ "tsBuildInfoFile": "./dist/.tsbuildinfo"
8
+ },
9
+ "include": ["src/**/*.ts"]
10
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "extends": "./tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "composite": false,
5
+ "declaration": false,
6
+ "declarationMap": false,
7
+ "noEmit": true,
8
+ "rootDir": "."
9
+ },
10
+ "include": ["src/**/*.ts", "tests/**/*.ts"]
11
+ }