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

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 +574 -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 +21 -13
  41. package/dist/packs.js +241 -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 +44 -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 +295 -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 +41 -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
@@ -0,0 +1,335 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * SessionStart + PostToolUse: resets `core.bare = true` back to `false` in a
4
+ * NORMAL repository's shared `.git/config`.
5
+ *
6
+ * Why this exists: Claude Code's `EnterWorktree`/`ExitWorktree` tools are
7
+ * documented to write `core.bare = true` into the shared config of a
8
+ * non-bare repo and never restore it (anthropics/claude-code#58345, #69802,
9
+ * both closed "not planned"). Git applies a shared `core.bare` to the MAIN
10
+ * worktree only (git-worktree's CONFIGURATION FILE section), so linked
11
+ * worktrees keep working while the root fails every command with "this
12
+ * operation must be run in a work tree" -- and `git worktree list` prints
13
+ * "(bare)" for it. Nothing in Git itself sets the key on a repo created
14
+ * without `--bare`, and no hook fires on a config write, so this checks at
15
+ * the moments it is known to happen (a session starting, a worktree tool
16
+ * returning) and repairs it.
17
+ *
18
+ * Reads files only -- no `git` subprocess, so it is cheap enough to run on
19
+ * every matching tool call. It only acts on a repository that is provably
20
+ * NOT bare: the common git dir must be named exactly `.git` AND contain an
21
+ * `index`. The name alone is not enough -- a bare clone stored in a
22
+ * directory called `.git` (`git clone --bare <url> proj/.git`, a common
23
+ * worktree layout) passes it, and flipping that one would leave `proj/` a
24
+ * phantom main worktree with an empty tree. A bare repo never has a
25
+ * common-dir `index` (linked worktrees keep theirs under `worktrees/<name>/`),
26
+ * while a normal repo has one after its first checkout or commit; the
27
+ * trade-off is that a brand-new repo with no checkout yet is left alone.
28
+ * Only the `bare` key of the `[core]` section is edited, byte-for-byte
29
+ * otherwise, and only when it reads literally `true`. The write follows
30
+ * git's own protocol (`config.lock` created exclusively, then renamed, the
31
+ * original file mode kept), so it can't clobber a concurrent `git config`; a
32
+ * freshly held lock is skipped quietly and retried on the next trigger, but
33
+ * one older than a minute is a leftover from a crashed git, which would
34
+ * otherwise block the repair forever, so it is reported (never deleted: the
35
+ * hook can't know no git is running).
36
+ *
37
+ * Always exits 0 -- advisory infrastructure, never a gate. Stderr is not
38
+ * shown on exit 0, so a repair that was attempted and FAILED is reported on
39
+ * stdout as `systemMessage` + `additionalContext` instead, with the manual
40
+ * fix.
41
+ */
42
+ import process from "node:process";
43
+ import path from "node:path";
44
+ import {
45
+ chmodSync,
46
+ closeSync,
47
+ openSync,
48
+ readFileSync,
49
+ realpathSync,
50
+ renameSync,
51
+ rmSync,
52
+ statSync,
53
+ writeSync,
54
+ } from "node:fs";
55
+ import { fileURLToPath } from "node:url";
56
+
57
+ /**
58
+ * The filesystem calls of the WRITE phase (everything after `core.bare =
59
+ * true` is confirmed). `repairCoreBare` takes an optional partial override
60
+ * of these as a test seam, so a failure at an exact step -- a `statSync`
61
+ * that throws while another process holds the lock, a `renameSync` that
62
+ * fails after this run created its own -- can be injected deterministically
63
+ * on every OS instead of engineered with permissions. Production callers
64
+ * never pass one.
65
+ */
66
+ const realFs = {
67
+ chmodSync,
68
+ closeSync,
69
+ openSync,
70
+ renameSync,
71
+ rmSync,
72
+ statSync,
73
+ writeSync,
74
+ };
75
+
76
+ /** A `config.lock` older than this is treated as left behind by a crashed git. */
77
+ const STALE_LOCK_MS = 60_000;
78
+
79
+ /**
80
+ * Walks up from `cwd` to the first `.git` and resolves the COMMON git dir:
81
+ * the `.git` directory itself, or, for a linked worktree (a `.git` FILE
82
+ * reading `gitdir: <path>`), the directory named by that git dir's
83
+ * `commondir` file. Returns "" when none can be resolved. A submodule's
84
+ * `.git` file has no `commondir`, so it resolves to its own git dir, whose
85
+ * name isn't `.git` -- left alone on purpose.
86
+ *
87
+ * @param {string} cwd
88
+ * @returns {string}
89
+ */
90
+ function findCommonDir(cwd) {
91
+ let dir = path.resolve(cwd);
92
+ for (;;) {
93
+ const dotGit = path.join(dir, ".git");
94
+ const stat = statSync(dotGit, { throwIfNoEntry: false });
95
+ if (stat !== undefined) {
96
+ if (stat.isDirectory()) return dotGit;
97
+ if (!stat.isFile()) return "";
98
+ const match = /^gitdir:\s*(.+)$/m.exec(readFileSync(dotGit, "utf8"));
99
+ if (match === null) return "";
100
+ const gitDir = path.resolve(dir, match[1].trim());
101
+ const commonFile = path.join(gitDir, "commondir");
102
+ if (statSync(commonFile, { throwIfNoEntry: false }) === undefined) {
103
+ return gitDir;
104
+ }
105
+ return path.resolve(gitDir, readFileSync(commonFile, "utf8").trim());
106
+ }
107
+ const parent = path.dirname(dir);
108
+ if (parent === dir) return "";
109
+ dir = parent;
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Rewrites a `bare = true` line inside the `[core]` section to `bare =
115
+ * false`, leaving every other byte alone. Returns the new text, or `null`
116
+ * when there was nothing to change. Only the literal `true` is recognised
117
+ * (git also accepts `yes`/`on`/`1`; Claude Code writes `true`).
118
+ *
119
+ * @param {string} text
120
+ * @returns {string | null}
121
+ */
122
+ function resetCoreBare(text) {
123
+ const lines = text.split("\n");
124
+ let inCore = false;
125
+ let changed = false;
126
+ const out = lines.map((line) => {
127
+ const section = /^\s*\[([^\]]*)\]/.exec(line);
128
+ if (section !== null) inCore = /^core$/i.test(section[1].trim());
129
+ if (!inCore) return line;
130
+ const bare = /^(\s*bare\s*=\s*)true(\s*(?:[#;].*)?\r?)$/i.exec(line);
131
+ if (bare === null) return line;
132
+ changed = true;
133
+ return `${bare[1]}false${bare[2]}`;
134
+ });
135
+ return changed ? out.join("\n") : null;
136
+ }
137
+
138
+ /**
139
+ * Quotes `value` for a POSIX shell only when it needs it, so the manual-fix
140
+ * commands in a message can be pasted as-is even from a path with spaces.
141
+ *
142
+ * @param {string} value
143
+ * @returns {string}
144
+ */
145
+ function shellQuote(value) {
146
+ return /^[\w@%+=:,./-]+$/.test(value)
147
+ ? value
148
+ : `'${value.replaceAll("'", `'\\''`)}'`;
149
+ }
150
+
151
+ /** @param {unknown} cause */
152
+ function firstLine(cause) {
153
+ const message = cause instanceof Error ? cause.message : String(cause);
154
+ return message.split("\n")[0];
155
+ }
156
+
157
+ /**
158
+ * Resets `core.bare = true` to `false` in the shared config of the NORMAL
159
+ * repository containing `cwd`, if it is set. Never throws. `error` is only
160
+ * present when a repair was needed and could not be completed, so the repo
161
+ * is still broken.
162
+ *
163
+ * @param {string} cwd
164
+ * @param {Partial<typeof realFs>} [io] Test seam: overrides for the write-phase fs calls.
165
+ * @returns {{ repaired: boolean; configPath: string; error?: string; staleLock?: string }}
166
+ */
167
+ export function repairCoreBare(cwd, io = {}) {
168
+ const fsx = { ...realFs, ...io };
169
+ let configPath = "";
170
+ let updated;
171
+ try {
172
+ const commonDir = findCommonDir(cwd);
173
+ if (
174
+ commonDir === "" ||
175
+ path.basename(commonDir) !== ".git" ||
176
+ statSync(path.join(commonDir, "index"), { throwIfNoEntry: false }) ===
177
+ undefined
178
+ ) {
179
+ return { repaired: false, configPath: "" };
180
+ }
181
+ configPath = path.join(commonDir, "config");
182
+ updated = resetCoreBare(readFileSync(configPath, "utf8"));
183
+ } catch (cause) {
184
+ // Locating or reading the config failed before we knew anything was
185
+ // wrong: a missing file just means "not a repo we can repair".
186
+ if (cause?.code === "ENOENT" || cause?.code === "ENOTDIR") {
187
+ return { repaired: false, configPath };
188
+ }
189
+ process.stderr.write(
190
+ `repair-core-bare: could not check core.bare (${firstLine(cause)}).\n`,
191
+ );
192
+ return { repaired: false, configPath };
193
+ }
194
+ if (updated === null) return { repaired: false, configPath };
195
+
196
+ // From here on `core.bare = true` is confirmed, so a failure leaves the
197
+ // repository broken and must be reported, never swallowed.
198
+ const lock = `${configPath}.lock`;
199
+ let fd;
200
+ // Only a lock THIS run created may be removed on failure: otherwise a
201
+ // failed stat/open would delete the `config.lock` of a live git process.
202
+ let ownsLock = false;
203
+ try {
204
+ const mode = fsx.statSync(configPath).mode & 0o777;
205
+ try {
206
+ fd = fsx.openSync(lock, "wx", mode);
207
+ } catch (cause) {
208
+ if (cause?.code === "EEXIST") {
209
+ // Usually git (or another hook run) holds the lock right now: retry
210
+ // on the next trigger rather than racing it. An old one is a crashed
211
+ // git's leftover that would block every future repair, so say so.
212
+ const held = fsx.statSync(lock, { throwIfNoEntry: false });
213
+ if (held === undefined || Date.now() - held.mtimeMs < STALE_LOCK_MS) {
214
+ return { repaired: false, configPath };
215
+ }
216
+ const minutes = Math.floor((Date.now() - held.mtimeMs) / 60_000);
217
+ return {
218
+ repaired: false,
219
+ configPath,
220
+ error: `${lock} has been held for ${minutes} min, so no git process is likely still using it`,
221
+ staleLock: lock,
222
+ };
223
+ }
224
+ throw cause;
225
+ }
226
+ ownsLock = true;
227
+ fsx.writeSync(fd, updated);
228
+ fsx.closeSync(fd);
229
+ fd = undefined;
230
+ fsx.chmodSync(lock, mode);
231
+ fsx.renameSync(lock, configPath);
232
+ return { repaired: true, configPath };
233
+ } catch (cause) {
234
+ if (fd !== undefined) {
235
+ try {
236
+ fsx.closeSync(fd);
237
+ } catch {
238
+ // The original failure below is the one worth reporting.
239
+ }
240
+ }
241
+ if (ownsLock) {
242
+ try {
243
+ fsx.rmSync(lock, { force: true });
244
+ } catch (cleanup) {
245
+ process.stderr.write(
246
+ `repair-core-bare: left ${lock} behind (${firstLine(cleanup)}).\n`,
247
+ );
248
+ }
249
+ }
250
+ return { repaired: false, configPath, error: firstLine(cause) };
251
+ }
252
+ }
253
+
254
+ function isEntryPoint() {
255
+ try {
256
+ return realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
257
+ } catch {
258
+ return false;
259
+ }
260
+ }
261
+
262
+ /**
263
+ * Reads the hook payload from stdin. Resolves to "" for a TTY, a stream
264
+ * error, or no EOF within `timeoutMs`, so the hook can never hang a tool
265
+ * call or exit non-zero over its own input.
266
+ *
267
+ * @param {number} timeoutMs
268
+ * @returns {Promise<string>}
269
+ */
270
+ function readStdin(timeoutMs) {
271
+ if (process.stdin.isTTY) return Promise.resolve("");
272
+ return new Promise((resolve) => {
273
+ const chunks = [];
274
+ const done = () => {
275
+ clearTimeout(timer);
276
+ process.stdin.removeAllListeners();
277
+ process.stdin.pause();
278
+ resolve(Buffer.concat(chunks).toString("utf8"));
279
+ };
280
+ const timer = setTimeout(done, timeoutMs);
281
+ timer.unref();
282
+ process.stdin.on("data", (chunk) => chunks.push(chunk));
283
+ process.stdin.on("end", done);
284
+ process.stdin.on("error", done);
285
+ });
286
+ }
287
+
288
+ if (isEntryPoint()) {
289
+ let input = {};
290
+ try {
291
+ input = JSON.parse(await readStdin(2000));
292
+ } catch {
293
+ // No or invalid payload: fall back to the process cwd.
294
+ }
295
+ const cwd =
296
+ typeof input?.cwd === "string" && input.cwd !== ""
297
+ ? input.cwd
298
+ : process.cwd();
299
+ const { repaired, configPath, error, staleLock } = repairCoreBare(cwd);
300
+ const eventName =
301
+ typeof input?.hook_event_name === "string" && input.hook_event_name !== ""
302
+ ? input.hook_event_name
303
+ : "SessionStart";
304
+ if (repaired) {
305
+ process.stdout.write(
306
+ JSON.stringify({
307
+ hookSpecificOutput: {
308
+ hookEventName: eventName,
309
+ additionalContext:
310
+ `repair-core-bare: core.bare was true in ${configPath} (a known ` +
311
+ "EnterWorktree/ExitWorktree side effect, anthropics/claude-code#58345) " +
312
+ "and has been reset to false.",
313
+ },
314
+ }),
315
+ );
316
+ } else if (error !== undefined) {
317
+ const reset = `\`git config --file ${shellQuote(configPath)} core.bare false\``;
318
+ const fix =
319
+ staleLock === undefined
320
+ ? `run ${reset}`
321
+ : `first \`rm ${shellQuote(staleLock)}\` (only if no git process is running), then run ${reset}`;
322
+ process.stdout.write(
323
+ JSON.stringify({
324
+ systemMessage: `repair-core-bare: core.bare is still true in ${configPath} (${error}); ${fix}.`,
325
+ hookSpecificOutput: {
326
+ hookEventName: eventName,
327
+ additionalContext:
328
+ `repair-core-bare FAILED to reset core.bare in ${configPath} (${error}). ` +
329
+ `git commands in the main checkout will fail until it is fixed: ${fix}.`,
330
+ },
331
+ }),
332
+ );
333
+ }
334
+ process.exit(0);
335
+ }
@@ -0,0 +1,139 @@
1
+ ---
2
+ name: working-in-worktrees
3
+ description: >-
4
+ Creates, lists, syncs, and safely tears down an isolated git worktree per
5
+ unit of work -- required once the `worktrees` pack is installed, since
6
+ `guard-worktree-only.mjs` blocks every src/tests write outside one, on any
7
+ branch. Use for /working-in-worktrees, "start a worktree", "work in a
8
+ worktree", "what worktrees are open", "sync this worktree with main", or
9
+ "finish this worktree" -- and from `starting-work`'s own branch step, which
10
+ hands off here automatically when this skill is installed.
11
+ ---
12
+
13
+ # working-in-worktrees
14
+
15
+ Once this pack is installed, `guard-worktree-only.mjs` blocks a src/tests
16
+ write outside a linked worktree under `.claude/worktrees/` -- on any branch,
17
+ for any caller. `starting-work`'s plain `git switch -c` is no longer enough
18
+ by itself: it puts you on the right branch, but not inside a worktree the
19
+ guard will accept. This skill is the missing piece.
20
+
21
+ Hub-only. Never dispatched as a spoke's own tool.
22
+
23
+ ## Modes
24
+
25
+ ### start `<slug>`
26
+
27
+ 1. `git fetch origin` (or the project's own default-branch equivalent).
28
+ 2. Create the worktree with a plain `git worktree add .claude/worktrees/<slug> -b feat/<slug> origin/main`
29
+ (or `fix/<slug>` for a bug fix) -- **not** `EnterWorktree(name: <slug>)`,
30
+ which names the branch `worktree-<slug>` instead. Create it with the
31
+ final branch name up front, so no rename is ever needed.
32
+ 3. `EnterWorktree(path: .claude/worktrees/<slug>)` to switch the session's
33
+ working directory into it (this is the documented way to adopt session
34
+ tracking for a worktree you created yourself -- see the tool's own "on
35
+ first entry from the launch directory, the path must appear in
36
+ `git worktree list`" note).
37
+ **Known side effect:** Claude Code's `EnterWorktree`/`ExitWorktree` have
38
+ been reported to leave `core.bare = true` in the main repository's shared
39
+ `.git/config` (anthropics/claude-code#58345, #69802, both closed "not
40
+ planned"), which breaks `git status` in the main checkout while worktrees
41
+ keep working. This pack's `repair-core-bare.mjs` hook resets it after
42
+ either tool runs; if `git status` ever says "this operation must be run in
43
+ a work tree", the manual fix is `git config --local core.bare false`. Prefer
44
+ the `path:` and `keep` forms used in this skill; `EnterWorktree(name: ...)` and
45
+ `ExitWorktree(remove)` are the calls the upstream reports tie to it.
46
+ 4. Copy any of `.env`, `.env.local`, `.env.*.local` that exist in the main
47
+ checkout into the new worktree (the same list as `.worktreeinclude`). `.worktreeinclude` (this pack ships one) only fires for a
48
+ worktree Claude Code itself creates -- since this skill creates the
49
+ directory with plain `git worktree add`, that processing never runs here,
50
+ so the copy is done by hand.
51
+ 5. `pnpm install --frozen-lockfile --prefer-offline` inside the worktree.
52
+ `ensure-worktree-deps.mjs`'s own `SessionStart` hook is a backstop for
53
+ `claude --worktree`/a resumed session, not a substitute for this step --
54
+ it does not fire on a mid-session `EnterWorktree`, which is what steps 2-3
55
+ just did.
56
+
57
+ ### status
58
+
59
+ `git worktree list --porcelain`, then for each entry under
60
+ `.claude/worktrees/`: ahead/behind counts against its upstream
61
+ (`git rev-list --left-right --count <branch>...origin/<branch>`), dirty
62
+ state (`git -C <worktree> status --porcelain`), and whether it's locked
63
+ (a `locked` line in the porcelain output).
64
+
65
+ ### sync `<slug>`
66
+
67
+ Inside `.claude/worktrees/<slug>`: `git fetch origin` then
68
+ `git rebase origin/main`. Report a conflict rather than resolving it
69
+ silently -- the person driving decides how. If this branch was already
70
+ pushed and has an open PR, a rebase means the next push needs
71
+ `--force-with-lease`, never a bare `--force` (CLAUDE.md's "never
72
+ `git push --force`") -- confirm with the user before that push, the same
73
+ as `creating-prs`'s own resync step does.
74
+
75
+ ### finish `<slug>`
76
+
77
+ The identical safe sequence `finishing-work`'s own worktree-aware steps
78
+ use, applied here directly (this skill's `finish` mode and that skill's
79
+ Step 2/4 exist for the same reason and must never drift apart):
80
+
81
+ 1. Confirm the PR for this branch actually merged (`gh pr view --json
82
+ state,mergedAt`) -- never remove a worktree whose work never landed
83
+ without asking first. Then `git fetch origin` and run
84
+ `git log feat/<slug> ^origin/main --oneline`: a squash merge never makes
85
+ the branch's commits ancestors of `main`, so a non-empty result is either
86
+ the squashed originals (expected) or a commit added after the PR merged
87
+ that is about to be abandoned -- compare it with the merged PR's commits
88
+ and ask before going further, exactly as `finishing-work` Step 4 does.
89
+ 2. If the session is still inside this worktree, `ExitWorktree(keep)` to
90
+ return to the main checkout.
91
+ 3. `git -C .claude/worktrees/<slug> status --porcelain` must be empty --
92
+ investigate first if not. This only sees tracked/untracked-but-not-
93
+ ignored changes; it says nothing about a gitignored file (a `.env`, an
94
+ in-progress scratch file) that exists ONLY in this worktree, so **confirm
95
+ with the user before removing**, not just when `status --porcelain` is
96
+ non-empty.
97
+ 4. `git worktree unlock .claude/worktrees/<slug>` -- only if locked, and
98
+ only after confirming with the user.
99
+ 5. `git worktree remove .claude/worktrees/<slug>` -- confirmed per step 3.
100
+ 6. `git worktree prune`.
101
+ 7. `git branch -d feat/<slug>` (or `-D` only with the user's go-ahead, per
102
+ the same squash-merge reasoning `finishing-work` documents).
103
+
104
+ ### fan-out
105
+
106
+ For genuinely independent units of work only -- not the sequential TDD
107
+ pipeline below. Dispatch each independent unit to its own subagent with
108
+ `isolation: "worktree"` in the Agent call; Claude Code creates and tracks
109
+ that worktree automatically (base ref governed by the `worktree.baseRef`
110
+ setting, default `"fresh"` = the remote default branch). Each unit's result
111
+ becomes its own PR -- a fanned-out worktree is not a place to accumulate one
112
+ combined change across several unrelated units.
113
+
114
+ `ensure-worktree-deps.mjs` only runs at `SessionStart`, so a worktree created
115
+ this way has **no `node_modules`** when its subagent starts: the first
116
+ `post-edit-verify` run would stop with a "no node_modules" error. Put the
117
+ install in every fan-out brief as its first step (`pnpm install
118
+ --frozen-lockfile --prefer-offline` inside that worktree) -- and dispatch a
119
+ writer spoke for it, since a read-only spoke cannot run it when the
120
+ `harness-extras` pack's `guard-readonly-bash` is installed.
121
+
122
+ ## Policy
123
+
124
+ - **One worktree per unit of work.** Don't reuse a worktree across unrelated
125
+ tasks, and don't split one task across worktrees.
126
+ - **The TDD pipeline runs sequentially inside the SAME worktree, with no
127
+ `isolation` on any spoke.** `test-author` writes the failing tests,
128
+ `code-implementer` makes them pass, and the review spokes read the diff --
129
+ all inside the one worktree `start` created. A spoke dispatched from a hub
130
+ session that is itself inside a worktree inherits that same isolation
131
+ (Claude Code's own enforcement explicitly covers "every subagent Claude
132
+ spawns from the isolated session"), so this is safe by construction: a
133
+ spoke given its own separate `isolation: "worktree"` here would branch
134
+ from `baseRef` and never see the RED tests `test-author` just wrote.
135
+ - **Per-call `isolation: "worktree"` is only for the fan-out case above** --
136
+ independent units with no shared in-progress state to see.
137
+ - **Never `git stash` inside a worktree.** The stash stack is shared across
138
+ every worktree of a repository; stashing here can silently pop into (or
139
+ clobber) unrelated work in the main checkout or another worktree.
@@ -0,0 +1,11 @@
1
+ # Copies gitignored local-secrets files into every worktree Claude Code
2
+ # creates itself (`claude --worktree`, `isolation: "worktree"`,
3
+ # `EnterWorktree` without a `path`) -- gitignore syntax, per
4
+ # code.claude.com/docs/en/worktrees. Only ever copies a file that is ALSO
5
+ # gitignored, so nothing tracked is ever duplicated. This does NOT fire for
6
+ # a worktree the `working-in-worktrees` skill's `start` mode creates with a
7
+ # plain `git worktree add` -- that mode copies these same files by hand; see
8
+ # its own step for why.
9
+ .env
10
+ .env.local
11
+ .env.*.local
@@ -0,0 +1,64 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "name": "worktrees",
4
+ "description": "Enforces that all development happens inside isolated git worktrees rather than the main checkout: a skill that creates, lists, syncs, and safely tears down a worktree per unit of work; a hook that installs dependencies into a freshly created worktree automatically; a hook that resets the `core.bare = true` Claude Code's worktree tools leave in a normal repo's shared config; and a guard that blocks a src/tests write outside a worktree the same way guard-branch-isolation.mjs blocks one on main.",
5
+ "modes": ["fresh", "adopt"],
6
+ "budget": {
7
+ "agents": 0,
8
+ "skills": 1,
9
+ "hooks": 3,
10
+ "workflows": 0,
11
+ "scripts": 0
12
+ },
13
+ "requires": {
14
+ "paths": ["bin/lib/protected-paths.mjs"]
15
+ },
16
+ "wiring": {
17
+ "settings": {
18
+ "SessionStart": [
19
+ {
20
+ "matcher": "startup|resume",
21
+ "hooks": [
22
+ {
23
+ "type": "command",
24
+ "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/ensure-worktree-deps.mjs\"",
25
+ "timeout": 300
26
+ },
27
+ {
28
+ "type": "command",
29
+ "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/repair-core-bare.mjs\"",
30
+ "timeout": 30
31
+ }
32
+ ]
33
+ }
34
+ ],
35
+ "PreToolUse": [
36
+ {
37
+ "matcher": "Write|Edit",
38
+ "hooks": [
39
+ {
40
+ "type": "command",
41
+ "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/guard-worktree-only.mjs\"",
42
+ "timeout": 30
43
+ }
44
+ ]
45
+ }
46
+ ],
47
+ "PostToolUse": [
48
+ {
49
+ "matcher": "EnterWorktree|ExitWorktree|Agent",
50
+ "hooks": [
51
+ {
52
+ "type": "command",
53
+ "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/repair-core-bare.mjs\"",
54
+ "timeout": 30
55
+ }
56
+ ]
57
+ }
58
+ ]
59
+ },
60
+ "packageScripts": {},
61
+ "verifySteps": []
62
+ },
63
+ "adoptNotes": "ensure-worktree-deps.mjs runs `pnpm install --frozen-lockfile --prefer-offline` automatically the first time a session starts inside a freshly created worktree with no node_modules yet -- this runs every dependency's lifecycle scripts the same way any `pnpm install` does, limited only by whatever `allowBuilds`/`onlyBuiltDependencies` policy the project's own pnpm config already sets (see typescript-guidance's pnpm supply-chain coverage if none is set). If the project sets `worktree.symlinkDirectories: [\"node_modules\"]` in .claude/settings.json, the hook detects the symlink and skips -- installing there would mutate the main checkout's own node_modules through the symlink. guard-worktree-only.mjs blocks every src/tests write outside a linked worktree under .claude/worktrees/, on any branch, for any caller (hub or writer spoke) -- this is stricter than guard-branch-isolation.mjs (which only blocks on main) and guard-hub-src-writes.mjs (which only blocks the hub); confirm the team actually wants every change routed through a worktree before installing this pack, since it changes the day-to-day workflow, not just adds a nicety. `.claude/worktrees/` should already be gitignored (the baseline's own .gitignore covers this); if adopting into a project with a hand-rolled .gitignore that doesn't, add it before installing. repair-core-bare.mjs resets `core.bare = true` to `false` in a non-bare repo's shared .git/config at session start and after EnterWorktree/ExitWorktree/Agent calls: Claude Code's worktree tools are documented to leave it set (anthropics/claude-code#58345, #69802), which breaks `git status` in the main checkout. It only acts on a provably non-bare repository (its common git dir is named .git and contains an index), never on a genuine bare repo -- including a bare clone stored in a directory called .git -- and reports a failed repair on stdout rather than silently."
64
+ }
@@ -1,31 +0,0 @@
1
- {
2
- "schemaVersion": 1,
3
- "name": "statusline",
4
- "description": "A five-row Claude Code statusLine -- session, model, context, quota, work -- plus a per-subagent row renderer, both width-fit to the real terminal. statusLine is the only documented surface carrying live context-window pressure; no hook event receives token data.",
5
- "modes": ["fresh", "adopt"],
6
- "budget": {
7
- "agents": 0,
8
- "skills": 0,
9
- "hooks": 3,
10
- "workflows": 0,
11
- "scripts": 0
12
- },
13
- "wiring": {
14
- "settings": {},
15
- "settingsTopLevel": {
16
- "statusLine": {
17
- "type": "command",
18
- "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/statusline.mjs\"",
19
- "refreshInterval": 30,
20
- "padding": 1
21
- },
22
- "subagentStatusLine": {
23
- "type": "command",
24
- "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/subagent-statusline.mjs\""
25
- }
26
- },
27
- "packageScripts": {},
28
- "verifySteps": []
29
- },
30
- "adoptNotes": "The three scripts read only the stdin payload, .git/HEAD (via node:fs, never a git subprocess) and process.availableMemory()/os.totalmem(), so they install anywhere a .claude/ directory exists -- no dependency on the baseline's file layout, and no gate to wire. The one real adopt risk is the settings keys: a project that already defines a top-level statusLine or subagentStatusLine collides, and its existing value must be shown and decided on, never overwritten. A .claude/settings.local.json or the user's own ~/.claude/settings.json statusLine also shadows the project one -- check both before concluding the pack is wired. rate_limits.spend_limit and prompt_cache need Claude Code v2.1.251+, and the per-subagent model/contextWindowSize/effort fields need v2.1.205+/v2.1.214+; each renders only when present, so an older Claude Code degrades to fewer segments rather than breaking. The scripts behave the same on macOS and Linux; the memory segment uses process.availableMemory() (Node 22+), and on macOS with an older Node it is hidden rather than shown from os.freemem(), which undercounts there. Below roughly 40 columns the layout drops segments by priority rather than wrapping."
31
- }