@monte3l/groundwork 1.0.0-rc.2 → 1.0.0-rc.3

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 (170) hide show
  1. package/README.md +13 -5
  2. package/bin/m3l-groundwork.mjs +36 -2
  3. package/dist/assets.d.ts +10 -0
  4. package/dist/assets.js +14 -0
  5. package/dist/baseline-stage.d.ts +173 -0
  6. package/dist/baseline-stage.js +215 -0
  7. package/dist/caps.d.ts +4 -1
  8. package/dist/caps.js +17 -3
  9. package/dist/conflicts.d.ts +23 -2
  10. package/dist/conflicts.js +89 -11
  11. package/dist/customize-paths.d.ts +92 -0
  12. package/dist/customize-paths.js +115 -0
  13. package/dist/emit.d.ts +50 -1
  14. package/dist/emit.js +121 -13
  15. package/dist/fatal.d.ts +70 -0
  16. package/dist/fatal.js +132 -0
  17. package/dist/format-error.d.ts +113 -0
  18. package/dist/format-error.js +553 -0
  19. package/dist/fs-guard.d.ts +142 -0
  20. package/dist/fs-guard.js +222 -0
  21. package/dist/git.js +10 -1
  22. package/dist/harness/conformance.js +2 -0
  23. package/dist/harness/frontmatter.js +2 -13
  24. package/dist/harness/grade.js +37 -8
  25. package/dist/harness/rules.d.ts +20 -3
  26. package/dist/harness/rules.js +108 -6
  27. package/dist/harness/types.d.ts +13 -0
  28. package/dist/harness/types.js +3 -0
  29. package/dist/inventory.d.ts +77 -3
  30. package/dist/inventory.js +103 -12
  31. package/dist/jsonc.d.ts +49 -2
  32. package/dist/jsonc.js +140 -7
  33. package/dist/main.d.ts +62 -3
  34. package/dist/main.js +538 -67
  35. package/dist/merge-json.d.ts +47 -4
  36. package/dist/merge-json.js +120 -8
  37. package/dist/mode.js +12 -2
  38. package/dist/pack-stage.d.ts +210 -0
  39. package/dist/pack-stage.js +287 -0
  40. package/dist/packs.d.ts +19 -13
  41. package/dist/packs.js +231 -28
  42. package/dist/palette.d.ts +23 -0
  43. package/dist/palette.js +22 -0
  44. package/dist/plugin.d.ts +122 -6
  45. package/dist/plugin.js +687 -47
  46. package/dist/report.d.ts +21 -2
  47. package/dist/report.js +93 -5
  48. package/dist/staging.d.ts +176 -0
  49. package/dist/staging.js +375 -0
  50. package/dist/survey/fs-walk.d.ts +34 -2
  51. package/dist/survey/fs-walk.js +70 -5
  52. package/dist/survey/internal/blocked-path.d.ts +27 -0
  53. package/dist/survey/internal/blocked-path.js +129 -0
  54. package/dist/survey/internal/package-json.d.ts +13 -0
  55. package/dist/survey/internal/package-json.js +37 -0
  56. package/dist/survey/internal/read-guard.d.ts +178 -0
  57. package/dist/survey/internal/read-guard.js +276 -0
  58. package/dist/survey/survey-docs.d.ts +17 -2
  59. package/dist/survey/survey-docs.js +61 -21
  60. package/dist/survey/survey-harness.d.ts +20 -2
  61. package/dist/survey/survey-harness.js +87 -46
  62. package/dist/survey/survey-shape.d.ts +17 -2
  63. package/dist/survey/survey-shape.js +60 -46
  64. package/dist/survey/survey-toolchain.d.ts +16 -1
  65. package/dist/survey/survey-toolchain.js +69 -66
  66. package/dist/survey/survey.js +6 -4
  67. package/dist/survey/types.d.ts +79 -0
  68. package/dist/survey/types.js +2 -6
  69. package/dist/term.d.ts +80 -0
  70. package/dist/term.js +145 -0
  71. package/dist/tokens.js +2 -0
  72. package/dist/toolchain/conformance.js +2 -0
  73. package/dist/toolchain/grade.js +26 -8
  74. package/dist/toolchain/rules.d.ts +13 -4
  75. package/dist/toolchain/rules.js +16 -0
  76. package/dist/toolchain/tsconfig-chain.d.ts +2 -0
  77. package/dist/toolchain/tsconfig-chain.js +32 -8
  78. package/dist/toolchain/types.d.ts +16 -4
  79. package/dist/toolchain/types.js +3 -0
  80. package/package.json +4 -3
  81. package/plugin/skills/customize/SKILL.md +292 -18
  82. package/plugin/src/domain-map.ts +39 -14
  83. package/plugin/src/index.ts +5 -1
  84. package/plugin/src/kind-facet-map.ts +25 -8
  85. package/plugin/src/pack-map.ts +147 -25
  86. package/plugin/src/plugin-map.ts +236 -0
  87. package/templates/core/.claude/agents/Explore.md +0 -1
  88. package/templates/core/.claude/agents/code-implementer.md +4 -4
  89. package/templates/core/.claude/agents/code-reviewer.md +4 -4
  90. package/templates/core/.claude/agents/silent-failure-hunter.md +2 -2
  91. package/templates/core/.claude/agents/test-author.md +7 -5
  92. package/templates/core/.claude/hooks/guard-branch-isolation.mjs +56 -10
  93. package/templates/core/.claude/hooks/guard-double-background.mjs +13 -8
  94. package/templates/core/.claude/hooks/guard-git-push-signed.mjs +12 -9
  95. package/templates/core/.claude/hooks/guard-hub-src-writes.mjs +1890 -38
  96. package/templates/core/.claude/hooks/guard-js-extension.mjs +2 -1
  97. package/templates/core/.claude/hooks/guard-no-commonjs.mjs +7 -5
  98. package/templates/core/.claude/hooks/guard-secret-writes.mjs +7 -5
  99. package/templates/core/.claude/hooks/inject-decision-gate.mjs +12 -9
  100. package/templates/core/.claude/hooks/post-edit-verify.mjs +276 -98
  101. package/templates/core/.claude/rules/agent-dispatch.md +7 -0
  102. package/templates/core/.claude/rules/tests.md +2 -2
  103. package/templates/core/.claude/settings.json +5 -0
  104. package/templates/core/.claude/skills/finishing-work/SKILL.md +58 -10
  105. package/templates/core/.claude/skills/harness-guidance/SKILL.md +16 -7
  106. package/templates/core/.claude/skills/starting-work/SKILL.md +5 -0
  107. package/templates/core/.claude/skills/triaging-ci/SKILL.md +9 -8
  108. package/templates/core/.claude/skills/typescript-guidance/SKILL.md +137 -20
  109. package/templates/core/.claude/skills/typescript-guidance/references/area-catalog.md +135 -0
  110. package/templates/core/.claude/skills/typescript-guidance/references/tooling-sources.md +113 -0
  111. package/templates/core/.claude/skills/writing-commits/SKILL.md +2 -2
  112. package/templates/core/.github/dependabot.yml +18 -0
  113. package/templates/core/.github/workflows/ci.yml +15 -15
  114. package/templates/core/.github/workflows/dependency-review.yml +2 -2
  115. package/templates/core/.github/workflows/security-audit.yml +10 -4
  116. package/templates/core/.prettierignore +4 -0
  117. package/templates/core/CLAUDE.md +53 -3
  118. package/templates/core/README.md +30 -10
  119. package/templates/core/_gitignore +17 -0
  120. package/templates/core/bin/check-exports.mjs +11 -5
  121. package/templates/core/bin/lib/agent-roster.mjs +1 -1
  122. package/templates/core/bin/lib/frontmatter.mjs +4 -2
  123. package/templates/core/bin/lib/harness-rules.mjs +169 -20
  124. package/templates/core/bin/lib/protected-paths.mjs +93 -5
  125. package/templates/core/bin/lib/toolchain-rules.mjs +187 -26
  126. package/templates/core/bin/lib/verify-steps.mjs +29 -11
  127. package/templates/core/bin/verify.mjs +12 -0
  128. package/templates/core/eslint.config.js +5 -0
  129. package/templates/core/package.json +7 -7
  130. package/templates/core/vitest.config.ts +8 -1
  131. package/templates/packs/README.md +35 -24
  132. package/templates/packs/github/files/.claude/skills/reviewing-dependabot-prs/SKILL.md +133 -0
  133. package/templates/packs/github/files/.claude/skills/triaging-scan-alerts/SKILL.md +88 -0
  134. package/templates/packs/github/files/.claude/skills/watching-pr-checks/SKILL.md +94 -0
  135. package/templates/packs/github/files/.github/workflows/claude-pr-review.yml +267 -0
  136. package/templates/packs/github/files/.github/workflows/claude.yml +106 -0
  137. package/templates/packs/github/pack.json +19 -0
  138. package/templates/packs/harness-extras/files/.claude/hooks/guard-readonly-bash.mjs +1122 -65
  139. package/templates/packs/harness-extras/files/.claude/hooks/reinject-compact-handoff.mjs +147 -13
  140. package/templates/packs/{statusline → harness-extras}/files/.claude/hooks/statusline.mjs +6 -2
  141. package/templates/packs/harness-extras/files/.claude/hooks/write-compact-handoff.mjs +227 -44
  142. package/templates/packs/harness-extras/pack.json +18 -13
  143. package/templates/packs/publishing/files/.changeset/README.md +25 -0
  144. package/templates/packs/publishing/files/.changeset/config.json +7 -0
  145. package/templates/packs/publishing/files/.github/release-tools/package.json +9 -0
  146. package/templates/packs/publishing/files/.github/workflows/release.yml +293 -0
  147. package/templates/packs/publishing/files/REUSE.toml +28 -0
  148. package/templates/packs/publishing/files/bin/check-dts-deps.mjs +234 -0
  149. package/templates/packs/publishing/files/bin/check-license-headers.mjs +217 -0
  150. package/templates/packs/publishing/files/bin/check-publish-version.mjs +149 -0
  151. package/templates/packs/publishing/files/bin/lib/npm-publish-args.mjs +86 -0
  152. package/templates/packs/publishing/files/bin/pnpm-publish-shim.mjs +86 -0
  153. package/templates/packs/publishing/pack.json +35 -0
  154. package/templates/packs/{harness-extras → quality}/files/bin/check-file-budget.mjs +6 -4
  155. package/templates/packs/quality/pack.json +29 -0
  156. package/templates/packs/supply-chain/files/.github/workflows/gitleaks.yml +52 -0
  157. package/templates/packs/supply-chain/files/.github/workflows/scorecard.yml +48 -0
  158. package/templates/packs/supply-chain/files/.gitleaks.toml +2 -0
  159. package/templates/packs/supply-chain/pack.json +19 -0
  160. package/templates/packs/worktrees/files/.claude/hooks/ensure-worktree-deps.mjs +283 -0
  161. package/templates/packs/worktrees/files/.claude/hooks/guard-worktree-only.mjs +245 -0
  162. package/templates/packs/worktrees/files/.claude/hooks/repair-core-bare.mjs +335 -0
  163. package/templates/packs/worktrees/files/.claude/skills/working-in-worktrees/SKILL.md +139 -0
  164. package/templates/packs/worktrees/files/.worktreeinclude +11 -0
  165. package/templates/packs/worktrees/pack.json +64 -0
  166. package/templates/packs/statusline/pack.json +0 -31
  167. /package/templates/packs/{statusline → harness-extras}/files/.claude/hooks/statusline-layout.mjs +0 -0
  168. /package/templates/packs/{statusline → harness-extras}/files/.claude/hooks/subagent-statusline.mjs +0 -0
  169. /package/templates/packs/{harness-extras → quality}/files/.claude/agents/type-design-analyzer.md +0 -0
  170. /package/templates/packs/{harness-extras → quality}/files/bin/file-budget-baseline.json +0 -0
@@ -1,3 +1,6 @@
1
+ // SPDX-FileCopyrightText: Copyright the m3l-groundwork contributors
2
+ // SPDX-License-Identifier: MIT
3
+
1
4
  /**
2
5
  * The pack recommendation table `/customize` reads in Step 1 to pre-select
3
6
  * which `templates/packs/` pack(s) to offer, with the evidence shown
@@ -16,50 +19,169 @@ export interface PackRecommendation {
16
19
  }
17
20
 
18
21
  /**
19
- * `harness-extras`'s four artifacts (a type-design-analyzer agent, the
20
- * compaction-handoff hook pair, a read-only Bash guard, a file-budget gate)
21
- * are language- and harness-level, not domain-level -- they apply to any
22
- * TypeScript project regardless of what it's building.
22
+ * `harness-extras`'s remaining artifacts (the compaction-handoff hook pair
23
+ * and a read-only Bash guard) are harness-level, not domain-level -- they
24
+ * apply to any TypeScript project regardless of what it's building. The
25
+ * type-design-analyzer agent and the file-budget gate it once also carried
26
+ * now live in the separate `quality` pack. Its folded-in
27
+ * statusLine scripts read only the stdin payload, `.git/HEAD` (via
28
+ * `node:fs`, never a `git` subprocess) and `os.freemem()`/`os.totalmem()`,
29
+ * and `statusLine` is the only documented surface carrying live
30
+ * `context_window.used_percentage`: no hook event receives token or context
31
+ * data, so "when to compact" can live nowhere else.
23
32
  */
24
33
  function recommendHarnessExtras(): PackRecommendation {
25
34
  return {
26
35
  name: "harness-extras",
27
36
  recommended: true,
28
37
  because:
29
- "its four artifacts (a type-design review agent, compaction-handoff " +
30
- "hooks, a read-only Bash guard, a file-budget gate) are language- " +
31
- "and harness-level, not tied to any particular project kind.",
38
+ "its compaction-handoff hooks (which carry work across a context " +
39
+ "compaction) and read-only Bash guard are harness-level, not tied " +
40
+ "to any particular project kind; its " +
41
+ "statusLine is the only surface that exposes live context-window " +
42
+ "pressure -- no hook event receives token data -- and reads only the " +
43
+ "stdin payload plus local git and memory state. It does occupy five " +
44
+ "terminal rows (session, model, context, quota, work).",
32
45
  };
33
46
  }
34
47
 
35
48
  /**
36
- * `statusline`'s three scripts read only the stdin payload, `.git/HEAD` (via
37
- * `node:fs`, never a `git` subprocess) and `os.freemem()`/`os.totalmem()` --
38
- * nothing about any project kind. `statusLine` is also the only documented
39
- * surface carrying live `context_window.used_percentage`: no hook event
40
- * receives token or context data, so "when to compact" can live nowhere else.
49
+ * `github`'s workflow assumes only GitHub, which this baseline's whole
50
+ * toolchain (CI, Dependency Review, rulesets) already does -- nothing about
51
+ * any project kind. Beyond the mention-mode Action (`claude.yml`) it ships a
52
+ * second workflow, `claude-pr-review.yml`, which posts an automated Claude
53
+ * review comment on every PR, plus three GitHub-operations skills:
54
+ * `reviewing-dependabot-prs` (classifies and batch-merges open Dependabot
55
+ * PRs), `triaging-scan-alerts` (triages open code-scanning alerts to
56
+ * file:line) and `watching-pr-checks`. It is not zero-setup: it adds two
57
+ * GitHub Actions workflows and needs an auth secret the pack cannot create
58
+ * itself.
41
59
  */
42
- function recommendStatusline(): PackRecommendation {
60
+ function recommendGithub(): PackRecommendation {
43
61
  return {
44
- name: "statusline",
62
+ name: "github",
45
63
  recommended: true,
46
64
  because:
47
- "statusLine is the only surface that exposes live context-window " +
48
- "pressure -- no hook event receives token data -- and its scripts " +
49
- "read only the stdin payload plus local git and memory state, so " +
50
- "they apply to any project kind. It does occupy five terminal rows.",
65
+ "the baseline's toolchain already assumes GitHub (CI, Dependency " +
66
+ "Review, rulesets), so a Claude Code Action integration applies to " +
67
+ "any project kind. Alongside the mention-mode Action it adds a " +
68
+ "second workflow (claude-pr-review.yml) that posts an automated " +
69
+ "Claude review comment on every PR, plus skills for classifying and " +
70
+ "batch-merging open Dependabot PRs (reviewing-dependabot-prs), " +
71
+ "triaging open code-scanning alerts to file:line " +
72
+ "(triaging-scan-alerts) and watching a PR's checks " +
73
+ "(watching-pr-checks). It does add two GitHub Actions workflows and " +
74
+ "needs an auth secret configured by hand -- the pack cannot create it.",
75
+ };
76
+ }
77
+
78
+ /**
79
+ * `publishing` is the one kind-scoped pack: a release pipeline (a changesets
80
+ * version PR, then a staged, provenance-attested npm publish via trusted
81
+ * publishing) plus SPDX/REUSE license headers. Secret scanning and OpenSSF
82
+ * Scorecard are not part of it -- they are the separate `supply-chain` pack,
83
+ * since they apply to any repo, published or not. A `library` or `cli`
84
+ * typically publishes an npm package; a
85
+ * `frontend` or `service` is typically deployed instead, so it is not
86
+ * pre-selected there -- but it stays on offer. It is fresh-mode only and
87
+ * needs one-time npm and GitHub setup the pack cannot perform itself.
88
+ */
89
+ function recommendPublishing(answers: InterviewAnswers): PackRecommendation {
90
+ const recommended = answers.kind === "library" || answers.kind === "cli";
91
+ return {
92
+ name: "publishing",
93
+ recommended,
94
+ because: recommended
95
+ ? "a library or CLI typically ships an npm package, so this pack's " +
96
+ "release pipeline applies: a changesets version PR, then a staged, " +
97
+ "provenance-attested npm publish via trusted publishing -- plus " +
98
+ "SPDX/REUSE license headers. The repository-hygiene scans that " +
99
+ "apply to any project, published or not, are the separate " +
100
+ "supply-chain pack. It is fresh-mode only and needs one-time npm and GitHub setup (the " +
101
+ "trusted publisher, the release credentials) the pack cannot do " +
102
+ "itself."
103
+ : "a frontend or service is typically deployed rather than published " +
104
+ "to a package registry, so a changesets/npm release pipeline is not " +
105
+ "recommended by default -- it is still available if this project " +
106
+ "does publish an npm package.",
107
+ };
108
+ }
109
+
110
+ /**
111
+ * `worktrees` is opt-in for every project kind: rather than adding a
112
+ * nicety on top of the existing workflow, it changes the workflow itself --
113
+ * every `src/`/`tests/` write must happen inside a git worktree, not the
114
+ * main checkout. That is a day-to-day process decision for the maintainer,
115
+ * not something any interview answer can infer, so it is never pre-selected
116
+ * -- but it stays on offer.
117
+ */
118
+ function recommendWorktrees(): PackRecommendation {
119
+ return {
120
+ name: "worktrees",
121
+ recommended: false,
122
+ because:
123
+ "it changes the day-to-day workflow rather than adding a nicety on " +
124
+ "top of it: every src/ and tests/ write has to happen inside a git " +
125
+ "worktree instead of the main checkout. That is a process choice " +
126
+ "for whoever works in this repo, not something any project kind " +
127
+ "implies, so it is opt-in -- still available if parallel, isolated " +
128
+ "worktree sessions are how this project wants to work.",
129
+ };
130
+ }
131
+
132
+ /**
133
+ * `supply-chain` is the half carved out of `publishing`: gitleaks secret
134
+ * scanning and an OpenSSF Scorecard run. Neither depends on shipping to a
135
+ * registry -- a deployed frontend or service leaks a secret or drifts on
136
+ * Scorecard's checks just as easily as a published library -- so it is
137
+ * recommended for every project kind. Both are CI workflows only, so it
138
+ * installs into an already-established project without touching its code.
139
+ */
140
+ function recommendSupplyChain(): PackRecommendation {
141
+ return {
142
+ name: "supply-chain",
143
+ recommended: true,
144
+ because:
145
+ "secret scanning (gitleaks) and an OpenSSF Scorecard run are useful " +
146
+ "to any project on GitHub, published or not, and install into an " +
147
+ "already-established project without touching its code.",
148
+ };
149
+ }
150
+
151
+ /**
152
+ * `quality` is the half carved out of `harness-extras`: a per-file size
153
+ * ratchet (`check-file-budget`) over `src/` and `tests/`, and the
154
+ * type-design-analyzer agent reviewing exported symbols' type design. Both
155
+ * are language-level -- they apply to any TypeScript project whatever it is
156
+ * building -- so it is recommended for every project kind.
157
+ */
158
+ function recommendQuality(): PackRecommendation {
159
+ return {
160
+ name: "quality",
161
+ recommended: true,
162
+ because:
163
+ "its per-file size ratchet (check-file-budget) stops src/ and tests/ " +
164
+ "files growing unchecked and its type-design-analyzer agent reviews " +
165
+ "the type design of exported symbols -- both are language-level, not " +
166
+ "tied to any project kind.",
51
167
  };
52
168
  }
53
169
 
54
170
  /**
55
- * Every pack's recommendation for the given interview answers. `answers`
56
- * is currently unused by either pack (neither recommendation varies by
57
- * kind), but the parameter exists now so a future kind-scoped pack (e.g.
58
- * a `publishing` pack recommended only for `kind: "library"`) needs no API
59
- * change here.
171
+ * Every pack's recommendation for the given interview answers. Only
172
+ * `publishing` varies by `answers.kind` today; `harness-extras`, `github`,
173
+ * `supply-chain` and `quality` are recommended for every kind, and
174
+ * `worktrees` is opt-in for every kind.
60
175
  */
61
176
  export function recommendPacks(
62
- _answers: InterviewAnswers,
177
+ answers: InterviewAnswers,
63
178
  ): PackRecommendation[] {
64
- return [recommendHarnessExtras(), recommendStatusline()];
179
+ return [
180
+ recommendHarnessExtras(),
181
+ recommendGithub(),
182
+ recommendPublishing(answers),
183
+ recommendWorktrees(),
184
+ recommendSupplyChain(),
185
+ recommendQuality(),
186
+ ];
65
187
  }
@@ -0,0 +1,236 @@
1
+ // SPDX-FileCopyrightText: Copyright the m3l-groundwork contributors
2
+ // SPDX-License-Identifier: MIT
3
+
4
+ /**
5
+ * The plugin recommendation table `/customize` reads in Step 3 to pre-select
6
+ * which built-in `claude-plugins-official` marketplace plugin(s) to offer,
7
+ * with the evidence shown alongside each recommendation -- the same
8
+ * "inference happens once, visibly, with its reasoning attached" principle
9
+ * `pack-map.ts` applies to `templates/packs/` bundles. A stored, unit-tested
10
+ * module rather than a judgment made afresh each run, so the same interview
11
+ * answers and context always produce the same recommendation.
12
+ */
13
+ import type { InterviewAnswers } from "./kind-facet-map.js";
14
+
15
+ /** The marketplace every plugin in this table ships from. */
16
+ const MARKETPLACE = "claude-plugins-official";
17
+
18
+ /**
19
+ * One plugin's recommendation: its fully-qualified `name@marketplace` id,
20
+ * whether `/customize` pre-selects it, the reasoning shown to the user, and
21
+ * any host prerequisite the plugin cannot install for itself.
22
+ */
23
+ export interface PluginRecommendation {
24
+ readonly id: string;
25
+ readonly recommended: boolean;
26
+ readonly because: string;
27
+ readonly prerequisites?: readonly string[];
28
+ }
29
+
30
+ /**
31
+ * What `/customize` knows beyond the interview answers by the time it
32
+ * recommends plugins: which packs the user chose, and whether the project
33
+ * authors its own skills. Both are optional; an absent field reads as "no".
34
+ */
35
+ export interface PluginRecommendationContext {
36
+ readonly chosenPacks?: readonly string[];
37
+ readonly hasCustomSkills?: boolean;
38
+ }
39
+
40
+ function pluginId(name: string): string {
41
+ return `${name}@${MARKETPLACE}`;
42
+ }
43
+
44
+ /**
45
+ * `context7` is not optional for a project bootstrapped from this baseline:
46
+ * `templates/core/.claude/agents/code-implementer.md` declares
47
+ * `mcpServers: [context7]`, and an agent's MCP grant names a server that
48
+ * some installed source must actually provide -- without the plugin enabled
49
+ * the grant resolves to nothing and fails silently (the gap the
50
+ * `agent-mcp-source` harness rubric rule flags). It works anonymously; an
51
+ * API key only raises rate limits.
52
+ */
53
+ function recommendContext7(): PluginRecommendation {
54
+ return {
55
+ id: pluginId("context7"),
56
+ recommended: true,
57
+ because:
58
+ "the baseline's code-implementer agent declares mcpServers: " +
59
+ "[context7], so without this plugin enabled that grant silently " +
60
+ "resolves to nothing -- the agent loses the library-docs lookup it " +
61
+ "was written to rely on. It works anonymously; a CONTEXT7_API_KEY is " +
62
+ "optional and only raises the rate limit.",
63
+ };
64
+ }
65
+
66
+ /**
67
+ * Every project this bootstrapper produces is TypeScript, so a TypeScript
68
+ * language server gives Claude go-to-definition, references and live
69
+ * diagnostics on any of them. The plugin wires the server up but does not
70
+ * ship the binary itself -- it must already be on `PATH`.
71
+ */
72
+ function recommendTypescriptLsp(): PluginRecommendation {
73
+ return {
74
+ id: pluginId("typescript-lsp"),
75
+ recommended: true,
76
+ because:
77
+ "every project this bootstrapper emits is TypeScript, so a language " +
78
+ "server gives Claude real go-to-definition, find-references and " +
79
+ "compiler diagnostics instead of text search, whatever the project " +
80
+ "kind. The plugin only wires the server up -- the binary itself has " +
81
+ "to be installed separately.",
82
+ prerequisites: [
83
+ "the typescript-language-server binary on PATH (for example via " +
84
+ "`pnpm add -g typescript-language-server typescript`)",
85
+ ],
86
+ };
87
+ }
88
+
89
+ /**
90
+ * The baseline ships a `CLAUDE.md` that is only useful while it stays true.
91
+ * `harness-guidance` refreshes the harness against upstream in periodic
92
+ * sweeps; `claude-md-management` covers the other half -- keeping the file
93
+ * in step with what the project itself learns between those sweeps.
94
+ */
95
+ function recommendClaudeMdManagement(): PluginRecommendation {
96
+ return {
97
+ id: pluginId("claude-md-management"),
98
+ recommended: true,
99
+ because:
100
+ "the baseline's CLAUDE.md only helps while it stays accurate; this " +
101
+ "plugin audits and updates it as the project changes, complementing " +
102
+ "harness-guidance's periodic refresh sweeps, which check the harness " +
103
+ "against upstream guidance rather than against the project's own " +
104
+ "day-to-day drift.",
105
+ };
106
+ }
107
+
108
+ /**
109
+ * The `github` plugin is the natural companion to the `github` pack: that
110
+ * pack's skills (`reviewing-dependabot-prs`, `triaging-scan-alerts`,
111
+ * `watching-pr-checks`) are `gh`-CLI workflows, and the plugin gives Claude
112
+ * structured GitHub access alongside them. Without the pack there is no
113
+ * GitHub-operations workflow in the project for it to serve, so it is only
114
+ * offered, not pre-selected.
115
+ */
116
+ function recommendGithub(
117
+ context: PluginRecommendationContext | undefined,
118
+ ): PluginRecommendation {
119
+ const recommended = context?.chosenPacks?.includes("github") ?? false;
120
+ return {
121
+ id: pluginId("github"),
122
+ recommended,
123
+ because: recommended
124
+ ? "the github pack was chosen, and its skills are gh-CLI GitHub " +
125
+ "workflows -- reviewing and batch-merging Dependabot PRs, triaging " +
126
+ "code-scanning alerts to file:line, watching a PR's checks -- so " +
127
+ "structured GitHub access through this plugin serves them directly."
128
+ : "not preselected because the github pack was not chosen: without " +
129
+ "its GitHub-operations skills there is no workflow here that needs " +
130
+ "structured GitHub access. It stays on offer if the project works " +
131
+ "heavily with issues and PRs anyway.",
132
+ };
133
+ }
134
+
135
+ /**
136
+ * `security-guidance` earns its cost where the attack surface or the
137
+ * rigor bar is highest: a `service` handles untrusted network input, and a
138
+ * `thorough` CI depth is the user asking for the stricter posture
139
+ * explicitly. The cost is real and named rather than hidden -- Edit/Write
140
+ * pattern hooks, a Stop-hook LLM review that spends tokens on every turn,
141
+ * and a commit reviewer -- so it is not pre-selected elsewhere.
142
+ */
143
+ function recommendSecurityGuidance(
144
+ answers: InterviewAnswers,
145
+ ): PluginRecommendation {
146
+ const recommended =
147
+ answers.kind === "service" || answers.ciDepth === "thorough";
148
+ return {
149
+ id: pluginId("security-guidance"),
150
+ recommended,
151
+ because: recommended
152
+ ? "a service handles untrusted input, or thorough CI depth asks for " +
153
+ "the stricter posture, so security review is worth its cost here. " +
154
+ "That cost is real: pattern hooks on every Edit/Write, a Stop-hook " +
155
+ "LLM review that spends tokens at the end of every turn, and a " +
156
+ "commit reviewer."
157
+ : "for a non-service project at standard or minimal CI depth, its " +
158
+ "Edit/Write pattern hooks, per-turn Stop-hook LLM review (tokens " +
159
+ "on every turn) and commit reviewer cost more than the added " +
160
+ "scrutiny is likely to return -- still available if the project " +
161
+ "handles sensitive data.",
162
+ prerequisites: ["Python 3.8+ on PATH (its hooks are Python scripts)"],
163
+ };
164
+ }
165
+
166
+ /**
167
+ * `skill-creator` scaffolds, evaluates and iterates on skills. The baseline's
168
+ * own skills are maintained by this repo, not by the project, so it only
169
+ * pays for itself in a project that writes skills of its own.
170
+ */
171
+ function recommendSkillCreator(
172
+ context: PluginRecommendationContext | undefined,
173
+ ): PluginRecommendation {
174
+ const recommended = context?.hasCustomSkills === true;
175
+ return {
176
+ id: pluginId("skill-creator"),
177
+ recommended,
178
+ because: recommended
179
+ ? "this project authors its own skills, and skill-creator helps " +
180
+ "draft, evaluate and tune them -- including their triggering " +
181
+ "descriptions -- rather than iterating on them by hand."
182
+ : "only useful to a project that authors its own skills; the " +
183
+ "baseline's skills come maintained from upstream, so there is " +
184
+ "nothing here for it to work on yet.",
185
+ };
186
+ }
187
+
188
+ /**
189
+ * `claude-code-setup` recommends automations (hooks, skills, MCP servers,
190
+ * subagents) for a codebase -- which is exactly the job `/customize` is
191
+ * already doing, against a baseline designed for it. Running both would
192
+ * produce two competing sets of suggestions, so it is never pre-selected;
193
+ * it is listed with that reason rather than silently omitted, so the user
194
+ * can see why. TypeScript-toolchain gaps are a distinct question from Claude
195
+ * Code automation and are answered instead by the baseline
196
+ * `typescript-guidance` skill's `gaps` mode.
197
+ */
198
+ function recommendClaudeCodeSetup(): PluginRecommendation {
199
+ return {
200
+ id: pluginId("claude-code-setup"),
201
+ recommended: false,
202
+ because:
203
+ "its automation recommender overlaps what /customize is already " +
204
+ "doing -- tailoring hooks, skills and agents to this project against " +
205
+ "the baseline -- so enabling it would produce a second, competing " +
206
+ "set of suggestions. TypeScript-toolchain gaps are a separate " +
207
+ "question from Claude Code automation, and are answered instead by " +
208
+ "the baseline typescript-guidance skill's gaps mode with " +
209
+ "live-researched, cited recommendations. " +
210
+ "Listed so the choice is visible, not silently left out.",
211
+ };
212
+ }
213
+
214
+ /**
215
+ * Every built-in marketplace plugin's recommendation for the given interview
216
+ * answers and context, always the same seven in the same order: `context7`,
217
+ * `typescript-lsp` and `claude-md-management` unconditionally; `github` when
218
+ * the `github` pack was chosen; `security-guidance` for a `service` or a
219
+ * `thorough` CI depth; `skill-creator` when the project has its own skills;
220
+ * `claude-code-setup` never. A pure function -- identical inputs always
221
+ * produce an equal result.
222
+ */
223
+ export function recommendPlugins(
224
+ answers: InterviewAnswers,
225
+ context?: PluginRecommendationContext,
226
+ ): PluginRecommendation[] {
227
+ return [
228
+ recommendContext7(),
229
+ recommendTypescriptLsp(),
230
+ recommendClaudeMdManagement(),
231
+ recommendGithub(context),
232
+ recommendSecurityGuidance(answers),
233
+ recommendSkillCreator(context),
234
+ recommendClaudeCodeSetup(),
235
+ ];
236
+ }
@@ -4,7 +4,6 @@ description: Fast read-only search agent for locating and understanding code. Us
4
4
  tools: Read, Grep, Glob, Bash, WebSearch, WebFetch
5
5
  disallowedTools: Agent
6
6
  model: claude-haiku-4-5
7
- effort: low
8
7
  maxTurns: 40
9
8
  color: cyan
10
9
  ---
@@ -4,8 +4,8 @@ description: Writer spoke for the TDD build pipeline. Given a contract and a set
4
4
  tools: Read, Write, Edit, Grep, Glob, Bash, mcp__context7__resolve-library-id, mcp__context7__query-docs
5
5
  disallowedTools: Agent
6
6
  mcpServers: [context7]
7
- model: claude-sonnet-5
8
- effort: high
7
+ model: claude-opus-5-5
8
+ effort: medium
9
9
  permissionMode: acceptEdits
10
10
  maxTurns: 40
11
11
  color: cyan
@@ -111,8 +111,8 @@ id first, then query for the specific behavior in question — don't fetch
111
111
  broad documentation you won't use.
112
112
 
113
113
  **Precedence: installed types are the pinned truth and win on conflict.**
114
- This project pins exact dependency versions, while context7 returns docs for
115
- whatever version it has indexed — a disagreement between the two is expected
114
+ The installed dependency version (see `pnpm-lock.yaml`) is what runs, while
115
+ context7 returns docs for whatever version it has indexed — a disagreement between the two is expected
116
116
  and is not evidence the types are wrong. Never widen or reinterpret a type
117
117
  based on context7 output alone; use it to understand behavior the types are
118
118
  silent on, not to override them.
@@ -3,8 +3,8 @@ name: code-reviewer
3
3
  description: Read-only reviewer for this project's source changes. Applies the four-part quality checklist and SOLID checks to a diff. Use after writing or changing source code, before commit.
4
4
  tools: Read, Grep, Glob, Bash
5
5
  disallowedTools: Agent
6
- model: claude-sonnet-5
7
- effort: high
6
+ model: claude-opus-5-5
7
+ effort: medium
8
8
  maxTurns: 40
9
9
  color: blue
10
10
  ---
@@ -69,8 +69,8 @@ on it.
69
69
  a PreToolUse hook then exits 0, which means _allow_, and a verify gate goes
70
70
  green having checked nothing. `.pathname` is percent-encoded while
71
71
  `process.argv[1]` is a decoded path, so it also never matches on a path with
72
- spaces or non-ASCII characters. Inline the helper; a shared module would cost
73
- a hook slot against the baseline's cap.
72
+ spaces or non-ASCII characters. Inline the helper: every hook carries its own copy of this guard block,
73
+ so each one stays an independent entrypoint.
74
74
  - The package's `exports` map is the public contract — flag any change to it
75
75
  as a semver event and check the Conventional Commit matches.
76
76
  - TSDoc on exported symbols.
@@ -3,8 +3,8 @@ name: silent-failure-hunter
3
3
  description: Read-only error-handling auditor. Hunts for silent failures — swallowed exceptions, unchained causes, empty catch blocks, optional-chaining that masks errors, and retry/poll logic that exhausts without surfacing — against this project's error hierarchy and error-handling rules. Use after implementing or changing any code that has try/catch, async/await, optional chaining on fallible calls, or retry/poll loops. Complements code-reviewer (general quality).
4
4
  tools: Read, Grep, Glob, Bash
5
5
  disallowedTools: Agent
6
- model: claude-sonnet-5
7
- effort: high
6
+ model: claude-opus-5-5
7
+ effort: medium
8
8
  maxTurns: 40
9
9
  color: yellow
10
10
  ---
@@ -3,8 +3,8 @@ name: test-author
3
3
  description: Writes Vitest tests for a source export — happy path, failure path, and expectTypeOf type-level tests where the type is the contract. This is the tests-first (RED) spoke of the TDD loop; it writes tests from the documented contract before the implementation exists and confirms they fail for the right reason. Also usable to backfill tests for existing code. It writes tests only — never the implementation, and never reviews implementation quality.
4
4
  tools: Read, Grep, Glob, Edit, Write, Bash
5
5
  disallowedTools: Agent
6
- model: claude-sonnet-5
7
- effort: high
6
+ model: claude-sonnet-5-5
7
+ effort: medium
8
8
  permissionMode: acceptEdits
9
9
  maxTurns: 40
10
10
  color: green
@@ -180,9 +180,11 @@ expect(() =>
180
180
  so the hub can dispatch `code-implementer` precisely. This keeps the suite
181
181
  green and self-resolves visibly: once the real fix lands, `test.fails`
182
182
  reports an XPASS, the signal to flip it to a normal `test`.
183
- - Do not use real filesystem mutations in tests (`mkdtempSync`, `mkdirSync`,
184
- `writeFileSync`, `rmSync`, etc.); mock the filesystem instead
185
- (`vi.spyOn(fs, method)` or `vi.mock('node:fs')`).
183
+ - Mock the filesystem by default (`vi.spyOn(fs, method)` or
184
+ `vi.mock('node:fs')`). The one exception, matching `tests.md`: when the
185
+ unit under test's contract IS real filesystem behavior (a walker, a
186
+ parser reading files), use a per-test `mkdtemp` sandbox torn down in the
187
+ same test -- never a fixed path, and never a mutation outside it.
186
188
  - **The mock target must track the implementation's I/O primitive.** If the
187
189
  implementation moves from one primitive to another, your tests must
188
190
  re-mock the **new** one — the old mock silently stops intercepting
@@ -26,11 +26,15 @@
26
26
  * Blocks by exiting 2 with a message on stderr.
27
27
  */
28
28
  import process from "node:process";
29
- import { realpathSync } from "node:fs";
29
+ import { existsSync, realpathSync } from "node:fs";
30
30
  import { execFileSync } from "node:child_process";
31
31
  import { dirname, relative, resolve } from "node:path";
32
32
  import { fileURLToPath } from "node:url";
33
- import { isProtectedPath } from "../../bin/lib/protected-paths.mjs";
33
+ import {
34
+ canonicalize,
35
+ isAbsoluteLike,
36
+ isProtectedPath,
37
+ } from "../../bin/lib/protected-paths.mjs";
34
38
  export { isProtectedPath };
35
39
 
36
40
  /**
@@ -73,11 +77,13 @@ export function isMainOrDetachedOnMain(git = defaultGit) {
73
77
  return false;
74
78
  }
75
79
 
76
- // Deliberately inlined in every hook rather than shared: caps.ts counts
77
- // .claude/hooks/*.mjs files against a hard limit, so a helper module would
78
- // cost a hook slot. `import.meta.url` is symlink-resolved but `process.argv[1]`
79
- // is not, so comparing them directly is false under any symlinked path and the
80
- // guard body would never run -- exit 0, i.e. fail open.
80
+ // Kept as a duplicated, self-contained block in every hook file rather than
81
+ // imported from a shared helper -- each hook stays a single independent
82
+ // file, which keeps this project's hook count easy to reason about against
83
+ // CLAUDE.md's hook budget. `import.meta.url` is symlink-resolved but
84
+ // `process.argv[1]` is not, so comparing them directly would be false under
85
+ // a symlinked invocation path -- and the guard below would then never run,
86
+ // i.e. silently fail open (exit 0) instead of blocking.
81
87
  function isEntryPoint() {
82
88
  try {
83
89
  return realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
@@ -98,13 +104,53 @@ if (isEntryPoint()) {
98
104
  }
99
105
 
100
106
  const filePath = input.tool_input?.file_path ?? "";
107
+ // Cheap, unscoped pre-check first (same cost as before this file's
108
+ // scoping fix): anything that can't possibly be a src/tests path by raw
109
+ // substring is dismissed with no git shell-out at all.
101
110
  if (!isProtectedPath(filePath)) process.exit(0);
102
111
 
103
- const fileDir = dirname(resolve(filePath));
104
- const git = defaultGitFor(fileDir);
112
+ // Bind git to the nearest EXISTING ancestor of the write target, not the
113
+ // target directory itself -- a Write creating a brand-new nested
114
+ // directory (e.g. `src/newdir/a.ts` where `newdir/` doesn't exist yet)
115
+ // means `git -C <that dir>` fails outright ("cannot change to ... No
116
+ // such file or directory"), which would silently break EVERY git call
117
+ // below, including branch detection -- the guard would then never learn
118
+ // it's on `main` at all and allow the write through unblocked. Any
119
+ // ancestor within the same worktree resolves the same repo root and
120
+ // branch, so walking up costs nothing in accuracy.
121
+ let probeDir = dirname(resolve(filePath));
122
+ while (!existsSync(probeDir)) {
123
+ const parent = dirname(probeDir);
124
+ if (parent === probeDir) break; // filesystem root; let git fail naturally
125
+ probeDir = parent;
126
+ }
127
+ const git = defaultGitFor(probeDir);
128
+ const worktreeRoot = git(["rev-parse", "--show-toplevel"]);
129
+ // Re-check scoped to the file's OWN worktree root (more accurate than a
130
+ // guess from CLAUDE_PROJECT_DIR/cwd, and what this guard already resolves
131
+ // everything else against): a checkout whose own path merely contains the
132
+ // substring "/src/" above the worktree root (e.g. a clone at
133
+ // ~/src/other-project) must not count as protected just because of that.
134
+ // A non-git directory leaves worktreeRoot "" and skips this re-check,
135
+ // same as the header comment's existing "never blocks" behavior. Both
136
+ // sides go through `canonicalize` (case-correct, symlinks resolved)
137
+ // before comparing -- see its own doc comment for why a raw string
138
+ // comparison isn't safe here (macOS's case-insensitive filesystem and its
139
+ // `/tmp`/`/var` symlinks both make two spellings of the identical file
140
+ // compare as different paths otherwise). Only attempted for an ABSOLUTE
141
+ // filePath: canonicalize() resolves a relative one against this process's
142
+ // own cwd, which isn't necessarily the anchor a relative payload was
143
+ // meant against -- the fast pre-check above already matched it correctly
144
+ // as-is, so a relative path just keeps that verdict.
145
+ if (
146
+ worktreeRoot !== "" &&
147
+ isAbsoluteLike(filePath) &&
148
+ !isProtectedPath(canonicalize(filePath), canonicalize(worktreeRoot))
149
+ ) {
150
+ process.exit(0);
151
+ }
105
152
 
106
153
  if (isMainOrDetachedOnMain(git)) {
107
- const worktreeRoot = git(["rev-parse", "--show-toplevel"]);
108
154
  const inDifferentTree =
109
155
  worktreeRoot !== "" && resolve(worktreeRoot) !== resolve(process.cwd());
110
156
  const location = inDifferentTree