feat(worktree): declare the post-create hook command outside the repo - #75
Merged
Conversation
Handoff item 4 claimed `scripts/lessons.py` does not exist. It ships in the skill package (the docs' path is skill-relative), and it runs here through Azure CLI's bundled Python despite the Microsoft Store aliases being dead. Verified the hand-maintained store against it: `status` reports 7 lessons / confirmed=2, and re-rendering a scratch copy reproduces LESSONS.md and lessons.json identically, so the round-3 hand edits were format-correct. Records the exact invocation (--root is a top-level arg) and marks the item resolved. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…1..14) Spec for reading `postCreateCommand` from the workspace-level `.app/config.json` (`postCreateCommands[<repoName>]`) as a fallback when the repo declares none. Owner decisions: workspace file as the home, repo wins, per-repo keys only. 14 ACs, all main-process unit-testable; no renderer surface and no new real-process test. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds `workspacePostCreateCommand(workspacePath, repoName)` beside `workspaceTemplates` in the same reader module: the out-of-repo declaration site, `<workspace>/.app/config.json` under `postCreateCommands[<repoName>]` (HWC-02). Read on use, wrong-typed map or value collapses to null, malformed JSON logs once and falls back (HWC-05..08). Key matching is exact first, then a unique case-insensitive match, because Windows folder names are case-insensitive (AD-005) so "code" for a folder named "Code" is a slip rather than another repo; two variants with no exact match are ambiguous and resolve to no command plus one log (HWC-09). Own keys only, so a repo named `toString` cannot match the prototype. 24 tests. Nothing wired yet — resolution and precedence land next. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…file TESTING.md:15 pins one test file per module, `<module>.test.ts`. The new reader lives in workspace-config.ts, so its 24 tests belong in workspace-config.test.ts — not in a separate file named after the function. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…-second `resolvePostCreateCommand(repoPath)` composes the two readers: the repo's own `postCreateCommand` wins, and only when it declares nothing — any WPC-06 shape, including malformed JSON — does the workspace's `postCreateCommands[<repoName>]` apply (HWC-01, HWC-02). Exactly one command ever runs. Both levels come from `repoPath` alone: scanRepos only finds a repo as a direct child of its workspace, so the workspace is `dirname` and the key is `basename` (HWC-03). No signature change, so the decorator and worktree-manager are untouched. A drive root or empty path has no name to key on and resolves to null (HWC-12); derivation is lexical, so a repo outside a configured workspace is simply unmatched (HWC-13). 15 tests. Not wired into the hook yet. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Restores the lint warning count to its pre-existing 18 (the new code had added 5 prettier warnings: a wrapped signature and a wrapped it.each table). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`withPostCreateHook`'s `readCommand` now points at `resolvePostCreateCommand` instead of `repoPostCreateCommand`, so every create path — New Worktree, Start Work and workflow `ctx.worktree.create` — honours the workspace-level declaration through the single wrapper they all share (HWC-01, HWC-02). Structural, like WPC-10: one `readCommand` wiring in the process, and `repoPostCreateCommand`'s only production consumer is now the resolver. Verified by typecheck, build and lint (0 errors, 18 pre-existing warnings). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
README now shows both declaration sites — `<workspace>/.app/config.json`'s `postCreateCommands[<repoName>]` first, since keeping a shared team repo clean is the reason the level exists — plus the repo-wins precedence, per-repo key matching, and how the workspace is derived. Also records the two command-shape gotchas measured against the real SetupSkills.cmd: the leading `.\` and `< NUL` for the trailing `pause`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
AD-015 amends AD-013 decision 1: the command may be declared in `<workspace>/.app/config.json` under `postCreateCommands[<repoName>]`, the repo still wins, per-repo keys only. Records the lexical dirname/basename derivation that keeps `readCommand`'s signature unchanged. validation.md: 14/14 ACs with file:line evidence, 9/12 mutants killed with the three survivors reasoned as equivalence artifacts, and the standalone-pass caveat (no Verifier sub-agent available, so author != verifier is unmet). Handoff now covers both in-flight features and documents the environment defect found while validating: on Node v24.9.0 every `fs.rmSync` shape silently no-ops on non-ASCII paths, which is why one pre-existing worktree-manager test fails on a clean tree. Production is unaffected (async `rm` only). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Created a worktree for M:\Triade\source\Code from the New Worktree dialog
(driven over CDP against the dev app) with the command coming only from
M:\Triade\source\.app\config.json. Create completed with no hook-failure
advisory and both .claude\skills and .codex\skills landed in the new
worktree as reparse points into its own .github\skills.
Records the incidental finding that the global {repo}-{id} worktree
template renders a numberless branch down to the repo folder itself, and
that the dev build reads %APPDATA%\playground rather than the installed
nightly's userData.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…status The handoff said the removal feature was unpushed with no PR and no issue; origin/main (7cc8c76) is in fact the PR #73 merge, with issue #72 closed on 2026-07-31. Corrects that, and notes that removal-branch commit 5e22450 was made after #73 merged and so reaches main via #75 instead. Also records why #75 targets main rather than stacking on the removal branch, and the two hand-testing gotchas found while discharging the end-to-end criterion. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The post-create hook command can now be declared outside the repo it initializes.
AD-013 put it inside:
<repo>\.app\config.json, keypostCreateCommand. For a shared team repo thatis a dead end — either a permanent
?? .app/ingit status, or a PR into the team repo just torecord one developer's local automation. This PR adds a second declaration site in the workspace file
that already exists and is already hand-authored for
branchTemplate/worktreeTemplate:AD-013 is extended, not reversed. The repo still wins; the workspace entry is only the fallback,
so WPC-01/WPC-06 keep holding verbatim and every pre-existing test passes unmodified. Exactly one
command runs per create — never both levels. Per-repo keys only: no
"*"default and no bareworkspace-level string, so a newly cloned repo runs nothing until it is named.
Nothing about how the command runs changes —
cwd, thePLAYGROUND_*env, the 120 s timeout, the4000-char output tail, the run-iff-created rule and the keep-the-worktree-on-failure semantics are all
untouched. This PR changes only where the string comes from.
Decision record: AD-015 in
.specs/STATE.md. Spec + validation:.specs/features/worktree-hook-workspace-config/.Closes #74
Changes
repo-config.ts— newworkspacePostCreateCommand(workspaceDir, repoName)reader, plus aresolvePostCreateCommand(repoPath)composer that wraps the two readers repo-first,workspace-second. Key matching is exact first, then a unique case-insensitive match (Windows
folder names are case-insensitive, AD-005); two-or-more case-insensitive matches with no exact
match resolve to no command plus one log rather than an arbitrary winner.
post-create-hook.tswiring —resolvePostCreateCommandbecomeswithPostCreateHook'sreadCommand.readCommand's signature is unchanged, becausescanReposis a single-levelscan (
repo-scanner.ts:20-38) — a repo is always a direct child of its workspace — so theworkspace is
dirname(repoPath)and the key isbasename(repoPath), derived purely lexicallywith no lookup against
AppConfig.workspaces.worktree-manager.tsandworkflow-ctx.tsareuntouched.
README.md— documents both declaration sites, the repo-wins precedence, and the accepteduntrusted-repo-content risk (which shrinks at workspace level, since you author that file
yourself).
.specs/— AD-015, the HWC-01..14 spec, and the validation report.5e22450(docs(specs): correct the lessons-storenote), which was made after PR Worktree removal fault tolerance: delete first, deregister second #73 merged and so never reached
main. This branch was originallycut off the removal branch; by the time it was ready, Worktree removal fault tolerance: delete first, deregister second #73 had merged, so it targets
maindirectlyrather than stacking.
Testing
605 tests passing, typecheck clean, lint 0 errors / 18 pre-existing warnings.
All 14 ACs (HWC-01..14) Verified by a standalone fresh-eyes pass with a discrimination sensor —
9/12 mutants killed; the 3 survivors are recorded equivalence artifacts. Note author ≠ verifier is
unmet (this harness is configured without Verifier sub-agents), so the sensor is the compensating
control rather than a second opinion.
End-to-end, on the real repo: a worktree create for
M:\Triade\source\Codefrom the New Worktreedialog, driven over CDP against the dev app, with
M:\Triade\source\.app\config.jsonas the onlysource of the command (
M:\Triade\source\Code\.app\config.jsonis deleted). Create completed with nohook-failure advisory, the dialog closed and selected the new worktree, and both
.claude\skillsand.codex\skillslanded as reparse points into that worktree's own.github\skills(14 entries each).The throwaway worktree was then removed delete-first per AD-014. Full run in
validation.md.Screenshots / Notes
One pre-existing test failure is expected on this machine and is not a regression. On Node
v24.9.0 here, every
fs.rmSyncshape silently no-ops when any path component contains a non-ASCIIcharacter; the fixtures root at
realpathSync.native(tmpdir())=C:\Users\OtávioBogoni\…, soworktree-manager.test.ts > removeWorktree > force-removes a worktree with mixed dirtfails on aclean tree. No production file uses
rmSync(dir-remover.tsuses asyncrm, re-measured correct onnon-ASCII trees). CI runs on
windows-latestunder a different profile name and is unaffected.Follow-ups, not part of this PR: the removal feature's live smoke (
scripts/smoke-remove.mjs) andthe Danger-section visual pass are still open against already-merged code (WRFT-06); WRFT-07
(create-time leftover collision) is specified and deferred.
Checklist
Closes #74)