Skip to content

feat(config): project configuration with a committed .worktree.yml (#181) - #186

Merged
baldurpan merged 10 commits into
mainfrom
feature/181-project-config
Oct 29, 2026
Merged

baldurpan merged 10 commits into
mainfrom
feature/181-project-config

Conversation

@baldurpan

Copy link
Copy Markdown
Contributor

Closes #181

A committed .worktree.yml, read from origin/HEAD, lets a project state its shared conventions once instead of in every clone. The plan and its finished ledger are in #181. It also absorbs #128 and #118.

What landed

  1. .worktree.yml schema, loader and the published JSON Schema (docs/public/schema/v1.json)
  2. Precedence (git config over the file) and the simple team keys
  3. Rules engine and Jira branch-type rules
  4. GitHub, Linear and ClickUp branch-type rules, and a branchName template
  5. worktree branch --dry-run to preview a branch name and the rules that produced it
  6. copy patterns for new worktrees
  7. postCreate steps, run only after per-person approval
  8. worktree config shows where each value comes from, writes .worktree.yml with --project, and offers migration

Each phase passed both gates; the ledger in #181 carries every commit sha.

Release notes

Three notes in .changeset/ (CLI minor, skill patch, runtime dependencies patch). This is not released: it waits on the release pull request. The docs site deploys on merge because docs/** changed.

Owner checks left for a person

None of these is a known defect.

… Schema (#181)

Phase 1 of #181. Adds yaml 2.9.1 and zod 4.6.7, the v1 project-config schema
in src/lib/project-config.ts with its generated docs/public/schema/v1.json and
a drift test, and the loader: reads .worktree.yml from origin/HEAD (personal
defaultSourceBranch as fallback, WORKTREE_PROJECT_CONFIG override), refuses
secret keys and malformed files with exit 2 naming file, ref, key path and
line, and warns on unknown and personal keys. Adds git helpers to resolve
origin/HEAD and read a blob at a ref, and an untrimmed option on
runCapturing so cited line numbers match the committed file. No consumer
reads it yet.

Gate 1: pnpm check, pnpm typecheck, pnpm --filter docs typecheck,
pnpm build, pnpm test (1493 passed), pnpm docs:test (69 passed), test
markers clean, all exit 0.
Gate 2: reviewer subagent, FAIL then PASS after one loopback (unresolvable
ref was silent, prototype-named keys bypassed validation, trimmed blob
shifted line numbers).
…181)

Phase 2 of #181. Team keys (defaultSourceBranch, the three branchPrefix
keys, jira.host, clickup.workspaceId) now resolve flag, then git config,
then the project file, then the built-in default, and verifyConfig accepts
a key the file supplies. branch fetches once, before it reads the project
file, so a bad file stops it with exit 2 before anything is created; a
file-supplied jira.host is used without a prompt and a personal one
overrides it. Adds the Project Configuration docs page and points
Recommended Team Baseline, the Jira guide and the branch and config pages
at it.

Gate 1: pnpm check, pnpm typecheck, pnpm --filter docs typecheck (24
routes), pnpm build, pnpm test (1538 passed), pnpm docs:test (69 passed),
test markers clean, all exit 0.
Gate 2: reviewer subagent, FAIL then PASS WITH NOTES after one loopback
(two pages still described jira.host as git-config only).
)

Phase 3 of #181. Adds a pure branch-type rules engine (src/lib/branch-type.ts):
ordered rules, first match wins, every field in a `when` must match, a list
of values matches any, ids compare exactly and names case-insensitively,
`otherwise` sets the kind for an unmatched issue and without it there is no
prefix. A rule naming a field the issue lacks warns, naming the field and
the rule's position; a display name shared by two fields is an error naming
both ids. With `jira.branchType` set, Jira asks for `expand=names` and the
rules replace the built-in mapping; without it the request and the result
are unchanged. Docs: the rules section of Project Configuration, and the
Jira guide, branch and configuration pages say the built-in mapping applies
only without rules.

Gate 1: pnpm check, pnpm typecheck, pnpm --filter docs typecheck,
pnpm build, pnpm test (1566 passed), pnpm docs:test (69 passed), test
markers clean, all exit 0.
Gate 2: reviewer subagent, FAIL then PASS WITH NOTES after one loopback
(the new section had cut a subsection off from its parent heading).
…chName template (#181)

Phase 4 of #181. GitHub (Type, Labels), Linear (Labels, with ids) and
ClickUp (Tags) produce normalised fields and use the rules engine when the
project file sets their `branchType`; without rules each provider's request
and result are unchanged, and GitHub's built-in fallback stays no prefix
(#118: `otherwise` opts in). A `branchName` template ({prefix}, {id},
{slug}; default `{prefix}{id}-{slug}`, today's name) applies to every
issue-derived name, Jira's included, never to a positional one, and a name
it renders still goes through isValidBranchName. Docs: the three provider
guides, the template on Project Configuration, and the branch page, README
and skill no longer state the name shape as fixed.

Gate 1: pnpm check, pnpm typecheck, pnpm --filter docs typecheck,
pnpm build, pnpm test (1619 passed), pnpm docs:test (69 passed), test
markers clean, all exit 0.
Gate 2: reviewer subagent, FAIL then PASS WITH NOTES after one loopback
(the branch page and README stated the name shape as fixed; the Linear
guide omitted label ids).
…te rules (#181)

Phase 5 of #181. `branch --dry-run` with an issue flag fetches the issue,
evaluates the project file's rules and prints the branch name, the rule
that matched (its position and text, the built-in mapping, or none), and
every non-empty field with its id, name and values, plus a paste-ready
`when:` line per value. It creates, assigns, writes and asks nothing;
under --json it prints one dry-run document in place of the branch
document. Jira asks expand=names and Linear asks label ids on every dry
run. Without an issue flag it is a usage error. Docs: the flag on the
branch page and how to write rules with it on Project Configuration.

Gate 1: pnpm check, pnpm typecheck, pnpm --filter docs typecheck,
pnpm build, pnpm test (1651 passed), pnpm docs:test (69 passed), test
markers clean, all exit 0.
Gate 2: reviewer subagent, PASS WITH NOTES.
…181)

Phase 6 of #181. The `copy` patterns in .worktree.yml join the env-file
pathspecs as `:(glob)` entries under the same untracked-and-ignored
selection, keeping the node_modules exclusion, so a gitignored file such
as config/master.key is copied and a tracked one never is. `branch` and
`checkout` both apply them, and copied files are listed in
envFilesCopied. `checkout` now reads the project file, so a bad one stops
it with exit 2 before a worktree exists. With no file or no `copy` key the
ls-files call is unchanged. Docs: the env-files guide, Project
Configuration, the branch page, README and the skill.

Gate 1: pnpm check, pnpm typecheck, pnpm --filter docs typecheck,
pnpm build, pnpm test (1667 passed), pnpm docs:test (69 passed), test
markers clean, all exit 0.
Gate 2: reviewer subagent, FAIL, FAIL, then PASS WITH NOTES after two
loopbacks (the real-git copy test read the machine's global git config,
then git's default excludes file).
…proval (#181)

Phase 7 of #181. The project file's postCreate steps run in order in the
new worktree, by argv with no shell, each bounded by the install timeout,
stopping at the first failure. Before anything is created, branch checks a
per-person approval: a SHA-256 over the trimmed steps as a JSON array,
kept in local git config under an internal key outside CONFIG_NAMES. An
unapproved or changed list is shown and asked about in a terminal (no
exits 1 with nothing created) and fails closed non-interactively with exit
2 naming `worktree config --approve` and `--no-install`; the hash is never
printed. A personal postCreate, --no-install and --dry-run need no
approval. A step may not hold control or formatting characters, and file
text quoted in loader warnings and errors is shown with visible escapes,
so neither can redraw or hide the steps being approved. `installed` gains
`steps` and `from`. Docs: approval on Project Configuration, the branch
page's install section and JSON table, the config and configuration
pages, README, the skill and context7.json.

Gate 1: pnpm check, pnpm typecheck, pnpm --filter docs typecheck,
pnpm build, pnpm test (1717 passed), pnpm docs:test (69 passed), test
markers clean, all exit 0.
Gate 2: reviewer subagent, FAIL, FAIL, then PASS WITH NOTES after two
loopbacks (control characters in a step, then an unknown key's name in a
warning, could make the approval prompt show something other than what
runs; the branch page said a missing file-step program is skipped).
…and offer migration (#181)

Phase 8 of #181. `worktree config` now knows the project file: the wizard
asks nothing the file answers (including through branch's delegated
--missing run) and says once which keys it sets; `config <key> <value>`
for a file-set key shows both values and asks, or sets and warns
non-interactively; `--list` marks each value `(.worktree.yml)` or
`(overrides .worktree.yml: …)` in plain text and summarises the
file-only keys. `worktree config --project` writes a commented
.worktree.yml with the $schema line from the team keys in git config,
never a personal or secret key, uncommitted, and refuses to overwrite.
Interactive branch (after open) and config runs offer `--project` once
per repository when there is no file, and offer to remove local keys the
file covers with the same value once per file content; neither runs
under --json, --yes, CI or without a TTY. Docs: config, configuration,
project-configuration and getting-started pages, README and the skill.

Gate 1: pnpm check, pnpm typecheck, pnpm --filter docs typecheck,
pnpm build, pnpm test (1777 passed), pnpm docs:test (69 passed), test
markers clean, all exit 0.
Gate 2: reviewer subagent, FAIL then PASS WITH NOTES after one loopback
(the wizard branch delegates to still asked for keys the file sets).
craig-o-curtis
craig-o-curtis previously approved these changes Oct 29, 2026

@craig-o-curtis craig-o-curtis left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approving. This is a big change but it holds together well. I read the source diff closely: the loader, precedence, the rules engine, postCreate approval, copy, the branch/config/checkout changes and all four integrations. CI is green, so I didn't rerun the gates.

What I checked and found sound:

  • postCreate can't run without approval. runInstall is called only from branch, after ensurePostCreateApproved, and both read the same cached load. --no-install skips both. A personal postCreate overrides the file in both places, and a whitespace-only one can't slip through, because gitGetConfigValue trims. A non-interactive run fails closed, and the hash never shows up in a message.
  • copy patterns can't reach outside the repo. Leading /, leading : and .. are refused, every pattern gets :(glob), and the existing ignored-only, untracked-only selection still applies.
  • Text from the file is escaped before it's printed (showVisibly), and steps with control or formatting characters are refused by the schema.
  • The published schema has a drift test against docs/public/schema/v1.json, and --dry-run is well covered, including that it never asks for approval.
  • A broken file stays contained. verifyConfig and the team-key reads exist only in branch, so a bad file can't break list, remove or cleanup, and config downgrades it to a warning so people can still work around it.

Non-blocking notes. Any of these can be a follow-up:

  1. An ambiguous rule field exits with the wrong code, and nothing tests it. AmbiguousFieldError (src/lib/branch-type.ts:39) extends plain Error, so a rule naming a field that two fields share (common with Jira custom fields such as two "Team" fields) exits 1 / failed. Every other bad-file case exits 2 / invalid_value. It also stops --dry-run before it lists fields, though the message does name the ids. Suggest extending InvalidValueError/ProjectConfigError and adding a test, since no test or doc covers this path today.
  2. jira.host sends credentials with no prompt. I see this is a deliberate choice (D17), and the docs section on it is good. Still, the threat model is uneven: postCreate needs per-person approval, while a merged jira.host change sends every teammate's Jira email and token to a new host with no prompt. Worth considering approving it once per value, the way steps are approved, or at least warning when the file's host differs from the last one used.
  3. Approval is asked before the reopen check. ensurePostCreateApproved runs before reopenOrRefuseTakenBranch (branch.ts:915 vs :924). A non-interactive worktree branch <name-that-already-has-a-worktree> in a repo with unapproved steps exits on missing approval, not with the reopen/refuse guidance, and an interactive run asks for approval it then doesn't use.
  4. The approvalRequired comment overstates what it protects. Its rationale ("a script that could read it from an error could store it") overclaims: the hash is SHA-256 over the public steps, so anything that can write local git config can compute it. The real gate is the TTY requirement. Fine to keep the hash out of messages; the comment could just say why.
  5. FYI: every branch run now prunes. It now always runs git fetch --prune up front. Before, it ran a plain git fetch inside gitCreateWorktree, and only --source pruned. Probably harmless, maybe even better, but it's a behaviour change that the changeset doesn't mention.

The changesets look right to me: minor for the new CLI surface, patch for the deps and the skill.

… the reopen check (#181)

Review of #186: AmbiguousFieldError now extends ProjectConfigError, so a rule
naming a shared display name exits 2 with invalid_value like any bad file.
The postCreate approval is asked after the taken-branch check, so a taken
name gets the reopen guidance. Corrects the approval-hash comment and adds a
note for the --prune every branch run now does.
@baldurpan

Copy link
Copy Markdown
Contributor Author

Thanks for the close read. Taking the five notes in order. Points 1, 3, 4 and 5 are fixed in 882264d, and point 2 is filed as #187.

  1. Ambiguous field exit code: fixed. AmbiguousFieldError now extends ProjectConfigError, so it exits 2 with invalid_value, like every other bad-file case. One correction: a test did exist (branch-type.test.ts, "fails naming both ids…"), but it pinned only the message. A new test pins the class, exit 2 and invalid_value. The project-configuration page now says the run and a dry run stop with exit 2. --dry-run still stops at that point rather than listing fields. I left that as it is, because the message already names the ids a dry run would have found.
  2. jira.host with no prompt: filed as Warn when .worktree.yml changes jira.host from the host credentials were last sent to #187, scoped narrower. D17 records that the maintainer considered per-person approval of a file-supplied host and rejected it on 2026-10-28, so I haven't reopened that here. Your lighter option, warning once when the file's host differs from the last one credentials went to, wasn't weighed. Warn when .worktree.yml changes jira.host from the host credentials were last sent to #187 proposes exactly that, with no prompt.
  3. Approval before the reopen check: fixed. ensurePostCreateApproved now runs after reopenOrRefuseTakenBranch. It is still before assignment and creation, so the docs' "checked before anything is created" stays true. New test: a taken name in a non-interactive run with unapproved steps gets the worktree open guidance, nothing is asked and no approval is stored.
  4. approvalRequired comment: fixed. It now says the hash is not a secret, because anything that can write local git config can compute it, and that the real gate is that only a terminal run asks.
  5. --prune on every run: noted. Added .changeset/branch-fetch-prune.md (patch), which says worktree branch now prunes on every run where before only --source did.

Gate 1 (pnpm check, both typechecks, build, pnpm test, pnpm docs:test, the focused-test grep) is green locally on 882264d. This push will have dismissed your approval, sorry for the extra round.

@baldurpan
baldurpan merged commit 23f4b74 into main Oct 29, 2026
2 checks passed
@baldurpan
baldurpan deleted the feature/181-project-config branch October 29, 2026 12:37
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.

project-config

2 participants