traits 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/Assessment.ts +14 -0
  2. package/LICENSE +203 -0
  3. package/Source.ts +13 -0
  4. package/Trait.ts +51 -0
  5. package/TraitConflict.ts +52 -0
  6. package/TraitDerivation.ts +137 -0
  7. package/TraitEvaluation.ts +69 -0
  8. package/TraitStore.ts +88 -0
  9. package/TraitsConfig.ts +57 -0
  10. package/TraitsContext.ts +11 -0
  11. package/_util/Approval.ts +59 -0
  12. package/_util/Git.ts +25 -0
  13. package/_util/TraitsConfigProvider.ts +22 -0
  14. package/_util/TraitsTelemetry.ts +20 -0
  15. package/_util/words.ts +555 -0
  16. package/commands/init.ts +73 -0
  17. package/dist/Assessment.d.ts +12 -0
  18. package/dist/Assessment.d.ts.map +1 -0
  19. package/dist/Assessment.js +12 -0
  20. package/dist/Assessment.js.map +1 -0
  21. package/dist/Source.d.ts +21 -0
  22. package/dist/Source.d.ts.map +1 -0
  23. package/dist/Source.js +10 -0
  24. package/dist/Source.js.map +1 -0
  25. package/dist/Trait.d.ts +12 -0
  26. package/dist/Trait.d.ts.map +1 -0
  27. package/dist/Trait.js +44 -0
  28. package/dist/Trait.js.map +1 -0
  29. package/dist/TraitConflict.d.ts +56 -0
  30. package/dist/TraitConflict.d.ts.map +1 -0
  31. package/dist/TraitConflict.js +47 -0
  32. package/dist/TraitConflict.js.map +1 -0
  33. package/dist/TraitDerivation.d.ts +99 -0
  34. package/dist/TraitDerivation.d.ts.map +1 -0
  35. package/dist/TraitDerivation.js +109 -0
  36. package/dist/TraitDerivation.js.map +1 -0
  37. package/dist/TraitEvaluation.d.ts +68 -0
  38. package/dist/TraitEvaluation.d.ts.map +1 -0
  39. package/dist/TraitEvaluation.js +61 -0
  40. package/dist/TraitEvaluation.js.map +1 -0
  41. package/dist/TraitStore.d.ts +17 -0
  42. package/dist/TraitStore.d.ts.map +1 -0
  43. package/dist/TraitStore.js +57 -0
  44. package/dist/TraitStore.js.map +1 -0
  45. package/dist/TraitsConfig.d.ts +14 -0
  46. package/dist/TraitsConfig.d.ts.map +1 -0
  47. package/dist/TraitsConfig.js +40 -0
  48. package/dist/TraitsConfig.js.map +1 -0
  49. package/dist/TraitsContext.d.ts +11 -0
  50. package/dist/TraitsContext.d.ts.map +1 -0
  51. package/dist/TraitsContext.js +4 -0
  52. package/dist/TraitsContext.js.map +1 -0
  53. package/dist/_util/Approval.d.ts +28 -0
  54. package/dist/_util/Approval.d.ts.map +1 -0
  55. package/dist/_util/Approval.js +48 -0
  56. package/dist/_util/Approval.js.map +1 -0
  57. package/dist/_util/Git.d.ts +15 -0
  58. package/dist/_util/Git.d.ts.map +1 -0
  59. package/dist/_util/Git.js +17 -0
  60. package/dist/_util/Git.js.map +1 -0
  61. package/dist/_util/TraitsConfigProvider.d.ts +3 -0
  62. package/dist/_util/TraitsConfigProvider.d.ts.map +1 -0
  63. package/dist/_util/TraitsConfigProvider.js +17 -0
  64. package/dist/_util/TraitsConfigProvider.js.map +1 -0
  65. package/dist/_util/TraitsTelemetry.d.ts +3 -0
  66. package/dist/_util/TraitsTelemetry.d.ts.map +1 -0
  67. package/dist/_util/TraitsTelemetry.js +12 -0
  68. package/dist/_util/TraitsTelemetry.js.map +1 -0
  69. package/dist/_util/words.d.ts +2 -0
  70. package/dist/_util/words.d.ts.map +1 -0
  71. package/dist/_util/words.js +556 -0
  72. package/dist/_util/words.js.map +1 -0
  73. package/dist/commands/init.d.ts +4 -0
  74. package/dist/commands/init.d.ts.map +1 -0
  75. package/dist/commands/init.js +67 -0
  76. package/dist/commands/init.js.map +1 -0
  77. package/dist/index.d.ts +10 -0
  78. package/dist/index.d.ts.map +1 -0
  79. package/dist/index.js +10 -0
  80. package/dist/index.js.map +1 -0
  81. package/dist/main.d.ts +3 -0
  82. package/dist/main.d.ts.map +1 -0
  83. package/dist/main.js +73 -0
  84. package/dist/main.js.map +1 -0
  85. package/dist/package.json +53 -0
  86. package/dist/toolkit/CheckTraits.d.ts +79 -0
  87. package/dist/toolkit/CheckTraits.d.ts.map +1 -0
  88. package/dist/toolkit/CheckTraits.js +110 -0
  89. package/dist/toolkit/CheckTraits.js.map +1 -0
  90. package/dist/toolkit/DeriveTrait.d.ts +108 -0
  91. package/dist/toolkit/DeriveTrait.d.ts.map +1 -0
  92. package/dist/toolkit/DeriveTrait.js +54 -0
  93. package/dist/toolkit/DeriveTrait.js.map +1 -0
  94. package/dist/toolkit/toolkit.d.ts +3 -0
  95. package/dist/toolkit/toolkit.d.ts.map +1 -0
  96. package/dist/toolkit/toolkit.js +6 -0
  97. package/dist/toolkit/toolkit.js.map +1 -0
  98. package/index.ts +9 -0
  99. package/main.ts +113 -0
  100. package/package.json +46 -0
  101. package/skills/traits/SKILL.md +307 -0
  102. package/toolkit/CheckTraits.ts +150 -0
  103. package/toolkit/DeriveTrait.ts +59 -0
  104. package/toolkit/toolkit.ts +9 -0
  105. 1289345810861-0.06061636237427592/lib/traits.js +0 -665
  106. 1289345810861-0.06061636237427592/package.json +0 -7
@@ -0,0 +1,57 @@
1
+ import type { NodeServices } from "@effect/platform-node"
2
+ import { Array, Effect, Layer, Path, pipe, Schema as S, Semaphore } from "effect"
3
+ import { DecisionModel, LanguageModel } from "effect/ai"
4
+ import { HttpClient } from "effect/http"
5
+
6
+ import { getWorktreeRoot } from "./_util/Git.ts"
7
+ import { TraitsContext } from "./TraitsContext.ts"
8
+
9
+ export type TraitsConfig = typeof TraitsConfig.Type
10
+ export const TraitsConfig = S.Struct({
11
+ excludeGitignored: S.Boolean.pipe(S.optional).annotate({
12
+ description: "Exclude Git-ignored files from checks by default. Set false to disable the Git file-set filter."
13
+ }),
14
+ excludes: S.Array(S.NonEmptyString).pipe(S.optional).annotate({
15
+ description:
16
+ "Filenames or native filesystem glob patterns excluded from trait checks, relative to traits.ts or absolute. Use directory/** to exclude a subtree. Bare directories are not expanded; escape glob characters in literal filenames using native glob syntax."
17
+ }),
18
+ layerDecisionModel: S.declare(
19
+ (
20
+ value
21
+ ): value is Layer.Layer<DecisionModel.DecisionModel, any, NodeServices.NodeServices | HttpClient.HttpClient> =>
22
+ Layer.isLayer(value)
23
+ ),
24
+ layerLanguageModel: S.declare(
25
+ (
26
+ value
27
+ ): value is Layer.Layer<LanguageModel.LanguageModel, any, NodeServices.NodeServices | HttpClient.HttpClient> =>
28
+ Layer.isLayer(value)
29
+ )
30
+ })
31
+
32
+ export const layer = Effect.gen(function* () {
33
+ const path = yield* Path.Path
34
+ const configRoot = yield* getWorktreeRoot
35
+ const url = yield* path.toFileUrl(path.join(configRoot, "traits.ts"))
36
+ const module = yield* Effect.promise(() => import(url.href))
37
+ const config = yield* pipe(module.default, S.decodeUnknownEffect(TraitsConfig))
38
+ return Layer.mergeAll(
39
+ // oxlint-disable-next-line effecttsgo/any-unknown-in-error-context
40
+ config.layerDecisionModel.pipe(Layer.orDie),
41
+ // oxlint-disable-next-line effecttsgo/any-unknown-in-error-context
42
+ config.layerLanguageModel.pipe(Layer.orDie),
43
+ Layer.effect(
44
+ TraitsContext,
45
+ Effect.gen(function* () {
46
+ const excludes = Array.appendAll(
47
+ Array.flatMap([".git", ".worktrees"], (directory) => [`**/${directory}`, `**/${directory}/**`]),
48
+ Array.map(config.excludes ?? [], (pattern) =>
49
+ path.isAbsolute(pattern) ? path.relative(configRoot, pattern) : pattern
50
+ )
51
+ )
52
+ const semaphore = yield* Semaphore.make(4)
53
+ return { configRoot, excludeGitignored: config.excludeGitignored ?? true, excludes, semaphore }
54
+ })
55
+ )
56
+ )
57
+ }).pipe(Layer.unwrap)
@@ -0,0 +1,11 @@
1
+ import { Context, Semaphore } from "effect"
2
+
3
+ export class TraitsContext extends Context.Service<
4
+ TraitsContext,
5
+ {
6
+ readonly configRoot: string
7
+ readonly excludeGitignored: boolean
8
+ readonly excludes: ReadonlyArray<string>
9
+ readonly semaphore: Semaphore.Semaphore
10
+ }
11
+ >()("traits/TraitsContext") {}
@@ -0,0 +1,59 @@
1
+ import { Effect, Option, Schema as S, String } from "effect"
2
+ import { McpSchema, McpServer } from "effect/ai"
3
+
4
+ export type Approval = typeof Approval.Type
5
+ export const Approval = S.TaggedUnion({
6
+ Approved: {},
7
+ ApprovalRequired: { message: S.String },
8
+ RevisionRequired: { feedback: S.String },
9
+ Declined: {}
10
+ })
11
+
12
+ export const request = Effect.fn("Approval.request")(function* (options: {
13
+ readonly message: string
14
+ readonly approved?: boolean | undefined
15
+ }) {
16
+ const client = yield* Effect.serviceOption(McpSchema.McpServerClient).pipe(Effect.map(Option.getOrUndefined))
17
+ const supportsForm =
18
+ client !== undefined &&
19
+ client.clientCapabilities.elicitation !== undefined &&
20
+ (client.clientCapabilities.elicitation.form !== undefined ||
21
+ client.clientCapabilities.elicitation.url === undefined)
22
+ const globalForms =
23
+ client !== undefined &&
24
+ (String.toLowerCase(client.clientInfo.name) === "opencode" ||
25
+ typeof client.requestMetadata?.["ai.opencode/sessionID"] === "string")
26
+
27
+ if (client === undefined || !supportsForm || globalForms) {
28
+ return options.approved === true
29
+ ? Approval.cases.Approved.make({})
30
+ : Approval.cases.ApprovalRequired.make({ message: options.message })
31
+ }
32
+
33
+ const response = yield* McpServer.elicit({
34
+ message: options.message,
35
+ schema: S.Struct({
36
+ decision: S.Literals(["approve", "revise", "skip"]).annotate({
37
+ description: "Approve this exact proposal, request a revision, or skip it."
38
+ }),
39
+ feedback: S.String.pipe(S.optionalKey).annotate({ description: "Optional revision or alternative wording." })
40
+ })
41
+ }).pipe(
42
+ Effect.provideService(McpSchema.McpServerClient, client),
43
+ Effect.catchTags({
44
+ ElicitationDeclined: (error) => (error.cause === undefined ? Effect.undefined : error)
45
+ })
46
+ )
47
+ if (response === undefined || response.decision === "skip") return Approval.cases.Declined.make({})
48
+ if (response.decision === "revise" || (response.feedback !== undefined && String.trim(response.feedback) !== "")) {
49
+ const feedback = response.feedback === undefined ? "" : String.trim(response.feedback)
50
+ const guidance = String.stripMargin(`
51
+ | Ask the user for revision details using an interactive question with free-text input.
52
+ | Keep this proposal pending; do not infer approval or cancel it because of a related follow-up task.
53
+ `)
54
+ return Approval.cases.RevisionRequired.make({
55
+ feedback: feedback !== "" ? feedback : guidance
56
+ })
57
+ }
58
+ return Approval.cases.Approved.make({})
59
+ })
package/_util/Git.ts ADDED
@@ -0,0 +1,25 @@
1
+ import { Data, Effect, Path, String } from "effect"
2
+ import { ChildProcess, ChildProcessSpawner } from "effect/process"
3
+
4
+ export class GitRootError extends Data.TaggedError("GitRootError")<{ readonly message: string }> {}
5
+
6
+ const resolveRoot = Effect.fn("Git.resolveRoot")(function* (command: ChildProcess.Command) {
7
+ const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
8
+ return yield* spawner.string(command).pipe(
9
+ Effect.map(String.trim),
10
+ Effect.filterOrFail(
11
+ String.isNonEmpty,
12
+ () => new GitRootError({ message: "Run from inside a Git repository or worktree" })
13
+ )
14
+ )
15
+ })
16
+
17
+ /** The current checkout root, including when running in a linked worktree. */
18
+ export const getWorktreeRoot = resolveRoot(ChildProcess.make`git rev-parse --show-toplevel`)
19
+
20
+ /** The main checkout root, shared by its linked worktrees. */
21
+ export const getGitRoot = Effect.gen(function* () {
22
+ const path = yield* Path.Path
23
+ const gitDir = yield* resolveRoot(ChildProcess.make`git rev-parse --path-format=absolute --git-common-dir`)
24
+ return path.dirname(gitDir)
25
+ })
@@ -0,0 +1,22 @@
1
+ import { Array, ConfigProvider, Effect, FileSystem, Path } from "effect"
2
+
3
+ import { getGitRoot, getWorktreeRoot } from "./Git.ts"
4
+
5
+ export const layer = ConfigProvider.layerAdd(
6
+ Effect.gen(function* () {
7
+ const fs = yield* FileSystem.FileSystem
8
+ const path = yield* Path.Path
9
+ const worktreeRoot = yield* getWorktreeRoot
10
+ const gitRoot = yield* getGitRoot
11
+ const files = Array.dedupe([path.join(worktreeRoot, ".env"), path.join(gitRoot, ".env")])
12
+ let provider = ConfigProvider.fromUnknown({})
13
+ for (const file of files) {
14
+ if (yield* fs.exists(file)) {
15
+ provider = yield* ConfigProvider.fromDotEnv({ path: file }).pipe(
16
+ Effect.map((value) => ConfigProvider.orElse(provider, value))
17
+ )
18
+ }
19
+ }
20
+ return provider
21
+ }).pipe(Effect.withSpan("TraitsConfigProvider.load"))
22
+ )
@@ -0,0 +1,20 @@
1
+ import { Layer, pipe, String } from "effect"
2
+ import { Url } from "effect/http"
3
+ import { OtlpLogger, OtlpSerialization, OtlpTracer } from "effect/observability"
4
+
5
+ import PackageJson from "../package.json" with { type: "json" }
6
+
7
+ export const layer = (endpoint: URL | undefined) => {
8
+ if (endpoint === undefined) return Layer.empty
9
+ const url = (signal: string) =>
10
+ pipe(
11
+ endpoint,
12
+ Url.setPathname(`${pipe(endpoint.pathname, String.replace(/\/$/, ""))}/v1/${signal}`),
13
+ Url.setHash("")
14
+ ).href
15
+ const resource = { serviceName: "traits", serviceVersion: PackageJson.version }
16
+ return Layer.mergeAll(
17
+ OtlpTracer.layer({ url: url("traces"), resource }),
18
+ OtlpLogger.layer({ url: url("logs"), resource })
19
+ ).pipe(Layer.provide(OtlpSerialization.layerJson))
20
+ }