rhachet-roles-ehmpathy 1.9.1 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (20) hide show
  1. package/dist/logic/roles/coach/.briefs/claude.context-caching.md +76 -0
  2. package/dist/logic/roles/mechanic/.briefs/lessons/code.prod.typescript.types/bivariance_vs_contravariance.[lesson].md +95 -0
  3. package/dist/logic/roles/mechanic/.briefs/patterns/code.prod.errors.failfast/bad-practices/forbid.failhide.md +19 -0
  4. package/dist/logic/roles/mechanic/.briefs/patterns/code.prod.errors.failfast/best-practice/prefer.HelpfulError.wrap.md +54 -0
  5. package/dist/logic/roles/mechanic/.briefs/patterns/code.prod.narrative/bad-practices/forbid.else.md +54 -0
  6. package/dist/logic/roles/mechanic/.briefs/patterns/code.prod.narrative/best-practice/early-returns.named-checks.[demo].md +181 -0
  7. package/dist/logic/roles/mechanic/.briefs/patterns/code.prod.repo.structure/bad-practices/forbid.index.ts.md +3 -0
  8. package/dist/logic/roles/mechanic/.briefs/patterns/code.test.howto/best-practice/howto.diagnose.[lesson].md +14 -0
  9. package/dist/logic/roles/mechanic/.briefs/patterns/lang.terms/best-practice/require.order.noun_adj.md +39 -0
  10. package/dist/logic/roles/mechanic/.skills/git.worktree.common.sh +58 -0
  11. package/dist/logic/roles/mechanic/.skills/git.worktree.del.sh +51 -0
  12. package/dist/logic/roles/mechanic/.skills/git.worktree.get.sh +51 -0
  13. package/dist/logic/roles/mechanic/.skills/git.worktree.set.sh +108 -0
  14. package/dist/logic/roles/mechanic/.skills/git.worktree.sh +46 -0
  15. package/dist/logic/roles/mechanic/.skills/git.worktree.test.sh +229 -0
  16. package/dist/logic/roles/mechanic/.skills/init.bhuild.sh +260 -0
  17. package/dist/logic/roles/mechanic/.skills/init.claude.permissions.sh +109 -0
  18. package/dist/logic/roles/mechanic/.skills/init.claude.sh +35 -0
  19. package/dist/logic/roles/mechanic/.skills/run.test.sh +8 -2
  20. package/package.json +2 -2
@@ -0,0 +1,76 @@
1
+ # 🧠 .brief: Claude Context Caching
2
+
3
+ ## .what
4
+ Claude's context caching mechanism that reduces token costs by caching prompt prefixes
5
+
6
+ ## .how.it.works
7
+
8
+ ### cache key = exact prefix match
9
+ - Claude caches content based on the **exact sequence of tokens from the start** of the conversation
10
+ - a cache hit requires the **exact same prefix** to be present
11
+ - cache is valid for ~5 minutes of inactivity
12
+
13
+ ### prefix-based matching
14
+ ```
15
+ Turn 1: [system prompt] + [briefs: 30k tokens] → writes to cache
16
+ Turn 2: [system prompt] + [briefs: 30k tokens] + [user msg] + [response] → cache hit ✅
17
+ Turn 3: [system prompt] + [briefs: 30k tokens] + [history] + [new msg] → cache hit ✅
18
+ ```
19
+
20
+ ### when cache hits occur
21
+
22
+ ✅ **cache hit (free reads)** when:
23
+ - the content appears at the **exact same position** in the context
24
+ - everything before it is **identical** to the cached version
25
+ - less than ~5 minutes of inactivity
26
+
27
+ ❌ **cache miss (full tokens charged)** when:
28
+ - anything changed in the context **before** the cached content (even a single token)
29
+ - the content moved to a different position in the conversation
30
+ - more than ~5 minutes passed with no activity
31
+ - new conversation started (different session)
32
+
33
+ ## .implications.for.briefs
34
+
35
+ ### sessionstart hooks
36
+ when loading briefs via sessionstart hooks:
37
+
38
+ 1. **first boot**: full tokens cached (e.g., 30k tokens written to cache)
39
+ 2. **within same session**: cached if prefix matches (free reads)
40
+ 3. **new session**: full tokens charged again (new conversation = different prefix)
41
+
42
+ ### optimization strategies
43
+ to maximize cache hits:
44
+
45
+ 1. **load briefs once per session** (sessionstart hook pattern)
46
+ 2. **keep them at consistent position** (ideally at the start)
47
+ 3. **avoid regenerating them** if content hasn't changed
48
+ 4. **batch related work** in same session to reuse cache
49
+
50
+ ## .key.insight
51
+
52
+ the cache **does not move tokens around** or do LRU eviction - it's purely prefix-based matching
53
+
54
+ this means:
55
+ - you can't "refresh" cached content by using it again
56
+ - cached content must appear in the exact same position to hit
57
+ - any prefix changes invalidate the entire cache for that position
58
+
59
+ ## .cost.model
60
+
61
+ ### cache writes
62
+ - **first use**: 25% of normal input token cost
63
+ - **cache write**: happens on first occurrence of new content
64
+
65
+ ### cache reads
66
+ - **cache hit**: 10% of normal input token cost
67
+ - **cache miss**: 100% of normal input token cost (no cache benefit)
68
+
69
+ ### example: 30k token briefs
70
+ - **first load**: ~7.5k tokens charged (25% write cost)
71
+ - **subsequent hits**: ~3k tokens charged (10% read cost)
72
+ - **cache miss**: 30k tokens charged (100% cost)
73
+
74
+ ## .sources
75
+ - based on Anthropic's context caching documentation
76
+ - observed behavior in production usage
@@ -0,0 +1,95 @@
1
+ # method syntax for bivariance
2
+
3
+ ## .what
4
+
5
+ typescript treats arrow function properties and method syntax differently for variance:
6
+
7
+ - **arrow function syntax** = contravariant on parameters
8
+ - **method syntax** = bivariant on parameters
9
+
10
+ ```ts
11
+ // arrow function syntax (contravariant)
12
+ interface Dao {
13
+ get: (input: SomeInput) => Promise<Result>;
14
+ }
15
+
16
+ // method syntax (bivariant)
17
+ interface Dao {
18
+ get(input: SomeInput): Promise<Result>;
19
+ }
20
+ ```
21
+
22
+ ---
23
+
24
+ ## .why this matters
25
+
26
+ when checking if a specific type is assignable to a generic type (e.g., `Dao<Specific>` to `Dao<any>`), contravariance on function parameters can cause unexpected failures.
27
+
28
+ ### example: the problem
29
+
30
+ ```ts
31
+ interface DeclastructDao<TResourceClass> {
32
+ get: {
33
+ // arrow syntax = contravariant
34
+ byPrimary?: (input: RefByPrimary<TResourceClass>) => Promise<Resource | null>;
35
+ };
36
+ }
37
+
38
+ // specific dao where RefByPrimary resolves to {uuid: string}
39
+ const specificDao: DeclastructDao<typeof MyResource> = { ... };
40
+
41
+ // generic dao shape where RefByPrimary<any> resolves to {}
42
+ type GenericDao = DeclastructDao<any>;
43
+
44
+ // ❌ fails: {uuid: string} is not assignable to {}
45
+ const generic: GenericDao = specificDao;
46
+ ```
47
+
48
+ the contravariant check asks: "can a function accepting `{uuid: string}` be used where a function accepting `{}` is expected?"
49
+
50
+ answer: **no** — a function requiring `uuid` can't safely accept an empty object.
51
+
52
+ ### example: the fix
53
+
54
+ ```ts
55
+ interface DeclastructDao<TResourceClass> {
56
+ get: {
57
+ // method syntax = bivariant
58
+ byPrimary?(input: RefByPrimary<TResourceClass>): Promise<Resource | null>;
59
+ };
60
+ }
61
+
62
+ // ✅ works: bivariance allows assignment in either direction
63
+ const generic: GenericDao = specificDao;
64
+ ```
65
+
66
+ ---
67
+
68
+ ## .when to use
69
+
70
+ use method syntax when:
71
+
72
+ 1. you need interfaces with generic type parameters to be assignable to their `<any>` variants
73
+ 2. you're defining dao/repository patterns where specific implementations must fit into generic collections
74
+ 3. the interface will be used in `Record<string, Interface<any>>` shapes
75
+
76
+ ---
77
+
78
+ ## .tradeoff
79
+
80
+ bivariance is slightly less type-safe at the boundary — typescript won't catch certain mismatches. however, this is often the correct behavior for:
81
+
82
+ - generic collections of typed implementations
83
+ - plugin/provider patterns
84
+ - dao registries
85
+
86
+ the types remain fully enforced when actually calling the methods on a specific instance.
87
+
88
+ ---
89
+
90
+ ## .reference
91
+
92
+ this is intentional typescript behavior. method syntax preserves the historical bivariant behavior from before `--strictFunctionTypes` was introduced, because it matches how most developers expect object subtyping to work.
93
+
94
+ - [typescript handbook: type compatibility](https://www.typescriptlang.org/docs/handbook/type-compatibility.html)
95
+ - [typescript pr: strict function types](https://github.com/microsoft/TypeScript/pull/18654)
@@ -0,0 +1,19 @@
1
+ failhide = any scenario where you hide real errors
2
+
3
+ the most common patterns is
4
+
5
+ try/catch
6
+
7
+ ---
8
+
9
+ the only cases where a trycatch is allowed is if the catch has an allowlist of the errors it catches and CAREFULLY handles those, while throwing the rest up
10
+
11
+ otherwise, its a failhide hazard, where real errors will be silently hidden, leading to defects or hours of pointless debugging
12
+
13
+ never failhide
14
+
15
+ always failfast
16
+
17
+ ---
18
+
19
+ this is a mega blocker
@@ -0,0 +1,54 @@
1
+ in some cases, its useful
2
+
3
+ to try/catch
4
+
5
+ to make an error more observable
6
+
7
+ ---
8
+
9
+ e.g.,
10
+
11
+ ```ts
12
+ try {
13
+ // do something that throws
14
+ await doSomething({ userUuid })
15
+ } catch (error) {
16
+ if (!(error instanceof Error)) throw error;
17
+ throw Error(`doSomething.error: ${error.message}, for userUuid=${userUuid}`, { cause });
18
+ }
19
+ ```
20
+
21
+ in these cases, its best to use HelpfulError.wrap instead
22
+ - it automatically instantiates a HelpfulError (or more specific variant if specified) for maximum observability
23
+
24
+ e.g.,
25
+
26
+ ```ts
27
+ import { HelpfulError } from 'helpful-errors';
28
+
29
+ await HelpfulError.wrap(
30
+ async () => await doSomething({ userUuid }),
31
+ {
32
+ message: 'doSomething.error',
33
+ metadata: {
34
+ userUuid,
35
+ }
36
+ }
37
+ )()
38
+ ```
39
+
40
+ or
41
+
42
+ ```ts
43
+ import { UnexpectedCodePathError } from 'helpful-errors';
44
+
45
+ await UnexpectedCodePathError.wrap(
46
+ async () => await doSomething({ userUuid }),
47
+ {
48
+ message: 'doSomething.error',
49
+ metadata: {
50
+ userUuid,
51
+ }
52
+ }
53
+ )()
54
+ ```
@@ -0,0 +1,54 @@
1
+ else is a code REAK
2
+
3
+ its not even a smell
4
+
5
+ its such a bad smell that it reaks
6
+
7
+ ---
8
+
9
+ else's can _always_ be better rewriten as an IFFE+EarlyReturn
10
+
11
+ ---
12
+
13
+ see the narrative flow breifs for more context on the desired goal
14
+
15
+
16
+ -------
17
+
18
+ e.g.,
19
+
20
+ > no if else; only early returns; else is a code REAK
21
+
22
+ ● You're right - the if/else violates the flow:narrative tactic. Let me
23
+ refactor to use early returns only:
24
+
25
+ ● Update(src/domain.operations/vpcTunnel/setVpcTunnel.ts)
26
+ ⎿ Updated src/domain.operations/vpcTunnel/setVpcTunnel.ts with 5
27
+ additions and 3 removals
28
+ 81 if (portInUse) {
29
+ 82 const cacheFilePresent = await isFilePresent({ path:
30
+ cachePath });
31
+ 83
32
+ 84 + // port used by unknown process; kill it to reclaim
33
+ 85 + if (!cacheFilePresent) {
34
+ 86 + killProcessOnPort({ port: input.from.port });
35
+ 87 + }
36
+ 88 +
37
+ 89 // check if existing tunnel is ours and healthy
38
+ 90 if (cacheFilePresent) {
39
+ 91 const cacheContent = await fs.readFile(cachePath,
40
+ 'utf-8');
41
+ ...
42
+ 100 // cleanup stale tunnel
43
+ 101 if (isProcessAlive({ pid: cache.pid }))
44
+ process.kill(cache.pid, 'SIGTERM');
45
+ 102 await fs.rm(cachePath, { force: true });
46
+ 103 - } else {
47
+ 104 - // port used by another process; kill it to reclaim
48
+ 105 - killProcessOnPort({ port: input.from.port });
49
+ 103 }
50
+ 104 }
51
+ 105
52
+
53
+ ● Removed the else block - now both conditions are independent if statements
54
+ with no branching maze.
@@ -0,0 +1,181 @@
1
+
2
+ ● The check now walks up the prototype chain starting from error.constructor, checking if any class in the
3
+ inheritance hierarchy is named 'BadRequestError'. This handles cases where a custom error class extends
4
+ BadRequestError (or extends something that extends it, etc.).
5
+
6
+ > lets actually create a new constant called "isExtensionOfBadRequestError"
7
+ ⎿ Found 3 new diagnostic issues in 1 file (ctrl+o to expand)
8
+
9
+ ● Update(src/logic/middlewares/badRequestErrorMiddleware.ts)
10
+ ⎿ Updated src/logic/middlewares/badRequestErrorMiddleware.ts with 4 additions and 2 removals
11
+ 8 */
12
+ 9 export const decideIsBadRequestError = ({ error }: { error: Error }) => {
13
+ 10 const isInstanceOfBadRequestError = error instanceof BadRequestError;
14
+ 11 - const isNamedAfterBadRequestError = (() => {
15
+ 12 - let proto = error.constructor;
16
+ 11 + const isNamedAfterBadRequestError =
17
+ 12 + error.constructor.name === 'BadRequestError';
18
+ 13 + const isExtensionOfBadRequestError = (() => {
19
+ 14 + let proto = Object.getPrototypeOf(error.constructor);
20
+ 15 while (proto) {
21
+ 16 if (proto.name === 'BadRequestError') return true;
22
+ 17 proto = Object.getPrototypeOf(proto);
23
+
24
+ ● Now I need to add the new constant to the return statement:
25
+
26
+ ● Update(src/logic/middlewares/badRequestErrorMiddleware.ts)
27
+ ⎿ Updated src/logic/middlewares/badRequestErrorMiddleware.ts with 1 addition and 1 removal
28
+ 18 }
29
+ 19 return false;
30
+ 20 })();
31
+ 21 - return isInstanceOfBadRequestError || isNamedAfterBadRequestError;
32
+ 21 + return isInstanceOfBadRequestError || isNamedAfterBadRequestError
33
+ + || isExtensionOfBadRequestError;
34
+ 22 };
35
+ 23
36
+ 24 /**
37
+
38
+ ● Done. Now decideIsBadRequestError has three separate checks:
39
+
40
+ 1. isInstanceOfBadRequestError - uses instanceof check
41
+ 2. isNamedAfterBadRequestError - checks if the error's constructor is named 'BadRequestError'
42
+ 3. isExtensionOfBadRequestError - walks up the prototype chain to check if any parent class is named
43
+ 'BadRequestError'
44
+
45
+ > add a header comment for each constant
46
+ ⎿ Found 1 new diagnostic issue in 1 file (ctrl+o to expand)
47
+
48
+ ● Update(src/logic/middlewares/badRequestErrorMiddleware.ts)
49
+ ⎿ Updated src/logic/middlewares/badRequestErrorMiddleware.ts with 11 additions and 1 removal
50
+ 7 * .what = procedure for deciding whether an error is a bad request error
51
+ 8 */
52
+ 9 export const decideIsBadRequestError = ({ error }: { error: Error }) => {
53
+ 10 + // check if the error is an instance of BadRequestError
54
+ 11 const isInstanceOfBadRequestError = error instanceof BadRequestError;
55
+ 12 +
56
+ 13 + // check if the error's constructor is named 'BadRequestError'
57
+ 14 const isNamedAfterBadRequestError =
58
+ 15 error.constructor.name === 'BadRequestError';
59
+ 16 +
60
+ 17 + // check if any parent class in the prototype chain is named 'BadRequestError'
61
+ 18 const isExtensionOfBadRequestError = (() => {
62
+ 19 let proto = Object.getPrototypeOf(error.constructor);
63
+ 20 while (proto) {
64
+ ...
65
+ 18 }
66
+ 19 return false;
67
+ 20 })();
68
+ 21 - return isInstanceOfBadRequestError || isNamedAfterBadRequestError ||
69
+ - isExtensionOfBadRequestError;
70
+ 21 +
71
+ 22 + return (
72
+ 23 + isInstanceOfBadRequestError ||
73
+ 24 + isNamedAfterBadRequestError ||
74
+ 25 + isExtensionOfBadRequestError
75
+ 26 + );
76
+ 27 };
77
+ 28
78
+ 29 /**
79
+
80
+ ● Done. Added header comments for each constant and also fixed the formatting on the return statement to
81
+ satisfy the eslint rule.
82
+
83
+ > use early returns after each check, rather than needoing to evalaute all three
84
+ ⎿ Found 3 new diagnostic issues in 1 file (ctrl+o to expand)
85
+
86
+ ● Update(src/logic/middlewares/badRequestErrorMiddleware.ts)
87
+ ⎿ Updated src/logic/middlewares/badRequestErrorMiddleware.ts with 8 additions and 16 removals
88
+ 8 */
89
+ 9 export const decideIsBadRequestError = ({ error }: { error: Error }) => {
90
+ 10 // check if the error is an instance of BadRequestError
91
+ 11 - const isInstanceOfBadRequestError = error instanceof BadRequestError;
92
+ 11 + if (error instanceof BadRequestError) return true;
93
+ 12
94
+ 13 // check if the error's constructor is named 'BadRequestError'
95
+ 14 - const isNamedAfterBadRequestError =
96
+ 15 - error.constructor.name === 'BadRequestError';
97
+ 14 + if (error.constructor.name === 'BadRequestError') return true;
98
+ 15
99
+ 16 // check if any parent class in the prototype chain is named 'BadRequestError'
100
+ 17 - const isExtensionOfBadRequestError = (() => {
101
+ 18 - let proto = Object.getPrototypeOf(error.constructor);
102
+ 19 - while (proto) {
103
+ 20 - if (proto.name === 'BadRequestError') return true;
104
+ 21 - proto = Object.getPrototypeOf(proto);
105
+ 22 - }
106
+ 23 - return false;
107
+ 24 - })();
108
+ 17 + let proto = Object.getPrototypeOf(error.constructor);
109
+ 18 + while (proto) {
110
+ 19 + if (proto.name === 'BadRequestError') return true;
111
+ 20 + proto = Object.getPrototypeOf(proto);
112
+ 21 + }
113
+ 22
114
+ 23 - return (
115
+ 24 - isInstanceOfBadRequestError ||
116
+ 25 - isNamedAfterBadRequestError ||
117
+ 26 - isExtensionOfBadRequestError
118
+ 27 - );
119
+ 23 + return false;
120
+ 24 };
121
+ 25
122
+ 26 /**
123
+
124
+ ● Done. Now the function uses early returns, so it short-circuits as soon as any check passes without needing
125
+ to evaluate the remaining checks.
126
+
127
+ > header comment on the last return, too
128
+ ⎿ Found 3 new diagnostic issues in 1 file (ctrl+o to expand)
129
+
130
+ ● Update(src/logic/middlewares/badRequestErrorMiddleware.ts)
131
+ ⎿ Updated src/logic/middlewares/badRequestErrorMiddleware.ts with 1 addition
132
+ 20 proto = Object.getPrototypeOf(proto);
133
+ 21 }
134
+ 22
135
+ 23 + // otherwise, not a bad request error
136
+ 24 return false;
137
+ 25 };
138
+ 26
139
+
140
+ ● Done.
141
+
142
+ > keep the names of the constants though; if (isXyz) is important for greater readability
143
+ ⎿ Found 3 new diagnostic issues in 1 file (ctrl+o to expand)
144
+
145
+ ● Update(src/logic/middlewares/badRequestErrorMiddleware.ts)
146
+ ⎿ Updated src/logic/middlewares/badRequestErrorMiddleware.ts with 14 additions and 7 removals
147
+ 8 */
148
+ 9 export const decideIsBadRequestError = ({ error }: { error: Error }) => {
149
+ 10 // check if the error is an instance of BadRequestError
150
+ 11 - if (error instanceof BadRequestError) return true;
151
+ 11 + const isInstanceOfBadRequestError = error instanceof BadRequestError;
152
+ 12 + if (isInstanceOfBadRequestError) return true;
153
+ 13
154
+ 14 // check if the error's constructor is named 'BadRequestError'
155
+ 15 - if (error.constructor.name === 'BadRequestError') return true;
156
+ 15 + const isNamedAfterBadRequestError =
157
+ 16 + error.constructor.name === 'BadRequestError';
158
+ 17 + if (isNamedAfterBadRequestError) return true;
159
+ 18
160
+ 19 // check if any parent class in the prototype chain is named 'BadRequestError'
161
+ 20 - let proto = Object.getPrototypeOf(error.constructor);
162
+ 21 - while (proto) {
163
+ 22 - if (proto.name === 'BadRequestError') return true;
164
+ 23 - proto = Object.getPrototypeOf(proto);
165
+ 24 - }
166
+ 20 + const isExtensionOfBadRequestError = (() => {
167
+ 21 + let proto = Object.getPrototypeOf(error.constructor);
168
+ 22 + while (proto) {
169
+ 23 + if (proto.name === 'BadRequestError') return true;
170
+ 24 + proto = Object.getPrototypeOf(proto);
171
+ 25 + }
172
+ 26 + return false;
173
+ 27 + })();
174
+ 28 + if (isExtensionOfBadRequestError) return true;
175
+ 29
176
+ 30 // otherwise, not a bad request error
177
+ 31 return false;
178
+
179
+ ● Done. Now each check has a named constant (isInstanceOfBadRequestError, isNamedAfterBadRequestError,
180
+ isExtensionOfBadRequestError) followed by an early return, giving both readability and short-circuit
181
+ evaluation.
@@ -0,0 +1,3 @@
1
+ the _only_ time an index.ts file is acceptable is if its
2
+ 1. the entrypoint of the public package, if this is a public package
3
+ 2. the entrypoint of a dao, if it is a dao
@@ -0,0 +1,14 @@
1
+ if you have a failing test
2
+
3
+ 1. make the error super clear via
4
+ 1. observable error messages
5
+ 2. clear log trails
6
+
7
+ 2. when you detect which subcomponent has unexpected behavior
8
+ 1. cover that subscomponent with tests of its own (unit, integration, etc)
9
+ 2. if it does not have its own procedure, breakit out into its own procedure (own function + own file + own test suite)
10
+
11
+ that way
12
+ 1. its easy to see exactly what is failing
13
+ 2. its easy to prove we fixed it (or verify that the behavior is / isnt expected)
14
+ 3. we can systematically track down the bug, one subcomponnent at a time
@@ -0,0 +1,39 @@
1
+ always use [noun][adjective] order
2
+
3
+ e.g.,
4
+
5
+ instead of
6
+
7
+ currentOwner = [adj][noun]
8
+
9
+ prefer
10
+
11
+ ownerCurrent = [noun][adj]
12
+
13
+
14
+ ---
15
+
16
+ instead of
17
+
18
+
19
+ foundUser
20
+
21
+ prefer
22
+
23
+ userFound
24
+
25
+ ----
26
+
27
+
28
+ why?
29
+
30
+ because it enables autocomplete via common prefixes
31
+
32
+ e.g.,
33
+
34
+ userBefore
35
+ userAfter
36
+ userCreated
37
+ etc
38
+
39
+ can see via autocomplete suggestions all the variants of user available in the current scope, rather than need to remember what the variants are
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env bash
2
+ ######################################################################
3
+ # .what = shared helpers for git worktree management
4
+ #
5
+ # .why = centralizes path resolution and branch sanitization logic
6
+ # used by git.worktree.{get,set,del}.sh scripts
7
+ #
8
+ # .how = source this file to get access to:
9
+ # - resolve_worktrees_dir: computes $REPO_WORKTREES_DIR
10
+ # - sanitize_branch_name: converts branch to worktree name
11
+ # - get_repo_name: extracts repo name from gitroot
12
+ #
13
+ # guarantee:
14
+ # - works from root repo or from within a worktree
15
+ # - consistent path resolution across all worktree scripts
16
+ ######################################################################
17
+
18
+ # resolve the worktrees directory for this repo
19
+ # handles both root repo and worktree contexts
20
+ resolve_worktrees_dir() {
21
+ local gitroot
22
+ gitroot="$(git rev-parse --show-toplevel)"
23
+
24
+ local reponame
25
+ reponame="$(basename "$gitroot")"
26
+
27
+ # detect if we're in a worktree (path contains _worktrees)
28
+ if [[ "$gitroot" == *"_worktrees"* ]]; then
29
+ # we're in a worktree - reuse same _worktrees/$reponame dir
30
+ echo "${gitroot%/*}"
31
+ else
32
+ # root repo - compute sibling _worktrees dir
33
+ echo "$(dirname "$gitroot")/_worktrees/$reponame"
34
+ fi
35
+ }
36
+
37
+ # sanitize branch name for use as directory name
38
+ # vlad/practs => vlad.practs
39
+ sanitize_branch_name() {
40
+ local branch="$1"
41
+ echo "${branch//\//.}"
42
+ }
43
+
44
+ # get the repo name (works from root repo or worktree)
45
+ get_repo_name() {
46
+ local gitroot
47
+ gitroot="$(git rev-parse --show-toplevel)"
48
+
49
+ # detect if we're in a worktree (path contains _worktrees)
50
+ if [[ "$gitroot" == *"_worktrees"* ]]; then
51
+ # extract repo name from _worktrees/$reponame/$worktree path
52
+ # gitroot = /path/to/_worktrees/$reponame/$worktree
53
+ local worktrees_parent="${gitroot%/*}" # /path/to/_worktrees/$reponame
54
+ basename "$worktrees_parent"
55
+ else
56
+ basename "$gitroot"
57
+ fi
58
+ }
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env bash
2
+ ######################################################################
3
+ # .what = remove a git worktree
4
+ #
5
+ # .why = clean up worktrees no longer needed
6
+ #
7
+ # .how = removes worktree at @gitroot/../_worktrees/$reponame/$branch
8
+ #
9
+ # usage:
10
+ # git.worktree.del.sh <branch>
11
+ #
12
+ # guarantee:
13
+ # - idempotent: [DELETE] if exists, [SKIP] if not found
14
+ # - works from root repo or from within a worktree
15
+ ######################################################################
16
+
17
+ set -euo pipefail
18
+
19
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
20
+
21
+ # source shared helpers
22
+ source "$SCRIPT_DIR/git.worktree.common.sh"
23
+
24
+ # parse arguments
25
+ BRANCH="${1:-}"
26
+
27
+ # validate branch argument
28
+ if [[ -z "$BRANCH" ]]; then
29
+ echo "error: branch name required"
30
+ echo "usage: git.worktree.del.sh <branch>"
31
+ exit 1
32
+ fi
33
+
34
+ # resolve paths
35
+ REPO_WORKTREES_DIR="$(resolve_worktrees_dir)"
36
+ WORKTREE_NAME="$(sanitize_branch_name "$BRANCH")"
37
+ WORKTREE_PATH="$REPO_WORKTREES_DIR/$WORKTREE_NAME"
38
+
39
+ # delete logic
40
+ if [[ -d "$WORKTREE_PATH" ]]; then
41
+ # remove worktree via git
42
+ git worktree remove "$WORKTREE_PATH" --force 2>/dev/null || {
43
+ # fallback: manual removal if git worktree remove fails
44
+ rm -rf "$WORKTREE_PATH"
45
+ git worktree prune
46
+ }
47
+
48
+ echo "[DELETE] $WORKTREE_NAME"
49
+ else
50
+ echo "[SKIP] $WORKTREE_NAME (not found)"
51
+ fi