@llm4ts/runner 2.54.1 → 3.2.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.
package/dist/Tools.js ADDED
@@ -0,0 +1,142 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { dirname, isAbsolute, join, resolve } from "node:path";
4
+ import * as Effect from "effect/Effect";
5
+ import * as Schema from "effect/Schema";
6
+ import { builtinTools } from "@llm4ts/flow/tools/Builtin";
7
+ import { isToolName, renderUsage, specOf } from "@llm4ts/flow/tools/Tool";
8
+ const tierRank = { project: 0, global: 1, builtin: 2 };
9
+ export const catalogEntries = (kits) => {
10
+ const byName = new Map();
11
+ for (const tool of builtinTools) {
12
+ byName.set(tool.name, { _tag: "builtin", name: tool.name, tool });
13
+ }
14
+ const ordered = [...kits].sort((left, right) => tierRank[right.tier] - tierRank[left.tier]);
15
+ for (const kit of ordered) {
16
+ for (const name of kit.tools) {
17
+ if (!isToolName(name)) {
18
+ continue;
19
+ }
20
+ const file = ["ts", "js"]
21
+ .map((extension) => join(kit.root, "tools", `${name}.${extension}`))
22
+ .find((candidate) => existsSync(candidate));
23
+ if (file !== undefined) {
24
+ byName.set(name, { _tag: "kit", name, kit, file });
25
+ }
26
+ }
27
+ }
28
+ return [...byName.values()].sort((left, right) => left.name.localeCompare(right.name));
29
+ };
30
+ export const findEntry = (catalog, name) => catalog.find((entry) => entry.name === name);
31
+ export class PresetError extends Schema.TaggedError()("PresetError", {
32
+ preset: Schema.String,
33
+ message: Schema.String
34
+ }) {
35
+ }
36
+ export const resolvePreset = (catalog, name, extra) => {
37
+ const preset = catalog.presets.find((candidate) => candidate.name === name);
38
+ if (preset === undefined) {
39
+ return Effect.succeed(undefined);
40
+ }
41
+ const [toolName, ...rest] = preset.command;
42
+ if (toolName === undefined) {
43
+ return Effect.fail(new PresetError({ preset: name, message: "preset names no tool" }));
44
+ }
45
+ const entry = findEntry(catalog.entries, toolName);
46
+ if (entry === undefined) {
47
+ return Effect.fail(new PresetError({
48
+ preset: name,
49
+ message: `preset runs '${toolName}', which is not a catalog tool (run \`llm4ts tool list\`)`
50
+ }));
51
+ }
52
+ return Effect.succeed({ preset, entry, argv: [...rest, ...extra] });
53
+ };
54
+ /**
55
+ * The repository a worktree belongs to: `git rev-parse --git-common-dir`
56
+ * points back at the main checkout from any worktree, so the project tier
57
+ * (`<repo>/.llm4ts/kits`) is found without a file in the worktree or an
58
+ * environment variable the executor would strip (ADR 0048). Falls back to
59
+ * `cwd` outside a repository.
60
+ */
61
+ export const projectRootOf = (cwd) => {
62
+ try {
63
+ const common = execFileSync("git", ["rev-parse", "--git-common-dir"], {
64
+ cwd,
65
+ encoding: "utf8",
66
+ stdio: ["ignore", "pipe", "ignore"]
67
+ }).trim();
68
+ if (common.length === 0) {
69
+ return cwd;
70
+ }
71
+ const absolute = isAbsolute(common) ? common : resolve(cwd, common);
72
+ return dirname(absolute);
73
+ }
74
+ catch {
75
+ return cwd;
76
+ }
77
+ };
78
+ /** One line per catalog entry and preset, for `llm4ts tool list`. */
79
+ export const renderCatalog = (catalog, specs) => {
80
+ const lines = [];
81
+ for (const entry of catalog.entries) {
82
+ const spec = specs.get(entry.name);
83
+ const origin = entry._tag === "builtin" ? "" : ` [${entry.kit.name} kit]`;
84
+ lines.push(spec === undefined
85
+ ? `${entry.name}${origin}`
86
+ : `${renderUsage(spec)}${origin}\n ${spec.purpose}`);
87
+ }
88
+ if (catalog.presets.length > 0) {
89
+ lines.push("", "Presets (pack: command):");
90
+ for (const preset of catalog.presets) {
91
+ lines.push(` ${preset.name} (${preset.pack}): ${preset.command.join(" ")}`);
92
+ }
93
+ }
94
+ return lines.join("\n");
95
+ };
96
+ export const catalogJson = (catalog, specs) => ({
97
+ tools: catalog.entries.map((entry) => {
98
+ const spec = specs.get(entry.name);
99
+ return {
100
+ name: entry.name,
101
+ origin: entry._tag === "builtin" ? "builtin" : `kit:${entry.kit.name}`,
102
+ ...(spec === undefined ? {} : { purpose: spec.purpose, usage: renderUsage(spec) })
103
+ };
104
+ }),
105
+ presets: catalog.presets.map((preset) => ({
106
+ name: preset.name,
107
+ pack: preset.pack,
108
+ command: preset.command.join(" ")
109
+ }))
110
+ });
111
+ /** Exit status for a report: 0 ok, 1 issues. Errors exit 2 (the CLI's own). */
112
+ export const reportExitCode = (report) => (report.ok ? 0 : 1);
113
+ /**
114
+ * The paragraph a coder's brief carries (ADR 0048): one rule, then each
115
+ * preset of the story's pack with the command to run. `LLM4TS_TOOL_BRIEF=off`
116
+ * drops it, for a run without the catalog to compare against.
117
+ */
118
+ export const toolBrief = (presets, specs, environment = process.env) => {
119
+ if (environment.LLM4TS_TOOL_BRIEF?.trim().toLowerCase() === "off") {
120
+ return undefined;
121
+ }
122
+ const lines = [
123
+ "Tools: for validating a document, comparing two outputs, or checking a contract, run the",
124
+ "project's tool instead of writing a script — `llm4ts tool <name> [--flag value]` prints JSON",
125
+ "(ok, summary, issues). `llm4ts tool list` shows every tool; `llm4ts tool describe <name>`",
126
+ "its arguments. Never write a Python or Node one-liner for a job a tool covers."
127
+ ];
128
+ if (presets.length > 0) {
129
+ lines.push("", "This pack's presets (run them by name from the worktree root):");
130
+ for (const preset of presets) {
131
+ const [toolName, ...rest] = preset.command;
132
+ const spec = toolName === undefined ? undefined : specs.get(toolName);
133
+ const fixed = rest.length === 0 ? "" : ` (fixed: ${rest.join(" ")})`;
134
+ const purpose = spec === undefined ? "" : ` — ${spec.purpose}`;
135
+ lines.push(`- \`llm4ts tool ${preset.name}\`${fixed}${purpose}`);
136
+ }
137
+ }
138
+ return lines.join("\n");
139
+ };
140
+ /** Specs of the built-in tools, known without running anything. */
141
+ export const builtinSpecs = () => new Map(builtinTools.map((tool) => [tool.name, specOf(tool)]));
142
+ //# sourceMappingURL=Tools.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Tools.js","sourceRoot":"","sources":["../src/Tools.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llm4ts/runner",
3
- "version": "2.54.1",
3
+ "version": "3.2.0",
4
4
  "description": "Node runner, CLI, terminal, and MCP composition for llm4ts",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -13,11 +13,12 @@
13
13
  "src"
14
14
  ],
15
15
  "exports": {
16
+ "./ToolMain": "./dist/ToolMain.js",
17
+ "./Tools": "./dist/Tools.js",
16
18
  ".": "./dist/index.js",
17
19
  "./Cli": "./dist/Cli.js",
18
20
  "./AgentTree": "./dist/AgentTree.js",
19
21
  "./AgentTreeSurface": "./dist/AgentTreeSurface.js",
20
- "./CoderPolicy": "./dist/CoderPolicy.js",
21
22
  "./Connectors": "./dist/Connectors.js",
22
23
  "./Costs": "./dist/Costs.js",
23
24
  "./Doctor": "./dist/Doctor.js",
@@ -37,7 +38,6 @@
37
38
  "./NodeTemporaryFiles": "./dist/NodeTemporaryFiles.js",
38
39
  "./NodeWorkspace": "./dist/NodeWorkspace.js",
39
40
  "./Otel": "./dist/Otel.js",
40
- "./NodeRuntime": "./dist/NodeRuntime.js",
41
41
  "./Packs": "./dist/Packs.js",
42
42
  "./Profile": "./dist/Profile.js",
43
43
  "./PiModels": "./dist/PiModels.js",
@@ -75,8 +75,8 @@
75
75
  "typescript"
76
76
  ],
77
77
  "dependencies": {
78
- "@llm4ts/core": "2.54.1",
79
- "@llm4ts/flow": "2.54.1"
78
+ "@llm4ts/core": "3.2.0",
79
+ "@llm4ts/flow": "3.2.0"
80
80
  },
81
81
  "peerDependencies": {
82
82
  "effect": "4.0.0"
package/src/Kits.ts CHANGED
@@ -29,6 +29,8 @@ export interface DiscoveredKit {
29
29
  readonly packs: ReadonlyArray<string>
30
30
  /** Flow names: every `flows/<name>.ts|js` (not `lib/` or `test/`). */
31
31
  readonly flows: ReadonlyArray<string>
32
+ /** Tool names: every `tools/<name>.ts|js` (ADR 0048), run by `llm4ts tool`. */
33
+ readonly tools: ReadonlyArray<string>
32
34
  readonly shadows: ReadonlyArray<KitTier>
33
35
  }
34
36
 
@@ -118,7 +120,14 @@ const readKit = (
118
120
  .map((entry) => entry.replace(/\.(ts|js)$/, ""))
119
121
  .sort()
120
122
  : []
121
- if (packs.length === 0 && flows.length === 0) {
123
+ const toolsDir = join(root, "tools")
124
+ const tools = isDirectory(toolsDir)
125
+ ? readdirSync(toolsDir)
126
+ .filter((entry) => entry.endsWith(".ts") || entry.endsWith(".js"))
127
+ .map((entry) => entry.replace(/\.(ts|js)$/, ""))
128
+ .sort()
129
+ : []
130
+ if (packs.length === 0 && flows.length === 0 && tools.length === 0) {
122
131
  return undefined
123
132
  }
124
133
  const readme = join(root, "README.md")
@@ -131,7 +140,8 @@ const readKit = (
131
140
  tier,
132
141
  ...(description === undefined ? {} : { description }),
133
142
  packs,
134
- flows
143
+ flows,
144
+ tools
135
145
  }
136
146
  }
137
147
 
package/src/Profile.ts CHANGED
@@ -65,11 +65,16 @@ export const Gap = Schema.Struct({
65
65
  })
66
66
  export type Gap = typeof Gap.Type
67
67
 
68
- /** The coder's tool calls by kind of work (build, install and git count as other). */
68
+ /**
69
+ * The coder's tool calls by kind of work (build, install and git count as
70
+ * other). `script` is the ad-hoc Python/Node one-liners, heredocs and scratch
71
+ * files it improvised; profiles saved before it existed decode it as 0.
72
+ */
69
73
  export const ToolCalls = Schema.Struct({
70
74
  explore: Schema.Int,
71
75
  edit: Schema.Int,
72
76
  test: Schema.Int,
77
+ script: Schema.Int.pipe(Schema.withDecodingDefaultKey(Effect.succeed(0))),
73
78
  other: Schema.Int
74
79
  })
75
80
  export type ToolCalls = typeof ToolCalls.Type
@@ -146,7 +151,7 @@ export const ProfileReport = Schema.Struct({
146
151
  models: Schema.Array(ModelRow),
147
152
  gates: Schema.Array(GateRow),
148
153
  tools: Schema.Array(CountRow),
149
- /** Coder tool time by kind of work: explore, edit, test, build, install, git, other. */
154
+ /** Coder tool time by kind of work: explore, edit, test, script, build, install, git, other. */
150
155
  toolCategories: Schema.Array(CountRow),
151
156
  waits: Schema.Array(CountRow),
152
157
  /** Timed on the run's events, in no story: the run's own planning, or a seat no story owns. */
@@ -184,7 +189,7 @@ interface Lane {
184
189
  tasks: number
185
190
  /** When the coder's first task began: setup and task planning come before it. */
186
191
  firstTask: number | undefined
187
- toolCalls: { explore: number; edit: number; test: number; other: number }
192
+ toolCalls: { explore: number; edit: number; test: number; script: number; other: number }
188
193
  firstEdit: number | undefined
189
194
  exploreBeforeEdit: number
190
195
  unverifiedClaims: number
@@ -363,7 +368,7 @@ export const profileOf = (inputs: ReadonlyArray<TreeInput>): ProfileReport => {
363
368
  openTools: 0,
364
369
  tasks: 0,
365
370
  firstTask: undefined,
366
- toolCalls: { explore: 0, edit: 0, test: 0, other: 0 },
371
+ toolCalls: { explore: 0, edit: 0, test: 0, script: 0, other: 0 },
367
372
  firstEdit: undefined,
368
373
  exploreBeforeEdit: 0,
369
374
  unverifiedClaims: 0,
@@ -397,7 +402,10 @@ export const profileOf = (inputs: ReadonlyArray<TreeInput>): ProfileReport => {
397
402
  lane?.timed.push({ at, event })
398
403
  if (lane !== undefined && event.kind === "tool" && event.category !== undefined) {
399
404
  const kind =
400
- event.category === "explore" || event.category === "edit" || event.category === "test"
405
+ event.category === "explore" ||
406
+ event.category === "edit" ||
407
+ event.category === "test" ||
408
+ event.category === "script"
401
409
  ? event.category
402
410
  : "other"
403
411
  lane.toolCalls[kind] += 1
@@ -808,6 +816,15 @@ const findingsOf = (facts: {
808
816
  ]
809
817
  : []
810
818
  )
819
+ const improvised = facts.stories.flatMap((story) =>
820
+ story.toolCalls.script >= 3
821
+ ? [
822
+ {
823
+ text: `${story.id}: ${story.toolCalls.script} ad-hoc scripts — a pack tool preset (llm4ts tool) would make this repeatable`
824
+ }
825
+ ]
826
+ : []
827
+ )
811
828
  const growth = facts.stories.flatMap((story) =>
812
829
  story.turns.promptGrowth &&
813
830
  story.turns.firstPrompt !== undefined &&
@@ -819,7 +836,7 @@ const findingsOf = (facts: {
819
836
  ]
820
837
  : []
821
838
  )
822
- return [...ranked, ...fabricated, ...growth]
839
+ return [...ranked, ...fabricated, ...improvised, ...growth]
823
840
  }
824
841
 
825
842
  const table = (
@@ -908,6 +925,7 @@ export const renderProfile = (report: ProfileReport): string => {
908
925
  story.toolCalls.explore +
909
926
  story.toolCalls.edit +
910
927
  story.toolCalls.test +
928
+ story.toolCalls.script +
911
929
  story.toolCalls.other ===
912
930
  0
913
931
  )
@@ -922,6 +940,7 @@ export const renderProfile = (report: ProfileReport): string => {
922
940
  "explore",
923
941
  "edit",
924
942
  "test",
943
+ "script",
925
944
  "other",
926
945
  "before 1st edit",
927
946
  "1st edit at"
@@ -932,6 +951,7 @@ export const renderProfile = (report: ProfileReport): string => {
932
951
  String(story.toolCalls.explore),
933
952
  String(story.toolCalls.edit),
934
953
  String(story.toolCalls.test),
954
+ String(story.toolCalls.script),
935
955
  String(story.toolCalls.other),
936
956
  story.firstEditMs === undefined ? "" : `${story.exploreBeforeEdit} explore`,
937
957
  story.firstEditMs === undefined ? "never" : duration(story.firstEditMs)
@@ -1091,6 +1111,7 @@ export const profileDelta = (before: ProfileReport, after: ProfileReport): Profi
1091
1111
  count("coder turns", (r) => sum(r.stories.map((s) => s.turns.count))),
1092
1112
  count("coder edits", (r) => sum(r.stories.map((s) => s.toolCalls.edit)), "none"),
1093
1113
  count("coder test runs", (r) => sum(r.stories.map((s) => s.toolCalls.test)), "none"),
1114
+ count("coder scripts", (r) => sum(r.stories.map((s) => s.toolCalls.script))),
1094
1115
  count("reviewer calls", (r) => roleCalls(r, "reviewer")),
1095
1116
  count("judge calls", (r) => roleCalls(r, "judge")),
1096
1117
  count("gate runs", (r) => sum(r.gates.map((g) => g.runs))),
@@ -0,0 +1,76 @@
1
+ import * as Effect from "effect/Effect"
2
+ import * as Schema from "effect/Schema"
3
+ import * as Layer from "effect/Layer"
4
+ import { SoapTransport, makeNodeSoapTransport } from "@llm4ts/flow/endpoint/Transport"
5
+ import { ToolContext, ToolReport, ToolSpec, specOf, type ToolShape } from "@llm4ts/flow/tools/Tool"
6
+ import { NodePlainFileStoreLive } from "./NodePlainFileStore.ts"
7
+
8
+ /**
9
+ * The main of a kit tool file (`kits/<kit>/tools/<name>.ts`, ADR 0048):
10
+ * `llm4ts tool` runs the file as a Node child and speaks this protocol —
11
+ * `--describe` prints the `ToolSpec`; otherwise the parsed arguments arrive
12
+ * as one JSON object on stdin and the `ToolReport` goes out on stdout. Exit
13
+ * 0 when the report is ok, 1 when it carries issues, 2 when the tool could
14
+ * not run (the error is JSON on stdout too, `{"error": …}`). `--project
15
+ * <root>` names the project the tool runs for (default: the current
16
+ * directory); secrets resolve from this process's environment.
17
+ */
18
+ export const runToolMain = (tool: ToolShape): void => {
19
+ if (process.argv.includes("--describe")) {
20
+ process.stdout.write(`${JSON.stringify(Schema.encodeSync(ToolSpec)(specOf(tool)))}\n`)
21
+ return
22
+ }
23
+ void readStdin().then((text) => {
24
+ let values: Record<string, unknown>
25
+ try {
26
+ const parsed: unknown = text.trim().length === 0 ? {} : JSON.parse(text)
27
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
28
+ throw new Error("stdin must hold one JSON object of arguments")
29
+ }
30
+ values = Object.fromEntries(Object.entries(parsed))
31
+ } catch (error) {
32
+ process.stdout.write(
33
+ `${JSON.stringify({ error: error instanceof Error ? error.message : String(error) })}\n`
34
+ )
35
+ process.exitCode = 2
36
+ return
37
+ }
38
+ const projectFlag = process.argv.indexOf("--project")
39
+ const projectRoot =
40
+ projectFlag >= 0 && process.argv[projectFlag + 1] !== undefined
41
+ ? process.argv[projectFlag + 1]
42
+ : process.cwd()
43
+ return Effect.runPromise(
44
+ tool.execute(values).pipe(
45
+ Effect.provide(
46
+ Layer.mergeAll(
47
+ NodePlainFileStoreLive,
48
+ Layer.succeed(ToolContext)({ projectRoot, environment: process.env }),
49
+ Layer.succeed(SoapTransport)(makeNodeSoapTransport())
50
+ )
51
+ ),
52
+ Effect.match({
53
+ onFailure: (error) => {
54
+ process.stdout.write(`${JSON.stringify({ error: error.message })}\n`)
55
+ process.exitCode = 2
56
+ },
57
+ onSuccess: (report) => {
58
+ process.stdout.write(`${JSON.stringify(Schema.encodeSync(ToolReport)(report))}\n`)
59
+ process.exitCode = report.ok ? 0 : 1
60
+ }
61
+ })
62
+ )
63
+ )
64
+ })
65
+ }
66
+
67
+ const readStdin = (): Promise<string> =>
68
+ new Promise((resolvePromise, rejectPromise) => {
69
+ const chunks: Array<Buffer> = []
70
+ process.stdin.on("data", (chunk: Buffer) => chunks.push(chunk))
71
+ process.stdin.on("end", () => resolvePromise(Buffer.concat(chunks).toString("utf8")))
72
+ process.stdin.on("error", rejectPromise)
73
+ })
74
+
75
+ /** The child's stdout as the parent reads it: a report, or the error it printed. */
76
+ export const ChildToolOutput = Schema.Union([Schema.Struct({ error: Schema.String }), ToolReport])
package/src/Tools.ts ADDED
@@ -0,0 +1,204 @@
1
+ import { execFileSync } from "node:child_process"
2
+ import { existsSync } from "node:fs"
3
+ import { dirname, isAbsolute, join, resolve } from "node:path"
4
+ import * as Effect from "effect/Effect"
5
+ import * as Schema from "effect/Schema"
6
+ import { builtinTools } from "@llm4ts/flow/tools/Builtin"
7
+ import type { ToolReport, ToolSpec } from "@llm4ts/flow/tools/Tool"
8
+ import { isToolName, renderUsage, specOf, type ToolShape } from "@llm4ts/flow/tools/Tool"
9
+ import type { ToolPreset } from "@llm4ts/flow/Pack"
10
+ import type { DiscoveredKit } from "./Kits.ts"
11
+
12
+ /**
13
+ * The tool catalog (ADR 0048): the built-in tools, every discovered kit's
14
+ * `tools/<name>.ts`, and the presets of the packs in reach. A kit tool with
15
+ * a built-in's name shadows it, project kits first, as flows do.
16
+ */
17
+ export type CatalogEntry =
18
+ | { readonly _tag: "builtin"; readonly name: string; readonly tool: ToolShape }
19
+ | {
20
+ readonly _tag: "kit"
21
+ readonly name: string
22
+ readonly kit: DiscoveredKit
23
+ readonly file: string
24
+ }
25
+
26
+ export interface ToolCatalog {
27
+ readonly entries: ReadonlyArray<CatalogEntry>
28
+ readonly presets: ReadonlyArray<ToolPreset & { readonly pack: string }>
29
+ }
30
+
31
+ const tierRank = { project: 0, global: 1, builtin: 2 } as const
32
+
33
+ export const catalogEntries = (kits: ReadonlyArray<DiscoveredKit>): ReadonlyArray<CatalogEntry> => {
34
+ const byName = new Map<string, CatalogEntry>()
35
+ for (const tool of builtinTools) {
36
+ byName.set(tool.name, { _tag: "builtin", name: tool.name, tool })
37
+ }
38
+ const ordered = [...kits].sort((left, right) => tierRank[right.tier] - tierRank[left.tier])
39
+ for (const kit of ordered) {
40
+ for (const name of kit.tools) {
41
+ if (!isToolName(name)) {
42
+ continue
43
+ }
44
+ const file = ["ts", "js"]
45
+ .map((extension) => join(kit.root, "tools", `${name}.${extension}`))
46
+ .find((candidate) => existsSync(candidate))
47
+ if (file !== undefined) {
48
+ byName.set(name, { _tag: "kit", name, kit, file })
49
+ }
50
+ }
51
+ }
52
+ return [...byName.values()].sort((left, right) => left.name.localeCompare(right.name))
53
+ }
54
+
55
+ export const findEntry = (
56
+ catalog: ReadonlyArray<CatalogEntry>,
57
+ name: string
58
+ ): CatalogEntry | undefined => catalog.find((entry) => entry.name === name)
59
+
60
+ /**
61
+ * A preset is `tool --flag value …`: the catalog tool, then its arguments,
62
+ * with whatever the caller appends. A preset never names another preset.
63
+ */
64
+ export interface ResolvedPreset {
65
+ readonly preset: ToolPreset & { readonly pack: string }
66
+ readonly entry: CatalogEntry
67
+ readonly argv: ReadonlyArray<string>
68
+ }
69
+
70
+ export class PresetError extends Schema.TaggedError<PresetError>()("PresetError", {
71
+ preset: Schema.String,
72
+ message: Schema.String
73
+ }) {}
74
+
75
+ export const resolvePreset = (
76
+ catalog: ToolCatalog,
77
+ name: string,
78
+ extra: ReadonlyArray<string>
79
+ ): Effect.Effect<ResolvedPreset | undefined, PresetError> => {
80
+ const preset = catalog.presets.find((candidate) => candidate.name === name)
81
+ if (preset === undefined) {
82
+ return Effect.succeed(undefined)
83
+ }
84
+ const [toolName, ...rest] = preset.command
85
+ if (toolName === undefined) {
86
+ return Effect.fail(new PresetError({ preset: name, message: "preset names no tool" }))
87
+ }
88
+ const entry = findEntry(catalog.entries, toolName)
89
+ if (entry === undefined) {
90
+ return Effect.fail(
91
+ new PresetError({
92
+ preset: name,
93
+ message: `preset runs '${toolName}', which is not a catalog tool (run \`llm4ts tool list\`)`
94
+ })
95
+ )
96
+ }
97
+ return Effect.succeed({ preset, entry, argv: [...rest, ...extra] })
98
+ }
99
+
100
+ /**
101
+ * The repository a worktree belongs to: `git rev-parse --git-common-dir`
102
+ * points back at the main checkout from any worktree, so the project tier
103
+ * (`<repo>/.llm4ts/kits`) is found without a file in the worktree or an
104
+ * environment variable the executor would strip (ADR 0048). Falls back to
105
+ * `cwd` outside a repository.
106
+ */
107
+ export const projectRootOf = (cwd: string): string => {
108
+ try {
109
+ const common = execFileSync("git", ["rev-parse", "--git-common-dir"], {
110
+ cwd,
111
+ encoding: "utf8",
112
+ stdio: ["ignore", "pipe", "ignore"]
113
+ }).trim()
114
+ if (common.length === 0) {
115
+ return cwd
116
+ }
117
+ const absolute = isAbsolute(common) ? common : resolve(cwd, common)
118
+ return dirname(absolute)
119
+ } catch {
120
+ return cwd
121
+ }
122
+ }
123
+
124
+ /** One line per catalog entry and preset, for `llm4ts tool list`. */
125
+ export const renderCatalog = (
126
+ catalog: ToolCatalog,
127
+ specs: ReadonlyMap<string, ToolSpec>
128
+ ): string => {
129
+ const lines: Array<string> = []
130
+ for (const entry of catalog.entries) {
131
+ const spec = specs.get(entry.name)
132
+ const origin = entry._tag === "builtin" ? "" : ` [${entry.kit.name} kit]`
133
+ lines.push(
134
+ spec === undefined
135
+ ? `${entry.name}${origin}`
136
+ : `${renderUsage(spec)}${origin}\n ${spec.purpose}`
137
+ )
138
+ }
139
+ if (catalog.presets.length > 0) {
140
+ lines.push("", "Presets (pack: command):")
141
+ for (const preset of catalog.presets) {
142
+ lines.push(` ${preset.name} (${preset.pack}): ${preset.command.join(" ")}`)
143
+ }
144
+ }
145
+ return lines.join("\n")
146
+ }
147
+
148
+ export const catalogJson = (
149
+ catalog: ToolCatalog,
150
+ specs: ReadonlyMap<string, ToolSpec>
151
+ ): typeof Schema.Json.Type => ({
152
+ tools: catalog.entries.map((entry) => {
153
+ const spec = specs.get(entry.name)
154
+ return {
155
+ name: entry.name,
156
+ origin: entry._tag === "builtin" ? "builtin" : `kit:${entry.kit.name}`,
157
+ ...(spec === undefined ? {} : { purpose: spec.purpose, usage: renderUsage(spec) })
158
+ }
159
+ }),
160
+ presets: catalog.presets.map((preset) => ({
161
+ name: preset.name,
162
+ pack: preset.pack,
163
+ command: preset.command.join(" ")
164
+ }))
165
+ })
166
+
167
+ /** Exit status for a report: 0 ok, 1 issues. Errors exit 2 (the CLI's own). */
168
+ export const reportExitCode = (report: ToolReport): number => (report.ok ? 0 : 1)
169
+
170
+ /**
171
+ * The paragraph a coder's brief carries (ADR 0048): one rule, then each
172
+ * preset of the story's pack with the command to run. `LLM4TS_TOOL_BRIEF=off`
173
+ * drops it, for a run without the catalog to compare against.
174
+ */
175
+ export const toolBrief = (
176
+ presets: ReadonlyArray<ToolPreset>,
177
+ specs: ReadonlyMap<string, ToolSpec>,
178
+ environment: Readonly<Record<string, string | undefined>> = process.env
179
+ ): string | undefined => {
180
+ if (environment.LLM4TS_TOOL_BRIEF?.trim().toLowerCase() === "off") {
181
+ return undefined
182
+ }
183
+ const lines = [
184
+ "Tools: for validating a document, comparing two outputs, or checking a contract, run the",
185
+ "project's tool instead of writing a script — `llm4ts tool <name> [--flag value]` prints JSON",
186
+ "(ok, summary, issues). `llm4ts tool list` shows every tool; `llm4ts tool describe <name>`",
187
+ "its arguments. Never write a Python or Node one-liner for a job a tool covers."
188
+ ]
189
+ if (presets.length > 0) {
190
+ lines.push("", "This pack's presets (run them by name from the worktree root):")
191
+ for (const preset of presets) {
192
+ const [toolName, ...rest] = preset.command
193
+ const spec = toolName === undefined ? undefined : specs.get(toolName)
194
+ const fixed = rest.length === 0 ? "" : ` (fixed: ${rest.join(" ")})`
195
+ const purpose = spec === undefined ? "" : ` — ${spec.purpose}`
196
+ lines.push(`- \`llm4ts tool ${preset.name}\`${fixed}${purpose}`)
197
+ }
198
+ }
199
+ return lines.join("\n")
200
+ }
201
+
202
+ /** Specs of the built-in tools, known without running anything. */
203
+ export const builtinSpecs = (): ReadonlyMap<string, ToolSpec> =>
204
+ new Map(builtinTools.map((tool) => [tool.name, specOf(tool)]))
@@ -1,12 +0,0 @@
1
- import * as Schema from "effect/Schema";
2
- import type { Grants } from "@llm4ts/core/Capability";
3
- import { CliConnectorConfig } from "@llm4ts/core/ConnectorConfig";
4
- declare const AppliedCoderPolicy_base: Schema.Class<AppliedCoderPolicy, Schema.Struct<{
5
- readonly config: typeof CliConnectorConfig;
6
- readonly unenforceable: Schema.$Array<Schema.String>;
7
- }>, {}>;
8
- export declare class AppliedCoderPolicy extends AppliedCoderPolicy_base {
9
- }
10
- export declare const applyCoderPolicy: (config: CliConnectorConfig, grants: Grants) => AppliedCoderPolicy;
11
- export {};
12
- //# sourceMappingURL=CoderPolicy.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"CoderPolicy.d.ts","sourceRoot":"","sources":["../src/CoderPolicy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAEvC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAA;AACrD,OAAO,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAA;;;;;AAGjE,qBAAa,kBAAmB,SAAQ,uBAGtC;CAAG;AAgDL,eAAO,MAAM,gBAAgB,GAC3B,QAAQ,kBAAkB,EAC1B,QAAQ,MAAM,KACb,kBAqBF,CAAA"}