Merge branch 'master' into archive-agent-notes-sweep-2
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-23-personal-staging-maintenance-skills.md: a7ccc5b1e0f13e880c58a93d2e4c2cd4f06e2a93
|
||||
2026-07-23-personal-staging-maintenance-skills.zh.md: db1595c83da0ad93e9ba9055b5a3d7fe7cfe1706
|
||||
@@ -0,0 +1,35 @@
|
||||
# Agent Note: Personal staging maintenance skills
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-07-23-personal-staging-maintenance-skills.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
Personal dsh customizations need a repeatable way to locate the installed source, isolate task work, serialize integration, and incorporate upstream changes without rewriting the checkout used by running sessions. User-local instructions solve this for one installation but cannot guide other users or remain synchronized with repository installer behavior.
|
||||
|
||||
## Decision
|
||||
|
||||
The repository distributes [`dsh-customize`](../../../../skills/dsh-customize/SKILL.md), [`dsh-upgrade`](../../../../skills/dsh-upgrade/SKILL.md), and [`dsh-upstream-customization`](../../../../skills/dsh-upstream-customization/SKILL.md) from its root `skills/` directory. Their descriptions name both the operation and user requests that select it. The shipped TUI supplies that directory to the local skill provider at startup, below project and user roots in discovery priority. The workflows derive the active checkout and staging branch from the installed launcher rather than a user-specific path or branch name, defer to repository-local instructions, require task worktrees, and serialize staging mutations with the staging worktree's established `.agents/merge.lock`.
|
||||
|
||||
Before rebasing, an upgrade inspects the Git log and commit ranges to identify incoming upstream changes, personal commits, duplicates, and likely conflicts. It drops customizations already supplied upstream; when only a documentary local diff remains for such a customization, it also drops that account unless it adds an independently useful current contract absent upstream. Each attempt uses one UTC basic timestamp for its independent `dsh-staging-<timestamp>` sibling clone, local `dsh-upgrade/prepare-<timestamp>` branch, new `dsh-staging/<timestamp>` branch, private upstream and recovery refs, and launcher backup. The sibling name does not derive from the current directory name, and collisions fail rather than acquiring ad hoc suffixes. The workflow derives the current DSH process source from the process command and runtime environment rather than the shell working directory, then treats the repository and checkout behind the installed launcher as immutable except for holding its existing merge lock.
|
||||
|
||||
After validation in the independent clone, the workflow creates and verifies the timestamped staging branch, then atomically moves the launcher once from the unchanged old staging checkout to the new staging checkout. The launcher never targets a preparation, feature, review, publication, or detached checkout. Failure before cutover leaves the installed checkout and launcher unchanged; failure after cutover restores and verifies the launcher backup. The old staging checkout, its branch, the recovery ref, and the launcher backup remain available until a restarted process proves that DSH runs from the new staging branch and the user explicitly approves rollback cleanup.
|
||||
|
||||
`dsh-upstream-customization` owns upstream publication independently from local maintenance and upgrades. It recommends bug fixes, additive non-conflicting plugin features, and visual improvements; intrusive changes require maintainer approval first. At the end of an upgrade, the agent classifies remaining customizations, explains their upstream value, recommends whether to propose each one, and asks which named candidate the user wants to upstream. Only that selection loads the publication workflow; each feature still requires explicit approval before a push or draft PR. Approved changes start from current upstream `master` without unrelated personal commits. Draft PRs for TUI features preferably include a screenshot from the assembled application after credentials and personal data are removed. `dsh-customize` requires interactive TUI behavior to be exercised in a dedicated tmux session before integration.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep the workflows user-scoped.** This preserves personal flexibility but prevents other users from discovering the same safety rules and lets the workflow drift from the installer shipped by the repository.
|
||||
|
||||
**Rebase the active staging checkout in place.** This is simpler but changes many files during preparation, can disrupt new dsh launches, and cannot provide atomic publication or an unchanged rollback checkout.
|
||||
|
||||
**Update the existing staging checkout after moving the launcher elsewhere.** This retains one staging path but requires a mid-upgrade launcher target that is not a staging branch and still rewrites a checkout that may host a running process.
|
||||
|
||||
**Lock only the final branch switch.** This shortens lock duration but permits a customization merge against the old base while the rebase is being prepared, invalidating the prepared history.
|
||||
|
||||
**Open one upstream PR for all personal changes.** This reduces branch management but publishes unrelated customizations and removes the user's per-feature approval boundary.
|
||||
|
||||
## Consequences
|
||||
|
||||
Upgrade preparation holds the installed staging merge lock while dependencies and checks run, so local customization integration waits for a consistent result. One upgrade creates an independent timestamped clone and staging branch, performs one atomic launcher cutover, and requires one restart afterward; it never writes into the repository or checkout behind the launcher except to hold its existing lock. Each workflow records preconditions, repeats them before mutation, inspects state after interrupted mutations, restores the launcher backup on cutover failure, reruns failed checks after correction, and reports final state. The old staging checkout remains rollback storage until explicit user-approved cleanup. Checked-in evaluations cover selection, process-source protection, unsafe repository states, rollback, and publication authorization; repository documentation checks validate skill links and formatting, while technical review remains responsible for Git and filesystem correctness.
|
||||
@@ -0,0 +1,35 @@
|
||||
# Agent Note: 个人集成分支维护 skill(技能)
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-07-23-personal-staging-maintenance-skills.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
个人 dsh 定制需要一套可重复执行的方法,用于定位已安装的源码、隔离各项任务的修改、串行集成变更,并在不改写运行中会话所用检出的前提下合入上游变更。用户本地指令能解决某一套安装中的问题,却无法指导其他用户,也无法持续与仓库安装脚本的行为保持同步。
|
||||
|
||||
## 决策
|
||||
|
||||
仓库从其根 `skills/` 目录分发 [`dsh-customize`](../../../../skills/dsh-customize/SKILL.md)、[`dsh-upgrade`](../../../../skills/dsh-upgrade/SKILL.md) 和 [`dsh-upstream-customization`](../../../../skills/dsh-upstream-customization/SKILL.md)。它们的描述同时说明操作内容和选择该 skill 的用户请求。分发的 TUI 在启动时将该目录提供给本地 skill 提供方,在发现优先级上位于项目根目录和用户根目录之后。这些 skill 根据已安装的启动器而非个人路径或分支名称定位当前生效的检出和集成分支,遵从仓库内指令,要求使用任务 worktree,并利用集成分支所在 worktree 的既有 `.agents/merge.lock`,串行执行每一次个人集成分支修改。
|
||||
|
||||
升级流程在变基前检查 Git 日志和提交范围,以识别将进入升级的上游变更、个人提交、重复内容和可能发生冲突的区域。它会丢弃上游已经提供的定制;如果这类定制在本地只剩说明性差异,也会一并丢弃,除非该说明包含上游缺失且可独立使用的当前契约。每次升级尝试使用同一个 UTC 基本格式时间戳,用于其独立的 `dsh-staging-<timestamp>` 同级克隆、本地 `dsh-upgrade/prepare-<timestamp>` 分支、新的 `dsh-staging/<timestamp>` 分支、私有的上游引用与恢复引用,以及启动器备份。同级克隆的名称不派生自当前目录名,名称冲突会直接失败,而不是追加临时后缀。流程根据进程命令和运行时环境而非 shell 工作目录推导当前 DSH 进程的源码位置,随后将已安装启动器所指向的仓库和检出视为不可变,唯一例外是持有其既有合并锁。
|
||||
|
||||
在独立克隆中验证通过后,工作流会创建并验证带时间戳的集成分支,然后以原子方式将启动器从保持不变的旧集成分支检出一次性切换到新集成分支检出。启动器绝不会指向准备、功能、评审、发布或处于分离状态的检出。切换前的失败会让已安装的检出和启动器保持不变;切换后的失败则恢复并验证启动器备份。旧的集成分支检出、其分支、恢复引用和启动器备份会一直保留,直到重启后的进程证明 DSH 运行于新的集成分支,且用户明确批准回滚清理为止。
|
||||
|
||||
`dsh-upstream-customization` 独立于本地维护和升级,负责向上游发布。它推荐 bug 修复、附加式且不冲突的插件功能,以及视觉改进;侵入式变更需先取得维护者批准。在升级结束时,agent 会对剩余定制进行分类、说明其上游价值、建议是否提交,并询问用户希望向上游贡献哪个具名候选项。只有用户做出选择后才会加载发布工作流;每项功能在推送或创建草稿 PR(Pull Request)前仍必须得到明确批准。获批的变更均以当前上游 `master` 为起点,不带入无关的个人提交。TUI 功能的草稿 PR 建议在移除凭证与个人数据后,附上完整应用的截图。`dsh-customize` 要求在集成前于专用 tmux 会话中检验交互式 TUI 行为。
|
||||
|
||||
## 备选方案
|
||||
|
||||
**将这些工作流限定在用户本地。** 这样可以保留个人使用的灵活性,但其他用户无法发现同一套安全规则,工作流也可能逐渐偏离仓库分发的安装脚本行为。
|
||||
|
||||
**在当前集成分支检出中原地变基。** 此方案更简单,但准备期间会修改大量文件,可能干扰新的 dsh 启动,也无法实现原子发布或提供一份保持不变的回滚检出。
|
||||
|
||||
**在将启动器迁往别处后更新现有的集成分支检出。** 此方案可以保留单一的集成分支路径,却要求在升级中途让启动器指向一个并非集成分支的目标,且仍会改写可能承载运行中进程的检出。
|
||||
|
||||
**只在最终切换分支时加锁。** 这样可以缩短持锁时间,却允许写入方在变基准备期间继续基于旧基线合并定制变更,导致准备好的历史失效。
|
||||
|
||||
**用一个上游 PR 发布所有个人变更。** 这会减少分支管理工作,却会发布无关的定制,并取消用户按功能逐项批准的边界。
|
||||
|
||||
## 影响
|
||||
|
||||
升级准备流程在安装依赖和运行检查期间持有已安装集成分支的合并锁,因此本地定制的集成必须等待一致的结果。一次升级会创建独立的带时间戳的克隆和集成分支,执行一次原子的启动器切换,并在切换后要求重启一次;除持有其既有锁之外,升级绝不会写入启动器所指向的仓库或检出。各工作流会记录前置条件、在修改前重复检查、在修改被中断后检查状态、在切换失败时恢复启动器备份、修复后重新运行失败的检查,并报告最终状态。旧的集成分支检出会作为回滚存储一直保留,直到用户明确批准清理为止。仓库内评估覆盖 skill 选择、进程源码保护、不安全的仓库状态、回滚和发布授权;仓库文档检查会验证 skill 的链接和格式,Git 与文件系统操作的正确性仍由技术评审负责。
|
||||
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-27-explicit-change-scope-report.md
|
||||
2026-07-27-explicit-change-scope-report.md: 2cce567940a142ed1f4699f4dc67322ed69565e9
|
||||
2026-07-27-explicit-change-scope-report.zh.md: cc08df3ee4f3d681bc4ae8b2b0eab3588dfe1a73
|
||||
@@ -0,0 +1,39 @@
|
||||
# Agent Note: Report an explicit repository change scope
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-07-27-explicit-change-scope-report.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The [pre-push workflow](../../../skills/dsh-pre-push-checks/SKILL.md) needs the diff against the actual base, but constructing `origin/<current-branch>` fails for a new worktree branch that tracks `origin/master` before its first push and misstates a stacked branch whose PR targets another feature branch. The [code-review](../../../skills/dsh-code-review/SKILL.md) and [documentation-audit](../../../skills/dsh-doc-standards/SKILL.md) workflows need the same current-base judgment.
|
||||
|
||||
An incorrect range undermines evidence selection because it can omit affected paths. A three-dot committed diff also says nothing about Git's separate staged, unstaged, and untracked layers.
|
||||
|
||||
## Decision
|
||||
|
||||
The root `change-scope` command requires `--base <ref>`, accepts `--head <ref>` with `HEAD` as the default, and offers a versioned `--json` form. It resolves both inputs to commits with ambiguity detection and requires one merge base before writing output. The report records the repository root without normalizing legal path whitespace, current branch, configured upstream, input refs, resolved base, head, and merge-base commit IDs, plus sorted committed, staged, unstaged, and untracked path sets. Path records are split at raw NUL bytes; the repository root, branch, upstream, and every path are decoded as strict UTF-8. An invalid value aborts the report before output instead of substituting characters or collapsing distinct values.
|
||||
|
||||
Committed paths compare the resolved merge base with the resolved head. Dirty path sets always describe the current worktree and index, even when `--head` names another commit. Every Git probe disables configured filesystem monitors and optional lock-taking; diff configuration cannot hide submodules or invoke external diff or text-conversion drivers, and rename detection is disabled so both sides of a rename remain visible.
|
||||
|
||||
The command never guesses or fetches a base, queries a hosting provider, or selects tests. Each calling workflow verifies current remote or stack state, supplies the base explicitly, and uses the factual report as input to semantic review or evidence selection.
|
||||
|
||||
Focused temporary-repository tests cover a fresh branch tracking `origin/master` without a same-name remote, its post-push upstream, a worktree path ending in legal whitespace, a stacked non-master base, every dirty layer, a configured filesystem monitor remaining unexecuted, distinct non-UTF-8 POSIX paths and branch or upstream names failing without partial output, invalid, ambiguous, and non-commit refs, deterministic human/JSON parity, and unchanged refs, index, config, and status after reporting.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep an ad hoc diff command plus a prose fallback.** This avoids a repository script but leaves normal new-worktree and stacked-base topologies inconsistent across workflows, and it omits dirty layers.
|
||||
|
||||
**Infer the base from the configured upstream.** An upstream may be `origin/master` before the first push, the same feature branch after a push, or a head branch whose PR targets another feature branch. No inference is correct for every topology.
|
||||
|
||||
**Query GitHub for the base inside the command.** This couples a local read-only report to one forge and to network credentials, yet still cannot resolve a branch with no PR.
|
||||
|
||||
**Generate required tests from changed paths.** Paths cannot establish behavior reached through configuration, dynamic loading, subprocesses, workers, built artifacts, or providers. Evidence selection remains judgment under the pre-push workflow.
|
||||
|
||||
## Consequences
|
||||
|
||||
The explicit input makes an incorrect base possible but visible: both input refs and all three resolved commit IDs appear in either output form. Callers pay the small cost of verifying and fetching the live base before running the command.
|
||||
|
||||
The string schema deliberately cannot represent non-UTF-8 path bytes. A repository containing them must rename those paths before it can produce a report, preserving exact scope instead of returning a lossy one.
|
||||
|
||||
The repository owns one Git-topology helper and focused tests. In return, pre-push selection, code review, and documentation audit share a deterministic, read-only account of committed and local changes without importing forge or policy concerns.
|
||||
@@ -0,0 +1,39 @@
|
||||
# Agent Note: 显式报告仓库变更范围
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-07-27-explicit-change-scope-report.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
[pre-push 工作流](../../../skills/dsh-pre-push-checks/SKILL.md)需要取得相对于实际基准的 diff,但按 `origin/<current-branch>` 构造引用存在两类问题:对于第一次推送前跟踪 `origin/master`、尚无同名远端分支的新 worktree 分支,该引用无法解析;对于 PR(Pull Request)以另一功能分支为基准的堆叠分支,该引用会错误描述基准。[代码评审](../../../skills/dsh-code-review/SKILL.md)与[文档审计](../../../skills/dsh-doc-standards/SKILL.md)工作流同样需要判断当前基准。
|
||||
|
||||
错误的范围可能遗漏受影响的路径,从而削弱证据选择。三点范围产生的已提交 diff 也完全无法说明 Git 中彼此独立的已暂存、未暂存与未跟踪层。
|
||||
|
||||
## 决策
|
||||
|
||||
根目录的 `change-scope` 命令要求提供 `--base <ref>`,接受可选的 `--head <ref>`(默认为 `HEAD`),并提供带版本号的 `--json` 输出格式。该命令会检测歧义,将两个输入解析为 commit,并要求二者恰好有一个合并基点,之后才会输出结果。报告记录仓库根目录(不对路径中的合法空白字符作规范化处理)、当前分支、配置的上游、输入引用、解析后的基准、头部与合并基点 commit ID,以及排序后的已提交、已暂存、未暂存和未跟踪路径集合。路径记录先按原始 NUL 字节切分;仓库根目录、分支、上游和每条路径都以严格 UTF-8 解码。遇到无效值时,命令会在写出任何结果前失败,不会用替换字符代替无效字节或把不同值合并为一条。
|
||||
|
||||
已提交路径由解析后的合并基点与头部之间的比较得出。即使 `--head` 指定其他 commit,各类未提交路径集合仍始终描述当前 worktree 与索引。每次 Git 探测都会禁用配置的文件系统监视器和可选加锁;diff 配置不能隐藏子模块,也不能调用外部 diff 或文本转换驱动;系统禁用重命名检测,因此重命名前后的路径都会保留在结果中。
|
||||
|
||||
该命令从不猜测或获取基准,不查询代码托管提供方,也不选择测试。调用该命令的每个工作流都会验证当前远端或堆叠状态、显式提供基准,并将这份事实报告作为语义评审或证据选择的输入。
|
||||
|
||||
聚焦的临时仓库测试覆盖以下情形:新分支跟踪 `origin/master` 但没有同名远端分支;同一分支推送后的上游配置;以合法空白字符结尾的 worktree 路径;堆叠分支以非 master 分支为基准;所有未提交改动层;配置的文件系统监视器不会执行;互异的非 UTF-8 POSIX 路径、分支名或上游名会使报告失败且不产生部分输出;无效、有歧义及不指向 commit 的引用;人类可读输出与 JSON 输出保持确定性一致。测试还确认生成报告前后,引用、索引、配置与状态均不发生变化。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**继续使用临时拼装的 diff 命令,辅以文字化回退说明。** 这种方式无需添加仓库脚本,但不同工作流对常见的新 worktree 与堆叠基准拓扑仍会作出不一致处理,而且无法涵盖未提交改动层。
|
||||
|
||||
**根据配置的上游推断基准。** 第一次推送前,上游可能是 `origin/master`;推送后,它可能是同一功能分支;也可能是一个头部分支,而其 PR 以另一功能分支为基准。没有一种推断能够适用于所有拓扑。
|
||||
|
||||
**在命令内查询 GitHub 以确定基准。** 这会把本地只读报告绑定到单一代码托管平台与网络凭证,却仍然无法解析尚无 PR 的分支。
|
||||
|
||||
**根据变更路径生成必需的测试。** 变更路径无法揭示经由配置、动态加载、子进程、worker、构建产物或提供方触达的行为。pre-push 工作流仍须通过判断来选择证据。
|
||||
|
||||
## 结果
|
||||
|
||||
显式输入仍可能指定错误的基准,但这种错误是可见的:两种输出格式都会显示输入引用与解析出的三个 commit ID。调用方需要付出少量成本,在运行该命令前验证实时基准并从远端获取它。
|
||||
|
||||
字符串 schema 有意不表示非 UTF-8 路径字节。含有这类路径的仓库必须先重命名这些路径才能生成报告,以此保持范围精确,而非返回有损结果。
|
||||
|
||||
仓库需要维护一个 Git 拓扑辅助工具及相应的聚焦测试。由此,pre-push 证据选择、代码评审与文档审计可以共享一份确定且只读的已提交及本地变更说明,而不必混入代码托管平台或策略职责。
|
||||
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-27-worktree-local-lefthook.md
|
||||
2026-07-27-worktree-local-lefthook.md: d18f6c1bf8fe240759ad48f67ca6b231000eaf2c
|
||||
2026-07-27-worktree-local-lefthook.zh.md: 42a1625a3b2ec7b00942dc46b0c9c64058ecd2fc
|
||||
@@ -0,0 +1,41 @@
|
||||
# Agent Note: Make Lefthook installation worktree-local
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-07-27-worktree-local-lefthook.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
Every `pnpm install` runs the root [`postinstall`](../../../../package.json), whose [`install-lefthook.mjs`](../../../../scripts/install-lefthook.mjs) invokes `lefthook install --force`. Linked Git worktrees otherwise share the common repository's default hooks directory, so an install in any worktree can rewrite hooks used by every other worktree.
|
||||
|
||||
Lefthook-generated hooks prefer an absolute binary path captured from the installing worktree before trying their current-worktree fallback. Shared hooks can therefore run another worktree's pinned binary until that worktree disappears, while concurrent installs write the same files.
|
||||
|
||||
## Decision
|
||||
|
||||
Hook installation is worktree-scoped. With `CI=true` or `GITHUB_ACTIONS=true`, the installer returns before Git discovery or mutation because automated jobs do not consume contributor hooks. Otherwise, it requires Git 2.26 or newer for configuration-scope provenance, upgrades a format-0 repository to format 1, enables `extensions.worktreeConfig`, and assigns the current worktree an absolute `core.hooksPath` at `$GIT_DIR/dsh-hooks`.
|
||||
|
||||
Before upgrading format 0, the installer refuses direct common-config `extensions.*`; it also refuses direct `core.worktree` or `core.bare=true` and non-empty dormant worktree configs that enabling the extension would activate. The migration removes direct `core.bare=false` because false is Git's default. The common repository config and every existing `config.worktree` must be regular files. These checks disable include expansion because Git's repository-format parser also ignores included targets. A repository-scoped lock serializes migration and hook writes; its process ID, random token, file identity, and exact contents must still match at release. Dead or invalid locks require manual recovery rather than automatic breaking.
|
||||
|
||||
Each hook directory carries a JSON ownership marker containing the absolute path last published to worktree config. After a checkout moves, that marker permits replacement of only the exact stale owned value. Before Lefthook runs, the marker and every existing generated hook must be unaliased regular files. The installer resolves the effective scope, origin, and value of `core.hooksPath`, including active `config.worktree` includes; it refuses command-scoped paths, unowned worktree-scoped paths, and unowned reserved directories. An inherited system, global, or common-repository path requires `DSH_LEFTHOOK_ALLOW_HOOKS_PATH_OVERRIDE=1`, which opts only the current worktree into Lefthook. Inactive `includeIf` targets are not recursively inspected because they do not affect the current configuration. Command-scoped Git configuration is removed from the Lefthook subprocess environment after validation.
|
||||
|
||||
If Lefthook fails after changing `core.hooksPath`, the installer restores the previous worktree value; a rollback failure is reported alongside the installation failure. Existing files in `$GIT_COMMON_DIR/hooks` are never removed or rewritten. Focused installer tests pin isolation, migration refusal, ownership and relocation, concurrent installation, custom paths, and rollback.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep the shared generated hooks and rely on their current-worktree fallback.** The captured absolute path wins while its worktree exists, so the fallback does not provide version or lifecycle isolation.
|
||||
|
||||
**Point every worktree at one checked-in `.githooks` directory.** A relative tracked directory removes generated absolute paths, but changing the shared `core.hooksPath` can disable hooks in older worktrees whose branches do not contain that directory and still couples every worktree to one shared configuration value.
|
||||
|
||||
**Build a general hook-manager chaining layer.** Ordering, argument forwarding, failure semantics, and upgrades become repository-owned behavior unrelated to Lefthook isolation. The installer instead refuses worktree-specific custom paths and makes the narrower inherited-path override explicit.
|
||||
|
||||
**Whitelist provider-specific CI credential-include paths.** Contributor hooks are unused in CI, so path exemptions would couple installer safety to provider checkout internals and weaken strict validation for contributor installs. The CI no-op avoids repository mutation without any exemptions.
|
||||
|
||||
**Stop installing hooks automatically.** Manual setup avoids shared writes but makes the repository's cheap commit and push checks optional by accident, especially in short-lived agent worktrees.
|
||||
|
||||
## Consequences
|
||||
|
||||
Installing or removing one worktree no longer changes another worktree's active hooks, binary path, or generated hook bytes. Concurrent installs are serialized and repeated installation is idempotent, while the jobs and latency boundary owned by [Fast local Git hooks](2026-07-22-fast-local-git-hooks.md) stay unchanged.
|
||||
|
||||
The repository becomes a Git format-1 repository after the first installation. The installer requires Git 2.26 for `--show-scope`; the worktree-config extension itself predates that command. Custom worktree hook managers require an explicit integration choice; inherited hook paths can coexist across other worktrees, but opting the current worktree into Lefthook means those inherited hooks do not run there unless the contributor chains them through `lefthook.yml`.
|
||||
|
||||
Legacy common hooks remain on disk for unupgraded worktrees. They can become stale, but removing them automatically would break a registered worktree whose branch has not adopted this installer.
|
||||
@@ -0,0 +1,41 @@
|
||||
# Agent Note: 让 Lefthook 安装限定于各 worktree
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-07-27-worktree-local-lefthook.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
每次运行 `pnpm install` 都会执行根目录的 [`postinstall`](../../../../package.json),其中的 [`install-lefthook.mjs`](../../../../scripts/install-lefthook.mjs) 会调用 `lefthook install --force`。若无额外配置,关联的 Git worktree 共用同一仓库的默认钩子目录,因此在任一 worktree 中安装都可能改写其他所有 worktree 使用的钩子。
|
||||
|
||||
Lefthook 生成的钩子会优先使用安装时从对应 worktree 记录的绝对二进制文件路径,之后才尝试当前 worktree 的回退路径。因此,共享钩子会一直运行另一个 worktree 固定版本的二进制文件,直到该 worktree 消失;并发安装还会写入同一组文件。
|
||||
|
||||
## 决策
|
||||
|
||||
钩子安装以 worktree 为作用域。当 `CI=true` 或 `GITHUB_ACTIONS=true` 时,安装程序会在探测 Git 或做出任何变更之前返回,因为自动化任务不会使用贡献者钩子。否则,为了获取配置作用域的来源信息,安装程序要求 Git 2.26 或更高版本;它会将格式版本为 0 的仓库升级到格式版本 1,启用 `extensions.worktreeConfig`,并将当前 worktree 的 `core.hooksPath` 设为指向 `$GIT_DIR/dsh-hooks` 的绝对路径。
|
||||
|
||||
升级格式 0 之前,安装程序会拒绝共用配置中直接设置的 `extensions.*`;它还会拒绝直接设置的 `core.worktree` 或 `core.bare=true`,以及启用扩展后将被激活的非空且尚未生效的 worktree 配置。迁移会移除直接设置的 `core.bare=false`,因为 false 是 Git 的默认值。共用仓库配置和每个已有的 `config.worktree` 都必须是常规文件。这些检查会禁用 include 展开,因为 Git 的仓库格式解析器也会忽略 include 目标。仓库级锁会串行化迁移和钩子写入;释放时,锁的进程 ID、随机令牌、文件身份和完整内容必须仍然匹配。所属进程已结束或内容无效的锁必须手动恢复,不会被自动破坏。
|
||||
|
||||
每个钩子目录都有一个 JSON 所有权标记,其中包含上次写入 worktree 配置的绝对路径。检出目录移动后,该标记只允许替换确切的陈旧自有值。Lefthook 运行前,所有权标记和每个已有的生成钩子都必须是不带别名的常规文件。安装程序会解析 `core.hooksPath` 的生效作用域、来源和值,包括通过当前生效的 `config.worktree` include 加载的值;它会拒绝命令作用域路径、非自有的 worktree 作用域路径以及非自有的保留目录。继承自系统、全局或共用仓库配置的路径必须设置 `DSH_LEFTHOOK_ALLOW_HOOKS_PATH_OVERRIDE=1`,从而只让当前 worktree 显式启用 Lefthook。未生效的 `includeIf` 目标不会被递归检查,因为它们不影响当前配置。完成验证后,Lefthook 子进程的环境会移除命令作用域的 Git 配置。
|
||||
|
||||
若 Lefthook 在更改 `core.hooksPath` 后失败,安装程序会恢复先前的 worktree 值;若回滚失败,会与安装失败一并报告。`$GIT_COMMON_DIR/hooks` 中的现有文件绝不会被移除或改写。聚焦的安装程序测试固定了隔离、迁移拒绝、所有权和检出目录移动、并发安装、自定义路径及回滚行为。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**保留共享的生成钩子,并依赖其当前 worktree 回退路径。** 只要对应 worktree 仍存在,记录的绝对路径就会优先生效,因此回退路径无法提供版本或生命周期隔离。
|
||||
|
||||
**让每个 worktree 都指向同一个纳入版本控制的 `.githooks` 目录。** 使用受版本控制的相对目录可以消除生成的绝对路径,但更改共享的 `core.hooksPath` 可能会禁用旧 worktree 中的钩子,因为其分支并不包含该目录;同时,每个 worktree 仍然耦合于同一个共享配置值。
|
||||
|
||||
**构建通用的钩子管理器串联层。** 执行顺序、参数转发、失败语义和升级都会成为仓库自行负责的行为,却与 Lefthook 隔离无关。因此,安装程序会拒绝 worktree 专属的自定义路径,只将范围更窄的继承路径覆盖设为显式操作。
|
||||
|
||||
**将特定 CI 提供商的凭据 include 路径加入白名单。** CI 不使用贡献者钩子,因此路径豁免会使安装程序的安全性耦合于提供商的检出目录内部结构,并削弱贡献者安装时的严格验证。CI 无操作方案无需任何豁免即可避免修改仓库。
|
||||
|
||||
**停止自动安装钩子。** 手动设置可以避免共享写入,却会使仓库中低成本的提交与推送检查意外变成可选项,短期存在、由 agent(智能体)使用的 worktree 尤其容易受到影响。
|
||||
|
||||
## 后果
|
||||
|
||||
安装或移除任一 worktree 不再改变其他 worktree 的生效钩子、二进制文件路径或生成的钩子字节。并发安装会串行执行,重复安装保持幂等;[快速本地 Git 钩子](2026-07-22-fast-local-git-hooks.md)所规定的任务与延迟边界保持不变。
|
||||
|
||||
首次安装后,仓库会采用 Git 格式版本 1。安装程序需要 Git 2.26 来使用 `--show-scope`;worktree 配置扩展本身的出现早于该命令。自定义 worktree 钩子管理器需要明确选择集成方式;继承钩子路径可继续供其他 worktree 使用,但当前 worktree 显式启用 Lefthook 后,其中不会运行这些继承钩子,除非贡献者通过 `lefthook.yml` 将其串联起来。
|
||||
|
||||
旧的共用钩子会为尚未升级的 worktree 保留在磁盘上。它们可能逐渐陈旧,但自动删除这些钩子会破坏已注册但所在分支尚未采用本安装程序的 worktree。
|
||||
Reference in New Issue
Block a user