@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.
- package/README.md +13 -5
- package/bin/m3l-groundwork.mjs +36 -2
- package/dist/assets.d.ts +10 -0
- package/dist/assets.js +14 -0
- package/dist/baseline-stage.d.ts +173 -0
- package/dist/baseline-stage.js +215 -0
- package/dist/caps.d.ts +4 -1
- package/dist/caps.js +17 -3
- package/dist/conflicts.d.ts +23 -2
- package/dist/conflicts.js +89 -11
- package/dist/customize-paths.d.ts +92 -0
- package/dist/customize-paths.js +115 -0
- package/dist/emit.d.ts +50 -1
- package/dist/emit.js +121 -13
- package/dist/fatal.d.ts +70 -0
- package/dist/fatal.js +132 -0
- package/dist/format-error.d.ts +113 -0
- package/dist/format-error.js +553 -0
- package/dist/fs-guard.d.ts +142 -0
- package/dist/fs-guard.js +222 -0
- package/dist/git.js +10 -1
- package/dist/harness/conformance.js +2 -0
- package/dist/harness/frontmatter.js +2 -13
- package/dist/harness/grade.js +37 -8
- package/dist/harness/rules.d.ts +20 -3
- package/dist/harness/rules.js +108 -6
- package/dist/harness/types.d.ts +13 -0
- package/dist/harness/types.js +3 -0
- package/dist/inventory.d.ts +77 -3
- package/dist/inventory.js +103 -12
- package/dist/jsonc.d.ts +49 -2
- package/dist/jsonc.js +140 -7
- package/dist/main.d.ts +62 -3
- package/dist/main.js +538 -67
- package/dist/merge-json.d.ts +47 -4
- package/dist/merge-json.js +120 -8
- package/dist/mode.js +12 -2
- package/dist/pack-stage.d.ts +210 -0
- package/dist/pack-stage.js +287 -0
- package/dist/packs.d.ts +19 -13
- package/dist/packs.js +231 -28
- package/dist/palette.d.ts +23 -0
- package/dist/palette.js +22 -0
- package/dist/plugin.d.ts +122 -6
- package/dist/plugin.js +687 -47
- package/dist/report.d.ts +21 -2
- package/dist/report.js +93 -5
- package/dist/staging.d.ts +176 -0
- package/dist/staging.js +375 -0
- package/dist/survey/fs-walk.d.ts +34 -2
- package/dist/survey/fs-walk.js +70 -5
- package/dist/survey/internal/blocked-path.d.ts +27 -0
- package/dist/survey/internal/blocked-path.js +129 -0
- package/dist/survey/internal/package-json.d.ts +13 -0
- package/dist/survey/internal/package-json.js +37 -0
- package/dist/survey/internal/read-guard.d.ts +178 -0
- package/dist/survey/internal/read-guard.js +276 -0
- package/dist/survey/survey-docs.d.ts +17 -2
- package/dist/survey/survey-docs.js +61 -21
- package/dist/survey/survey-harness.d.ts +20 -2
- package/dist/survey/survey-harness.js +87 -46
- package/dist/survey/survey-shape.d.ts +17 -2
- package/dist/survey/survey-shape.js +60 -46
- package/dist/survey/survey-toolchain.d.ts +16 -1
- package/dist/survey/survey-toolchain.js +69 -66
- package/dist/survey/survey.js +6 -4
- package/dist/survey/types.d.ts +79 -0
- package/dist/survey/types.js +2 -6
- package/dist/term.d.ts +80 -0
- package/dist/term.js +145 -0
- package/dist/tokens.js +2 -0
- package/dist/toolchain/conformance.js +2 -0
- package/dist/toolchain/grade.js +26 -8
- package/dist/toolchain/rules.d.ts +13 -4
- package/dist/toolchain/rules.js +16 -0
- package/dist/toolchain/tsconfig-chain.d.ts +2 -0
- package/dist/toolchain/tsconfig-chain.js +32 -8
- package/dist/toolchain/types.d.ts +16 -4
- package/dist/toolchain/types.js +3 -0
- package/package.json +4 -3
- package/plugin/skills/customize/SKILL.md +292 -18
- package/plugin/src/domain-map.ts +39 -14
- package/plugin/src/index.ts +5 -1
- package/plugin/src/kind-facet-map.ts +25 -8
- package/plugin/src/pack-map.ts +147 -25
- package/plugin/src/plugin-map.ts +236 -0
- package/templates/core/.claude/agents/Explore.md +0 -1
- package/templates/core/.claude/agents/code-implementer.md +4 -4
- package/templates/core/.claude/agents/code-reviewer.md +4 -4
- package/templates/core/.claude/agents/silent-failure-hunter.md +2 -2
- package/templates/core/.claude/agents/test-author.md +7 -5
- package/templates/core/.claude/hooks/guard-branch-isolation.mjs +56 -10
- package/templates/core/.claude/hooks/guard-double-background.mjs +13 -8
- package/templates/core/.claude/hooks/guard-git-push-signed.mjs +12 -9
- package/templates/core/.claude/hooks/guard-hub-src-writes.mjs +1890 -38
- package/templates/core/.claude/hooks/guard-js-extension.mjs +2 -1
- package/templates/core/.claude/hooks/guard-no-commonjs.mjs +7 -5
- package/templates/core/.claude/hooks/guard-secret-writes.mjs +7 -5
- package/templates/core/.claude/hooks/inject-decision-gate.mjs +12 -9
- package/templates/core/.claude/hooks/post-edit-verify.mjs +276 -98
- package/templates/core/.claude/rules/agent-dispatch.md +7 -0
- package/templates/core/.claude/rules/tests.md +2 -2
- package/templates/core/.claude/settings.json +5 -0
- package/templates/core/.claude/skills/finishing-work/SKILL.md +58 -10
- package/templates/core/.claude/skills/harness-guidance/SKILL.md +16 -7
- package/templates/core/.claude/skills/starting-work/SKILL.md +5 -0
- package/templates/core/.claude/skills/triaging-ci/SKILL.md +9 -8
- package/templates/core/.claude/skills/typescript-guidance/SKILL.md +137 -20
- package/templates/core/.claude/skills/typescript-guidance/references/area-catalog.md +135 -0
- package/templates/core/.claude/skills/typescript-guidance/references/tooling-sources.md +113 -0
- package/templates/core/.claude/skills/writing-commits/SKILL.md +2 -2
- package/templates/core/.github/dependabot.yml +18 -0
- package/templates/core/.github/workflows/ci.yml +15 -15
- package/templates/core/.github/workflows/dependency-review.yml +2 -2
- package/templates/core/.github/workflows/security-audit.yml +10 -4
- package/templates/core/.prettierignore +4 -0
- package/templates/core/CLAUDE.md +53 -3
- package/templates/core/README.md +30 -10
- package/templates/core/_gitignore +17 -0
- package/templates/core/bin/check-exports.mjs +11 -5
- package/templates/core/bin/lib/agent-roster.mjs +1 -1
- package/templates/core/bin/lib/frontmatter.mjs +4 -2
- package/templates/core/bin/lib/harness-rules.mjs +169 -20
- package/templates/core/bin/lib/protected-paths.mjs +93 -5
- package/templates/core/bin/lib/toolchain-rules.mjs +187 -26
- package/templates/core/bin/lib/verify-steps.mjs +29 -11
- package/templates/core/bin/verify.mjs +12 -0
- package/templates/core/eslint.config.js +5 -0
- package/templates/core/package.json +7 -7
- package/templates/core/vitest.config.ts +8 -1
- package/templates/packs/README.md +35 -24
- package/templates/packs/github/files/.claude/skills/reviewing-dependabot-prs/SKILL.md +133 -0
- package/templates/packs/github/files/.claude/skills/triaging-scan-alerts/SKILL.md +88 -0
- package/templates/packs/github/files/.claude/skills/watching-pr-checks/SKILL.md +94 -0
- package/templates/packs/github/files/.github/workflows/claude-pr-review.yml +267 -0
- package/templates/packs/github/files/.github/workflows/claude.yml +106 -0
- package/templates/packs/github/pack.json +19 -0
- package/templates/packs/harness-extras/files/.claude/hooks/guard-readonly-bash.mjs +1122 -65
- package/templates/packs/harness-extras/files/.claude/hooks/reinject-compact-handoff.mjs +147 -13
- package/templates/packs/{statusline → harness-extras}/files/.claude/hooks/statusline.mjs +6 -2
- package/templates/packs/harness-extras/files/.claude/hooks/write-compact-handoff.mjs +227 -44
- package/templates/packs/harness-extras/pack.json +18 -13
- package/templates/packs/publishing/files/.changeset/README.md +25 -0
- package/templates/packs/publishing/files/.changeset/config.json +7 -0
- package/templates/packs/publishing/files/.github/release-tools/package.json +9 -0
- package/templates/packs/publishing/files/.github/workflows/release.yml +293 -0
- package/templates/packs/publishing/files/REUSE.toml +28 -0
- package/templates/packs/publishing/files/bin/check-dts-deps.mjs +234 -0
- package/templates/packs/publishing/files/bin/check-license-headers.mjs +217 -0
- package/templates/packs/publishing/files/bin/check-publish-version.mjs +149 -0
- package/templates/packs/publishing/files/bin/lib/npm-publish-args.mjs +86 -0
- package/templates/packs/publishing/files/bin/pnpm-publish-shim.mjs +86 -0
- package/templates/packs/publishing/pack.json +35 -0
- package/templates/packs/{harness-extras → quality}/files/bin/check-file-budget.mjs +6 -4
- package/templates/packs/quality/pack.json +29 -0
- package/templates/packs/supply-chain/files/.github/workflows/gitleaks.yml +52 -0
- package/templates/packs/supply-chain/files/.github/workflows/scorecard.yml +48 -0
- package/templates/packs/supply-chain/files/.gitleaks.toml +2 -0
- package/templates/packs/supply-chain/pack.json +19 -0
- package/templates/packs/worktrees/files/.claude/hooks/ensure-worktree-deps.mjs +283 -0
- package/templates/packs/worktrees/files/.claude/hooks/guard-worktree-only.mjs +245 -0
- package/templates/packs/worktrees/files/.claude/hooks/repair-core-bare.mjs +335 -0
- package/templates/packs/worktrees/files/.claude/skills/working-in-worktrees/SKILL.md +139 -0
- package/templates/packs/worktrees/files/.worktreeinclude +11 -0
- package/templates/packs/worktrees/pack.json +64 -0
- package/templates/packs/statusline/pack.json +0 -31
- /package/templates/packs/{statusline → harness-extras}/files/.claude/hooks/statusline-layout.mjs +0 -0
- /package/templates/packs/{statusline → harness-extras}/files/.claude/hooks/subagent-statusline.mjs +0 -0
- /package/templates/packs/{harness-extras → quality}/files/.claude/agents/type-design-analyzer.md +0 -0
- /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
|
-
}
|
/package/templates/packs/{statusline → harness-extras}/files/.claude/hooks/statusline-layout.mjs
RENAMED
|
File without changes
|
/package/templates/packs/{statusline → harness-extras}/files/.claude/hooks/subagent-statusline.mjs
RENAMED
|
File without changes
|
/package/templates/packs/{harness-extras → quality}/files/.claude/agents/type-design-analyzer.md
RENAMED
|
File without changes
|
|
File without changes
|