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 +81 -5
- package/dist/_chunks/skills.mjs +35 -18
- package/dist/_chunks/skills.mjs.map +1 -1
- package/dist/index.d.mts +56 -6
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +226 -57
- package/dist/index.mjs.map +1 -1
- package/dist/sandbox-local.d.mts +11 -3
- package/dist/sandbox-local.d.mts.map +1 -1
- package/dist/sandbox-local.mjs +50 -7
- package/dist/sandbox-local.mjs.map +1 -1
- package/dist/skills/generate-package-skill/SKILL.md +116 -63
- package/dist/skills/generate-package-skill/assets/harness-request.md +3 -0
- package/dist/skills/generate-package-skill/scripts/serve-fixture.mjs +233 -0
- package/dist/skills/generate-project-skill/SKILL.md +1 -0
- package/dist/skills/review-skill/SKILL.md +4 -2
- package/dist/skills/skilld/SKILL.md +91 -2
- package/dist/skills.d.mts +8 -3
- package/dist/skills.d.mts.map +1 -1
- package/package.json +2 -2
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
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
-
|
|
362
|
-
|
|
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
|
-
|
|
381
|
-
const
|
|
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 (
|
|
410
|
+
if (contract._tag === "ProjectSkill") issues.push(...checkProjectSkill({
|
|
388
411
|
markdown: source,
|
|
389
|
-
projectPaths:
|
|
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
|
-
|
|
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
|
|
505
|
-
if (stat.isSymbolicLink()) return
|
|
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
|
|
512
|
-
const path =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
539
|
-
if (content.byteLength !== openedStat.size) return
|
|
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(
|
|
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
|
-
|
|
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 =
|
|
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:
|
|
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
|
|
1363
|
+
await writePreparedSource(active, prepared, signal);
|
|
1230
1364
|
await userOnSession?.(sessionOptions);
|
|
1231
1365
|
}
|
|
1232
1366
|
}
|
|
1233
1367
|
});
|
|
1234
|
-
const sessionResult = await agent.createSession({
|
|
1235
|
-
|
|
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 =
|
|
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.
|
|
1245
|
-
const prompt = renderRequest(requestContent(skill), sourcePath, currentSkillPath, outputPath, prepared.
|
|
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:
|
|
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,
|
|
1252
|
-
|
|
1253
|
-
|
|
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.
|
|
1256
|
-
const validated = validateGeneratedSkill(prepared.
|
|
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.
|
|
1424
|
+
if (!prepared.destination) return err({
|
|
1259
1425
|
_tag: "InvalidInput",
|
|
1260
1426
|
message: "Skill destination is required."
|
|
1261
1427
|
});
|
|
1262
|
-
|
|
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
|
|