Skip to content

feat(worktree): declare the post-create hook command outside the repo - #75

Merged
obogoni merged 11 commits into
mainfrom
feature/worktree-hook-workspace-config
Aug 5, 2026
Merged

feat(worktree): declare the post-create hook command outside the repo#75
obogoni merged 11 commits into
mainfrom
feature/worktree-hook-workspace-config

Conversation

@obogoni

@obogoni obogoni commented Aug 5, 2026

Copy link
Copy Markdown
Owner

Summary

The post-create hook command can now be declared outside the repo it initializes.

AD-013 put it inside: <repo>\.app\config.json, key postCreateCommand. For a shared team repo that
is a dead end — either a permanent ?? .app/ in git status, or a PR into the team repo just to
record 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:

// M:\Triade\source\.app\config.json  — one workspace, many repos
{
  "postCreateCommands": {
    "Code": ".\\SetupSkills.cmd < NUL"
  }
}

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 bare
workspace-level string, so a newly cloned repo runs nothing until it is named.

Nothing about how the command runs changes — cwd, the PLAYGROUND_* env, the 120 s timeout, the
4000-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 — new workspacePostCreateCommand(workspaceDir, repoName) reader, plus a
    resolvePostCreateCommand(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.ts wiringresolvePostCreateCommand becomes withPostCreateHook's
    readCommand. readCommand's signature is unchanged, because scanRepos is a single-level
    scan (repo-scanner.ts:20-38) — a repo is always a direct child of its workspace — so the
    workspace is dirname(repoPath) and the key is basename(repoPath), derived purely lexically
    with no lookup against AppConfig.workspaces. worktree-manager.ts and workflow-ctx.ts are
    untouched.
  • README.md — documents both declaration sites, the repo-wins precedence, and the accepted
    untrusted-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.
  • Carries one commit from the removal branch, 5e22450 (docs(specs): correct the lessons-store
    note
    ), which was made after PR Worktree removal fault tolerance: delete first, deregister second #73 merged and so never reached main. This branch was originally
    cut off the removal branch; by the time it was ready, Worktree removal fault tolerance: delete first, deregister second #73 had merged, so it targets main directly
    rather than stacking.

Testing

  • Manual test
  • Automated tests added/updated

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\Code from the New Worktree
dialog, driven over CDP against the dev app, with M:\Triade\source\.app\config.json as the only
source of the command (M:\Triade\source\Code\.app\config.json is deleted). Create completed with no
hook-failure advisory, the dialog closed and selected the new worktree, and both .claude\skills and
.codex\skills landed 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.rmSync shape silently no-ops when any path component contains a non-ASCII
character; the fixtures root at realpathSync.native(tmpdir()) = C:\Users\OtávioBogoni\…, so
worktree-manager.test.ts > removeWorktree > force-removes a worktree with mixed dirt fails on a
clean tree. No production file uses rmSync (dir-remover.ts uses async rm, re-measured correct on
non-ASCII trees). CI runs on windows-latest under a different profile name and is unaffected.

Follow-ups, not part of this PR: the removal feature's live smoke (scripts/smoke-remove.mjs) and
the Danger-section visual pass are still open against already-merged code (WRFT-06); WRFT-07
(create-time leftover collision) is specified and deferred.

Checklist

  • Self-reviewed the diff
  • No secrets or credentials committed
  • Related issues linked (Closes #74)

Otávio Bogoni and others added 11 commits July 31, 2026 17:21
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>
@obogoni
obogoni merged commit 9d825d6 into main Aug 5, 2026
2 checks passed
@obogoni
obogoni deleted the feature/worktree-hook-workspace-config branch August 5, 2026 14:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Worktree post-create hook: declare the command outside the repo

1 participant