rhachet-roles-ehmpathy 1.9.1 → 1.10.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.
@@ -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,260 @@
1
+ #!/usr/bin/env bash
2
+ ######################################################################
3
+ # .what = initialize a .behavior directory for bhuild thoughtroute
4
+ #
5
+ # .why = standardize the behavior-driven development thoughtroute
6
+ # by scaffolding a structured directory with:
7
+ # - wish definition
8
+ # - vision statement
9
+ # - acceptance criteria
10
+ # - research prompts
11
+ # - distillation prompts
12
+ # - blueprint prompts
13
+ # - roadmap prompts
14
+ # - execution prompts
15
+ # - feedback template
16
+ #
17
+ # .how = creates .behavior/v${isodate}.${behaviorname}/ with
18
+ # all necessary scaffold files for the bhuild thoughtroute
19
+ #
20
+ # usage:
21
+ # init.bhuild.sh --name <behaviorname> [--dir <directory>]
22
+ #
23
+ # guarantee:
24
+ # - creates .behavior/ if missing
25
+ # - creates versioned behavior directory
26
+ # - findserts all thoughtroute files (creates if missing, skips if exists)
27
+ # - idempotent: safe to rerun
28
+ # - fail-fast on errors
29
+ ######################################################################
30
+
31
+ set -euo pipefail
32
+
33
+ # parse arguments
34
+ BEHAVIOR_NAME=""
35
+ TARGET_DIR="$PWD"
36
+ while [[ $# -gt 0 ]]; do
37
+ case $1 in
38
+ --name)
39
+ BEHAVIOR_NAME="$2"
40
+ shift 2
41
+ ;;
42
+ --dir)
43
+ TARGET_DIR="$2"
44
+ shift 2
45
+ ;;
46
+ *)
47
+ echo "error: unknown argument '$1'"
48
+ echo "usage: init.bhuild.sh --name <behaviorname> [--dir <directory>]"
49
+ exit 1
50
+ ;;
51
+ esac
52
+ done
53
+
54
+ # validate required arguments
55
+ if [[ -z "$BEHAVIOR_NAME" ]]; then
56
+ echo "error: --name is required"
57
+ echo "usage: init.bhuild.sh --name <behaviorname> [--dir <directory>]"
58
+ exit 1
59
+ fi
60
+
61
+ # generate isodate in format YYYY_MM_DD
62
+ ISO_DATE=$(date +%Y_%m_%d)
63
+
64
+ # trim trailing .behavior from TARGET_DIR if present
65
+ TARGET_DIR="${TARGET_DIR%/.behavior}"
66
+ TARGET_DIR="${TARGET_DIR%.behavior}"
67
+
68
+ # construct behavior directory path (absolute)
69
+ BEHAVIOR_DIR="$TARGET_DIR/.behavior/v${ISO_DATE}.${BEHAVIOR_NAME}"
70
+
71
+ # compute relative path from caller's $PWD for file contents
72
+ BEHAVIOR_DIR_REL="$(realpath --relative-to="$PWD" "$TARGET_DIR")/.behavior/v${ISO_DATE}.${BEHAVIOR_NAME}"
73
+ # normalize: remove leading ./ if present
74
+ BEHAVIOR_DIR_REL="${BEHAVIOR_DIR_REL#./}"
75
+
76
+ # create behavior directory (idempotent)
77
+ mkdir -p "$BEHAVIOR_DIR"
78
+
79
+ # helper: findsert file (create if missing, skip if exists)
80
+ findsert() {
81
+ local filepath="$1"
82
+ if [[ -f "$filepath" ]]; then
83
+ echo " [KEEP] $(basename "$filepath")"
84
+ return 0
85
+ fi
86
+ cat > "$filepath"
87
+ echo " [CREATE] $(basename "$filepath")"
88
+ }
89
+
90
+ # findsert 0.wish.md
91
+ findsert "$BEHAVIOR_DIR/0.wish.md" << 'EOF'
92
+ wish =
93
+
94
+ EOF
95
+
96
+ # findsert 1.vision.md
97
+ findsert "$BEHAVIOR_DIR/1.vision.md" << 'EOF'
98
+
99
+ EOF
100
+
101
+ # findsert 2.criteria.md
102
+ findsert "$BEHAVIOR_DIR/2.criteria.md" << 'EOF'
103
+ # usecase.1 = ...
104
+ given()
105
+ when()
106
+ then()
107
+ sothat()
108
+ then()
109
+ then()
110
+ sothat()
111
+ when()
112
+ then()
113
+
114
+ given()
115
+ ...
116
+
117
+ # usecase.2 = ...
118
+ ...
119
+ EOF
120
+
121
+ # findsert 3.1.research.domain._.v1.src
122
+ findsert "$BEHAVIOR_DIR/3.1.research.domain._.v1.src" << EOF
123
+ research the domain available in order to fulfill the wish declared
124
+ in $BEHAVIOR_DIR_REL/0.wish.md
125
+
126
+ specifically
127
+ - what are the domain objects that are involved with this wish
128
+ - entities
129
+ - events
130
+ - literals
131
+ - what are the domain operations
132
+ - getOne
133
+ - getAll
134
+ - setCreate
135
+ - setUpdate
136
+ - setDelete
137
+ - ...
138
+
139
+ ---
140
+
141
+ use web search to discover and research
142
+ - cite every claim
143
+ - number each citation
144
+ - clone exact quotes from each citation
145
+
146
+ focus on these sdk's for reference, if provided
147
+ -
148
+
149
+ ---
150
+
151
+ emit into $BEHAVIOR_DIR_REL/3.1.research.domain._.v1.i1.md
152
+ EOF
153
+
154
+ # findsert 3.2.distill.domain._.v1.src
155
+ findsert "$BEHAVIOR_DIR/3.2.distill.domain._.v1.src" << EOF
156
+ distill the declastruct domain.objects and domain.operations that would
157
+ - enable fulfillment of this wish $BEHAVIOR_DIR_REL/0.wish.md
158
+ - given the research declared here $BEHAVIOR_DIR_REL/3.1.research.domain._.v1.i1.md
159
+
160
+ procedure
161
+ 1. declare the usecases and envision the contract that would be used to fulfill the usecases
162
+ 2. declare the domain.objects, domain.operations, and access.daos that would fulfill this, via the declastruct pattern in this repo
163
+
164
+ emit into
165
+ - $BEHAVIOR_DIR_REL/3.2.distill.domain._.v1.i1.md
166
+ EOF
167
+
168
+ # findsert 3.3.blueprint.v1.src
169
+ findsert "$BEHAVIOR_DIR/3.3.blueprint.v1.src" << EOF
170
+ propose a blueprint for how we can implement the wish described
171
+ - in $BEHAVIOR_DIR_REL/0.wish.md
172
+
173
+ with the domain distillation declared
174
+ - in $BEHAVIOR_DIR_REL/3.2.distill.domain._.v1.i1.md
175
+
176
+ follow the patterns already present in this repo
177
+
178
+ ---
179
+
180
+ reference the below for full context
181
+ - $BEHAVIOR_DIR_REL/0.wish.md
182
+ - $BEHAVIOR_DIR_REL/3.1.research.domain._.v1.i1.md
183
+ - $BEHAVIOR_DIR_REL/3.2.distill.domain._.v1.i1.md
184
+
185
+
186
+ ---
187
+
188
+ emit to $BEHAVIOR_DIR_REL/3.3.blueprint.v1.i1.md
189
+ EOF
190
+
191
+ # findsert 4.1.roadmap.v1.src
192
+ findsert "$BEHAVIOR_DIR/4.1.roadmap.v1.src" << EOF
193
+ declare a roadmap,
194
+
195
+ - checklist style
196
+ - with ordered dependencies
197
+ - with behavioral acceptance criteria
198
+ - with behavioral acceptance verification at each step
199
+
200
+ for how to execute the blueprint specified in $BEHAVIOR_DIR_REL/3.3.blueprint.v1.i1.md
201
+
202
+ ---
203
+
204
+ emit into $BEHAVIOR_DIR_REL/4.1.roadmap.v1.i1.md
205
+ EOF
206
+
207
+ # findsert 5.1.execution.phase0_to_phaseN.v1.src
208
+ findsert "$BEHAVIOR_DIR/5.1.execution.phase0_to_phaseN.v1.src" << EOF
209
+ bootup your mechanic's role via \`npx rhachet roles boot --repo ehmpathy --role mechanic\`
210
+
211
+ then, execute
212
+ - phase0 to phaseN
213
+ of roadmap
214
+ - $BEHAVIOR_DIR_REL/4.1.roadmap.v1.i1.md
215
+
216
+ ref:
217
+ - $BEHAVIOR_DIR_REL/3.3.blueprint.v1.i1.md
218
+
219
+ ---
220
+
221
+ track your progress
222
+
223
+ emit todos and check them off into
224
+ - $BEHAVIOR_DIR_REL/5.1.execution.phase0_to_phaseN.v1.i1.md
225
+ EOF
226
+
227
+ # findsert .ref.[feedback].v1.[given].by_human.md
228
+ findsert "$BEHAVIOR_DIR/.ref.[feedback].v1.[given].by_human.md" << EOF
229
+ emit your response to the feedback into
230
+ - $BEHAVIOR_DIR_REL/.ref.[feedback].v1.[taken].by_robot.md
231
+
232
+ 1. emit your response checklist
233
+ 2. exec your response plan
234
+ 3. emit your response checkoffs into the checklist
235
+
236
+ ---
237
+
238
+ first, bootup your mechanics briefs again
239
+
240
+ npx rhachet roles boot --repo ehmpathy --role mechanic
241
+
242
+ ---
243
+ ---
244
+ ---
245
+
246
+
247
+ # blocker.1
248
+
249
+ ---
250
+
251
+ # nitpick.2
252
+
253
+ ---
254
+
255
+ # blocker.3
256
+ EOF
257
+
258
+ echo ""
259
+ echo "behavior thoughtroute initialized!"
260
+ echo " $BEHAVIOR_DIR"
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env bash
2
+ ######################################################################
3
+ # .what = bind mechanic permissions to Claude settings
4
+ #
5
+ # .why = the mechanic role needs conservative permissions to operate
6
+ # safely while still being productive.
7
+ #
8
+ # this script manages permissions in .claude/settings.local.json:
9
+ # • replaces existing allows entirely (conservative)
10
+ # • extends denies by appending new entries (conservative)
11
+ # • extends asks by appending new entries (conservative)
12
+ # • idempotent: safe to rerun
13
+ #
14
+ # .how = uses jq to merge the permissions configuration
15
+ # into the existing settings structure, creating it if absent.
16
+ #
17
+ # guarantee:
18
+ # ✔ creates .claude/settings.local.json if missing
19
+ # ✔ preserves existing settings (hooks, other configs)
20
+ # ✔ replaces allow list entirely
21
+ # ✔ appends to deny list (no duplicates)
22
+ # ✔ appends to ask list (no duplicates)
23
+ # ✔ idempotent: safe to rerun
24
+ # ✔ fail-fast on errors
25
+ ######################################################################
26
+
27
+ set -euo pipefail
28
+
29
+ PROJECT_ROOT="$PWD"
30
+ SETTINGS_FILE="$PROJECT_ROOT/.claude/settings.local.json"
31
+
32
+ # define the permissions configuration to apply
33
+ PERMISSIONS_CONFIG=$(cat <<'EOF'
34
+ {
35
+ "permissions": {
36
+ "deny": [
37
+ "Bash(git commit:*)"
38
+ ],
39
+ "ask": [
40
+ "Bash(chmod:*)",
41
+ "Bash(pnpm install:*)",
42
+ "Bash(pnpm add:*)"
43
+ ],
44
+ "allow": [
45
+ "WebSearch",
46
+ "WebFetch(domain:github.com)",
47
+ "WebFetch(domain:www.npmjs.com)",
48
+ "WebFetch(domain:hub.docker.com)",
49
+ "WebFetch(domain:raw.githubusercontent.com)",
50
+ "Bash(THOROUGH=true npm run test:*)",
51
+ "Bash(AWS_PROFILE=ahbode.dev npm run test:integration:*)",
52
+ "Bash(npm run fix:*)",
53
+ "Bash(AWS_PROFILE=ahbode.dev npx jest:*)",
54
+ "Bash(AWS_PROFILE=ahbode.dev npm run deploy:dev:*)",
55
+ "Bash(AWS_PROFILE=ahbode.dev STAGE=dev npm run test:acceptance:*)",
56
+ "Bash(npm run start:testdb:*)",
57
+ "Bash(cat:*)",
58
+ "Bash(unzip:*)",
59
+ "Bash(npm view:*)"
60
+ ]
61
+ }
62
+ }
63
+ EOF
64
+ )
65
+
66
+ # ensure .claude directory exists
67
+ mkdir -p "$(dirname "$SETTINGS_FILE")"
68
+
69
+ # initialize settings file if it doesn't exist
70
+ if [[ ! -f "$SETTINGS_FILE" ]]; then
71
+ echo "{}" > "$SETTINGS_FILE"
72
+ fi
73
+
74
+ # apply permissions:
75
+ # - replace allow entirely
76
+ # - append to deny (unique)
77
+ # - append to ask (unique)
78
+ jq --argjson perms "$PERMISSIONS_CONFIG" '
79
+ # ensure .permissions exists
80
+ .permissions //= {} |
81
+
82
+ # replace allow entirely with our config
83
+ .permissions.allow = $perms.permissions.allow |
84
+
85
+ # append to deny (unique entries only)
86
+ .permissions.deny = ((.permissions.deny // []) + $perms.permissions.deny | unique) |
87
+
88
+ # append to ask (unique entries only)
89
+ .permissions.ask = ((.permissions.ask // []) + $perms.permissions.ask | unique)
90
+ ' "$SETTINGS_FILE" > "$SETTINGS_FILE.tmp"
91
+
92
+ # check if any changes were made
93
+ if diff -q "$SETTINGS_FILE" "$SETTINGS_FILE.tmp" >/dev/null 2>&1; then
94
+ rm "$SETTINGS_FILE.tmp"
95
+ echo "👌 mechanic permissions already configured"
96
+ echo " $SETTINGS_FILE"
97
+ exit 0
98
+ fi
99
+
100
+ # atomic replace
101
+ mv "$SETTINGS_FILE.tmp" "$SETTINGS_FILE"
102
+
103
+ echo "🔐 mechanic permissions configured successfully!"
104
+ echo " $SETTINGS_FILE"
105
+ echo ""
106
+ echo "✨ permissions applied:"
107
+ echo " • allow: replaced entirely"
108
+ echo " • deny: extended (no duplicates)"
109
+ echo " • ask: extended (no duplicates)"
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env bash
2
+ ######################################################################
3
+ # .what = initialize Claude settings for mechanic role
4
+ #
5
+ # .why = mechanic needs both hooks and permissions configured to
6
+ # operate effectively. this script dispatches to both:
7
+ # • init.claude.hooks.sh - binds SessionStart hook
8
+ # • init.claude.permissions.sh - configures permissions
9
+ #
10
+ # single entry point for full Claude configuration.
11
+ #
12
+ # .how = runs both init scripts in sequence from the same directory.
13
+ #
14
+ # guarantee:
15
+ # ✔ runs both hooks and permissions initialization
16
+ # ✔ fail-fast on any error
17
+ # ✔ idempotent: safe to rerun
18
+ ######################################################################
19
+
20
+ set -euo pipefail
21
+
22
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
23
+
24
+ echo "🔧 init claude config for mechanic role..."
25
+ echo ""
26
+
27
+ # initialize hooks
28
+ "$SCRIPT_DIR/init.claude.hooks.sh"
29
+ echo ""
30
+
31
+ # initialize permissions
32
+ "$SCRIPT_DIR/init.claude.permissions.sh"
33
+ echo ""
34
+
35
+ echo "👌 claude config ready"
@@ -226,8 +226,14 @@ fi
226
226
  echo "> $TEST_COMMAND" | tee -a "$LOG_FILE"
227
227
  echo "" | tee -a "$LOG_FILE"
228
228
 
229
- eval "$TEST_COMMAND" 2>&1 | tee -a "$LOG_FILE"
230
- TEST_EXIT_CODE=${PIPESTATUS[0]}
229
+ # For unit tests, strip color codes from log file while preserving them in terminal output
230
+ if [[ "$TEST_TYPE" == "unit" ]]; then
231
+ eval "$TEST_COMMAND" 2>&1 | tee >(sed 's/\x1B\[[0-9;]*[JKmsu]//g' >> "$LOG_FILE")
232
+ TEST_EXIT_CODE=${PIPESTATUS[0]}
233
+ else
234
+ eval "$TEST_COMMAND" 2>&1 | tee -a "$LOG_FILE"
235
+ TEST_EXIT_CODE=${PIPESTATUS[0]}
236
+ fi
231
237
 
232
238
  echo "" | tee -a "$LOG_FILE"
233
239
 
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "rhachet-roles-ehmpathy",
3
3
  "author": "ehmpathy",
4
4
  "description": "empathetic software construction roles and skills, via rhachet",
5
- "version": "1.9.1",
5
+ "version": "1.10.0",
6
6
  "repository": "ehmpathy/rhachet-roles-ehmpathy",
7
7
  "homepage": "https://github.com/ehmpathy/rhachet-roles-ehmpathy",
8
8
  "keywords": [
@@ -26,7 +26,7 @@
26
26
  "fix:lint": "eslint -c ./.eslintrc.js src/**/*.ts --fix",
27
27
  "build:clean": "rm dist/ -rf",
28
28
  "build:compile": "tsc -p ./tsconfig.build.json",
29
- "build:complete": "rsync -a --prune-empty-dirs --include='*/' --exclude='**/.route/**' --exclude='**/.scratch/**' --include='**/*.template.md' --include='**/.briefs/**/*.md' --include='**/.briefs/*.md' --include='**/.skills/**/*.sh' --include='**/.skills/*.sh' --exclude='*' src/ dist/",
29
+ "build:complete": "rsync -a --prune-empty-dirs --include='*/' --exclude='**/.route/**' --exclude='**/.scratch/**' --exclude='**/.behavior/**' --include='**/*.template.md' --include='**/.briefs/**/*.md' --include='**/.briefs/*.md' --include='**/.skills/**/*.sh' --include='**/.skills/*.sh' --exclude='*' src/ dist/",
30
30
  "build": "npm run build:clean && npm run build:compile && npm run build:complete",
31
31
  "test:commits": "LAST_TAG=$(git describe --tags --abbrev=0 @^ 2> /dev/null || git rev-list --max-parents=0 HEAD) && npx commitlint --from $LAST_TAG --to HEAD --verbose",
32
32
  "test:types": "tsc -p ./tsconfig.build.json --noEmit",