skilld-harness 3.2.0 → 3.3.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/README.md CHANGED
@@ -11,6 +11,12 @@ It then promotes generated Skills with an atomic directory rename.
11
11
  pnpm add skilld-harness @ai-sdk/harness ws zod
12
12
  ```
13
13
 
14
+ Add one Harness adapter. The examples below use OpenCode:
15
+
16
+ ```sh
17
+ pnpm add @ai-sdk/harness-opencode
18
+ ```
19
+
14
20
  Use Node 22 or newer.
15
21
  The sandbox must provide POSIX `sh`, `rm`, `mkdir`, and GNU `find`.
16
22
 
@@ -35,7 +41,46 @@ const result = await skillHarness.run({
35
41
  ```
36
42
 
37
43
  Every run returns a tagged `Ok` or `Err` value.
38
- An installed Skill can include cleanup warnings after promotion.
44
+ The input tag sets the `Ok` value: `PackageSkill` and `ProjectSkill` return a `GeneratedSkill`, and `ReviewSkill` returns a `SkillReview`.
45
+ Both `Ok` and `Err` carry a `report`, so a failed run still reports its cost.
46
+ `report.usage` holds input, cached input, and output tokens.
47
+ `report.steps` is the number of model calls.
48
+ A token count is `undefined` when the adapter does not report it.
49
+ If the run fails before the Agent starts, `steps` is 0.
50
+ `report.warnings` names each source file the Harness left out for size, each `onEvent` failure, and each cleanup problem.
51
+ An `InvalidSkill` error lists each failed output check in `issues`.
52
+
53
+ The Harness checks the rules that the generation Skills state:
54
+
55
+ - The frontmatter contains only `name` and `description`.
56
+ - `SKILL.md` links every file under `references/`, by a Markdown link or an inline code path.
57
+ - A package Skill keeps `SKILL.md` under 500 lines and writes at most eight reference files.
58
+ - A project Skill points only at project files and gives at least one search command.
59
+
60
+ A run can take several minutes. Pass `onEvent` to follow the Agent:
61
+
62
+ ```ts
63
+ import type { SkillRun } from 'skilld-harness'
64
+ import { createSkillHarness } from 'skilld-harness'
65
+
66
+ declare const skillHarness: ReturnType<typeof createSkillHarness>
67
+ declare const input: SkillRun
68
+
69
+ await skillHarness.run(input, {
70
+ onEvent: (event) => {
71
+ if (event._tag === 'ToolCall')
72
+ console.log(`step ${event.step}: ${event.toolName}`)
73
+ if (event._tag === 'StepFinish')
74
+ console.log(`step ${event.step} used ${event.usage.outputTokens} output tokens`)
75
+ },
76
+ })
77
+ ```
78
+
79
+ The events are `StepStart`, `ToolCall`, and `StepFinish`. Steps count from 0.
80
+ A `ToolCall` event arrives after the tool returns.
81
+ If the adapter runs the tool itself, the event arrives when its step ends.
82
+ The run does not wait for a promise that `onEvent` returns.
83
+ If `onEvent` throws, or its promise or thenable rejects, the run continues, and `report.warnings` carries the error.
39
84
 
40
85
  Pass `fetch` to `createSkillHarness` when the host owns HTTP access.
41
86
  The default adapter uses the Node global fetch implementation.
@@ -63,11 +108,33 @@ The session runs in a new temporary directory and exposes one port on
63
108
  `127.0.0.1` for bridge-backed Harness adapters. `destroy` kills every process
64
109
  the session started and removes the directory.
65
110
 
111
+ Each session gets its own `HOME`, XDG directories, and `TMPDIR` under the
112
+ session root. Harness state, the adapter bootstrap, and installed Skills stay
113
+ inside the session, and `destroy` removes them. The Agent does not load your own
114
+ agent configuration, Skills, or MCP servers. Configure the adapter explicitly,
115
+ for example through `openCodeConfig`.
116
+
117
+ Processes receive a minimal environment: `PATH`, locale, terminal, and proxy
118
+ variables. Pass any other variable through the `env` option. The adapter reads
119
+ its credentials from your environment and hands them to the processes it starts.
120
+ For OpenCode Go, set `OPENCODE_API_KEY`. The adapter does not read the key that
121
+ `opencode auth login` stores.
122
+
123
+ Each session installs the adapter bootstrap again, because nothing persists
124
+ between sessions. With OpenCode, that is about 200 MB of downloads per run.
125
+
66
126
  | Option | Default | Effect |
67
127
  | --- | --- | --- |
68
128
  | `root` | a new temporary directory | Session root directory. |
69
129
  | `port` | a free port from the operating system | Bridge port. |
70
130
  | `keepRoot` | `false` | Keep the session root after `destroy`. |
131
+ | `env` | none | More environment variables for every process. |
132
+
133
+ The adapter prints this warning on every local run:
134
+ "credential brokering does not work. Falling back to less secure credential forwarding."
135
+ It is expected. The local sandbox cannot rewrite requests, so the adapter
136
+ gives the raw API key to the processes it starts. If the Agent must not see the key,
137
+ use a hosted sandbox provider that supports request transformations.
71
138
 
72
139
  The local sandbox needs POSIX `sh` at `/bin/sh` and GNU `find`, because the
73
140
  Harness inventories output with `find -printf`. It runs on Linux. It does not
@@ -80,10 +147,14 @@ policy, which stops at 64 files. Raise it on `createSkillHarness`:
80
147
  createSkillHarness({ harness, sandbox, outputPolicy: { maxOutputFiles: 512 } })
81
148
  ```
82
149
 
83
- **It applies no isolation.** Every process reaches the whole computer and the
84
- caller's environment. Use it for your own Skills on your own computer or on a
85
- self-hosted runner. Use a hosted sandbox provider when the Harness must contain
86
- what it runs.
150
+ **It applies no process isolation.** A separate `HOME` keeps state apart. It does
151
+ not stop a process from reading or writing any file your user account can reach.
152
+ Use it for your own Skills on your own computer or on a self-hosted runner. Use a
153
+ hosted sandbox provider when the Harness must contain what it runs.
154
+
155
+ A `LocalPackage` source copies the working tree, including uncommitted and
156
+ untracked files. A symbolic link anywhere in it stops the run, and the error
157
+ names the link. To use only committed files, pass a `git archive` export.
87
158
 
88
159
  ## Visible Skills
89
160
 
@@ -101,3 +172,8 @@ const skill = await loadSkilldMaintainedSkill('generate-project-skill')
101
172
 
102
173
  The same Skill files remain usable directly through an Agent.
103
174
  Direct runs remain user reviewed.
175
+
176
+ ## Develop against the local package
177
+
178
+ `package.json` exports `dist` files. Run `pnpm build` in `packages/harness`
179
+ before you link the package into another project. A stale `dist` fails on import.
@@ -1,4 +1,4 @@
1
- import { readFile } from "node:fs/promises";
1
+ import { readFile, readdir } from "node:fs/promises";
2
2
  import { dirname, isAbsolute, posix, relative, resolve, sep } from "node:path";
3
3
  import { parseDocument } from "yaml";
4
4
  import { fileURLToPath } from "node:url";
@@ -29,16 +29,27 @@ function parseManifest(source) {
29
29
  if (new Set(value).size !== value.length) throw new Error("skilld-maintained Skill manifest contains duplicate names.");
30
30
  return Object.freeze([...value]);
31
31
  }
32
- async function locateFile(path) {
33
- for (const root of skillRoots) {
34
- const value = await readFile(resolve(root, path), "utf8").catch((error) => {
32
+ async function locateFile(path, roots) {
33
+ for (const root of roots) {
34
+ const content = await readFile(resolve(root, path), "utf8").catch((error) => {
35
35
  if (error.code === "ENOENT") return null;
36
36
  throw error;
37
37
  });
38
- if (value !== null) return value;
38
+ if (content !== null) return {
39
+ root,
40
+ content
41
+ };
39
42
  }
40
43
  throw new Error(`skilld-maintained Skill file is missing: ${path}`);
41
44
  }
45
+ async function listFiles(dir, prefix = "") {
46
+ const entries = await readdir(resolve(dir, prefix), { withFileTypes: true });
47
+ return (await Promise.all(entries.map(async (entry) => {
48
+ const path = prefix ? `${prefix}/${entry.name}` : entry.name;
49
+ if (entry.isDirectory()) return listFiles(dir, path);
50
+ return entry.isFile() ? [path] : [];
51
+ }))).flat().sort();
52
+ }
42
53
  function splitSkill(source) {
43
54
  const match = source.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
44
55
  if (!match) throw new Error("skilld-maintained Skill frontmatter is invalid.");
@@ -54,24 +65,30 @@ function splitSkill(source) {
54
65
  content: match[2]
55
66
  };
56
67
  }
57
- async function harnessSkillNames() {
58
- return parseManifest(await locateFile("harness-skills.json"));
68
+ async function harnessSkillNames(roots = skillRoots) {
69
+ const { content } = await locateFile("harness-skills.json", roots);
70
+ return parseManifest(content);
59
71
  }
60
- async function skilldMaintainedSkillNames() {
61
- return parseManifest(await locateFile("skilld-maintained-skills.json"));
72
+ async function skilldMaintainedSkillNames(roots = skillRoots) {
73
+ const { content } = await locateFile("skilld-maintained-skills.json", roots);
74
+ return parseManifest(content);
62
75
  }
63
- async function loadSkilldMaintainedSkill(name) {
64
- if (!(await skilldMaintainedSkillNames()).includes(name)) throw new Error(`Unknown skilld-maintained Skill: ${name}`);
65
- const skill = splitSkill(await locateFile(`${name}/SKILL.md`));
76
+ async function loadSkilldMaintainedSkill(name, roots = skillRoots) {
77
+ if (!(await skilldMaintainedSkillNames(roots)).includes(name)) throw new Error(`Unknown skilld-maintained Skill: ${name}`);
78
+ const { root, content: source } = await locateFile(`${name}/SKILL.md`, roots);
79
+ const skill = splitSkill(source);
66
80
  if (skill.name !== name) throw new Error(`skilld-maintained Skill name does not match its directory: ${name}`);
67
- if (!(await harnessSkillNames()).includes(name)) return skill;
68
- const request = await locateFile(`${name}/assets/harness-request.md`);
81
+ if (!(await harnessSkillNames(roots)).includes(name)) return skill;
82
+ const skillDir = resolve(root, name);
83
+ const paths = (await listFiles(skillDir)).filter((path) => path !== "SKILL.md");
84
+ if (!paths.includes("assets/harness-request.md")) throw new Error(`skilld-maintained Skill file is missing: ${name}/assets/harness-request.md`);
85
+ const files = await Promise.all(paths.map(async (path) => ({
86
+ path,
87
+ content: await readFile(resolve(skillDir, path), "utf8")
88
+ })));
69
89
  return {
70
90
  ...skill,
71
- files: [{
72
- path: "assets/harness-request.md",
73
- content: request
74
- }]
91
+ files
75
92
  };
76
93
  }
77
94
  const DEFAULT_OUTPUT_POLICY = Object.freeze({
@@ -1 +1 @@
1
- {"version":3,"file":"skills.mjs","names":[],"sources":["../../src/internal/paths.ts","../../src/skills.ts"],"sourcesContent":["import { isAbsolute, posix, relative, resolve, sep } from 'node:path'\n\nexport function isSkillName(value: string): boolean {\n return value.length >= 1\n && value.length <= 64\n && /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value)\n}\n\nexport function resolveWithin(root: string, candidate: string): string | null {\n const absoluteRoot = resolve(root)\n const absoluteCandidate = resolve(root, candidate)\n const fromRoot = relative(absoluteRoot, absoluteCandidate)\n if (fromRoot === '' || (!fromRoot.startsWith(`..${sep}`) && fromRoot !== '..' && !isAbsolute(fromRoot)))\n return absoluteCandidate\n return null\n}\n\nexport function normalizeOutputPath(value: string): string | null {\n if (value.length === 0 || value.includes('\\\\') || value.includes('\\0') || posix.isAbsolute(value))\n return null\n const normalized = posix.normalize(value)\n if (normalized === '.' || normalized === '..' || normalized.startsWith('../') || normalized !== value)\n return null\n return normalized\n}\n","import type { HarnessV1Skill } from '@ai-sdk/harness'\nimport { readFile } from 'node:fs/promises'\nimport { dirname, resolve } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { parseDocument } from 'yaml'\nimport { isSkillName } from './internal/paths.ts'\n\nconst skillRoots = [\n resolve(dirname(fileURLToPath(import.meta.url)), '../skills'),\n resolve(dirname(fileURLToPath(import.meta.url)), 'skills'),\n resolve(dirname(fileURLToPath(import.meta.url)), '../../../skills'),\n] as const\n\nfunction parseManifest(source: string): ReadonlyArray<string> {\n const value = JSON.parse(source) as unknown\n if (!Array.isArray(value) || value.some(name => typeof name !== 'string' || !isSkillName(name)))\n throw new Error('skilld-maintained Skill manifest is invalid.')\n if (new Set(value).size !== value.length)\n throw new Error('skilld-maintained Skill manifest contains duplicate names.')\n return Object.freeze([...value])\n}\n\nasync function locateFile(path: string): Promise<string> {\n for (const root of skillRoots) {\n const value = await readFile(resolve(root, path), 'utf8').catch((error) => {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT')\n return null\n throw error\n })\n if (value !== null)\n return value\n }\n throw new Error(`skilld-maintained Skill file is missing: ${path}`)\n}\n\nfunction splitSkill(source: string): { name: string, description: string, content: string } {\n const match = source.match(/^---\\r?\\n([\\s\\S]*?)\\r?\\n---\\r?\\n([\\s\\S]*)$/)\n if (!match)\n throw new Error('skilld-maintained Skill frontmatter is invalid.')\n\n const document = parseDocument(match[1]!, { uniqueKeys: true })\n if (document.errors.length > 0)\n throw new Error('skilld-maintained Skill frontmatter is invalid.')\n const frontmatter = document.toJS() as unknown\n if (!frontmatter || typeof frontmatter !== 'object')\n throw new Error('skilld-maintained Skill frontmatter is invalid.')\n\n const values = frontmatter as Record<string, unknown>\n if (typeof values.name !== 'string' || typeof values.description !== 'string')\n throw new Error('skilld-maintained Skill frontmatter is incomplete.')\n\n return { name: values.name, description: values.description, content: match[2]! }\n}\n\nexport async function harnessSkillNames(): Promise<ReadonlyArray<string>> {\n const source = await locateFile('harness-skills.json')\n return parseManifest(source)\n}\n\nexport async function skilldMaintainedSkillNames(): Promise<ReadonlyArray<string>> {\n const source = await locateFile('skilld-maintained-skills.json')\n return parseManifest(source)\n}\n\nexport async function loadSkilldMaintainedSkill(name: string): Promise<HarnessV1Skill> {\n const names = await skilldMaintainedSkillNames()\n if (!names.includes(name))\n throw new Error(`Unknown skilld-maintained Skill: ${name}`)\n\n const source = await locateFile(`${name}/SKILL.md`)\n const skill = splitSkill(source)\n if (skill.name !== name)\n throw new Error(`skilld-maintained Skill name does not match its directory: ${name}`)\n\n const harnessSkills = await harnessSkillNames()\n if (!harnessSkills.includes(name))\n return skill\n\n const request = await locateFile(`${name}/assets/harness-request.md`)\n return {\n ...skill,\n files: [{ path: 'assets/harness-request.md', content: request }],\n }\n}\n\nexport const DEFAULT_OUTPUT_POLICY = Object.freeze({\n maxSourceFiles: 2_000,\n maxSourceFileBytes: 512 * 1024,\n maxSourceBytes: 50 * 1024 * 1024,\n maxOutputFiles: 64,\n maxOutputFileBytes: 512 * 1024,\n maxOutputBytes: 4 * 1024 * 1024,\n})\n"],"mappings":"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"}
1
+ {"version":3,"file":"skills.mjs","names":[],"sources":["../../src/internal/paths.ts","../../src/skills.ts"],"sourcesContent":["import { isAbsolute, posix, relative, resolve, sep } from 'node:path'\n\nexport function isSkillName(value: string): boolean {\n return value.length >= 1\n && value.length <= 64\n && /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value)\n}\n\nexport function resolveWithin(root: string, candidate: string): string | null {\n const absoluteRoot = resolve(root)\n const absoluteCandidate = resolve(root, candidate)\n const fromRoot = relative(absoluteRoot, absoluteCandidate)\n if (fromRoot === '' || (!fromRoot.startsWith(`..${sep}`) && fromRoot !== '..' && !isAbsolute(fromRoot)))\n return absoluteCandidate\n return null\n}\n\nexport function normalizeOutputPath(value: string): string | null {\n if (value.length === 0 || value.includes('\\\\') || value.includes('\\0') || posix.isAbsolute(value))\n return null\n const normalized = posix.normalize(value)\n if (normalized === '.' || normalized === '..' || normalized.startsWith('../') || normalized !== value)\n return null\n return normalized\n}\n","import type { HarnessV1Skill } from '@ai-sdk/harness'\nimport { readdir, readFile } from 'node:fs/promises'\nimport { dirname, resolve } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { parseDocument } from 'yaml'\nimport { isSkillName } from './internal/paths.ts'\n\nconst skillRoots = [\n resolve(dirname(fileURLToPath(import.meta.url)), '../skills'),\n resolve(dirname(fileURLToPath(import.meta.url)), 'skills'),\n resolve(dirname(fileURLToPath(import.meta.url)), '../../../skills'),\n] as const\n\nfunction parseManifest(source: string): ReadonlyArray<string> {\n const value = JSON.parse(source) as unknown\n if (!Array.isArray(value) || value.some(name => typeof name !== 'string' || !isSkillName(name)))\n throw new Error('skilld-maintained Skill manifest is invalid.')\n if (new Set(value).size !== value.length)\n throw new Error('skilld-maintained Skill manifest contains duplicate names.')\n return Object.freeze([...value])\n}\n\n/** Reads the first root that has `path`, and returns that root with the file. */\nasync function locateFile(path: string, roots: ReadonlyArray<string>): Promise<{ root: string, content: string }> {\n for (const root of roots) {\n const content = await readFile(resolve(root, path), 'utf8').catch((error) => {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT')\n return null\n throw error\n })\n if (content !== null)\n return { root, content }\n }\n throw new Error(`skilld-maintained Skill file is missing: ${path}`)\n}\n\n/** Lists every regular file under `dir`, nested folders included, as sorted POSIX paths. */\nasync function listFiles(dir: string, prefix = ''): Promise<ReadonlyArray<string>> {\n const entries = await readdir(resolve(dir, prefix), { withFileTypes: true })\n const nested = await Promise.all(entries.map(async (entry) => {\n const path = prefix ? `${prefix}/${entry.name}` : entry.name\n if (entry.isDirectory())\n return listFiles(dir, path)\n return entry.isFile() ? [path] : []\n }))\n return nested.flat().sort()\n}\n\nfunction splitSkill(source: string): { name: string, description: string, content: string } {\n const match = source.match(/^---\\r?\\n([\\s\\S]*?)\\r?\\n---\\r?\\n([\\s\\S]*)$/)\n if (!match)\n throw new Error('skilld-maintained Skill frontmatter is invalid.')\n\n const document = parseDocument(match[1]!, { uniqueKeys: true })\n if (document.errors.length > 0)\n throw new Error('skilld-maintained Skill frontmatter is invalid.')\n const frontmatter = document.toJS() as unknown\n if (!frontmatter || typeof frontmatter !== 'object')\n throw new Error('skilld-maintained Skill frontmatter is invalid.')\n\n const values = frontmatter as Record<string, unknown>\n if (typeof values.name !== 'string' || typeof values.description !== 'string')\n throw new Error('skilld-maintained Skill frontmatter is incomplete.')\n\n return { name: values.name, description: values.description, content: match[2]! }\n}\n\nexport async function harnessSkillNames(roots: ReadonlyArray<string> = skillRoots): Promise<ReadonlyArray<string>> {\n const { content } = await locateFile('harness-skills.json', roots)\n return parseManifest(content)\n}\n\nexport async function skilldMaintainedSkillNames(roots: ReadonlyArray<string> = skillRoots): Promise<ReadonlyArray<string>> {\n const { content } = await locateFile('skilld-maintained-skills.json', roots)\n return parseManifest(content)\n}\n\n/**\n * Loads one skilld-maintained Skill. A Harness Skill also carries every supporting file\n * in its folder, nested `scripts/` and `references/` folders included, so the Agent can read them.\n * `roots` lists the folders to search, first match wins.\n */\nexport async function loadSkilldMaintainedSkill(name: string, roots: ReadonlyArray<string> = skillRoots): Promise<HarnessV1Skill> {\n const names = await skilldMaintainedSkillNames(roots)\n if (!names.includes(name))\n throw new Error(`Unknown skilld-maintained Skill: ${name}`)\n\n const { root, content: source } = await locateFile(`${name}/SKILL.md`, roots)\n const skill = splitSkill(source)\n if (skill.name !== name)\n throw new Error(`skilld-maintained Skill name does not match its directory: ${name}`)\n\n const harnessSkills = await harnessSkillNames(roots)\n if (!harnessSkills.includes(name))\n return skill\n\n const skillDir = resolve(root, name)\n const paths = (await listFiles(skillDir)).filter(path => path !== 'SKILL.md')\n if (!paths.includes('assets/harness-request.md'))\n throw new Error(`skilld-maintained Skill file is missing: ${name}/assets/harness-request.md`)\n const files = await Promise.all(paths.map(async path => ({\n path,\n content: await readFile(resolve(skillDir, path), 'utf8'),\n })))\n return { ...skill, files }\n}\n\nexport const DEFAULT_OUTPUT_POLICY = Object.freeze({\n maxSourceFiles: 2_000,\n maxSourceFileBytes: 512 * 1024,\n maxSourceBytes: 50 * 1024 * 1024,\n maxOutputFiles: 64,\n maxOutputFileBytes: 512 * 1024,\n maxOutputBytes: 4 * 1024 * 1024,\n})\n"],"mappings":"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"}
package/dist/index.d.mts CHANGED
@@ -32,8 +32,37 @@ interface SkillOutputPolicy {
32
32
  readonly maxOutputFileBytes: number;
33
33
  readonly maxOutputBytes: number;
34
34
  }
35
+ /** Token counts the Agent reported. A count is undefined when the Harness adapter does not report it. */
36
+ interface SkillRunUsage {
37
+ /** Every input token, cached ones included. */
38
+ readonly inputTokens: number | undefined;
39
+ /** Input tokens read from the provider cache. */
40
+ readonly cachedInputTokens: number | undefined;
41
+ readonly outputTokens: number | undefined;
42
+ }
43
+ /** Progress of the Agent during a Skill run. A step is one model call. Steps count from 0. */
44
+ type SkillRunEvent = {
45
+ readonly _tag: 'StepStart';
46
+ readonly step: number;
47
+ } | {
48
+ readonly _tag: 'ToolCall';
49
+ readonly step: number;
50
+ readonly toolName: string;
51
+ readonly toolCallId: string;
52
+ readonly input: unknown;
53
+ } | {
54
+ readonly _tag: 'StepFinish';
55
+ readonly step: number;
56
+ readonly finishReason: string;
57
+ readonly usage: SkillRunUsage;
58
+ };
35
59
  interface SkillRunOptions {
36
60
  readonly signal?: AbortSignal;
61
+ /**
62
+ * Receives progress events while the Agent works. The run does not wait for a returned promise or thenable.
63
+ * If it throws or its promise rejects, the run continues and the result report carries a warning.
64
+ */
65
+ readonly onEvent?: (event: SkillRunEvent) => void;
37
66
  }
38
67
  interface SkillFile {
39
68
  readonly path: string;
@@ -50,8 +79,6 @@ interface GeneratedSkill {
50
79
  readonly outputDir: string;
51
80
  readonly files: ReadonlyArray<SkillFile>;
52
81
  readonly sourceAttempts: ReadonlyArray<SourceAttempt>;
53
- /** Cleanup problems after the new Skill reached its destination. */
54
- readonly warnings: ReadonlyArray<string>;
55
82
  }
56
83
  interface SkillReviewFinding {
57
84
  readonly level: 'error' | 'warning' | 'note';
@@ -97,15 +124,38 @@ type SkillRunError = {
97
124
  readonly _tag: 'Cancelled';
98
125
  readonly message: string;
99
126
  };
100
- type SkillRunResult = {
127
+ /** The value each Skill run input returns. */
128
+ interface SkillRunValues {
129
+ readonly PackageSkill: GeneratedSkill;
130
+ readonly ProjectSkill: GeneratedSkill;
131
+ readonly ReviewSkill: SkillReview;
132
+ }
133
+ /**
134
+ * What a Skill run cost, and what went wrong beside its outcome.
135
+ * Every result carries one, so a failed run still reports its cost.
136
+ */
137
+ interface SkillRunReport {
138
+ /** Tokens the Agent used. A run that failed before the Agent started reports no counts. */
139
+ readonly usage: SkillRunUsage;
140
+ /** Model calls the Agent made. */
141
+ readonly steps: number;
142
+ /**
143
+ * Source files the Harness left out for size, onEvent failures, and cleanup
144
+ * problems after the run.
145
+ */
146
+ readonly warnings: ReadonlyArray<string>;
147
+ }
148
+ type SkillRunResult<Tag extends SkillRun['_tag'] = SkillRun['_tag']> = {
101
149
  readonly _tag: 'Ok';
102
- readonly value: GeneratedSkill | SkillReview;
150
+ readonly value: SkillRunValues[Tag];
151
+ readonly report: SkillRunReport;
103
152
  } | {
104
153
  readonly _tag: 'Err';
105
154
  readonly error: SkillRunError;
155
+ readonly report: SkillRunReport;
106
156
  };
107
157
  interface SkillHarness {
108
- readonly run: (input: SkillRun, options?: SkillRunOptions) => Promise<SkillRunResult>;
158
+ readonly run: <Run extends SkillRun>(input: Run, options?: SkillRunOptions) => Promise<SkillRunResult<Run['_tag']>>;
109
159
  }
110
160
  interface CreateSkillHarnessOptions {
111
161
  readonly harness: HarnessV1;
@@ -118,5 +168,5 @@ interface CreateSkillHarnessOptions {
118
168
  readonly fetch?: (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
119
169
  }
120
170
  export declare function createSkillHarness(options: CreateSkillHarnessOptions): SkillHarness;
121
- export type { CreateSkillHarnessOptions, GeneratedSkill, PackageSource, SkillDestination, SkillFile, SkillHarness, SkillOutputPolicy, SkillReview, SkillReviewFinding, SkillRun, SkillRunError, SkillRunOptions, SkillRunResult, SourceAttempt };
171
+ export type { CreateSkillHarnessOptions, GeneratedSkill, PackageSource, SkillDestination, SkillFile, SkillHarness, SkillOutputPolicy, SkillReview, SkillReviewFinding, SkillRun, SkillRunError, SkillRunEvent, SkillRunOptions, SkillRunReport, SkillRunResult, SkillRunUsage, SkillRunValues, SourceAttempt };
122
172
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/harness.ts"],"mappings":";;UAGiB;WACN;WACA;;KAGC;WAEC;WACA;;WAGA;WACA;WACA;;KAGD;WAEC;WACA,QAAQ;WACR,aAAa;;WAGb;WACA;WACA,aAAa;;WAGb;WACA;;UAGI;WACN;WACA;WACA;WACA;WACA;WACA;;UAGM;WACN,SAAS;;UAGH;WACN;WACA;;UAGM;WACN;WACA;WACA;;UAGM;WACN;WACA;WACA;WACA,OAAO,cAAc;WACrB,gBAAgB,cAAc;;WAE9B,UAAU;;UAGJ;WACN;WACA;WACA;WACA;;UAGM;WACN;WACA;WACA,UAAU,cAAc;;KAGvB;WACK;WAA+B;;WAEjC;WACA;WACA,UAAU,cAAc;WACxB;;WAEE;WAA8B;WAA0B;;WACxD;WAA+B;WAA0B,QAAQ;;WACjE;WAAmC;WAA0B;;WAC7D;WAA6B;WAA0B;;WACvD;WAAkC;WAA0B;WAAuB;;WACnF;WAA4B;;KAEjC;WACK;WAAqB,OAAO,iBAAiB;;WAC7C;WAAsB,OAAO;;UAE7B;WACN,MAAM,OAAO,UAAU,UAAU,oBAAoB,QAAQ;;UAGvD;WACN,SAAS;;WAET,SAAS;;WAET,gBAAgB;WAChB,eAAe,QAAQ;;WAEvB,SAAS,gBAAgB,MAAM,SAAS,OAAO,gBAAgB,QAAQ;;wBCkElE,mBAAmB,SAAS,4BAA4B"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/harness.ts"],"mappings":";;UAGiB;WACN;WACA;;KAGC;WAEC;WACA;;WAGA;WACA;WACA;;KAGD;WAEC;WACA,QAAQ;WACR,aAAa;;WAGb;WACA;WACA,aAAa;;WAGb;WACA;;UAGI;WACN;WACA;WACA;WACA;WACA;WACA;;;UAIM;;WAEN;;WAEA;WACA;;;KAIC;WACK;WAA4B;;WAE9B;WACA;WACA;WACA;WACA;;WAGA;WACA;WACA;WACA,OAAO;;UAGL;WACN,SAAS;;;;;WAKT,WAAW,OAAO;;UAGZ;WACN;WACA;;UAGM;WACN;WACA;WACA;;UAGM;WACN;WACA;WACA;WACA,OAAO,cAAc;WACrB,gBAAgB,cAAc;;UAGxB;WACN;WACA;WACA;WACA;;UAGM;WACN;WACA;WACA,UAAU,cAAc;;KAGvB;WACK;WAA+B;;WAEjC;WACA;WACA,UAAU,cAAc;WACxB;;WAEE;WAA8B;WAA0B;;WACxD;WAA+B;WAA0B,QAAQ;;WACjE;WAAmC;WAA0B;;WAC7D;WAA6B;WAA0B;;WACvD;WAAkC;WAA0B;WAAuB;;WACnF;WAA4B;;;UAG5B;WACN,cAAc;WACd,cAAc;WACd,aAAa;;;;;;UAOP;;WAEN,OAAO;;WAEP;;;;;WAKA,UAAU;;KAGT,eAAe,YAAY,mBAAmB;WACzC;WAAqB,OAAO,eAAe;WAAe,QAAQ;;WAClE;WAAsB,OAAO;WAAwB,QAAQ;;UAE7D;WACN,MAAM,YAAY,UAAU,OAAO,KAAK,UAAU,oBAAoB,QAAQ,eAAe;;UAGvF;WACN,SAAS;;WAET,SAAS;;WAET,gBAAgB;WAChB,eAAe,QAAQ;;WAEvB,SAAS,gBAAgB,MAAM,SAAS,OAAO,gBAAgB,QAAQ;;wBC0GlE,mBAAmB,SAAS,4BAA4B"}