Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,18 @@ temp/
.claude/**/settings.local.json
# 本地 git worktree(Claude Code 的默认位置)。跟踪它会让工具把一个完整
# 的嵌套 checkout 当成仓库内容 —— 见 biome.json 的 includes 与 #439。
# 用 **/ 前缀与 biome.json 的 "!!**/.claude/worktrees" 覆盖范围保持一致:
# 在子目录里启动 Claude Code 时 worktree 会落在该子目录的 .claude/ 下。
# 这里保留 **/ 前缀:在子目录里启动 Claude Code 时 worktree 会落在该子目录的
# .claude/ 下。biome.json 那条反而必须锚定到项目根(不带 **/)——biome 按绝对
# 路径匹配遍历根,**/ 会让 worktree 自己也命中排除,从而在 worktree 内部把
# `biome check .` 变成「检查 0 个文件」并非零退出(#444)。git 没有这个反向失效,
# 因此两边的覆盖范围有意不同,而不是等价保护:
# 子目录下的 worktree(如 packages/x/.claude/worktrees/b)只被 git 忽略,不在
# biome 的排除范围内——从仓库根跑 `biome check .` 仍会撞上 #439 的
# nested-root-configuration 中止。已知限制,不是疏漏:`**/` 的反向失效(worktree
# 里所有门禁全灭)比它更严重,而 `!!**/.claude/worktrees/**` 在 biome 2.4.16 上实测
# 两边都成立,却会被 biome 自己的 useBiomeIgnoreFolder safe fix 改回坏形式,
# `pnpm lint:fix` 一跑就把 bug 写回来——这条实测结论没有回归覆盖,换 biome 版本
# 后要重新验证再采信。真出现子目录 worktree 时,把它移到仓库根的 .claude/worktrees/。
**/.claude/worktrees/

.turbo/
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ All notable changes to this project will be documented in this file.

### Fixed

- **tooling**: `pnpm lint` works again from inside a worktree under `.claude/worktrees/`. The exclusion added for #439 was `**/`-prefixed, and Biome matches the traversal root by absolute path — so when Biome ran *from* such a worktree the root matched its own exclusion and `biome check .` reported "Checked 0 files" and exited non-zero, taking the pre-commit and pre-push hooks with it. The pattern is now anchored to the project root, which still keeps Biome out of a nested checkout at the root (the #439 failure). `.gitignore` deliberately keeps `**/`: git has no equivalent reverse failure, so it can afford the wider match. The two are therefore not equivalent protection — a worktree under a subdirectory's `.claude/` is git-ignored but *not* excluded from Biome, so a root `biome check .` still hits the #439 abort there; move such a worktree to the repo-root `.claude/worktrees/`. Both directions of the root case are pinned by a test that runs the real binary against a synthetic project root. (#444)
- **tooling**: A git worktree under `.claude/worktrees/` — Claude Code's default location — no longer breaks the local quality gates, which also made the pre-commit and pre-push hooks unusable without `--no-verify`. The worktree is a full nested checkout, and it broke two gates independently: its own `biome.json` made root `biome check .` abort with a nested-root-configuration error before checking any file, and `git ls-files --others` reported it as a single trailing-slash entry — the way it reports an untracked *nested repository*, rather than recursing as it does for an ordinary directory — which `listPublicClaudeAssets()` then read with `readFileSync`, throwing `EISDIR`. Fixed on both levers, since Biome sets no `vcs.useIgnoreFile` and never consults `.gitignore`: a `**/.claude/worktrees/` ignore rule plus a matching `biome.json` `includes` exclusion. Directory entries are also dropped at the point where assets are read, rather than inside the listing helper, so a stray nested repository elsewhere under `.claude/` still trips the public-prefix and single-entrypoint assertions instead of being silently skipped — while no longer aborting the suite with `EISDIR`. (#439)

## [2.2.0] - 2026-07-30
Expand Down
2 changes: 1 addition & 1 deletion biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
"!!**/skills",
"!!**/docs",
"!!**/benchmarks",
"!!**/.claude/worktrees"
"!!.claude/worktrees"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

锚定后只覆盖仓库根的 .claude/worktrees.gitignore:55-57 的新注释同时声称 worktree 可能落在子目录的 .claude/ 下——请在此处(或 CHANGELOG)明确子目录 worktree 仍会触发 #439 的 nested-root-config 中止,别让读者以为两条规则等价保护。

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

锚定后不再覆盖子目录下的 .claude/worktrees(如 packages/x/.claude/worktrees/b),从仓库根跑 biome check . 会重现 #439 的 nested-root-configuration 中止;取舍本身合理,但建议补一个 follow-up issue 编号,别让这条限制只留在 .gitignore 注释里。

]
}
}
109 changes: 105 additions & 4 deletions scripts/tests/workflow-rules.test.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { readFileSync, statSync } from 'node:fs';
import { execFileSync, spawnSync } from 'node:child_process';
import {
copyFileSync,
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
statSync,
writeFileSync,
} from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import test from 'node:test';
import { assertGitWorkspaceRootMatchesCwd, getChangedFiles } from '../workflows/contract-check.mjs';
import {
Expand Down Expand Up @@ -506,14 +517,26 @@ test('local Claude state markdown remains ignored', () => {
// are pinned here: git must ignore the path, and Biome must not descend into
// it (Biome sets no vcs.useIgnoreFile, so .gitignore alone does not stop the
// nested-config error, which aborts the whole run before any file is checked).
// See #439.
// See #439, and #444 for why Biome's exclusion is anchored while git's is not.
test('a git worktree under .claude/worktrees does not break the local gates', () => {
assert.doesNotThrow(() =>
execFileSync('git', ['check-ignore', '-q', '.claude/worktrees/example-branch']),
);

const biomeConfig = JSON.parse(readFileSync('biome.json', 'utf8'));
assert.ok(biomeConfig.files.includes.includes('!!**/.claude/worktrees'));
assert.ok(
biomeConfig.files.includes.includes('!!.claude/worktrees'),
'biome must exclude .claude/worktrees, or a nested checkout aborts the whole run',
);
// Anchored, not `**/`-prefixed. Biome matches the traversal root by absolute
// path, so a `**/.claude/worktrees` pattern also matches the worktree itself
// when biome runs from inside one — `biome check .` then ignores everything
// and exits non-zero, killing every local gate in the worktree (#444). The
// behavioural test below pins both directions.
assert.ok(
!biomeConfig.files.includes.some((pattern) => /^!!\*\*\/\.claude\/worktrees/u.test(pattern)),
'a `**/`-prefixed worktrees exclusion matches the traversal root and disables lint inside a worktree (#444)',
);

// Assert the filter against synthetic input: with the ignore rule in place
// git no longer emits a directory entry, so listPublicClaudeAssets() cannot
Expand All @@ -524,6 +547,84 @@ test('a git worktree under .claude/worktrees does not break the local gates', ()
);
});

// The config assertion above pins the pattern's *shape*; this pins what the
// shape is for, by running the real binary against a synthetic project root.
// Both directions matter and they pull against each other: excluding the
// worktree hard enough to survive its nested biome.json (#439) is what made a
// `**/` pattern also swallow the worktree when it *is* the traversal root
// (#444). Skipped when the binary is absent (no `pnpm install`) or on Windows,
// where the shim name differs — CI runs this on Linux.
const biomeBin = join('node_modules', '.bin', 'biome');
test('biome ignores a nested worktree from the root but still checks one from inside', {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test(name, {…}, fn) 的中间对象参数排版很可能不是 Biome 的格式化输出(本仓无同形先例),会让 pnpm lint 失败;跑一次 pnpm lint 确认,必要时采用 biome format --write 的全参数展开写法。

skip: process.platform === 'win32' || !existsSync(biomeBin) ? 'biome binary unavailable' : false,
}, () => {
const biomeAbsolute = join(process.cwd(), biomeBin);
// The probe below reads "did biome traverse here?" off a planted
// noDoubleEquals diagnostic. Turning that rule off in biome.json would make
// the worktree run report nothing and fail as if traversal had regressed —
// the misdirected error message #439 and #444 are both about. Fail on the
// real cause instead. The `suspicious` group already disables five rules, so
// this is not a hypothetical edit.
// Biome accepts three ways to switch the probe off — `"off"`, `{ level:
// "off" }`, and dropping the recommended preset at either level — so check
// all of them rather than the one spelling in use today.
const linterRules = JSON.parse(readFileSync('biome.json', 'utf8')).linter?.rules;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里只读 linter?.ruleslinter.enabled: false 能绕过全部三条探针守卫并让失败信息指向 traversal 而非真实原因;顺手多读一层 linter,加一条 assert.notEqual(linter?.enabled, false, probeMessage)

const probeRule = linterRules?.suspicious?.noDoubleEquals;
const probeMessage =
'this test probes traversal via a planted noDoubleEquals diagnostic; pick another enabled rule if it gets disabled';
assert.notEqual(
typeof probeRule === 'string' ? probeRule : probeRule?.level,
'off',
probeMessage,
);
assert.notEqual(linterRules?.recommended, false, probeMessage);
assert.notEqual(linterRules?.suspicious?.recommended, false, probeMessage);
const root = mkdtempSync(join(tmpdir(), 'frontagent-worktree-lint-'));
try {
const worktree = join(root, '.claude', 'worktrees', 'example-branch');
mkdirSync(worktree, { recursive: true });
// Both project roots get the real config — the behaviour under test is a
// property of biome.json, so a hand-written stub would not be evidence.
copyFileSync('biome.json', join(root, 'biome.json'));
copyFileSync('biome.json', join(worktree, 'biome.json'));
// The root file is clean; the worktree file carries one recommended-rule
// error. Which run reports it is the traversal evidence — a file count
// would also be satisfied by biome checking the config files alone.
writeFileSync(join(root, 'sample.ts'), "export const sample = 'root';\n");
writeFileSync(
join(worktree, 'sample.ts'),
'export function sample(a: unknown, b: unknown) {\n return a == b;\n}\n',
);

const fromRoot = spawnSync(biomeAbsolute, ['check', '.'], { cwd: root, encoding: 'utf8' });
const rootOutput = `${fromRoot.stdout}${fromRoot.stderr}`;
// The nested biome.json is itself a root config: without the exclusion
// biome aborts with a nested-root-configuration error before checking
// anything, which is the #439 failure this must keep out.
assert.equal(fromRoot.status, 0, `biome failed at the root checkout:\n${rootOutput}`);
assert.doesNotMatch(
rootOutput,
/noDoubleEquals/u,
`the root run must not descend into the worktree (#439):\n${rootOutput}`,
);

const fromWorktree = spawnSync(biomeAbsolute, ['check', '.'], {
cwd: worktree,
encoding: 'utf8',
});
const worktreeOutput = `${fromWorktree.stdout}${fromWorktree.stderr}`;
// With a `**/` pattern this run reports "Checked 0 files" and exits 1 —
// the gate looks like it ran and failed, without inspecting anything.
assert.match(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

该断言把「worktree 被遍历」等同于「输出里出现 noDoubleEquals」;先断言复制进 fixture 的配置确实启用了这条规则,否则规则被禁用时会伪装成 #444 回归。

worktreeOutput,
/noDoubleEquals/u,
`worktree contents must still be checked (#444):\n${worktreeOutput}`,
);
} finally {
rmSync(root, { recursive: true, force: true });
}
});

test('Claude reusable assets are public while local state stays private', () => {
const publicClaudeAssets = listPublicClaudeAssets();

Expand Down
Loading