traits 0.5.1 → 0.5.3

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 (112) hide show
  1. package/Source.ts +1 -1
  2. package/Trait.ts +109 -4
  3. package/TraitConflict.ts +43 -4
  4. package/TraitDerivation.ts +104 -32
  5. package/TraitEvaluation.ts +104 -49
  6. package/TraitStore.ts +123 -44
  7. package/TraitsConfig.ts +16 -9
  8. package/TraitsContext.ts +3 -1
  9. package/_util/Frontmatter.ts +78 -0
  10. package/_util/Git.ts +6 -5
  11. package/bin.ts +3 -1
  12. package/dist/Source.js +1 -1
  13. package/dist/Source.js.map +1 -1
  14. package/dist/Trait.d.ts +39 -5
  15. package/dist/Trait.d.ts.map +1 -1
  16. package/dist/Trait.js +66 -3
  17. package/dist/Trait.js.map +1 -1
  18. package/dist/TraitConflict.d.ts +47 -15
  19. package/dist/TraitConflict.d.ts.map +1 -1
  20. package/dist/TraitConflict.js +38 -4
  21. package/dist/TraitConflict.js.map +1 -1
  22. package/dist/TraitDerivation.d.ts +85 -28
  23. package/dist/TraitDerivation.d.ts.map +1 -1
  24. package/dist/TraitDerivation.js +85 -23
  25. package/dist/TraitDerivation.js.map +1 -1
  26. package/dist/TraitEvaluation.d.ts +38 -8
  27. package/dist/TraitEvaluation.d.ts.map +1 -1
  28. package/dist/TraitEvaluation.js +84 -47
  29. package/dist/TraitEvaluation.js.map +1 -1
  30. package/dist/TraitStore.d.ts +54 -10
  31. package/dist/TraitStore.d.ts.map +1 -1
  32. package/dist/TraitStore.js +61 -37
  33. package/dist/TraitStore.js.map +1 -1
  34. package/dist/TraitsConfig.d.ts +4 -3
  35. package/dist/TraitsConfig.d.ts.map +1 -1
  36. package/dist/TraitsConfig.js +12 -5
  37. package/dist/TraitsConfig.js.map +1 -1
  38. package/dist/TraitsContext.d.ts +2 -1
  39. package/dist/TraitsContext.d.ts.map +1 -1
  40. package/dist/TraitsContext.js.map +1 -1
  41. package/dist/_util/Frontmatter.d.ts +37 -0
  42. package/dist/_util/Frontmatter.d.ts.map +1 -0
  43. package/dist/_util/Frontmatter.js +52 -0
  44. package/dist/_util/Frontmatter.js.map +1 -0
  45. package/dist/_util/Git.d.ts +2 -3
  46. package/dist/_util/Git.d.ts.map +1 -1
  47. package/dist/_util/Git.js +4 -1
  48. package/dist/_util/Git.js.map +1 -1
  49. package/dist/bin.js +3 -1
  50. package/dist/bin.js.map +1 -1
  51. package/dist/getIncludedFiles.d.ts +13 -0
  52. package/dist/getIncludedFiles.d.ts.map +1 -0
  53. package/dist/getIncludedFiles.js +26 -0
  54. package/dist/getIncludedFiles.js.map +1 -0
  55. package/dist/main.js +16 -12
  56. package/dist/main.js.map +1 -1
  57. package/dist/package.json +7 -2
  58. package/dist/test/Frontmatter.test.d.ts +2 -0
  59. package/dist/test/Frontmatter.test.d.ts.map +1 -0
  60. package/dist/test/Frontmatter.test.js +29 -0
  61. package/dist/test/Frontmatter.test.js.map +1 -0
  62. package/dist/test/Trait.test.d.ts +2 -0
  63. package/dist/test/Trait.test.d.ts.map +1 -0
  64. package/dist/test/Trait.test.js +144 -0
  65. package/dist/test/Trait.test.js.map +1 -0
  66. package/dist/test/TraitDerivation.test.d.ts +2 -0
  67. package/dist/test/TraitDerivation.test.d.ts.map +1 -0
  68. package/dist/test/TraitDerivation.test.js +173 -0
  69. package/dist/test/TraitDerivation.test.js.map +1 -0
  70. package/dist/test/TraitEvaluation.test.d.ts +2 -0
  71. package/dist/test/TraitEvaluation.test.d.ts.map +1 -0
  72. package/dist/test/TraitEvaluation.test.js +116 -0
  73. package/dist/test/TraitEvaluation.test.js.map +1 -0
  74. package/dist/test/TraitIgnores.test.js +29 -5
  75. package/dist/test/TraitIgnores.test.js.map +1 -1
  76. package/dist/test/TraitStore.test.d.ts +2 -0
  77. package/dist/test/TraitStore.test.d.ts.map +1 -0
  78. package/dist/test/TraitStore.test.js +120 -0
  79. package/dist/test/TraitStore.test.js.map +1 -0
  80. package/dist/test/TraitsInitialization.test.d.ts +2 -0
  81. package/dist/test/TraitsInitialization.test.d.ts.map +1 -0
  82. package/dist/test/TraitsInitialization.test.js +130 -0
  83. package/dist/test/TraitsInitialization.test.js.map +1 -0
  84. package/dist/toolkit/CheckTraits.d.ts +97 -13
  85. package/dist/toolkit/CheckTraits.d.ts.map +1 -1
  86. package/dist/toolkit/CheckTraits.js +28 -40
  87. package/dist/toolkit/CheckTraits.js.map +1 -1
  88. package/dist/toolkit/ConfigurationError.d.ts +3 -0
  89. package/dist/toolkit/ConfigurationError.d.ts.map +1 -0
  90. package/dist/toolkit/ConfigurationError.js +15 -0
  91. package/dist/toolkit/ConfigurationError.js.map +1 -0
  92. package/dist/toolkit/DeriveTrait.d.ts +137 -45
  93. package/dist/toolkit/DeriveTrait.d.ts.map +1 -1
  94. package/dist/toolkit/DeriveTrait.js +26 -14
  95. package/dist/toolkit/DeriveTrait.js.map +1 -1
  96. package/dist/traitIgnores.d.ts +18 -0
  97. package/dist/traitIgnores.d.ts.map +1 -1
  98. package/dist/traitIgnores.js +10 -7
  99. package/dist/traitIgnores.js.map +1 -1
  100. package/getIncludedFiles.ts +42 -0
  101. package/main.ts +17 -12
  102. package/package.json +7 -2
  103. package/skills/traits/SKILL.md +107 -51
  104. package/toolkit/CheckTraits.ts +34 -55
  105. package/toolkit/ConfigurationError.ts +27 -0
  106. package/toolkit/DeriveTrait.ts +27 -14
  107. package/traitIgnores.ts +14 -6
  108. package/_util/words.ts +0 -555
  109. package/dist/_util/words.d.ts +0 -2
  110. package/dist/_util/words.d.ts.map +0 -1
  111. package/dist/_util/words.js +0 -556
  112. package/dist/_util/words.js.map +0 -1
@@ -10,10 +10,39 @@ description: >-
10
10
  # Traits
11
11
 
12
12
  Traits are concise, durable coding rules. The filesystem implementation stores
13
- each body in `.agents/traits/<three_words-uuid>.md`, relative to its working
14
- directory. The full filename stem is the trait ID, for example
15
- `rose_karate_frog-2535ceab-5702-4ab4-932a-f87362adf8c6`. Read these files to
16
- discover existing rules and IDs; pass the full ID, not just the words, to tools.
13
+ each body in `.agents/traits/<name>.md`, relative to its working directory.
14
+ Frontmatter contains a unique descriptive kebab-case `name` without a redundant
15
+ `trait-` prefix and no separate `id`. The name is the canonical identity across
16
+ backends: use exact names for focused `check_traits({ traitNames: [...] })`
17
+ selections and ignore directives, not filenames. Duplicate names are rejected.
18
+ Renaming changes identity and requires updating references. Construct complete
19
+ records with the schema's `Trait.Trait.make`. `TraitStore` persists complete
20
+ traits and uses names for get, update, and remove. Updates retain the name; a
21
+ rename requires adding the new name and removing the old one after saving. The
22
+ `TraitStore` service class declares its shape inline; use
23
+ `TraitStore["Service"]` for its implementation type. Its filesystem layer is
24
+ colocated in the same module. Only the filesystem adapter builds `<name>.md`
25
+ paths.
26
+
27
+ Include a concise BAD-to-GOOD code example whenever meaningful. Pass its raw
28
+ unified diff as `diff` to `derive_trait` (removed `-` lines are BAD, added `+`
29
+ lines are GOOD), separately from `proposedTrait`. Omit it only when a code
30
+ example does not apply. Approval and retries cover the exact diff too. On disk,
31
+ `diff` is not frontmatter: put it in a Markdown `diff` fence directly after the
32
+ frontmatter, followed by a blank line, `---`, and the standalone body. Keep
33
+ examples out of the body and preserve them when updating a trait unless the
34
+ updated rule needs a changed example.
35
+
36
+ Keep frontmatter fields complementary. `mime` restricts file types; `when`
37
+ defines positive semantic scope. Write `when` values without a leading "When",
38
+ such as `when: editing React state`. Add `skip` only for an independent
39
+ exception inside that scope, not for its negation or a restriction already
40
+ expressed by another field. For example,
41
+ `when: importing from local project modules` already excludes external packages;
42
+ do not repeat that as a skip. Leave unnecessary optional fields absent. Keep the
43
+ body concise and standalone, preserving essential scope and exceptions without
44
+ repeated restatements. Reuse existing traits that already cover the same rule
45
+ rather than creating duplicates.
17
46
 
18
47
  ## Capture a reusable insight
19
48
 
@@ -32,15 +61,23 @@ rule; if an existing rule needs clearer wording, propose a clarification. Skip
32
61
  only clearly one-off requests, transient facts, or rules you can identify as
33
62
  already covered.
34
63
 
35
- Call `derive_trait` with the drafted rule first, leaving `approved` unset. Do
36
- not ask for approval separately before invoking: capable clients receive a
37
- native MCP form. Only if `ApprovalRequired` is returned, show the complete
38
- message in the interactive question tool with **Save trait**, **Revise**, and
39
- **Skip** and its automatic free-text option. Do not add a duplicate "Other"
40
- option. Use plain text only if that UI is unavailable. Approval to edit code
41
- alone does not approve persisting a trait: make the requested edit and propose
42
- the reusable rule before the final response. Do not stop after implementing an
43
- idiom and running code checks; preference capture is a separate responsibility.
64
+ When coverage is uncertain, call `derive_trait` with the specific preference; do
65
+ not skip capture because a broad trait or a combination of general traits seems
66
+ related. Let the tool assess coverage or clarification. For example, “prefer
67
+ Effect utilities” and “pass callbacks directly” are not sufficient grounds to
68
+ skip proposing “prefer native inline checks and use Effect predicate helpers
69
+ directly in callbacks or existing pipelines.”
70
+
71
+ Call `derive_trait` with the drafted rule and a descriptive kebab-case `name`
72
+ first, leaving `approved` unset. Do not ask for approval separately before
73
+ invoking: capable clients receive a native MCP form. Only if `ApprovalRequired`
74
+ is returned, show the complete message in the interactive question tool with
75
+ **Save trait**, **Revise**, and **Skip** and its automatic free-text option. Do
76
+ not add a duplicate "Other" option. Use plain text only if that UI is
77
+ unavailable. Approval to edit code alone does not approve persisting a trait:
78
+ make the requested edit and propose the reusable rule before the final response.
79
+ Do not stop after implementing an idiom and running code checks; preference
80
+ capture is a separate responsibility.
44
81
 
45
82
  ### Shared approval protocol
46
83
 
@@ -52,7 +89,7 @@ without form support use the same fallback.
52
89
 
53
90
  On `ApprovalRequired`, show its complete message in the interactive question UI
54
91
  with Save/Revise/Skip and free text. Save permits retrying the identical body
55
- and target IDs with `approved: true`. Revision or free-text input requires
92
+ and target names with `approved: true`. Revision or free-text input requires
56
93
  drafting the changed proposal and asking approval again, not silently setting
57
94
  approved. Skip means stop. Native `RevisionRequired` and `Declined` have the
58
95
  same meanings. Transport errors are failures, never declines or approvals.
@@ -88,45 +125,50 @@ authenticated consent token.
88
125
  3. Draft the complete rule as `proposedTrait`. Preserve essential requirements,
89
126
  scopes, and exceptions. Keep independently applicable rules separate; omit
90
127
  headings, repetition, and unnecessary explanation or examples.
91
- 4. Call `derive_trait({ proposedTrait })` first, without a separate approval
92
- prompt. This checks existing traits and writes only when the proposal is
93
- distinct from all of them. It does not independently decide whether the
94
- conversation contains a generalizable insight.
95
- 5. `Created` means the trait was saved and includes its body and generated ID.
96
- Do not submit it again. Use that ID for focused checks when needed.
128
+ 4. Call `derive_trait({ name, proposedTrait })` first, without a separate
129
+ approval prompt. This checks existing traits and writes only when the
130
+ proposal is distinct from all of them. It does not independently decide
131
+ whether the conversation contains a generalizable insight.
132
+ 5. `Created` means the trait was saved and includes its body and unique name. Do
133
+ not submit it again. Use that name for focused checks when needed.
97
134
  6. `Inconclusive` means nothing was saved and contains relationship findings;
98
135
  each finding's `reason` distinguishes conflict from inconclusive. Any
99
136
  inconclusive comparison blocks all writes. Inspect the existing bodies and
100
- IDs, clarify ambiguity, or revise the proposal. Confirmed conflicts otherwise
101
- enter approval-gated reconciliation within this same tool.
102
-
103
- `Covered.traits` means existing traits already express the proposal; reuse them
104
- without creating a duplicate. When there is exactly one clarification candidate
105
- and no covered or blocking result, the tool selects it automatically and
106
- requests approval of its current body and complete replacement. The proposal
107
- must preserve the existing rule's intent, requirements, scope, and exceptions;
108
- the tool does not generate a merged replacement from an incremental hint.
137
+ names, clarify ambiguity, or revise the proposal. Confirmed conflicts
138
+ otherwise enter approval-gated reconciliation within this same tool.
139
+
140
+ `Covered.traits` requires an independent second decision-model check of the
141
+ saved body and when, mime, and skip metadata against the entire proposal.
142
+ Partial overlap or similar intent is insufficient. Verified coverage gaps enter
143
+ approval-gated reconciliation, including needed applicability updates;
144
+ inconclusive verification blocks writes. `Covered.traits` means existing traits
145
+ already express the proposal; reuse them without creating a duplicate. When
146
+ there is exactly one clarification candidate and no covered or blocking result,
147
+ the tool selects it automatically and requests approval of its current body and
148
+ complete replacement. The proposal must preserve the existing rule's intent,
149
+ requirements, scope, and exceptions; the tool does not generate a merged
150
+ replacement from an incremental hint.
109
151
 
110
152
  Multiple candidates return `Clarification` with `traits` and `proposedTrait`
111
153
  without writing. Refine the proposal or clarify intent rather than choosing an
112
- ID arbitrarily. Do not supply a clarification target parameter.
154
+ name arbitrarily. Do not supply a clarification target parameter.
113
155
 
114
156
  Use the interactive question UI only on `ApprovalRequired`, then retry the same
115
157
  `proposedTrait` with `approved: true` after Save. The tool rechecks every
116
- existing trait and preserves the selected ID on update. If the store or selected
117
- action changes between approval and retry, obtain fresh approval rather than
118
- reusing the flag. `Updated` returns the saved trait. Never claim approval not
119
- given by the user or delete a trait merely needing clearer wording.
158
+ existing trait and preserves the selected name on clarification. If the store or
159
+ selected action changes between approval and retry, obtain fresh approval rather
160
+ than reusing the flag. `Updated` returns the saved trait. Never claim approval
161
+ not given by the user or delete a trait merely needing clearer wording.
120
162
 
121
163
  ## Reconcile conflicting traits
122
164
 
123
165
  `derive_trait` checks the proposal against every stored trait and automatically
124
166
  selects confirmed conflicts for reconciliation. There is no separate resolution
125
- tool and no ID-selection parameter. Inconclusive comparisons block
167
+ tool and no trait-selection parameter. Inconclusive comparisons block
126
168
  reconciliation rather than authorizing deletion.
127
169
 
128
170
  Before writing, the shared approval flow shows the selected originals with their
129
- IDs and bodies, the proposal, the removal plan, and the replacement policy.
171
+ names and bodies, the proposal, the removal plan, and the replacement policy.
130
172
  Approval authorizes model-generated reconciliation, not silent deletion. On
131
173
  `ApprovalRequired`, ask through the interactive UI and retry the same proposal
132
174
  with `approved: true` only after Save. If relationships or originals change,
@@ -135,8 +177,10 @@ obtain fresh approval.
135
177
  After approval, the tool evaluates each body and the proposal for decomposition,
136
178
  then generates standalone replacements. The proposal takes precedence in direct
137
179
  conflicts; unrelated requirements, scopes, and exceptions should be preserved.
138
- It saves the replacements before deleting the selected originals and returns
139
- `Resolved` with the saved traits and their new IDs.
180
+ It validates replacement names against each other and unaffected traits before
181
+ writing. It updates selected names reused by replacements, adds new names, then
182
+ deletes only selected originals whose names were not reused. It returns
183
+ `Resolved` with the saved traits and their unique names.
140
184
 
141
185
  Inspect those replacements. Do not call `derive_trait` again for the same
142
186
  proposal: resolution has already saved its content. The initial comparison
@@ -168,7 +212,7 @@ store before retrying; some changes may already be present.
168
212
  schema modifiers, and misplaced imports when their traits apply. Searches
169
213
  support review; a lack of matches is not proof that every trait is satisfied.
170
214
  5. Call `check_traits` on all changed TypeScript files after the latest edits.
171
- Before a commit or PR, omit `traitIds` so every stored trait is checked;
215
+ Before a commit or PR, omit `traitNames` so every stored trait is checked;
172
216
  focused checks supplement rather than replace that final review.
173
217
  6. Inspect findings, fix confirmed violations, and repeat direct inspection,
174
218
  tool checks, and relevant project verification after fixes. If any reviewed
@@ -186,13 +230,15 @@ For a justified file-scoped exception, add a line comment in the leading header,
186
230
  before any code:
187
231
 
188
232
  ```ts
189
- // traits-ignore-file <full-trait-id>: Explain why this trait cannot apply here.
233
+ // traits-ignore-file <trait-name>: Explain why this trait cannot apply here.
190
234
  ```
191
235
 
192
- Each directive requires a nonempty reason and exactly one full trait ID. Repeat
193
- the comment for additional exceptions. Only the referenced traits are skipped;
194
- other traits remain checked. Exceptions apply to all file chunks and focused
195
- checks. Comments after code are ignored.
236
+ Each directive requires a nonempty reason and exactly one unique trait name.
237
+ Names are case-sensitive; update directives after renaming a trait. Filenames
238
+ and partial names are not selectors. Repeat the comment for additional
239
+ exceptions. Only the referenced traits are skipped; other traits remain checked.
240
+ Exceptions apply to all file chunks and focused checks. Comments after code are
241
+ ignored.
196
242
 
197
243
  Read the referenced trait and verify the reason before adding or retaining an
198
244
  exception. Do not add directives merely to silence findings: fix applicable
@@ -205,9 +251,9 @@ their preference. Respect existing justified exceptions during manual review.
205
251
  `check_traits({ include: files })`.
206
252
  - After creating or resolving traits, explicitly review relevant TypeScript
207
253
  files. Saving traits does not automatically check code.
208
- - Omit `traitIds` for the full trait set. Supply IDs for focused follow-ups,
254
+ - Omit `traitNames` for the full trait set. Supply names for focused follow-ups,
209
255
  especially the replacements returned by resolution. An empty array selects no
210
- traits; unknown IDs fail the call.
256
+ traits; unknown names fail the call.
211
257
  - `include` accepts file paths or glob patterns, absolute or relative to the MCP
212
258
  working directory. Overlapping matches are deduplicated; unmatched patterns
213
259
  select no files. Use `traits/**/*.ts` rather than a bare directory. Glob
@@ -288,11 +334,21 @@ simultaneous installations in the same worktree. During development, use
288
334
  `node traits/bin.ts` with a Node version supporting native TypeScript execution;
289
335
  pass server flags after the filename.
290
336
 
291
- Configuration and providers currently initialize at server startup. Startup
292
- failures go to stderr and require fixing setup and reconnecting; whether the
293
- agent can see these diagnostics depends on its MCP client. Expected tool
294
- failures use MCP error results; unexpected defects receive a generic internal
295
- error rather than implementation details.
337
+ Configuration and providers initialize at server startup. If setup fails, the
338
+ connection stays alive with `traits_configuration_error` instead of the normal
339
+ tools. Call it for the original error diagnostics, help the user fix the
340
+ reported error, then restart the Traits MCP server. Diagnostics include the full
341
+ Effect cause and may contain sensitive values embedded in errors. Never ask for
342
+ credentials in chat. Expected tool failures use MCP error results; unexpected
343
+ runtime defects receive a generic internal error rather than implementation
344
+ details.
345
+
346
+ Stored trait parsing failures return `TraitParseError` with the file path,
347
+ original diagnostic, and migration guidance. Update all stored traits to the
348
+ currently installed package's `Trait` schema and `serialize` format, preserving
349
+ their intent and metadata, then retry. Do not treat the failed operation as a
350
+ successful check. The server stays connected; a restart is not needed for file
351
+ repairs, and no automatic migration is performed.
296
352
 
297
353
  Git-ignored files are excluded by default, with ignore rules refreshed on every
298
354
  check. Untracked files do not need to be added to Git to be evaluated. Tracked
@@ -1,11 +1,12 @@
1
- import { Array, Effect, FileSystem, Path, pipe, PlatformError, Schema as S, Stream, String } from "effect"
2
- import { AiError, DecisionModel, Tool, Toolkit } from "effect/ai"
3
- import { ChildProcess, ChildProcessSpawner } from "effect/process"
1
+ import { Array, Effect, FileSystem, flow, Path, pipe, Schema as S, String } from "effect"
2
+ import { DecisionModel, Tool, Toolkit } from "effect/ai"
3
+ import { ChildProcessSpawner } from "effect/process"
4
4
 
5
+ import { getIncludedFiles, GitFileListingError } from "../getIncludedFiles.ts"
5
6
  import { Source, SourceLocation } from "../Source.ts"
6
- import { TraitId } from "../Trait.ts"
7
- import { evaluate, requiresReview, TraitEvaluation } from "../TraitEvaluation.ts"
8
- import { traitIgnores } from "../traitIgnores.ts"
7
+ import * as Trait from "../Trait.ts"
8
+ import * as Evaluation from "../TraitEvaluation.ts"
9
+ import { excludeIgnored } from "../traitIgnores.ts"
9
10
  import { TraitsContext } from "../TraitsContext.ts"
10
11
  import { TraitStore, TraitStoreError } from "../TraitStore.ts"
11
12
 
@@ -22,8 +23,8 @@ export const CheckTraits = Tool.make("check_traits", {
22
23
  | Git-ignored files are excluded by default, with ignore rules refreshed on every call. Untracked files do not need to be added to Git to be evaluated. Tracked files remain eligible even if ignore rules match them. Set excludeGitignored: false in traits.ts to disable this filter; explicit excludes still apply. Git failures fail the check.
23
24
  | All entries follow native glob syntax, including escaping filenames with glob characters. No custom directory expansion or literal-path classification is applied.
24
25
  | Reads current full file contents, including unstaged edits, and checks each 200-line chunk against the selected traits in parallel.
25
- | Omit traitIds to check all stored traits; supply trait ids for a focused check of one or more traits. Unknown IDs fail the call.
26
- | A leading // traits-ignore-file <trait-id>: <reason> comment skips only that trait for the entire file, including focused checks. Directives require a reason and must precede code.
26
+ | Omit traitNames to check all stored traits; supply unique trait names for a focused check. Unknown names fail the call.
27
+ | A leading // traits-ignore-file <trait-name>: <reason> comment skips that trait for the entire file, including focused checks. Names are case-sensitive and must be updated after renaming. Directives require a reason and must precede code.
27
28
  | Returns findings with the trait, source text, absolute path, 1-based chunk startLine, and assessment classification and probabilities.
28
29
  | Classification distinguishes violation from inconclusive; probabilities are not calibrated guarantees. No exact offending lines are provided.
29
30
  | Diagnose uncertainty using the full distribution, not the selected label alone; inspect source and trait before changing code.
@@ -37,19 +38,26 @@ export const CheckTraits = Tool.make("check_traits", {
37
38
  | Ask for clarification when a fix changes behavior or exceeds authorized scope.
38
39
  | The tool diagnoses only; the calling agent is responsible for edits, verification, and reporting unresolved blockers.
39
40
  | Unmatched patterns select no files. An empty result means no failures for this selection, not a repository-wide guarantee.
40
- | Empty include/traitIds lists, no matched files, or an empty store check no pairs.
41
+ | Empty include/traitNames lists, no matched files, or an empty store check no pairs.
41
42
  `),
42
43
  parameters: S.Struct({
43
44
  include: S.Array(S.String).annotate({
44
45
  description:
45
46
  "Project-owned source, configuration, or documentation paths/globs, absolute or relative to the MCP working directory. Exclude third-party/generated files, secrets, and binaries. Overlapping matches are deduplicated; unmatched patterns select no files."
46
47
  }),
47
- traitIds: S.Array(TraitId).pipe(S.optional).annotate({
48
- description: "Optional trait ids for a focused review. Omit for all stored traits; an empty array selects none."
48
+ traitNames: S.Array(Trait.TraitName).pipe(S.optional).annotate({
49
+ description:
50
+ "Optional unique trait names for a focused review. Omit for all stored traits; an empty array selects none."
49
51
  })
50
52
  }),
51
- success: S.Array(TraitEvaluation),
52
- failure: S.Union([AiError.AiError, TraitStoreError]),
53
+ success: S.Array(Evaluation.TraitEvaluation),
54
+ failure: S.Union([
55
+ S.TaggedStruct("AiError", { message: S.String }),
56
+ TraitStoreError,
57
+ S.Struct({ ...Evaluation.TraitAssessmentTooLargeError.fields, message: S.String }),
58
+ S.Struct({ ...GitFileListingError.fields, message: S.String })
59
+ ]),
60
+ failureMode: "return",
53
61
  dependencies: [
54
62
  DecisionModel.DecisionModel,
55
63
  TraitStore,
@@ -72,39 +80,10 @@ export const layer = toolkit.toLayer({
72
80
  const fs = yield* FileSystem.FileSystem
73
81
  const path = yield* Path.Path
74
82
  const cwd = path.resolve()
75
- const includedFiles = context.excludeGitignored
76
- ? yield* Effect.scoped(
77
- Effect.gen(function* () {
78
- const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
79
- const process = yield* spawner.spawn(
80
- ChildProcess.make`git ls-files --cached --others --exclude-standard --full-name -z`.pipe(
81
- ChildProcess.setCwd(context.configRoot)
82
- )
83
- )
84
- const output = yield* process.stdout.pipe(Stream.decodeText(), Stream.mkString)
85
- const exitCode = yield* process.exitCode
86
- if (exitCode !== 0) {
87
- return yield* PlatformError.systemError({
88
- _tag: "Unknown",
89
- module: "ChildProcess",
90
- method: "git ls-files",
91
- description: `Process exited with code ${exitCode}`
92
- })
93
- }
94
- return new Set(
95
- pipe(
96
- output,
97
- String.split("\0"),
98
- Array.filter(String.isNonEmpty),
99
- Array.map((file) => path.resolve(context.configRoot, file))
100
- )
101
- )
102
- })
103
- )
104
- : undefined
105
- const traits = yield* params.traitIds === undefined
83
+ const includedFiles = yield* getIncludedFiles
84
+ const traits = yield* params.traitNames === undefined
106
85
  ? store.ls
107
- : Effect.forEach(Array.dedupe(params.traitIds), store.get, { concurrency: "unbounded" })
86
+ : Effect.forEach(Array.dedupe(params.traitNames), store.get, { concurrency: "unbounded" })
108
87
  const matches = yield* Effect.forEach(
109
88
  params.include,
110
89
  (pattern) => {
@@ -124,15 +103,17 @@ export const layer = toolkit.toLayer({
124
103
  )
125
104
  const paths = yield* Effect.filter(
126
105
  includedFiles === undefined ? resolved : Array.filter(resolved, (file) => includedFiles.has(file)),
127
- (file) => fs.stat(file).pipe(Effect.map((info) => info.type === "File")),
106
+ flow(
107
+ fs.stat,
108
+ Effect.map((info) => info.type === "File")
109
+ ),
128
110
  { concurrency: "unbounded" }
129
111
  )
130
- const results = yield* Effect.forEach(
112
+ return yield* Effect.forEach(
131
113
  paths,
132
114
  Effect.fn("CheckTraits.file")(function* (file) {
133
115
  const text = yield* fs.readFileString(file)
134
- const ignored = traitIgnores(text)
135
- const activeTraits = Array.filter(traits, (trait) => !ignored.has(trait.id))
116
+ const activeTraits = pipe(traits, excludeIgnored(text))
136
117
  const chunks = Array.chunksOf(String.split(text, "\n"), 200)
137
118
  const sources = Array.map(chunks, (lines, index) =>
138
119
  Source.make({
@@ -140,15 +121,13 @@ export const layer = toolkit.toLayer({
140
121
  location: SourceLocation.cases.File.make({ path: file, startLine: index * 200 + 1 })
141
122
  })
142
123
  )
143
- const results = yield* Effect.forEach(
124
+ return yield* pipe(
144
125
  sources,
145
- (source) => Effect.forEach(activeTraits, (trait) => evaluate(trait, source), { concurrency: "unbounded" }),
146
- { concurrency: "unbounded" }
126
+ Effect.forEach((source) => Evaluation.evaluateAll(activeTraits, source), { concurrency: "unbounded" }),
127
+ Effect.map(flow(Array.flatten, Array.filter(Evaluation.requiresReview)))
147
128
  )
148
- return pipe(results, Array.flatten, Array.filter(requiresReview))
149
129
  }),
150
130
  { concurrency: "unbounded" }
151
- )
152
- return Array.flatten(results)
131
+ ).pipe(Effect.map(Array.flatten))
153
132
  })
154
133
  })
@@ -0,0 +1,27 @@
1
+ import { Cause, Effect, Layer, Schema as S } from "effect"
2
+ import { McpServer, Tool, Toolkit } from "effect/ai"
3
+
4
+ const toolkit = Toolkit.make(
5
+ Tool.make("traits_configuration_error", {
6
+ description:
7
+ "Traits failed to initialize. Call this tool to learn where configuration failed. Help the user fix setup, then restart the Traits MCP server before using Traits. Never ask the user to paste credentials into chat.",
8
+ parameters: Tool.EmptyParams,
9
+ success: S.Never,
10
+ failure: S.String,
11
+ failureMode: "return"
12
+ }).annotate(Tool.Readonly, true)
13
+ )
14
+
15
+ export const layer = (cause: Cause.Cause<unknown>) =>
16
+ Cause.hasInterruptsOnly(cause)
17
+ ? Layer.effectDiscard(Effect.interrupt)
18
+ : McpServer.toolkit(toolkit).pipe(
19
+ Layer.provide(
20
+ toolkit.toLayer({
21
+ traits_configuration_error: () =>
22
+ Effect.fail(
23
+ `${Cause.pretty(cause)}\n\nFix the reported error, then restart the Traits MCP server and retry. Do not paste credentials into chat.`
24
+ )
25
+ })
26
+ )
27
+ )
@@ -1,7 +1,8 @@
1
1
  import { Schema as S, String } from "effect"
2
- import { AiError, DecisionModel, LanguageModel, McpSchema, Tool, Toolkit } from "effect/ai"
2
+ import { DecisionModel, LanguageModel, McpSchema, Tool, Toolkit } from "effect/ai"
3
3
 
4
- import { derive, TraitDerivation } from "../TraitDerivation.ts"
4
+ import * as Trait from "../Trait.ts"
5
+ import * as Derivation from "../TraitDerivation.ts"
5
6
  import { TraitsContext } from "../TraitsContext.ts"
6
7
  import { TraitStore, TraitStoreError } from "../TraitStore.ts"
7
8
 
@@ -17,36 +18,48 @@ export const DeriveTrait = Tool.make("derive_trait", {
17
18
  | If Revise supplies no concrete feedback, ask for revision details with the interactive question UI and free text. Do not guess or set approved: true.
18
19
  | Keep the proposal pending through related work and return to it afterward unless cancelled; obtain fresh approval of the complete revised body.
19
20
  | First decide whether the feedback generalizes beyond the current task and inspect existing trait files for coverage.
20
- | Supply a concise, standalone rule as proposedTrait, preserving its scope and exceptions; omit headings and unnecessary examples.
21
+ | Supply a concise, standalone rule as proposedTrait, preserving its scope and exceptions; omit headings.
22
+ | Include a separate diff showing BAD (-) versus GOOD (+) code whenever a useful example applies. Omit it only when a code example is not meaningful. Approval covers the exact diff as well as the body.
23
+ | Supply a unique descriptive kebab-case name without a redundant trait- prefix for a newly created trait; it is the canonical identity, frontmatter name, and Markdown filename stem. Duplicate names are rejected.
24
+ | Avoid repeated requirements and duplicate traits. Keep bodies standalone; do not strip essential scope or exceptions just to shorten them.
25
+ | When authoring trait frontmatter, let mime restrict file types and when define positive semantic scope. Include skip only for an independent exception within that scope, never its negation or a repetition of another field.
26
+ | Write when values without a leading When, for example when: editing React state.
21
27
  | Compare against all stored traits. Only a distinct proposal is saved as a new trait.
22
28
  | Inconclusive returns relationship findings and blocks all writes; clarify ambiguity or revise the proposal before retrying.
23
- | Confirmed conflicts enter reconciliation within this tool. Approval shows affected trait IDs/bodies, removals, the proposal, and replacement policy.
24
- | Nothing is replaced before approval. After approval, generate compatible standalone replacements, save them, then delete only the conflicting originals.
29
+ | Confirmed conflicts and independently verified coverage gaps enter reconciliation within this tool. Approval shows affected trait names, bodies, applicability metadata, removals, the proposal, and replacement policy.
30
+ | Nothing is replaced before approval. After approval, validate unique replacement names against unaffected traits, update reused selected names, save new names, then delete only selected originals whose names were not reused.
25
31
  | The proposal takes precedence only in direct conflicts; preserve unrelated requirements, scopes, and exceptions. Resolved returns saved replacements.
26
32
  | Do not submit the same proposal again after Resolved: it is already incorporated. Inspect replacements and check relevant TypeScript files.
27
33
  | Reconciliation is not transactional; on an error, inspect the store before retrying because some writes or removals may have completed.
28
- | Covered returns the existing traits representing the proposal; reuse them without writing a duplicate.
34
+ | Covered requires a second independent decision-model assessment of the saved body and when/mime/skip metadata against every proposed requirement. Similar intent or partial overlap is insufficient. Coverage gaps require approval-gated revision; inconclusive verification blocks writes.
35
+ | Covered returns only independently verified existing traits; reuse them without writing a duplicate.
29
36
  | A single clarification candidate is selected automatically for approval; provide a complete replacement body preserving existing requirements and exceptions.
30
37
  | Multiple clarification candidates return Clarification with those traits and the proposal without writing. Refine the proposal or clarify intent.
31
38
  | The shared approval flow shows the existing and proposed bodies; ask in the client UI only if ApprovalRequired is returned.
32
- | The tool rechecks all traits before updating, preserving the selected ID. Updated returns the saved trait.
39
+ | The tool rechecks all traits before updating, preserving the selected name on clarification. Updated returns the saved trait.
33
40
  | Never use approved: true for an unapproved replacement, and never resolve a mere clarification by deleting traits.
34
- | Created returns the saved trait including its generated ID. Every response is one tagged TraitDerivation outcome, never a bare array or void.
35
- | Then call check_traits on relevant TypeScript files, using returned trait IDs for focused reviews when appropriate.
41
+ | Created returns the saved trait including its unique name. Every response is one tagged TraitDerivation outcome, never a bare array or void.
42
+ | Then call check_traits on relevant TypeScript files, using returned trait names for focused reviews when appropriate.
36
43
  `),
37
44
  parameters: S.Struct({
45
+ diff: Trait.TraitDiff.pipe(S.optional),
46
+ name: Trait.TraitName.annotate({
47
+ description:
48
+ "Unique descriptive kebab-case identity and Markdown filename stem without a redundant trait- prefix for a newly created trait. Existing names are preserved on clarification."
49
+ }),
38
50
  proposedTrait: S.String.annotate({
39
51
  description:
40
- "The complete reusable rule to save, including scope and exceptions, rather than raw conversation text."
52
+ "A concise, standalone reusable rule with its essential scope and exceptions, not raw conversation text or repeated restatements of the same requirement."
41
53
  }),
42
54
  approved: S.Boolean.pipe(S.optional).annotate({
43
55
  description:
44
56
  "Fallback approval for the exact proposal and automatically selected action shown to the user, including any existing trait being updated. Set true only after approval; native clients still receive a form. Does not bypass relationship checks."
45
57
  })
46
58
  }),
47
- success: TraitDerivation,
48
- failure: S.Union([AiError.AiError, TraitStoreError, McpSchema.ElicitationDeclined]),
49
- dependencies: [DecisionModel.DecisionModel, LanguageModel.LanguageModel, TraitStore, TraitsContext]
59
+ success: Derivation.TraitDerivation,
60
+ failure: S.Union([S.TaggedStruct("AiError", { message: S.String }), TraitStoreError, McpSchema.ElicitationDeclined]),
61
+ failureMode: "return",
62
+ dependencies: [DecisionModel.DecisionModel, LanguageModel.LanguageModel, TraitsContext, TraitStore]
50
63
  })
51
64
  .annotate(Tool.Title, "Derive Trait")
52
65
  .annotate(Tool.Readonly, false)
@@ -55,5 +68,5 @@ export const DeriveTrait = Tool.make("derive_trait", {
55
68
  export const toolkit = Toolkit.make(DeriveTrait)
56
69
 
57
70
  export const layer = toolkit.toLayer({
58
- derive_trait: derive
71
+ derive_trait: Derivation.derive
59
72
  })
package/traitIgnores.ts CHANGED
@@ -1,14 +1,22 @@
1
- import { pipe, String } from "effect"
1
+ import { Array, flow, String } from "effect"
2
+
3
+ import type { Trait } from "./Trait.ts"
2
4
 
3
5
  /** File-scoped exceptions must appear in the leading line-comment header and include a reason. */
4
6
  export const traitIgnores = (text: string): ReadonlySet<string> => {
5
- const ids = new Set<string>()
7
+ const references = new Set<string>()
6
8
  for (const line of String.split(text, "\n")) {
7
9
  const trimmed = String.trim(line)
8
- if (trimmed === "" || pipe(trimmed, String.startsWith("#!"))) continue
9
- if (!pipe(trimmed, String.startsWith("//"))) break
10
+ if (trimmed === "" || trimmed.startsWith("#!")) continue
11
+ if (!trimmed.startsWith("//")) break
10
12
  const match = /^\/\/\s*traits-ignore-file\s+(\S+)\s*:\s*(\S.*)$/.exec(trimmed)
11
- if (match?.[1] !== undefined) ids.add(match[1])
13
+ const reference = match?.[1]
14
+ if (reference !== undefined) references.add(reference)
12
15
  }
13
- return ids
16
+ return references
14
17
  }
18
+
19
+ /** References match unique trait names exactly. */
20
+ export const excludeIgnored = flow(traitIgnores, (references) =>
21
+ Array.filter<Trait>((trait) => !references.has(trait.name))
22
+ )