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/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { DEFAULT_OUTPUT_POLICY, isSkillName, loadSkilldMaintainedSkill, normalizeOutputPath, resolveWithin } from "./_chunks/skills.mjs";
2
2
  import { lstat, mkdir, open, opendir, realpath, rename, rm, rmdir, unlink, writeFile } from "node:fs/promises";
3
- import { dirname, join, posix, relative, resolve, sep } from "node:path";
3
+ import { basename, dirname, join, posix, relative, resolve, sep } from "node:path";
4
4
  import { HarnessAgent } from "@ai-sdk/harness/agent";
5
5
  import { createHash, randomUUID } from "node:crypto";
6
6
  import { parseDocument } from "yaml";
@@ -275,14 +275,16 @@ const promoteSkill = async (rootDir, name, files, attempts) => {
275
275
  if (backupCleanup instanceof Error) warnings.push(`Previous Skill cleanup failed at ${backup}: ${backupCleanup.message}`);
276
276
  }
277
277
  return ok({
278
- _tag: "GeneratedSkill",
279
- name,
280
- outputDir: target,
281
- files: files.map((file) => ({
282
- path: file.path,
283
- bytes: file.content.byteLength
284
- })),
285
- sourceAttempts: attempts,
278
+ skill: {
279
+ _tag: "GeneratedSkill",
280
+ name,
281
+ outputDir: target,
282
+ files: files.map((file) => ({
283
+ path: file.path,
284
+ bytes: file.content.byteLength
285
+ })),
286
+ sourceAttempts: attempts
287
+ },
286
288
  warnings
287
289
  });
288
290
  }
@@ -357,14 +359,9 @@ function checkProjectSkill(input) {
357
359
  if (!searchCommand.test(commands)) issues.push("SKILL.md must give the Agent at least one search command it can repeat.");
358
360
  return issues;
359
361
  }
360
- const allowedFrontmatter = /* @__PURE__ */ new Set([
361
- "name",
362
- "description",
363
- "license",
364
- "compatibility",
365
- "metadata",
366
- "allowed-tools"
367
- ]);
362
+ const allowedFrontmatter = /* @__PURE__ */ new Set(["name", "description"]);
363
+ const packageSkillLineLimit = 500;
364
+ const packageSkillReferenceLimit = 8;
368
365
  const invalid = (issues) => err({
369
366
  _tag: "InvalidSkill",
370
367
  message: "Skill output failed deterministic checks.",
@@ -377,16 +374,42 @@ function decodeText(content) {
377
374
  return null;
378
375
  }
379
376
  }
380
- const isStringMap = (value) => value !== null && typeof value === "object" && !Array.isArray(value) && Object.values(value).every((item) => typeof item === "string");
381
- const validateGeneratedSkill = (name, files, project) => {
377
+ function linkTargets(markdown) {
378
+ const targets = /* @__PURE__ */ new Set();
379
+ const add = (raw) => {
380
+ const target = raw.trim().replace(/^<|>$/g, "").split(/\s/)[0].replace(/[#?].*$/, "").replace(/^\.\//, "");
381
+ if (target.length > 0) targets.add(target);
382
+ };
383
+ for (const match of markdown.matchAll(/\]\(([^)\n]+)\)/g)) add(match[1]);
384
+ for (const match of markdown.matchAll(/^\s*\[[^\]\n]+\]:\s*(\S+)/gm)) add(match[1]);
385
+ for (const match of markdown.matchAll(/`([^`\n]+)`/g)) add(match[1]);
386
+ return targets;
387
+ }
388
+ function lineCount(text) {
389
+ const lines = text.split("\n");
390
+ return lines.at(-1) === "" ? lines.length - 1 : lines.length;
391
+ }
392
+ function structureIssues(markdown, files, contract) {
393
+ const issues = [];
394
+ const referencePaths = files.map((file) => file.path).filter((path) => path.startsWith("references/")).sort();
395
+ if (contract._tag === "PackageSkill") {
396
+ const lines = lineCount(markdown);
397
+ if (lines >= packageSkillLineLimit) issues.push(`SKILL.md must stay under ${packageSkillLineLimit} lines; it has ${lines}.`);
398
+ if (referencePaths.length > packageSkillReferenceLimit) issues.push(`A package Skill may write at most ${packageSkillReferenceLimit} reference files; it wrote ${referencePaths.length}.`);
399
+ }
400
+ const linked = linkTargets(markdown);
401
+ for (const path of referencePaths) if (!linked.has(path)) issues.push(`SKILL.md must link ${path}.`);
402
+ return issues;
403
+ }
404
+ const validateGeneratedSkill = (name, files, contract) => {
382
405
  const issues = [];
383
406
  const skillFiles = files.filter((file) => file.path === "SKILL.md");
384
407
  if (skillFiles.length !== 1) return invalid(["Output must contain exactly one SKILL.md file."]);
385
408
  const source = decodeText(skillFiles[0].content);
386
409
  if (source === null) return invalid(["SKILL.md must contain valid UTF-8 text."]);
387
- if (project !== void 0) issues.push(...checkProjectSkill({
410
+ if (contract._tag === "ProjectSkill") issues.push(...checkProjectSkill({
388
411
  markdown: source,
389
- projectPaths: project,
412
+ projectPaths: contract.projectPaths,
390
413
  outputPaths: files.map((file) => file.path)
391
414
  }));
392
415
  const match = source.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
@@ -405,10 +428,7 @@ const validateGeneratedSkill = (name, files, project) => {
405
428
  if (frontmatter.name !== name) issues.push("Frontmatter name must match the Skill directory name.");
406
429
  if (typeof frontmatter.name !== "string" || !isSkillName(frontmatter.name)) issues.push("Frontmatter name is invalid.");
407
430
  if (typeof frontmatter.description !== "string" || frontmatter.description.trim().length === 0 || frontmatter.description.length > 1024) issues.push("Frontmatter description must contain 1 to 1024 characters.");
408
- if (frontmatter.license !== void 0 && typeof frontmatter.license !== "string") issues.push("Frontmatter license must be a string.");
409
- if (frontmatter.compatibility !== void 0 && (typeof frontmatter.compatibility !== "string" || frontmatter.compatibility.length > 500)) issues.push("Frontmatter compatibility must be a string of at most 500 characters.");
410
- if (frontmatter.metadata !== void 0 && !isStringMap(frontmatter.metadata)) issues.push("Frontmatter metadata must map strings to strings.");
411
- if (frontmatter["allowed-tools"] !== void 0 && typeof frontmatter["allowed-tools"] !== "string") issues.push("Frontmatter allowed-tools must be a string.");
431
+ issues.push(...structureIssues(source, files, contract));
412
432
  return issues.length === 0 ? ok(void 0) : invalid(issues);
413
433
  };
414
434
  const findingLevels = /* @__PURE__ */ new Set([
@@ -452,6 +472,9 @@ const validateSkillReview = (files) => {
452
472
  findings
453
473
  });
454
474
  };
475
+ function skippedFileWarnings(skipped, policy) {
476
+ return skipped.map((file) => `Source file ${file.path} was left out: ${file.bytes} bytes exceeds the ${policy.maxSourceFileBytes} byte file limit.`);
477
+ }
455
478
  const ignoredNames = /* @__PURE__ */ new Set([
456
479
  ".git",
457
480
  ".hg",
@@ -489,7 +512,10 @@ async function collectHostDirectory(directory, policy, sourceLabel = directory,
489
512
  if (canonicalRoot instanceof Error) return unavailable("Source directory cannot be resolved.", canonicalRoot);
490
513
  if (canonicalRoot !== root) return unavailable("Source path must not pass through a symbolic link.");
491
514
  const files = [];
515
+ const skippedFiles = [];
492
516
  let totalBytes = 0;
517
+ const relativePath = (absolute) => relative(root, absolute).split(sep).join("/");
518
+ const entryUnavailable = (absolute, message, cause, fix) => unavailable(`${message}: ${relativePath(absolute)}.${fix === void 0 ? "" : ` ${fix}`}`, cause);
493
519
  const walk = async (current) => {
494
520
  if (signal?.aborted) return cancelled();
495
521
  const canonicalCurrent = await realpath(current).catch((error) => error);
@@ -501,28 +527,32 @@ async function collectHostDirectory(directory, policy, sourceLabel = directory,
501
527
  if (ignoredNames.has(entry.name)) continue;
502
528
  const absolute = join(current, entry.name);
503
529
  const stat = await lstat(absolute).catch((error) => error);
504
- if (stat instanceof Error) return unavailable("Source entry cannot be read.", stat);
505
- if (stat.isSymbolicLink()) return unavailable("Source contains a symbolic link.");
530
+ if (stat instanceof Error) return entryUnavailable(absolute, "Source entry cannot be read", stat);
531
+ if (stat.isSymbolicLink()) return entryUnavailable(absolute, "Source contains a symbolic link", void 0, "Remove the link, or pass a clean export such as a `git archive` of the directory.");
506
532
  if (stat.isDirectory()) {
507
533
  const nested = await walk(absolute);
508
534
  if (nested._tag === "Err") return nested;
509
535
  continue;
510
536
  }
511
- if (!stat.isFile()) return unavailable("Source contains a special file.");
512
- const path = relative(root, absolute).split(sep).join("/");
537
+ if (!stat.isFile()) return entryUnavailable(absolute, "Source contains a special file");
538
+ const path = relativePath(absolute);
513
539
  const handle = await open(absolute, constants.O_RDONLY | constants.O_NOFOLLOW).catch((error) => error);
514
- if (handle instanceof Error) return unavailable("Source file cannot be opened without following links.", handle);
540
+ if (handle instanceof Error) return entryUnavailable(absolute, "Source file cannot be opened without following links", handle);
515
541
  const openedStat = await handle.stat().catch((error) => error);
516
542
  if (openedStat instanceof Error) {
517
543
  await handle.close();
518
- return unavailable("Source file cannot be inspected.", openedStat);
544
+ return entryUnavailable(absolute, "Source file cannot be inspected", openedStat);
519
545
  }
520
546
  if (!openedStat.isFile() || openedStat.dev !== stat.dev || openedStat.ino !== stat.ino) {
521
547
  await handle.close();
522
- return unavailable("Source file changed during collection.");
548
+ return entryUnavailable(absolute, "Source file changed during collection");
523
549
  }
524
550
  if (openedStat.size > policy.maxSourceFileBytes) {
525
551
  await handle.close();
552
+ skippedFiles.push({
553
+ path,
554
+ bytes: openedStat.size
555
+ });
526
556
  continue;
527
557
  }
528
558
  if (files.length >= policy.maxSourceFiles) {
@@ -535,8 +565,8 @@ async function collectHostDirectory(directory, policy, sourceLabel = directory,
535
565
  }
536
566
  const content = await handle.readFile().catch((error) => error);
537
567
  await handle.close();
538
- if (content instanceof Error) return unavailable("Source file cannot be read.", content);
539
- if (content.byteLength !== openedStat.size) return unavailable("Source file changed during collection.");
568
+ if (content instanceof Error) return entryUnavailable(absolute, "Source file cannot be read", content);
569
+ if (content.byteLength !== openedStat.size) return entryUnavailable(absolute, "Source file changed during collection");
540
570
  if (signal?.aborted) return cancelled();
541
571
  files.push({
542
572
  path,
@@ -550,8 +580,10 @@ async function collectHostDirectory(directory, policy, sourceLabel = directory,
550
580
  if (walked._tag === "Err") return walked;
551
581
  if (files.length === 0) return unavailable("Source directory has no usable files.");
552
582
  files.sort((left, right) => left.path.localeCompare(right.path));
583
+ skippedFiles.sort((left, right) => left.path.localeCompare(right.path));
553
584
  return ok({
554
585
  files,
586
+ skippedFiles,
555
587
  attempts: [{
556
588
  source: sourceLabel,
557
589
  status: "used"
@@ -619,6 +651,7 @@ async function extractArchive(compressed, policy, layout) {
619
651
  source.on("error", (error) => gunzip.destroy(error));
620
652
  source.pipe(gunzip);
621
653
  const files = [];
654
+ const skippedFiles = [];
622
655
  const seen = /* @__PURE__ */ new Set();
623
656
  const maxArchiveBytes = policy.maxSourceBytes + (policy.maxSourceFiles + 2) * blockBytes * 2;
624
657
  let archiveBytes = 0;
@@ -706,7 +739,7 @@ async function extractArchive(compressed, policy, layout) {
706
739
  };
707
740
  continue;
708
741
  }
709
- if (type !== "0" && type !== "\0") return unavailable$1("Source archive contains a linked or special entry.");
742
+ if (type !== "0" && type !== "\0") return unavailable$1(`Source archive contains a linked or special entry: ${nextPath ?? headerPath}.`);
710
743
  const path = archivePath(nextPath ?? headerPath, layout);
711
744
  nextPath = null;
712
745
  if (path === null) return unavailable$1("Source archive contains an invalid path.");
@@ -715,6 +748,10 @@ async function extractArchive(compressed, policy, layout) {
715
748
  fileEntries += 1;
716
749
  if (fileEntries > policy.maxSourceFiles) return unavailable$1("Source archive contains too many files.");
717
750
  if (size <= policy.maxSourceFileBytes && totalBytes + size > policy.maxSourceBytes) return unavailable$1("Source archive exceeds the total byte limit.");
751
+ if (size > policy.maxSourceFileBytes) skippedFiles.push({
752
+ path,
753
+ bytes: size
754
+ });
718
755
  entry = {
719
756
  kind: size > policy.maxSourceFileBytes ? "skip" : "file",
720
757
  path,
@@ -732,7 +769,11 @@ async function extractArchive(compressed, policy, layout) {
732
769
  if (entry !== null || pending.some((byte) => byte !== 0) || !ended) return unavailable$1("Source archive ended before its final entry.");
733
770
  if (files.length === 0) return unavailable$1("Source archive has no usable files.");
734
771
  files.sort((left, right) => left.path.localeCompare(right.path));
735
- return ok(files);
772
+ skippedFiles.sort((left, right) => left.path.localeCompare(right.path));
773
+ return ok({
774
+ files,
775
+ skippedFiles
776
+ });
736
777
  }
737
778
  const packageSpecPattern = /^(@[^/@]+\/[^/@]+|[^/@]+)(?:@(\S+))?$/;
738
779
  const exactVersionPattern = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[\da-z-]+(?:\.[\da-z-]+)*)?(?:\+[\da-z-]+(?:\.[\da-z-]+)*)?$/i;
@@ -1010,7 +1051,8 @@ async function prepareNpmPackage(spec, policy, fetchClient, signal) {
1010
1051
  return unavailable(message, [...attempts, skipped(version.dist.tarball, message)]);
1011
1052
  }
1012
1053
  attempts.push(used(version.dist.tarball));
1013
- let files = [...extracted.value];
1054
+ let files = [...extracted.value.files];
1055
+ let skippedFiles = [...extracted.value.skippedFiles];
1014
1056
  const repositoryArchive = githubArchiveUrl(version.repository ?? repository, version.gitHead);
1015
1057
  const available = remainingPolicy(policy, files);
1016
1058
  if (repositoryArchive !== null && available === null) attempts.push(skipped(repositoryArchive, "Prepared package files reached the source limit."));
@@ -1025,7 +1067,11 @@ async function prepareNpmPackage(spec, policy, fetchClient, signal) {
1025
1067
  const repositoryFiles = await extractResponse(fetchedRepository.value, available, "github");
1026
1068
  if (repositoryFiles._tag === "Err") attempts.push(skipped(repositoryArchive, repositoryFiles.error.message));
1027
1069
  else {
1028
- files = [...files, ...repositoryFiles.value.map((file) => ({
1070
+ files = [...files, ...repositoryFiles.value.files.map((file) => ({
1071
+ ...file,
1072
+ path: posix.join("repository", file.path)
1073
+ }))];
1074
+ skippedFiles = [...skippedFiles, ...repositoryFiles.value.skippedFiles.map((file) => ({
1029
1075
  ...file,
1030
1076
  path: posix.join("repository", file.path)
1031
1077
  }))];
@@ -1034,8 +1080,10 @@ async function prepareNpmPackage(spec, policy, fetchClient, signal) {
1034
1080
  }
1035
1081
  }
1036
1082
  files.sort((left, right) => left.path.localeCompare(right.path));
1083
+ skippedFiles.sort((left, right) => left.path.localeCompare(right.path));
1037
1084
  return ok({
1038
1085
  files,
1086
+ skippedFiles,
1039
1087
  attempts,
1040
1088
  npmResolution: {
1041
1089
  package: parsed.name,
@@ -1043,6 +1091,61 @@ async function prepareNpmPackage(spec, policy, fetchClient, signal) {
1043
1091
  }
1044
1092
  });
1045
1093
  }
1094
+ function addCount(total, count) {
1095
+ return total === void 0 ? count : count === void 0 ? total : total + count;
1096
+ }
1097
+ function isPromiseLike(value) {
1098
+ return (typeof value === "object" || typeof value === "function") && value !== null && typeof value.then === "function";
1099
+ }
1100
+ function createRunLog(onEvent) {
1101
+ const warnings = [];
1102
+ const eventFailures = /* @__PURE__ */ new Set();
1103
+ let usage = {
1104
+ inputTokens: void 0,
1105
+ cachedInputTokens: void 0,
1106
+ outputTokens: void 0
1107
+ };
1108
+ let steps = 0;
1109
+ const fail = (cause) => {
1110
+ eventFailures.add(`onEvent failed: ${cause instanceof Error ? cause.message : String(cause)}`);
1111
+ };
1112
+ return {
1113
+ emit: (event) => {
1114
+ if (event._tag === "StepFinish") {
1115
+ steps += 1;
1116
+ usage = {
1117
+ inputTokens: addCount(usage.inputTokens, event.usage.inputTokens),
1118
+ cachedInputTokens: addCount(usage.cachedInputTokens, event.usage.cachedInputTokens),
1119
+ outputTokens: addCount(usage.outputTokens, event.usage.outputTokens)
1120
+ };
1121
+ }
1122
+ if (!onEvent) return;
1123
+ try {
1124
+ const returned = onEvent(event);
1125
+ if (isPromiseLike(returned)) Promise.resolve(returned).catch(fail);
1126
+ } catch (cause) {
1127
+ fail(cause);
1128
+ }
1129
+ },
1130
+ warn: (warning) => warnings.push(warning),
1131
+ settle: (total, count) => {
1132
+ usage = total;
1133
+ steps = count;
1134
+ },
1135
+ report: () => ({
1136
+ usage,
1137
+ steps,
1138
+ warnings: [...warnings, ...eventFailures]
1139
+ })
1140
+ };
1141
+ }
1142
+ function toUsage(usage) {
1143
+ return {
1144
+ inputTokens: usage.inputTokens,
1145
+ cachedInputTokens: usage.inputTokenDetails.cacheReadTokens,
1146
+ outputTokens: usage.outputTokens
1147
+ };
1148
+ }
1046
1149
  function cancelled() {
1047
1150
  return err({
1048
1151
  _tag: "Cancelled",
@@ -1101,6 +1204,7 @@ async function prepareRun(input, policy, fetchClient, signal) {
1101
1204
  skillName: "generate-project-skill",
1102
1205
  outputName: input.destination.name,
1103
1206
  source: source.value,
1207
+ sourceDir: "",
1104
1208
  current: current.value,
1105
1209
  destination: input.destination
1106
1210
  });
@@ -1110,7 +1214,8 @@ async function prepareRun(input, policy, fetchClient, signal) {
1110
1214
  return source._tag === "Err" ? source : ok({
1111
1215
  skillName: "review-skill",
1112
1216
  outputName: "review",
1113
- source: source.value
1217
+ source: source.value,
1218
+ sourceDir: basename(input.skillDir)
1114
1219
  });
1115
1220
  }
1116
1221
  if (input.source._tag === "NpmPackage") {
@@ -1123,6 +1228,7 @@ async function prepareRun(input, policy, fetchClient, signal) {
1123
1228
  skillName: "generate-package-skill",
1124
1229
  outputName: input.destination.name,
1125
1230
  source: source.value,
1231
+ sourceDir: "",
1126
1232
  current: current.value,
1127
1233
  destination: input.destination
1128
1234
  });
@@ -1147,6 +1253,7 @@ async function prepareRun(input, policy, fetchClient, signal) {
1147
1253
  skillName: "generate-package-skill",
1148
1254
  outputName: input.destination.name,
1149
1255
  source: source.value,
1256
+ sourceDir: "",
1150
1257
  current: current.value,
1151
1258
  destination: input.destination
1152
1259
  });
@@ -1159,8 +1266,11 @@ function requestContent(skill) {
1159
1266
  function renderRequest(template, sourcePath, currentSkillPath, outputPath, skillName) {
1160
1267
  return template.replaceAll("{{SOURCE_PATH}}", sourcePath).replaceAll("{{CURRENT_SKILL_PATH}}", currentSkillPath).replaceAll("{{OUTPUT_PATH}}", outputPath).replaceAll("{{SKILL_NAME}}", skillName);
1161
1268
  }
1269
+ function sourcePathOf(active, prepared) {
1270
+ return posix.join(active.workDir, "input/source", prepared.sourceDir);
1271
+ }
1162
1272
  async function writePreparedSource(active, prepared, signal) {
1163
- const sourcePath = posix.join(active.workDir, "input/source");
1273
+ const sourcePath = sourcePathOf(active, prepared);
1164
1274
  const reset = await active.sandbox.run({
1165
1275
  command: "rm -rf -- \"$SKILLD_INPUT\" \"$SKILLD_OUTPUT\" && mkdir -p -- \"$SKILLD_INPUT\"",
1166
1276
  env: {
@@ -1184,6 +1294,7 @@ async function writePreparedSource(active, prepared, signal) {
1184
1294
  path: posix.join(active.workDir, "input/source-manifest.json"),
1185
1295
  content: `${JSON.stringify({
1186
1296
  sourceAttempts: prepared.source.attempts,
1297
+ skippedFiles: prepared.source.skippedFiles,
1187
1298
  npmResolution: prepared.source.npmResolution,
1188
1299
  hasCurrentSkill: prepared.current !== void 0
1189
1300
  }, null, 2)}\n`,
@@ -1206,17 +1317,40 @@ function createSkillHarness(options) {
1206
1317
  const sandboxConfig = { ...options.sandboxConfig };
1207
1318
  const harness = options.harness;
1208
1319
  const sandbox = options.sandbox;
1209
- return { run: async (input, runOptions = {}) => {
1320
+ return { run: (input, runOptions = {}) => runSkill(input, runOptions) };
1321
+ async function runSkill(input, runOptions) {
1322
+ const log = createRunLog(runOptions.onEvent);
1323
+ const outcome = await runLogged(input, runOptions, log);
1324
+ const report = log.report();
1325
+ return outcome._tag === "Ok" ? {
1326
+ _tag: "Ok",
1327
+ value: outcome.value,
1328
+ report
1329
+ } : {
1330
+ _tag: "Err",
1331
+ error: outcome.error,
1332
+ report
1333
+ };
1334
+ }
1335
+ async function runLogged(input, runOptions, log) {
1210
1336
  const parsed = parseSkillRun(input);
1211
1337
  if (parsed._tag === "Err") return parsed;
1212
1338
  const prepared = await prepareRun(parsed.value, policy, fetchClient, runOptions.signal);
1213
1339
  if (prepared._tag === "Err") return prepared;
1340
+ for (const warning of skippedFileWarnings(prepared.value.source.skippedFiles, policy)) log.warn(warning);
1214
1341
  const skill = await loadSkilldMaintainedSkill(prepared.value.skillName);
1342
+ const sandboxSession = await sandbox.createSession({ abortSignal: runOptions.signal }).then(ok, (cause) => err(cause));
1343
+ if (sandboxSession._tag === "Err") return toAgentError(sandboxSession.error, runOptions.signal);
1344
+ const result = await runAgent(prepared.value, skill, sandboxSession.value, log, runOptions.signal);
1345
+ const destroyed = await Promise.resolve(sandboxSession.value.destroy()).then(() => void 0, (cause) => cause);
1346
+ if (destroyed !== void 0) log.warn(`Sandbox session cleanup failed: ${destroyed instanceof Error ? destroyed.message : String(destroyed)}`);
1347
+ return result;
1348
+ }
1349
+ async function runAgent(prepared, skill, sandboxSession, log, signal) {
1215
1350
  let active;
1216
1351
  const userOnSession = sandboxConfig.onSession;
1217
1352
  const agent = new HarnessAgent({
1218
1353
  harness,
1219
- sandbox,
1220
1354
  skills: [skill],
1221
1355
  permissionMode: "allow-all",
1222
1356
  sandboxConfig: {
@@ -1226,44 +1360,79 @@ function createSkillHarness(options) {
1226
1360
  sandbox: sessionOptions.session,
1227
1361
  workDir: sessionOptions.sessionWorkDir
1228
1362
  };
1229
- await writePreparedSource(active, prepared.value, runOptions.signal);
1363
+ await writePreparedSource(active, prepared, signal);
1230
1364
  await userOnSession?.(sessionOptions);
1231
1365
  }
1232
1366
  }
1233
1367
  });
1234
- const sessionResult = await agent.createSession({ abortSignal: runOptions.signal }).then(ok, (cause) => err(cause));
1235
- if (sessionResult._tag === "Err") return toAgentError(sessionResult.error, runOptions.signal);
1368
+ const sessionResult = await agent.createSession({
1369
+ sandboxSession,
1370
+ abortSignal: signal
1371
+ }).then(ok, (cause) => err(cause));
1372
+ if (sessionResult._tag === "Err") return toAgentError(sessionResult.error, signal);
1236
1373
  const session = sessionResult.value;
1237
1374
  try {
1238
1375
  if (!active) return err({
1239
1376
  _tag: "AgentFailed",
1240
1377
  message: "Harness did not provide its sandbox session."
1241
1378
  });
1242
- const sourcePath = posix.join(active.workDir, "input/source");
1379
+ const sourcePath = sourcePathOf(active, prepared);
1243
1380
  const currentSkillPath = posix.join(active.workDir, "input/current-skill");
1244
- const outputPath = posix.join(active.workDir, "skilld-output", prepared.value.outputName);
1245
- const prompt = renderRequest(requestContent(skill), sourcePath, currentSkillPath, outputPath, prepared.value.outputName);
1381
+ const outputPath = posix.join(active.workDir, "skilld-output", prepared.outputName);
1382
+ const prompt = renderRequest(requestContent(skill), sourcePath, currentSkillPath, outputPath, prepared.outputName);
1383
+ let step = 0;
1246
1384
  const generated = await agent.generate({
1247
1385
  session,
1248
1386
  prompt,
1249
- abortSignal: runOptions.signal
1387
+ abortSignal: signal,
1388
+ onStepStart: (event) => {
1389
+ step = event.stepNumber;
1390
+ log.emit({
1391
+ _tag: "StepStart",
1392
+ step
1393
+ });
1394
+ },
1395
+ onToolExecutionStart: ({ toolCall }) => {
1396
+ log.emit({
1397
+ _tag: "ToolCall",
1398
+ step,
1399
+ toolName: toolCall.toolName,
1400
+ toolCallId: toolCall.toolCallId,
1401
+ input: toolCall.input
1402
+ });
1403
+ },
1404
+ onStepEnd: (result) => {
1405
+ log.emit({
1406
+ _tag: "StepFinish",
1407
+ step: result.stepNumber,
1408
+ finishReason: result.finishReason,
1409
+ usage: toUsage(result.usage)
1410
+ });
1411
+ }
1250
1412
  }).then(ok, (cause) => err(cause));
1251
- if (generated._tag === "Err") return toAgentError(generated.error, runOptions.signal);
1252
- if (runOptions.signal?.aborted) return cancelled();
1253
- const collected = await collectSandboxOutput(active.sandbox, outputPath, policy, runOptions.signal);
1413
+ if (generated._tag === "Err") return toAgentError(generated.error, signal);
1414
+ log.settle(toUsage(generated.value.totalUsage), generated.value.steps.length);
1415
+ if (signal?.aborted) return cancelled();
1416
+ const collected = await collectSandboxOutput(active.sandbox, outputPath, policy, signal);
1254
1417
  if (collected._tag === "Err") return collected;
1255
- if (prepared.value.skillName === "review-skill") return validateSkillReview(collected.value);
1256
- const validated = validateGeneratedSkill(prepared.value.outputName, collected.value, prepared.value.skillName === "generate-project-skill" ? prepared.value.source.files.map((file) => file.path) : void 0);
1418
+ if (prepared.skillName === "review-skill") return validateSkillReview(collected.value);
1419
+ const validated = validateGeneratedSkill(prepared.outputName, collected.value, prepared.skillName === "generate-project-skill" ? {
1420
+ _tag: "ProjectSkill",
1421
+ projectPaths: prepared.source.files.map((file) => file.path)
1422
+ } : { _tag: "PackageSkill" });
1257
1423
  if (validated._tag === "Err") return validated;
1258
- if (!prepared.value.destination) return err({
1424
+ if (!prepared.destination) return err({
1259
1425
  _tag: "InvalidInput",
1260
1426
  message: "Skill destination is required."
1261
1427
  });
1262
- return promoteSkill(prepared.value.destination.rootDir, prepared.value.destination.name, collected.value, prepared.value.source.attempts);
1428
+ const promoted = await promoteSkill(prepared.destination.rootDir, prepared.destination.name, collected.value, prepared.source.attempts);
1429
+ if (promoted._tag === "Err") return promoted;
1430
+ for (const warning of promoted.value.warnings) log.warn(warning);
1431
+ return ok(promoted.value.skill);
1263
1432
  } finally {
1264
1433
  await session.destroy();
1265
1434
  }
1266
- } };
1435
+ }
1267
1436
  }
1268
1437
  export { createSkillHarness };
1269
1438