@monte3l/groundwork 0.1.0-next.1 → 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 +16 -8
  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
@@ -53,9 +53,173 @@ may or may not touch.
53
53
 
54
54
  ## Step 0 — Reconcile (adopt mode only)
55
55
 
56
- 1. Look for `.groundwork/inventory.json`. **Absent → this is a fresh
57
- bootstrap; skip straight to Step 1.** Everything below this step applies
58
- only when it exists.
56
+ 1. Look for `.groundwork/inventory.json`.
57
+ - **Absent, and no `.groundwork/` directory either → this is a fresh
58
+ bootstrap; skip straight to Step 1.** Everything below this step applies
59
+ only when an inventory exists.
60
+ - **Absent, but `.groundwork/` exists → a previous CLI adopt run did not
61
+ complete** (the CLI deletes the old inventory before it stages anything
62
+ and writes the new one last, so no inventory beside a `.groundwork/`
63
+ directory means the run died part-way). **Stop. Never fall through to
64
+ the fresh flow** -- that would run fresh-mode tailoring on an
65
+ established project. Tell the user: "`.groundwork/` is incomplete: a
66
+ previous adopt run did not finish, or `inventory.json` was removed.
67
+ If this project was never adopted (it was bootstrapped fresh), delete
68
+ `.groundwork/` and run `/customize` again. Otherwise re-run
69
+ `npx @monte3l/groundwork@rc .` and then run `/customize` again."
70
+ Change nothing.
71
+ - **Present, but not valid JSON, or its `schemaVersion` is not an integer
72
+ of at least 1 → stop and change nothing.** Tell the user: "`.groundwork/inventory.json`
73
+ is not valid JSON or has no usable `schemaVersion`. Re-run
74
+ `npx @monte3l/groundwork@rc .` and then run `/customize` again."
75
+
76
+ **Check `inventory.schemaVersion` before reading anything else.** This
77
+ skill understands schema versions **1 through 5** (the highest it knows is
78
+ 5). If `schemaVersion` is **higher than 5**, the CLI that wrote it is newer
79
+ than this plugin: **stop and change nothing**, do not interpret the
80
+ inventory (a newer schema may have renamed or repurposed fields, and a
81
+ confident misreading is worse than none), and tell the user to update the
82
+ plugin and re-run `/customize`. Say which update applies to the copy
83
+ that is running. For the plugin install, run `/plugin update`. For a
84
+ project-local copy in `.claude/skills/customize/`, `/plugin update` does
85
+ not touch it, and re-running the CLI alone does not either: the CLI
86
+ never overwrites a copy that differs (nor removes any other entry the
87
+ project owns there, nor writes through a symlinked `.claude`), it writes a
88
+ fresh one to `.groundwork/customize/`, which Claude Code does not load. So delete
89
+ that directory first (this discards any local edits the project made to
90
+ its copy) and then re-run `npx @monte3l/groundwork@rc .`. A
91
+ copy in `.groundwork/customize/` is refreshed by re-running the CLI. What
92
+ each version added:
93
+
94
+ | `schemaVersion` | Adds | If absent |
95
+ | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
96
+ | 1 | the survey, `conflicts` | (the floor) |
97
+ | 2 | `packs` | no packs to offer; skip the pack question |
98
+ | 3 | `harnessGrade`, `harnessConformance` | skip the harness-grade starting point |
99
+ | 4 | `toolchainGrade`, `toolchainConformance` | skip the toolchain-grade starting point |
100
+ | 5 | `stagedBaseline` (`{ dir, suffix, files }`): the baseline additions staged at `.groundwork/baseline/`, each as `{ path, staged, sha256 }`; `stagedPacks` (per pack: `{ name, dir, suffix, manifest, files }`): every pack staged at `.groundwork/packs/<name>/`, its manifest and each file as `{ path, staged, sha256 }` | schema 1-4 only: read baseline additions from `inventory.templateRoot`; packs use the unsuffixed copy under `.groundwork/packs/` |
101
+
102
+ **For a schema 5 inventory only: verify the staged baseline now, before
103
+ anything is offered to the user.** A schema 1-4 inventory has no
104
+ `stagedBaseline`: skip this whole block (every bullet below, including
105
+ its stop list) and use the `inventory.templateRoot` fallback in Round 1.
106
+ The inventory lives in the project tree and is untrusted input, so check
107
+ it before trusting any entry:
108
+
109
+ - `stagedBaseline.dir` must equal `.groundwork/baseline` exactly, and
110
+ `stagedBaseline.suffix` must equal `.staged` exactly. Take both values
111
+ from this list, not from the inventory: a `dir` of `.` with an empty
112
+ `suffix` would make you "verify" live project files.
113
+ - For every entry of `inventory.stagedBaseline.files`, require
114
+ `staged === path + ".staged"`, and require `path` to be relative, free
115
+ of any `..` segment, and not absolute. Reject the entry if `path` is
116
+ `""` or `"."`. Reject it too if `path` contains a `\` or a `:` (a
117
+ Windows `..\..` or `C:foo` would slip past the other checks). Reject a
118
+ duplicate `path` or a duplicate `staged` among the entries. These fields
119
+ use `/` separators on every platform: `conflicts[].relPath`,
120
+ `packs[].fileConflicts[].relPath`, `stagedBaseline.dir`,
121
+ `stagedBaseline.files[].path` and its `.staged` name, `stagedPacks[].dir`,
122
+ `stagedPacks[].files[].path` and `stagedPacks[].files[].staged`. Other paths in
123
+ the inventory are not normalized: `templateRoot` and `targetDir` are
124
+ absolute native paths, as are the survey's tsconfig chain entries.
125
+ - The `absent` conflicts in `inventory.conflicts` and the paths in
126
+ `stagedBaseline.files` must name the same set of files. If they
127
+ disagree (an `absent` conflict with no staged entry, or a staged entry
128
+ that is not an `absent` conflict), that is a mismatch: stop, with the
129
+ same message as a hash mismatch, before anything is offered or approved.
130
+ - Read each staged file at `.groundwork/baseline/<staged>` **once**.
131
+ It must exist, and the SHA-256 of its **raw bytes** (no end-of-line
132
+ normalization, no decoding) must equal the entry's `sha256`. Compute it
133
+ with, for example, this one-liner (it targets a POSIX shell or Git Bash,
134
+ not `cmd.exe` or an old PowerShell), passing the file as a single quoted
135
+ argument in place of `<file>`:
136
+ `node -e 'process.stdout.write(require("crypto").createHash("sha256").update(require("fs").readFileSync(process.argv[1])).digest("hex"))' <file>`
137
+ Keep those same bytes for the install in Round 1 (substitute tokens into
138
+ them) rather than reading the file a second time.
139
+ - **Staged packs, in the same block.** For every entry of
140
+ `inventory.stagedPacks`:
141
+ - `dir` must equal `.groundwork/packs/<name>`, built from the entry's
142
+ own `name`, and `suffix` must equal `.staged`. Take both from this
143
+ list, not from the inventory.
144
+ - `name` must be a single path segment: not empty, not `.` or `..`, and
145
+ free of `/`, `\` and `:`. Reject a duplicate pack `name`.
146
+ - `manifest.path` must equal `pack.json` and `manifest.staged` must
147
+ equal `pack.json.staged`.
148
+ - For every entry of its `files`, require `staged === path + ".staged"`,
149
+ and apply the same path checks as the staged baseline's files above
150
+ (relative, no `..` segment, not absolute, not `""` or `"."`, no `\` or
151
+ `:`). Reject a duplicate `path` or a duplicate `staged` among one
152
+ pack's files.
153
+ - The pack names in `inventory.packs` and in `inventory.stagedPacks`
154
+ must be the same set, and for each pack its `fileConflicts[].relPath`
155
+ set must equal its `files[].path` set.
156
+ - Read the staged manifest at `<dir>/pack.json.staged` and each staged
157
+ file at `<dir>/files/<staged>` once, and apply the SHA-256 check above
158
+ to the raw bytes of each. Keep the bytes Step 0.1 read (hash-checked
159
+ when a command could run): Round 1's install and the prototype-key
160
+ check in Step 0.4(c) use them, with no second read.
161
+ - **No shell tool available.** If a Bash or other command-running tool is
162
+ available, compute the hash as above. If you cannot run a command, do
163
+ not work around it: write no scratch script; while verifying, make no
164
+ `Write` or `Edit` outside `.groundwork/` (Round 1's confirmed installs are
165
+ not part of verification), dispatch no subagent to look for a shell, and
166
+ search for a command tool at most once. Skip only the SHA-256
167
+ comparison. Run every other check in this block with the file tools
168
+ (`Read`, `Glob`, `Grep`): the files exist, `staged === path + ".staged"`,
169
+ the paths are relative with no `..`, `\` or `:`, no duplicates, `dir`
170
+ and `suffix` exact, the `absent` conflicts and `stagedBaseline.files`
171
+ name the same set, and no extra staged file exists: list the staged
172
+ files with the `Glob` pattern `.groundwork/baseline/**/*.staged` and
173
+ compare with `stagedBaseline.files`. A `Glob` can skip hidden paths
174
+ (most staged files are dot-paths), honour an ignore file or truncate a
175
+ long list, so a result shorter than `stagedBaseline.files.length`, or
176
+ that looks truncated, is undetermined: report it in the Step 0.4 summary
177
+ and continue, since every listed entry's existence is already checked
178
+ one by one; only an extra `*.staged` file, one whose path is not in
179
+ `stagedBaseline.files`, stops the run. Run the same structural checks
180
+ for `inventory.stagedPacks`: each pack's `<dir>/pack.json.staged` and
181
+ every `<dir>/files/<staged>` exists, `staged === path + ".staged"` with
182
+ paths that are relative and free of `..`, `\` and `:`, no duplicates,
183
+ `name`, `dir`, `suffix`, `manifest.path` and `manifest.staged` exact,
184
+ the pack names in `inventory.packs` and `inventory.stagedPacks` the
185
+ same set, and each pack's `fileConflicts[].relPath` and `files[].path`
186
+ the same set. List each pack's staged files with the `Glob` pattern
187
+ `.groundwork/packs/**/*.staged` (the same hidden-path and
188
+ truncation caveats apply) and compare with every listed manifest and
189
+ file: an extra `*.staged` file at any depth under `.groundwork/packs/` that is neither a listed manifest nor a listed file stops the run. This is a partial no-shell
190
+ substitute for what the hash proves, and it is not tamper-resistance.
191
+ Then spend no further turns on verification and go on to the deep
192
+ read. State this plainly at the top of your first message to the user (the Step 0.4 summary, not a
193
+ separate earlier stop), and ask whether to continue or stop in that
194
+ same confirmation: the SHA-256 check was skipped because no command
195
+ could be run; a passing check would only have proven that the staging
196
+ is complete and matches the inventory, not that the files are
197
+ untampered; and which structural checks you did verify instead. Also
198
+ record the skip in `.groundwork/adoption-report.md` when you write the
199
+ findings back in Step 0.3. A structural failure, including an extra staged file, still stops, exactly as the stop list below says.
200
+ - An empty `files` list is legitimate (nothing was missing from the
201
+ project) and creates no `.groundwork/baseline/` directory; that alone is
202
+ not a failure.
203
+ - **For a schema 5 inventory, a `stagedBaseline` that is missing, not an
204
+ object, or whose `files` is not an array also stops the run** (a
205
+ `files: {}` is not an empty list); so does a `stagedPacks` that is
206
+ missing or not an array, or a pack entry that is not an object or whose
207
+ `files` is not an array. **Any invalid entry, missing file or hash mismatch (including a
208
+ wrong `dir` or `suffix`), any duplicate `staged` or `path` or pack `name`, any
209
+ `absent` conflicts and `stagedBaseline.files` that do not name the same
210
+ set, any pack whose names or file paths disagree with `inventory.packs`, and
211
+ (no-shell path) an extra `*.staged` file not named in
212
+ `stagedBaseline.files`, a pack's `files` or a pack's manifest
213
+ means stop -- all of Round 1, including conflicts and packs -- and
214
+ change nothing.** This is the single stop list for the staged baseline and the staged packs.
215
+ Tell the user: "The staged baseline in `.groundwork/baseline/` or a staged pack in `.groundwork/packs/` is incomplete or does not match `.groundwork/inventory.json` (<the first entry that failed and why>). Re-run `npx @monte3l/groundwork@rc .` and then run `/customize` again." **Never fall back to `inventory.templateRoot` for a schema 5 inventory, packs included:** that fallback exists for a schema 1-4 inventory only, and only for the baseline additions (a schema 1-4 inventory's packs are read from their unsuffixed copy, see Step 0.4(c) and Round 1).
216
+ - What a passing check proves: the staging is complete and matches the
217
+ inventory. It does **not** prove the files are untampered -- anyone who
218
+ can edit the staged files can edit the inventory's hashes too.
219
+
220
+ An inventory with `schemaVersion` below 5 has no staged copy; its
221
+ approved additions are read from `inventory.templateRoot` (see Round 1).
222
+
59
223
  2. **The deep read.** The CLI's survey is an index, not an interpretation —
60
224
  it flagged what it found but could not parse (`needsReading: true` on
61
225
  git-hook config, workflow files; anything in `survey.undetermined`) and
@@ -95,7 +259,9 @@ may or may not touch.
95
259
  3. **Write the findings back** into `.groundwork/adoption-report.md`,
96
260
  replacing the CLI's index-level sections ("a `lefthook.yml` exists")
97
261
  with semantic ones ("pre-push runs lint and typecheck; tests do not
98
- gate").
262
+ gate"). If the no-shell bullet in step 1 applied, carry its skip note
263
+ into the rewritten report: this step replaces the report's sections, so
264
+ a note written earlier would be dropped.
99
265
  4. **Confirm.** Give a short summary in chat, then ask **one**
100
266
  `AskUserQuestion` covering: (a) _did this miss anything about your
101
267
  project?_ — the free-text option is the point of this question, not a
@@ -114,8 +280,26 @@ may or may not touch.
114
280
  dropped. This is index-level evidence from the CLI, not a kind-based
115
281
  judgment — see Step 3's note on revisiting it once the interview confirms
116
282
  the project's kind.
283
+
284
+ The same question also carries (d) if the no-shell bullet applied: say the
285
+ SHA-256 check was skipped, what a passing check would have proven,
286
+ which checks ran instead and any undetermined result, and
287
+ ask whether to continue or stop.
288
+
289
+ Before offering any pack, check each staged `.groundwork/packs/<name>/pack.json.staged` (the bytes Step 0.1 read, hash-checked when a command could run; not a fresh read):
290
+ if `__proto__`, `constructor` or `prototype` is a key of its `wiring.settings`
291
+ (hook event names), `wiring.settingsTopLevel` or `wiring.packageScripts`
292
+ (script names), stop. The project tree is untrusted, and the CLI refuses such a
293
+ pack before staging it, so a staged manifest carrying one was edited after
294
+ staging. Name the pack, the field and the key, tell the user to delete
295
+ `.groundwork/` and re-run the CLI, and offer nothing from this run. Change nothing.
296
+ For a schema 1-4 inventory (the layout of the CLI releases before the `.staged` convention) there is no `.staged` copy and no hash: read the unsuffixed `.groundwork/packs/<name>/pack.json` (not hash-verified) and run this same prototype-key check on it.
297
+
117
298
  5. **Record the confirmed decisions** to `.groundwork/adoption-decisions.json`
118
299
  so a compacted or resumed session doesn't silently lose them and re-ask.
300
+ A CLI re-run deletes this file along with the old inventory, because its
301
+ decisions were made against the previous staging; a decisions file found
302
+ beside a fresh inventory therefore belongs to this inventory.
119
303
 
120
304
  ## Step 1 — Interview
121
305
 
@@ -206,25 +390,53 @@ CI, husky locally, whatever it is), not to `lefthook.yml`/`ci.yml` by name.
206
390
  Concretely, adopt-mode Round 1 applies exactly three things, all already
207
391
  confirmed in Step 0.4:
208
392
 
209
- - The **approved additions** — files `templates/core` (at
210
- `inventory.templateRoot`) would add that the project doesn't have and the
211
- user approved adding.
393
+ - The **approved additions** — files `templates/core` would add that the
394
+ project doesn't have and the user approved adding. For a schema 5 inventory, install them from the staged copy Step 0.1 already verified,
395
+ using the bytes you read then (Step 0.1 also already checked that the
396
+ staged files match the `absent` conflicts). Write them to the project at `path` (the
397
+ `.staged` suffix stripped, never to the staged name), filling in the
398
+ `__KEY__` tokens with the project's real values as you copy, same as
399
+ staged packs. Only a **schema 1-4** inventory has no staged copy and reads
400
+ additions from `inventory.templateRoot`; if that path no longer exists (a
401
+ pruned `npx` cache, a deleted temp checkout), say so and ask for a CLI
402
+ re-run rather than guessing at the baseline's contents.
212
403
  - The **approved conflict resolutions** — for each divergent file the user
213
404
  decided on, apply that decision (keep theirs / take groundwork's / merge
214
405
  the named keys).
215
406
  - The **approved packs** — installed from `.groundwork/packs/<name>/` (the
216
- CLI's staged, self-contained copy — never `inventory.templateRoot`, which
217
- may not exist by the time this runs). Before installing, call
407
+ CLI's staged, self-contained copy of inert `.staged` files — never
408
+ `inventory.templateRoot`, which may not exist by the time this runs). Before installing, call
218
409
  `recommendPacks(answers)` from `pack-map.ts` (alongside this file, same
219
410
  copy mechanism as `kind-facet-map.ts`) with the now-confirmed
220
- `InterviewAnswers` and compare its verdict against Step 0.4's decision. For
221
- both shipped packs this never disagrees (neither recommendation varies by
222
- kind), but a future kind-scoped pack might — if it does, surface the
223
- conflict rather than silently overriding the user's Step 0.4 answer,
224
- mirroring Step 4's "the one exception" rule for guidance findings. To
225
- install: copy `.groundwork/packs/<name>/files/` into the project (respecting
226
- any approved per-file conflict decision the same way the baseline's own
227
- additions are applied), then translate `pack.json`'s `wiring` by hand
411
+ `InterviewAnswers` and compare its verdict against Step 0.4's decision.
412
+ `harness-extras`, `github`, `supply-chain`, `quality`, and `worktrees`
413
+ never disagree (none of the five's recommendation varies by kind or
414
+ answer -- `worktrees` is always `recommended: false`, since it changes the
415
+ day-to-day workflow rather than adding a nicety, and the other four are
416
+ always `recommended: true`), but `publishing`'s does (recommended for
417
+ `library`/`cli`, not for `frontend`/`service`) — if the comparison surfaces a real
418
+ conflict there or for any future kind-scoped pack, raise it rather than
419
+ silently overriding the user's Step 0.4 answer, mirroring Step 4's "the
420
+ one exception" rule for guidance findings. Before installing, check the
421
+ pack's own `modes`: a pack whose `modes` doesn't include `"adopt"` (today,
422
+ `publishing` — its release flow encodes decisions too project-specific to
423
+ apply blind) is never auto-installed here even if staged and approved;
424
+ instead, state in Step 6 that it needs a manual install (point at `.groundwork/packs/<name>/` and the pack's own `adoptNotes`, and say to strip the `.staged` suffix from every name when copying by hand and to replace every `__KEY__` token with the project's real value) and stop
425
+ there for that pack. Before merging any staged `pack.json.staged` wiring, repeat
426
+ the prototype-key check from Step 0.4(c): if a key of `wiring.settings`,
427
+ `wiring.settingsTopLevel` or `wiring.packageScripts` is `__proto__`,
428
+ `constructor` or `prototype`, stop the same way and change nothing. For an
429
+ adopt-capable pack, install each file from
430
+ `.groundwork/packs/<name>/files/<path>.staged`, using the bytes Step 0.1 read (hash-checked when a command could run), and write it to the project at `path` (the `.staged` suffix
431
+ stripped, never to the staged name), filling in the `__KEY__` tokens with
432
+ the project's real values as you copy (respecting any approved per-file
433
+ conflict decision the same way the baseline's own additions are applied).
434
+ For a schema 1-4 inventory, install each file from the unsuffixed
435
+ `.groundwork/packs/<name>/files/<path>` instead (no `.staged` suffix to strip,
436
+ not hash-verified; token substitution still applies).
437
+ Then, for either schema, translate `pack.json`'s `wiring` by hand, reading it
438
+ from `pack.json.staged` (the bytes Step 0.1 read) for a schema 5 inventory and
439
+ from the unsuffixed `pack.json` for schema 1-4,
228
440
  against what Step 0.2's deep read already found — a `.claude/settings.json`
229
441
  hook fragment merges the same way the baseline's own hook entries would;
230
442
  `wiring.settingsTopLevel` is a set of top-level keys (e.g. `statusLine`)
@@ -233,7 +445,13 @@ confirmed in Step 0.4:
233
445
  `mergeSettingsTopLevel` treats a differing value as a hard error and this
234
446
  hand-applied path must not be laxer than the automated one (also check
235
447
  `.claude/settings.local.json` and the user's `~/.claude/settings.json`,
236
- either of which can shadow a project `statusLine`);
448
+ either of which can shadow a project `statusLine`). For `harness-extras`
449
+ specifically, a `statusLine`/`subagentStatusLine` collision is never a
450
+ reason to fail the whole pack install: skip just those two settings keys
451
+ and the three statusline scripts (`statusline.mjs`, `statusline-layout.mjs`,
452
+ `subagent-statusline.mjs`) and install the pack's other artifacts (the
453
+ compaction-handoff hooks and `guard-readonly-bash`) normally, stating the
454
+ skip plainly in Step 6;
237
455
  `wiring.verifySteps` becomes a step in whatever this project's real gate
238
456
  runner is (a `package.json` script plus a line in its `lefthook.yml`/
239
457
  `.husky/pre-push`/CI workflow, written by hand to match its actual shape)
@@ -241,6 +459,49 @@ confirmed in Step 0.4:
241
459
  other artifacts and state plainly in Step 6 that the gate was not wired,
242
460
  rather than inventing a runner the project never asked for.
243
461
 
462
+ **Plugins (both modes), after packs are settled above.** Build a
463
+ `PluginRecommendationContext`: `chosenPacks` is whatever the packs decision
464
+ just above actually landed on (fresh: what `--pack` installed at bootstrap
465
+ time, detectable from the installed files -- e.g. `.changeset/config.json`
466
+ means `publishing`, `.github/workflows/claude-pr-review.yml` means `github`
467
+ (the only marker `plugin-map.ts` actually reads; `harness-extras` has no
468
+ plugin that varies by its presence, so it needs no marker here); adopt: the
469
+ packs Step 0.4(c) confirmed, revised by this step's own `recommendPacks`
470
+ comparison if it changed anything); `hasCustomSkills` is `false` for a fresh
471
+ bootstrap (nothing has authored a skill yet) and, for adopt mode, whatever
472
+ Step 0's survey found beyond the baseline's own known skill names and any
473
+ already-installed pack's skills (e.g. the `github` pack's three). Call
474
+ `recommendPlugins(answers, context)` from `plugin-map.ts`
475
+ (alongside this file, same copy mechanism as `pack-map.ts`) with the
476
+ now-confirmed `InterviewAnswers`, and ask **one** `AskUserQuestion`
477
+ (multi-select) offering all seven, pre-selected per each entry's
478
+ `recommended` boolean with its `because` shown as the evidence -- same
479
+ "visible reasoning" principle as every other inference in this skill.
480
+
481
+ Write every confirmed `true` entry into `.claude/settings.json`'s
482
+ `enabledPlugins` (`{"<id>": true}` per entry, e.g.
483
+ `"context7@claude-plugins-official": true`) -- additive and entry-by-entry,
484
+ same discipline as the CLI's own `mergeSettingsHooks` (which owns the
485
+ `hooks` block entry-by-entry, as opposed to `mergeSettingsTopLevel`'s
486
+ whole-key-at-a-time semantics): never remove or flip an entry the project
487
+ already sets explicitly (an existing `false` is a decision the user made,
488
+ not an oversight to correct), and never touch `.claude/settings.local.json`
489
+ or the user's own `~/.claude/settings.json` scope. `claude-plugins-official`
490
+ is a built-in marketplace, so no `extraKnownMarketplaces` entry is needed
491
+ for any of the seven.
492
+
493
+ **Committing this entry does not install the plugin for anyone.** A
494
+ project-scope `enabledPlugins: true` with no local install produces no
495
+ folder-trust auto-prompt -- Claude Code's `/plugin` Errors tab instead shows
496
+ "enabled in project settings but isn't installed here" until someone runs
497
+ the install by hand. So for every newly-`true` entry, print the exact
498
+ follow-up command in Step 6's report: `claude plugin install <id> --scope
499
+ project` (or `/plugin install <id>` inside a running session) -- the user,
500
+ and every collaborator who pulls this change, still has to run it once. Do
501
+ the same for each recommendation's `prerequisites` (the
502
+ `typescript-language-server` binary, Python 3.8+) -- print them as
503
+ follow-ups, never attempt to install them.
504
+
244
505
  Nothing else is touched. A project file the user didn't approve a change to
245
506
  stays exactly as it was.
246
507
 
@@ -263,6 +524,12 @@ Step 2's plan to decide which facet gets the deepest attention this run,
263
524
  not which facets it's allowed to touch — and enters plan mode with a
264
525
  remediation plan if it finds drift.
265
526
 
527
+ Also offer `typescript-guidance`'s `gaps` mode here, for gaps rather than
528
+ drift — what TypeScript-ecosystem tooling the project is missing entirely, as
529
+ opposed to either sweep's "is what's already configured still current." It
530
+ is not a third mandatory sweep: run it only if the user wants a tooling-gap
531
+ pass alongside the two refreshes.
532
+
266
533
  **In adopt mode**, a sweep's domain is the project's real files, classified
267
534
  by `domain-map.ts`'s `classifyPath` (the same module and glob lists that
268
535
  guard the emitted baseline — broadened to cover common non-baseline
@@ -303,3 +570,10 @@ the CLI's report missed (if anything), which conflicts were resolved and
303
570
  how, any domain-map coverage gap Step 4 surfaced, and **which packs were
304
571
  installed and what each wired** (or, for a pack whose gate had no runner to
305
572
  attach to, that it was skipped and why).
573
+
574
+ Also list **which plugins were enabled** (each newly-`true`
575
+ `enabledPlugins` entry, with the exact `claude plugin install <id> --scope
576
+ project` follow-up command it still needs) and which were offered but
577
+ declined, plus any prerequisite named against an enabled plugin
578
+ (`typescript-language-server` on `PATH`, Python 3.8+) as a follow-up the
579
+ user still has to satisfy.
@@ -1,13 +1,26 @@
1
+ // SPDX-FileCopyrightText: Copyright the m3l-groundwork contributors
2
+ // SPDX-License-Identifier: MIT
3
+
1
4
  /**
2
- * Which files each guidance sweep is responsible for. Used two ways:
3
- * against the emitted baseline, to confirm neither sweep has a blind spot
4
- * in `templates/core` before reporting a clean run (unit-tested directly
5
- * against the real tree in `tests/domain-map.test.ts`, so a new template
6
- * file added later can't silently fall outside both domains without a test
7
- * noticing); and in adopt mode, to classify a real pre-existing project's
8
- * files, which is why the glob lists also cover common non-baseline
9
- * equivalents (`.eslintrc.*`, `jest.config.*`, `.husky/**`, ...) alongside
10
- * the baseline's own exact filenames.
5
+ * Which files each guidance sweep is responsible for. A "guidance sweep" is
6
+ * the live research pass `/customize` runs over official upstream sources for
7
+ * one domain: `typescript-guidance` sweeps the TypeScript toolchain against
8
+ * official TypeScript sources, and `harness-guidance` sweeps the Claude Code
9
+ * harness against official Anthropic sources. Each sweep covers a fixed set of
10
+ * "facets" (research topics within its domain, such as `compiler-config-flags`
11
+ * or `hooks-lifecycle`); the interview's project "kind" (a `ProjectKind`: the
12
+ * archetype -- library, cli, frontend or service -- the project is classified
13
+ * as) only changes which facets get the most emphasis, never which files a
14
+ * sweep owns. This module answers the file-ownership question.
15
+ *
16
+ * Used two ways: against the emitted baseline, to confirm neither sweep has a
17
+ * blind spot in `templates/core` before reporting a clean run (unit-tested
18
+ * directly against the real tree in `tests/domain-map.test.ts`, so a new
19
+ * template file added later can't silently fall outside both domains without
20
+ * a test noticing); and in adopt mode, to classify a real pre-existing
21
+ * project's files, which is why the glob lists also cover common
22
+ * non-baseline equivalents (`.eslintrc.*`, `jest.config.*`, `.husky/**`,
23
+ * ...) alongside the baseline's own exact filenames.
11
24
  */
12
25
 
13
26
  /** Simple glob support: `**` matches any sequence (including `/`), `*` matches within a segment. */
@@ -28,7 +41,7 @@ export function matchesAnyGlob(
28
41
  return globs.some((glob) => globToRegExp(glob).test(path));
29
42
  }
30
43
 
31
- /** Every TypeScript-facing file `typescript-guidance` is responsible for. */
44
+ /** Every TypeScript-facing file the `typescript-guidance` sweep is responsible for. */
32
45
  export const TYPESCRIPT_DOMAIN_GLOBS = [
33
46
  "tsconfig*.json",
34
47
  "**/tsconfig*.json",
@@ -60,6 +73,11 @@ export const TYPESCRIPT_DOMAIN_GLOBS = [
60
73
  "bin/lib/*.mjs",
61
74
  "bin/lib/*.json",
62
75
  ".github/workflows/*.yml",
76
+ ".github/dependabot.yml",
77
+ ".github/release-tools/**",
78
+ ".changeset/**",
79
+ ".gitleaks.toml",
80
+ "REUSE.toml",
63
81
  "docs/research/typescript-refresh.md",
64
82
  "src/**",
65
83
  "tests/**",
@@ -69,16 +87,22 @@ export const TYPESCRIPT_DOMAIN_GLOBS = [
69
87
  * Harness-grader files that live under `bin/`. `bin/*.mjs` and
70
88
  * `bin/lib/*.mjs` are typescript-domain globs, so without this list the
71
89
  * grader's own rules would be swept by `typescript-guidance` -- wrong,
72
- * because they encode Claude Code harness guidance. Consulted before the
73
- * typescript list in `classifyPath`.
90
+ * because they encode Claude Code harness guidance. The Claude Code Action
91
+ * workflows (`claude.yml`, and the `github` pack's automated-review
92
+ * `claude-pr-review.yml`) are here for the same reason against
93
+ * `.github/workflows/*.yml`: their trigger, action pin and model choice are
94
+ * Anthropic guidance, not toolchain. Consulted before the typescript list in
95
+ * `classifyPath`.
74
96
  */
75
97
  export const HARNESS_OVERRIDE_GLOBS = [
76
98
  "bin/check-harness.mjs",
77
99
  "bin/lib/harness-rules.mjs",
78
100
  "bin/lib/frontmatter.mjs",
101
+ ".github/workflows/claude.yml",
102
+ ".github/workflows/claude-pr-review.yml",
79
103
  ] as const;
80
104
 
81
- /** Every `.claude/`-facing file `harness-guidance` is responsible for. */
105
+ /** Every `.claude/`-facing file the `harness-guidance` sweep is responsible for. */
82
106
  export const HARNESS_DOMAIN_GLOBS = [
83
107
  ".claude/settings.json",
84
108
  ".claude/settings.local.json",
@@ -90,11 +114,12 @@ export const HARNESS_DOMAIN_GLOBS = [
90
114
  ".claude/commands/**",
91
115
  ".claude-plugin/**",
92
116
  ".mcp.json",
117
+ ".worktreeinclude",
93
118
  "CLAUDE.md",
94
119
  "docs/research/harness-refresh.md",
95
120
  ] as const;
96
121
 
97
- /** Files neither sweep governs by design -- not a gap, an explicit exclusion. */
122
+ /** Files neither guidance sweep governs by design -- not a gap, an explicit exclusion. */
98
123
  export const NEUTRAL_GLOBS = [
99
124
  "README.md",
100
125
  ".gitignore",
@@ -1,4 +1,8 @@
1
- /** Public surface of `@m3l-groundwork/plugin`: the /customize skill's deterministic backing data. */
1
+ // SPDX-FileCopyrightText: Copyright the m3l-groundwork contributors
2
+ // SPDX-License-Identifier: MIT
3
+
4
+ /** Public surface of `@monte3l/groundwork-plugin`: the /customize skill's deterministic backing data. */
2
5
  export * from "./kind-facet-map.js";
3
6
  export * from "./domain-map.js";
4
7
  export * from "./pack-map.js";
8
+ export * from "./plugin-map.js";
@@ -1,14 +1,31 @@
1
+ // SPDX-FileCopyrightText: Copyright the m3l-groundwork contributors
2
+ // SPDX-License-Identifier: MIT
3
+
1
4
  /**
2
- * The kind-to-facet table `/customize` reads to plan its guidance-pass
3
- * research priority. A stored, unit-tested module -- not a judgment made
4
- * afresh each run -- so the same interview answers always produce the same
5
- * facet plan.
5
+ * The kind-to-facet table `/customize` reads to plan its guidance-sweep
6
+ * research priority. Three terms recur throughout this file:
7
+ *
8
+ * - A "kind" is a `ProjectKind`: the project archetype the interview
9
+ * classifies the project as (library, cli, frontend or service).
10
+ * - A "guidance sweep" is the live research pass `/customize` runs over
11
+ * official TypeScript and Anthropic sources, once per domain (TypeScript
12
+ * toolchain, Claude Code harness).
13
+ * - A "facet" is one fixed research topic within a domain -- for example
14
+ * `compiler-config-flags` is a TypeScript facet and `hooks-lifecycle` is a
15
+ * harness facet. A guidance sweep always covers every facet of its domain,
16
+ * whether or not the interview emphasized it.
17
+ *
18
+ * This table is a stored, unit-tested module -- not a judgment made afresh
19
+ * each run -- so the same interview answers always produce the same facet
20
+ * plan.
6
21
  *
7
- * Scope note (load-bearing): this table scopes research PRIORITY only. Both
8
- * guidance skills (templates/core/.claude/skills/typescript-guidance and harness-guidance)
9
- * retain full authority to amend anything in their domain regardless of what
22
+ * Scope note (load-bearing): this table scopes research PRIORITY only. It
23
+ * decides which facets get the most emphasis, never what a guidance sweep
24
+ * is allowed to touch. Both guidance skills
25
+ * (templates/core/.claude/skills/typescript-guidance and harness-guidance)
26
+ * retain full authority to amend anything in their domain, regardless of what
10
27
  * this table emphasizes -- see their SKILL.md "Authority" sections. A facet
11
- * with low priority here still gets swept; it just isn't the one a dedicated
28
+ * with low priority here still gets swept. It just isn't the one a dedicated
12
29
  * research agent digs into deepest.
13
30
  */
14
31