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.
- package/Source.ts +1 -1
- package/Trait.ts +109 -4
- package/TraitConflict.ts +43 -4
- package/TraitDerivation.ts +104 -32
- package/TraitEvaluation.ts +104 -49
- package/TraitStore.ts +123 -44
- package/TraitsConfig.ts +16 -9
- package/TraitsContext.ts +3 -1
- package/_util/Frontmatter.ts +78 -0
- package/_util/Git.ts +6 -5
- package/bin.ts +3 -1
- package/dist/Source.js +1 -1
- package/dist/Source.js.map +1 -1
- package/dist/Trait.d.ts +39 -5
- package/dist/Trait.d.ts.map +1 -1
- package/dist/Trait.js +66 -3
- package/dist/Trait.js.map +1 -1
- package/dist/TraitConflict.d.ts +47 -15
- package/dist/TraitConflict.d.ts.map +1 -1
- package/dist/TraitConflict.js +38 -4
- package/dist/TraitConflict.js.map +1 -1
- package/dist/TraitDerivation.d.ts +85 -28
- package/dist/TraitDerivation.d.ts.map +1 -1
- package/dist/TraitDerivation.js +85 -23
- package/dist/TraitDerivation.js.map +1 -1
- package/dist/TraitEvaluation.d.ts +38 -8
- package/dist/TraitEvaluation.d.ts.map +1 -1
- package/dist/TraitEvaluation.js +84 -47
- package/dist/TraitEvaluation.js.map +1 -1
- package/dist/TraitStore.d.ts +54 -10
- package/dist/TraitStore.d.ts.map +1 -1
- package/dist/TraitStore.js +61 -37
- package/dist/TraitStore.js.map +1 -1
- package/dist/TraitsConfig.d.ts +4 -3
- package/dist/TraitsConfig.d.ts.map +1 -1
- package/dist/TraitsConfig.js +12 -5
- package/dist/TraitsConfig.js.map +1 -1
- package/dist/TraitsContext.d.ts +2 -1
- package/dist/TraitsContext.d.ts.map +1 -1
- package/dist/TraitsContext.js.map +1 -1
- package/dist/_util/Frontmatter.d.ts +37 -0
- package/dist/_util/Frontmatter.d.ts.map +1 -0
- package/dist/_util/Frontmatter.js +52 -0
- package/dist/_util/Frontmatter.js.map +1 -0
- package/dist/_util/Git.d.ts +2 -3
- package/dist/_util/Git.d.ts.map +1 -1
- package/dist/_util/Git.js +4 -1
- package/dist/_util/Git.js.map +1 -1
- package/dist/bin.js +3 -1
- package/dist/bin.js.map +1 -1
- package/dist/getIncludedFiles.d.ts +13 -0
- package/dist/getIncludedFiles.d.ts.map +1 -0
- package/dist/getIncludedFiles.js +26 -0
- package/dist/getIncludedFiles.js.map +1 -0
- package/dist/main.js +16 -12
- package/dist/main.js.map +1 -1
- package/dist/package.json +7 -2
- package/dist/test/Frontmatter.test.d.ts +2 -0
- package/dist/test/Frontmatter.test.d.ts.map +1 -0
- package/dist/test/Frontmatter.test.js +29 -0
- package/dist/test/Frontmatter.test.js.map +1 -0
- package/dist/test/Trait.test.d.ts +2 -0
- package/dist/test/Trait.test.d.ts.map +1 -0
- package/dist/test/Trait.test.js +144 -0
- package/dist/test/Trait.test.js.map +1 -0
- package/dist/test/TraitDerivation.test.d.ts +2 -0
- package/dist/test/TraitDerivation.test.d.ts.map +1 -0
- package/dist/test/TraitDerivation.test.js +173 -0
- package/dist/test/TraitDerivation.test.js.map +1 -0
- package/dist/test/TraitEvaluation.test.d.ts +2 -0
- package/dist/test/TraitEvaluation.test.d.ts.map +1 -0
- package/dist/test/TraitEvaluation.test.js +116 -0
- package/dist/test/TraitEvaluation.test.js.map +1 -0
- package/dist/test/TraitIgnores.test.js +29 -5
- package/dist/test/TraitIgnores.test.js.map +1 -1
- package/dist/test/TraitStore.test.d.ts +2 -0
- package/dist/test/TraitStore.test.d.ts.map +1 -0
- package/dist/test/TraitStore.test.js +120 -0
- package/dist/test/TraitStore.test.js.map +1 -0
- package/dist/test/TraitsInitialization.test.d.ts +2 -0
- package/dist/test/TraitsInitialization.test.d.ts.map +1 -0
- package/dist/test/TraitsInitialization.test.js +130 -0
- package/dist/test/TraitsInitialization.test.js.map +1 -0
- package/dist/toolkit/CheckTraits.d.ts +97 -13
- package/dist/toolkit/CheckTraits.d.ts.map +1 -1
- package/dist/toolkit/CheckTraits.js +28 -40
- package/dist/toolkit/CheckTraits.js.map +1 -1
- package/dist/toolkit/ConfigurationError.d.ts +3 -0
- package/dist/toolkit/ConfigurationError.d.ts.map +1 -0
- package/dist/toolkit/ConfigurationError.js +15 -0
- package/dist/toolkit/ConfigurationError.js.map +1 -0
- package/dist/toolkit/DeriveTrait.d.ts +137 -45
- package/dist/toolkit/DeriveTrait.d.ts.map +1 -1
- package/dist/toolkit/DeriveTrait.js +26 -14
- package/dist/toolkit/DeriveTrait.js.map +1 -1
- package/dist/traitIgnores.d.ts +18 -0
- package/dist/traitIgnores.d.ts.map +1 -1
- package/dist/traitIgnores.js +10 -7
- package/dist/traitIgnores.js.map +1 -1
- package/getIncludedFiles.ts +42 -0
- package/main.ts +17 -12
- package/package.json +7 -2
- package/skills/traits/SKILL.md +107 -51
- package/toolkit/CheckTraits.ts +34 -55
- package/toolkit/ConfigurationError.ts +27 -0
- package/toolkit/DeriveTrait.ts +27 -14
- package/traitIgnores.ts +14 -6
- package/_util/words.ts +0 -555
- package/dist/_util/words.d.ts +0 -2
- package/dist/_util/words.d.ts.map +0 -1
- package/dist/_util/words.js +0 -556
- package/dist/_util/words.js.map +0 -1
package/skills/traits/SKILL.md
CHANGED
|
@@ -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/<
|
|
14
|
-
|
|
15
|
-
`
|
|
16
|
-
|
|
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
|
-
|
|
36
|
-
not
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
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
|
|
92
|
-
prompt. This checks existing traits and writes only when the
|
|
93
|
-
distinct from all of them. It does not independently decide
|
|
94
|
-
conversation contains a generalizable insight.
|
|
95
|
-
5. `Created` means the trait was saved and includes its body and
|
|
96
|
-
|
|
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
|
-
|
|
101
|
-
enter approval-gated reconciliation within this same tool.
|
|
102
|
-
|
|
103
|
-
`Covered.traits`
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
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
|
|
117
|
-
action changes between approval and retry, obtain fresh approval rather
|
|
118
|
-
reusing the flag. `Updated` returns the saved trait. Never claim approval
|
|
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
|
|
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
|
-
|
|
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
|
|
139
|
-
|
|
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 `
|
|
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 <
|
|
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
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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 `
|
|
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
|
|
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
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
package/toolkit/CheckTraits.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { Array, Effect, FileSystem, Path, pipe,
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
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
|
-
|
|
26
|
-
|
|
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/
|
|
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
|
-
|
|
48
|
-
description:
|
|
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([
|
|
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 =
|
|
76
|
-
|
|
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.
|
|
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
|
-
(
|
|
106
|
+
flow(
|
|
107
|
+
fs.stat,
|
|
108
|
+
Effect.map((info) => info.type === "File")
|
|
109
|
+
),
|
|
128
110
|
{ concurrency: "unbounded" }
|
|
129
111
|
)
|
|
130
|
-
|
|
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
|
|
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
|
-
|
|
124
|
+
return yield* pipe(
|
|
144
125
|
sources,
|
|
145
|
-
|
|
146
|
-
|
|
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
|
+
)
|
package/toolkit/DeriveTrait.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Schema as S, String } from "effect"
|
|
2
|
-
import {
|
|
2
|
+
import { DecisionModel, LanguageModel, McpSchema, Tool, Toolkit } from "effect/ai"
|
|
3
3
|
|
|
4
|
-
import
|
|
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
|
|
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
|
|
24
|
-
| Nothing is replaced before approval. After approval,
|
|
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
|
|
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
|
|
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
|
|
35
|
-
| Then call check_traits on relevant TypeScript files, using returned trait
|
|
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
|
-
"
|
|
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([
|
|
49
|
-
|
|
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 {
|
|
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
|
|
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 === "" ||
|
|
9
|
-
if (!
|
|
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
|
-
|
|
13
|
+
const reference = match?.[1]
|
|
14
|
+
if (reference !== undefined) references.add(reference)
|
|
12
15
|
}
|
|
13
|
-
return
|
|
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
|
+
)
|