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.
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 +128 -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 +67 -0
  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 +60 -10
  31. package/dist/TraitStore.d.ts.map +1 -1
  32. package/dist/TraitStore.js +57 -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.d.ts +3 -0
  50. package/dist/bin.d.ts.map +1 -0
  51. package/dist/bin.js +62 -0
  52. package/dist/bin.js.map +1 -0
  53. package/dist/getIncludedFiles.d.ts +13 -0
  54. package/dist/getIncludedFiles.d.ts.map +1 -0
  55. package/dist/getIncludedFiles.js +26 -0
  56. package/dist/getIncludedFiles.js.map +1 -0
  57. package/dist/main.js +15 -13
  58. package/dist/main.js.map +1 -1
  59. package/dist/package.json +8 -4
  60. package/dist/test/Frontmatter.test.d.ts +2 -0
  61. package/dist/test/Frontmatter.test.d.ts.map +1 -0
  62. package/dist/test/Frontmatter.test.js +29 -0
  63. package/dist/test/Frontmatter.test.js.map +1 -0
  64. package/dist/test/Trait.test.d.ts +2 -0
  65. package/dist/test/Trait.test.d.ts.map +1 -0
  66. package/dist/test/Trait.test.js +144 -0
  67. package/dist/test/Trait.test.js.map +1 -0
  68. package/dist/test/TraitDerivation.test.d.ts +2 -0
  69. package/dist/test/TraitDerivation.test.d.ts.map +1 -0
  70. package/dist/test/TraitDerivation.test.js +173 -0
  71. package/dist/test/TraitDerivation.test.js.map +1 -0
  72. package/dist/test/TraitEvaluation.test.d.ts +2 -0
  73. package/dist/test/TraitEvaluation.test.d.ts.map +1 -0
  74. package/dist/test/TraitEvaluation.test.js +116 -0
  75. package/dist/test/TraitEvaluation.test.js.map +1 -0
  76. package/dist/test/TraitIgnores.test.d.ts +2 -0
  77. package/dist/test/TraitIgnores.test.d.ts.map +1 -0
  78. package/dist/test/TraitIgnores.test.js +44 -0
  79. package/dist/test/TraitIgnores.test.js.map +1 -0
  80. package/dist/test/TraitStore.test.d.ts +2 -0
  81. package/dist/test/TraitStore.test.d.ts.map +1 -0
  82. package/dist/test/TraitStore.test.js +94 -0
  83. package/dist/test/TraitStore.test.js.map +1 -0
  84. package/dist/toolkit/CheckTraits.d.ts +121 -13
  85. package/dist/toolkit/CheckTraits.d.ts.map +1 -1
  86. package/dist/toolkit/CheckTraits.js +28 -36
  87. package/dist/toolkit/CheckTraits.js.map +1 -1
  88. package/dist/toolkit/DeriveTrait.d.ts +161 -45
  89. package/dist/toolkit/DeriveTrait.d.ts.map +1 -1
  90. package/dist/toolkit/DeriveTrait.js +26 -14
  91. package/dist/toolkit/DeriveTrait.js.map +1 -1
  92. package/dist/traitIgnores.d.ts +21 -0
  93. package/dist/traitIgnores.d.ts.map +1 -0
  94. package/dist/traitIgnores.js +20 -0
  95. package/dist/traitIgnores.js.map +1 -0
  96. package/getIncludedFiles.ts +42 -0
  97. package/main.ts +14 -13
  98. package/package.json +8 -4
  99. package/skills/traits/SKILL.md +129 -51
  100. package/toolkit/CheckTraits.ts +34 -51
  101. package/toolkit/DeriveTrait.ts +27 -14
  102. package/traitIgnores.ts +22 -0
  103. package/_util/words.ts +0 -555
  104. package/commands/init.ts +0 -73
  105. package/dist/_util/words.d.ts +0 -2
  106. package/dist/_util/words.d.ts.map +0 -1
  107. package/dist/_util/words.js +0 -556
  108. package/dist/_util/words.js.map +0 -1
  109. package/dist/commands/init.d.ts +0 -4
  110. package/dist/commands/init.d.ts.map +0 -1
  111. package/dist/commands/init.js +0 -67
  112. package/dist/commands/init.js.map +0 -1
@@ -1,10 +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"
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 traitIds to check all stored traits; supply trait ids for a focused check of one or more traits. Unknown IDs fail the call.
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/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.
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
- traitIds: S.Array(TraitId).pipe(S.optional).annotate({
46
- 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."
47
51
  })
48
52
  }),
49
- success: S.Array(TraitEvaluation),
50
- 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",
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 = context.excludeGitignored
74
- ? yield* Effect.scoped(
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.traitIds), store.get, { concurrency: "unbounded" })
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
- (file) => fs.stat(file).pipe(Effect.map((info) => info.type === "File")),
106
+ flow(
107
+ fs.stat,
108
+ Effect.map((info) => info.type === "File")
109
+ ),
126
110
  { concurrency: "unbounded" }
127
111
  )
128
- const results = yield* Effect.forEach(
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
- const results = yield* Effect.forEach(
124
+ return yield* pipe(
140
125
  sources,
141
- (source) => Effect.forEach(traits, (trait) => evaluate(trait, source), { concurrency: "unbounded" }),
142
- { concurrency: "unbounded" }
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
  })
@@ -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
  })
@@ -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
+ )