traits 0.5.0 → 0.5.2
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 +128 -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 +67 -0
- 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 +60 -10
- package/dist/TraitStore.d.ts.map +1 -1
- package/dist/TraitStore.js +57 -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.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +62 -0
- package/dist/bin.js.map +1 -0
- 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 +15 -13
- package/dist/main.js.map +1 -1
- package/dist/package.json +8 -4
- 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.d.ts +2 -0
- package/dist/test/TraitIgnores.test.d.ts.map +1 -0
- package/dist/test/TraitIgnores.test.js +44 -0
- package/dist/test/TraitIgnores.test.js.map +1 -0
- 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 +94 -0
- package/dist/test/TraitStore.test.js.map +1 -0
- package/dist/toolkit/CheckTraits.d.ts +121 -13
- package/dist/toolkit/CheckTraits.d.ts.map +1 -1
- package/dist/toolkit/CheckTraits.js +28 -36
- package/dist/toolkit/CheckTraits.js.map +1 -1
- package/dist/toolkit/DeriveTrait.d.ts +161 -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 +21 -0
- package/dist/traitIgnores.d.ts.map +1 -0
- package/dist/traitIgnores.js +20 -0
- package/dist/traitIgnores.js.map +1 -0
- package/getIncludedFiles.ts +42 -0
- package/main.ts +14 -13
- package/package.json +8 -4
- package/skills/traits/SKILL.md +129 -51
- package/toolkit/CheckTraits.ts +34 -51
- package/toolkit/DeriveTrait.ts +27 -14
- package/traitIgnores.ts +22 -0
- package/_util/words.ts +0 -555
- package/commands/init.ts +0 -73
- 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/dist/commands/init.d.ts +0 -4
- package/dist/commands/init.d.ts.map +0 -1
- package/dist/commands/init.js +0 -67
- package/dist/commands/init.js.map +0 -1
package/toolkit/CheckTraits.ts
CHANGED
|
@@ -1,10 +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
|
|
7
|
+
import * as Trait from "../Trait.ts"
|
|
8
|
+
import * as Evaluation from "../TraitEvaluation.ts"
|
|
9
|
+
import { excludeIgnored } from "../traitIgnores.ts"
|
|
8
10
|
import { TraitsContext } from "../TraitsContext.ts"
|
|
9
11
|
import { TraitStore, TraitStoreError } from "../TraitStore.ts"
|
|
10
12
|
|
|
@@ -21,7 +23,8 @@ export const CheckTraits = Tool.make("check_traits", {
|
|
|
21
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.
|
|
22
24
|
| All entries follow native glob syntax, including escaping filenames with glob characters. No custom directory expansion or literal-path classification is applied.
|
|
23
25
|
| Reads current full file contents, including unstaged edits, and checks each 200-line chunk against the selected traits in parallel.
|
|
24
|
-
| Omit
|
|
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.
|
|
25
28
|
| Returns findings with the trait, source text, absolute path, 1-based chunk startLine, and assessment classification and probabilities.
|
|
26
29
|
| Classification distinguishes violation from inconclusive; probabilities are not calibrated guarantees. No exact offending lines are provided.
|
|
27
30
|
| Diagnose uncertainty using the full distribution, not the selected label alone; inspect source and trait before changing code.
|
|
@@ -35,19 +38,26 @@ export const CheckTraits = Tool.make("check_traits", {
|
|
|
35
38
|
| Ask for clarification when a fix changes behavior or exceeds authorized scope.
|
|
36
39
|
| The tool diagnoses only; the calling agent is responsible for edits, verification, and reporting unresolved blockers.
|
|
37
40
|
| Unmatched patterns select no files. An empty result means no failures for this selection, not a repository-wide guarantee.
|
|
38
|
-
| Empty include/
|
|
41
|
+
| Empty include/traitNames lists, no matched files, or an empty store check no pairs.
|
|
39
42
|
`),
|
|
40
43
|
parameters: S.Struct({
|
|
41
44
|
include: S.Array(S.String).annotate({
|
|
42
45
|
description:
|
|
43
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."
|
|
44
47
|
}),
|
|
45
|
-
|
|
46
|
-
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."
|
|
47
51
|
})
|
|
48
52
|
}),
|
|
49
|
-
success: S.Array(TraitEvaluation),
|
|
50
|
-
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",
|
|
51
61
|
dependencies: [
|
|
52
62
|
DecisionModel.DecisionModel,
|
|
53
63
|
TraitStore,
|
|
@@ -70,39 +80,10 @@ export const layer = toolkit.toLayer({
|
|
|
70
80
|
const fs = yield* FileSystem.FileSystem
|
|
71
81
|
const path = yield* Path.Path
|
|
72
82
|
const cwd = path.resolve()
|
|
73
|
-
const includedFiles =
|
|
74
|
-
|
|
75
|
-
Effect.gen(function* () {
|
|
76
|
-
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
|
|
77
|
-
const process = yield* spawner.spawn(
|
|
78
|
-
ChildProcess.make`git ls-files --cached --others --exclude-standard --full-name -z`.pipe(
|
|
79
|
-
ChildProcess.setCwd(context.configRoot)
|
|
80
|
-
)
|
|
81
|
-
)
|
|
82
|
-
const output = yield* process.stdout.pipe(Stream.decodeText(), Stream.mkString)
|
|
83
|
-
const exitCode = yield* process.exitCode
|
|
84
|
-
if (exitCode !== 0) {
|
|
85
|
-
return yield* PlatformError.systemError({
|
|
86
|
-
_tag: "Unknown",
|
|
87
|
-
module: "ChildProcess",
|
|
88
|
-
method: "git ls-files",
|
|
89
|
-
description: `Process exited with code ${exitCode}`
|
|
90
|
-
})
|
|
91
|
-
}
|
|
92
|
-
return new Set(
|
|
93
|
-
pipe(
|
|
94
|
-
output,
|
|
95
|
-
String.split("\0"),
|
|
96
|
-
Array.filter(String.isNonEmpty),
|
|
97
|
-
Array.map((file) => path.resolve(context.configRoot, file))
|
|
98
|
-
)
|
|
99
|
-
)
|
|
100
|
-
})
|
|
101
|
-
)
|
|
102
|
-
: undefined
|
|
103
|
-
const traits = yield* params.traitIds === undefined
|
|
83
|
+
const includedFiles = yield* getIncludedFiles
|
|
84
|
+
const traits = yield* params.traitNames === undefined
|
|
104
85
|
? store.ls
|
|
105
|
-
: Effect.forEach(Array.dedupe(params.
|
|
86
|
+
: Effect.forEach(Array.dedupe(params.traitNames), store.get, { concurrency: "unbounded" })
|
|
106
87
|
const matches = yield* Effect.forEach(
|
|
107
88
|
params.include,
|
|
108
89
|
(pattern) => {
|
|
@@ -122,13 +103,17 @@ export const layer = toolkit.toLayer({
|
|
|
122
103
|
)
|
|
123
104
|
const paths = yield* Effect.filter(
|
|
124
105
|
includedFiles === undefined ? resolved : Array.filter(resolved, (file) => includedFiles.has(file)),
|
|
125
|
-
(
|
|
106
|
+
flow(
|
|
107
|
+
fs.stat,
|
|
108
|
+
Effect.map((info) => info.type === "File")
|
|
109
|
+
),
|
|
126
110
|
{ concurrency: "unbounded" }
|
|
127
111
|
)
|
|
128
|
-
|
|
112
|
+
return yield* Effect.forEach(
|
|
129
113
|
paths,
|
|
130
114
|
Effect.fn("CheckTraits.file")(function* (file) {
|
|
131
115
|
const text = yield* fs.readFileString(file)
|
|
116
|
+
const activeTraits = pipe(traits, excludeIgnored(text))
|
|
132
117
|
const chunks = Array.chunksOf(String.split(text, "\n"), 200)
|
|
133
118
|
const sources = Array.map(chunks, (lines, index) =>
|
|
134
119
|
Source.make({
|
|
@@ -136,15 +121,13 @@ export const layer = toolkit.toLayer({
|
|
|
136
121
|
location: SourceLocation.cases.File.make({ path: file, startLine: index * 200 + 1 })
|
|
137
122
|
})
|
|
138
123
|
)
|
|
139
|
-
|
|
124
|
+
return yield* pipe(
|
|
140
125
|
sources,
|
|
141
|
-
|
|
142
|
-
|
|
126
|
+
Effect.forEach((source) => Evaluation.evaluateAll(activeTraits, source), { concurrency: "unbounded" }),
|
|
127
|
+
Effect.map(flow(Array.flatten, Array.filter(Evaluation.requiresReview)))
|
|
143
128
|
)
|
|
144
|
-
return pipe(results, Array.flatten, Array.filter(requiresReview))
|
|
145
129
|
}),
|
|
146
130
|
{ concurrency: "unbounded" }
|
|
147
|
-
)
|
|
148
|
-
return Array.flatten(results)
|
|
131
|
+
).pipe(Effect.map(Array.flatten))
|
|
149
132
|
})
|
|
150
133
|
})
|
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
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Array, flow, String } from "effect"
|
|
2
|
+
|
|
3
|
+
import type { Trait } from "./Trait.ts"
|
|
4
|
+
|
|
5
|
+
/** File-scoped exceptions must appear in the leading line-comment header and include a reason. */
|
|
6
|
+
export const traitIgnores = (text: string): ReadonlySet<string> => {
|
|
7
|
+
const references = new Set<string>()
|
|
8
|
+
for (const line of String.split(text, "\n")) {
|
|
9
|
+
const trimmed = String.trim(line)
|
|
10
|
+
if (trimmed === "" || trimmed.startsWith("#!")) continue
|
|
11
|
+
if (!trimmed.startsWith("//")) break
|
|
12
|
+
const match = /^\/\/\s*traits-ignore-file\s+(\S+)\s*:\s*(\S.*)$/.exec(trimmed)
|
|
13
|
+
const reference = match?.[1]
|
|
14
|
+
if (reference !== undefined) references.add(reference)
|
|
15
|
+
}
|
|
16
|
+
return references
|
|
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
|
+
)
|