skilld-harness 3.1.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 +122 -1
- 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 +285 -55
- package/dist/index.mjs.map +1 -1
- package/dist/sandbox-local.d.mts +33 -0
- package/dist/sandbox-local.d.mts.map +1 -0
- package/dist/sandbox-local.mjs +240 -0
- package/dist/sandbox-local.mjs.map +1 -0
- package/dist/skills/generate-package-skill/SKILL.md +110 -47
- 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 +21 -6
- package/dist/skills/review-skill/SKILL.md +4 -2
- package/dist/skills/skilld/SKILL.md +117 -2
- package/dist/skills.d.mts +8 -3
- package/dist/skills.d.mts.map +1 -1
- package/package.json +6 -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
|
}
|
|
@@ -301,14 +303,65 @@ const promoteSkill = async (rootDir, name, files, attempts) => {
|
|
|
301
303
|
cause
|
|
302
304
|
});
|
|
303
305
|
};
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
306
|
+
const searchCommand = /\b(?:git grep|grep|rg)\b/;
|
|
307
|
+
const inlineCode = /`([^`\n]+)`/g;
|
|
308
|
+
const fencedBlock = /```[^\n]*\n[\s\S]*?```/g;
|
|
309
|
+
const denial = /\b(no|not|none|never|absent|missing|lacks|without|instead|rather|empty|undeclared|unavailable|nothing)\b|\bdoes ?n[o']t\b/i;
|
|
310
|
+
const creation = /\b(?:copy|create|write|generate)\b/i;
|
|
311
|
+
function fencedBlocks(markdown) {
|
|
312
|
+
return markdown.match(fencedBlock) ?? [];
|
|
313
|
+
}
|
|
314
|
+
function proseParagraphs(markdown) {
|
|
315
|
+
return markdown.replace(fencedBlock, "").split(/\n[ \t]*\n/).map((paragraph) => paragraph.replace(/\n/g, " ").trim()).filter((paragraph) => paragraph.length > 0);
|
|
316
|
+
}
|
|
317
|
+
const pathExtension = /\.(?:md|mdx|json|jsonc|ts|tsx|mts|cts|js|mjs|cjs|jsx|vue|svelte|txt|yml|yaml|toml|html|css|scss|rs|py|go|sh|lock|env|csv|xml)$/i;
|
|
318
|
+
const versionShape = /^v?\d+(?:\.\d+)*$/i;
|
|
319
|
+
const productName = /^[A-Za-z][A-Za-z0-9_]*\.[A-Za-z0-9]+$/;
|
|
320
|
+
function pathCandidate(token) {
|
|
321
|
+
if (token.length === 0 || /\s/.test(token)) return null;
|
|
322
|
+
if (/^[-$#<{[]/.test(token)) return null;
|
|
323
|
+
if (token.includes("://") || /[()|;&"'@:]/.test(token)) return null;
|
|
324
|
+
const trimmed = token.replace(/^\.\//, "").replace(/\/+$/, "");
|
|
325
|
+
if (trimmed.length === 0 || trimmed.startsWith("..")) return null;
|
|
326
|
+
if (versionShape.test(trimmed) || productName.test(trimmed) || /\/\d+$/.test(trimmed)) return null;
|
|
327
|
+
if (!trimmed.includes("/") && !pathExtension.test(trimmed)) return null;
|
|
328
|
+
return trimmed;
|
|
329
|
+
}
|
|
330
|
+
function literalPrefix(candidate) {
|
|
331
|
+
const wildcard = candidate.search(/[*?]/);
|
|
332
|
+
if (wildcard === -1) return candidate;
|
|
333
|
+
return candidate.slice(0, wildcard).replace(/[^/]*$/, "").replace(/\/+$/, "");
|
|
334
|
+
}
|
|
335
|
+
function resolves(candidate, known) {
|
|
336
|
+
if (known.has(candidate)) return true;
|
|
337
|
+
const prefix = literalPrefix(candidate);
|
|
338
|
+
if (prefix.length === 0) return true;
|
|
339
|
+
for (const path of known) {
|
|
340
|
+
if (path.startsWith(`${candidate}.`)) return true;
|
|
341
|
+
if (path === prefix || path.startsWith(`${prefix}/`)) return true;
|
|
342
|
+
}
|
|
343
|
+
return false;
|
|
344
|
+
}
|
|
345
|
+
function checkProjectSkill(input) {
|
|
346
|
+
const issues = [];
|
|
347
|
+
const known = /* @__PURE__ */ new Set([...input.projectPaths, ...input.outputPaths]);
|
|
348
|
+
const unknown = /* @__PURE__ */ new Set();
|
|
349
|
+
for (const paragraph of proseParagraphs(input.markdown)) {
|
|
350
|
+
if (denial.test(paragraph) || creation.test(paragraph)) continue;
|
|
351
|
+
for (const match of paragraph.matchAll(inlineCode)) {
|
|
352
|
+
const candidate = pathCandidate(match[1].trim());
|
|
353
|
+
if (candidate !== null && !resolves(candidate, known)) unknown.add(candidate);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
for (const path of [...unknown].sort()) issues.push(`SKILL.md points at a path the project does not contain: ${path}.`);
|
|
357
|
+
const inline = [...input.markdown.matchAll(inlineCode)].map((match) => match[1]);
|
|
358
|
+
const commands = [...fencedBlocks(input.markdown), ...inline].join("\n");
|
|
359
|
+
if (!searchCommand.test(commands)) issues.push("SKILL.md must give the Agent at least one search command it can repeat.");
|
|
360
|
+
return issues;
|
|
361
|
+
}
|
|
362
|
+
const allowedFrontmatter = /* @__PURE__ */ new Set(["name", "description"]);
|
|
363
|
+
const packageSkillLineLimit = 500;
|
|
364
|
+
const packageSkillReferenceLimit = 8;
|
|
312
365
|
const invalid = (issues) => err({
|
|
313
366
|
_tag: "InvalidSkill",
|
|
314
367
|
message: "Skill output failed deterministic checks.",
|
|
@@ -321,13 +374,44 @@ function decodeText(content) {
|
|
|
321
374
|
return null;
|
|
322
375
|
}
|
|
323
376
|
}
|
|
324
|
-
|
|
325
|
-
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) => {
|
|
326
405
|
const issues = [];
|
|
327
406
|
const skillFiles = files.filter((file) => file.path === "SKILL.md");
|
|
328
407
|
if (skillFiles.length !== 1) return invalid(["Output must contain exactly one SKILL.md file."]);
|
|
329
408
|
const source = decodeText(skillFiles[0].content);
|
|
330
409
|
if (source === null) return invalid(["SKILL.md must contain valid UTF-8 text."]);
|
|
410
|
+
if (contract._tag === "ProjectSkill") issues.push(...checkProjectSkill({
|
|
411
|
+
markdown: source,
|
|
412
|
+
projectPaths: contract.projectPaths,
|
|
413
|
+
outputPaths: files.map((file) => file.path)
|
|
414
|
+
}));
|
|
331
415
|
const match = source.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
|
|
332
416
|
if (!match) return invalid(["SKILL.md must start with YAML frontmatter."]);
|
|
333
417
|
if (match[2]?.trim().length === 0) issues.push("SKILL.md instructions must not be empty.");
|
|
@@ -344,10 +428,7 @@ const validateGeneratedSkill = (name, files) => {
|
|
|
344
428
|
if (frontmatter.name !== name) issues.push("Frontmatter name must match the Skill directory name.");
|
|
345
429
|
if (typeof frontmatter.name !== "string" || !isSkillName(frontmatter.name)) issues.push("Frontmatter name is invalid.");
|
|
346
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.");
|
|
347
|
-
|
|
348
|
-
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.");
|
|
349
|
-
if (frontmatter.metadata !== void 0 && !isStringMap(frontmatter.metadata)) issues.push("Frontmatter metadata must map strings to strings.");
|
|
350
|
-
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));
|
|
351
432
|
return issues.length === 0 ? ok(void 0) : invalid(issues);
|
|
352
433
|
};
|
|
353
434
|
const findingLevels = /* @__PURE__ */ new Set([
|
|
@@ -391,6 +472,9 @@ const validateSkillReview = (files) => {
|
|
|
391
472
|
findings
|
|
392
473
|
});
|
|
393
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
|
+
}
|
|
394
478
|
const ignoredNames = /* @__PURE__ */ new Set([
|
|
395
479
|
".git",
|
|
396
480
|
".hg",
|
|
@@ -428,7 +512,10 @@ async function collectHostDirectory(directory, policy, sourceLabel = directory,
|
|
|
428
512
|
if (canonicalRoot instanceof Error) return unavailable("Source directory cannot be resolved.", canonicalRoot);
|
|
429
513
|
if (canonicalRoot !== root) return unavailable("Source path must not pass through a symbolic link.");
|
|
430
514
|
const files = [];
|
|
515
|
+
const skippedFiles = [];
|
|
431
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);
|
|
432
519
|
const walk = async (current) => {
|
|
433
520
|
if (signal?.aborted) return cancelled();
|
|
434
521
|
const canonicalCurrent = await realpath(current).catch((error) => error);
|
|
@@ -440,28 +527,32 @@ async function collectHostDirectory(directory, policy, sourceLabel = directory,
|
|
|
440
527
|
if (ignoredNames.has(entry.name)) continue;
|
|
441
528
|
const absolute = join(current, entry.name);
|
|
442
529
|
const stat = await lstat(absolute).catch((error) => error);
|
|
443
|
-
if (stat instanceof Error) return
|
|
444
|
-
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.");
|
|
445
532
|
if (stat.isDirectory()) {
|
|
446
533
|
const nested = await walk(absolute);
|
|
447
534
|
if (nested._tag === "Err") return nested;
|
|
448
535
|
continue;
|
|
449
536
|
}
|
|
450
|
-
if (!stat.isFile()) return
|
|
451
|
-
const path =
|
|
537
|
+
if (!stat.isFile()) return entryUnavailable(absolute, "Source contains a special file");
|
|
538
|
+
const path = relativePath(absolute);
|
|
452
539
|
const handle = await open(absolute, constants.O_RDONLY | constants.O_NOFOLLOW).catch((error) => error);
|
|
453
|
-
if (handle instanceof Error) return
|
|
540
|
+
if (handle instanceof Error) return entryUnavailable(absolute, "Source file cannot be opened without following links", handle);
|
|
454
541
|
const openedStat = await handle.stat().catch((error) => error);
|
|
455
542
|
if (openedStat instanceof Error) {
|
|
456
543
|
await handle.close();
|
|
457
|
-
return
|
|
544
|
+
return entryUnavailable(absolute, "Source file cannot be inspected", openedStat);
|
|
458
545
|
}
|
|
459
546
|
if (!openedStat.isFile() || openedStat.dev !== stat.dev || openedStat.ino !== stat.ino) {
|
|
460
547
|
await handle.close();
|
|
461
|
-
return
|
|
548
|
+
return entryUnavailable(absolute, "Source file changed during collection");
|
|
462
549
|
}
|
|
463
550
|
if (openedStat.size > policy.maxSourceFileBytes) {
|
|
464
551
|
await handle.close();
|
|
552
|
+
skippedFiles.push({
|
|
553
|
+
path,
|
|
554
|
+
bytes: openedStat.size
|
|
555
|
+
});
|
|
465
556
|
continue;
|
|
466
557
|
}
|
|
467
558
|
if (files.length >= policy.maxSourceFiles) {
|
|
@@ -474,8 +565,8 @@ async function collectHostDirectory(directory, policy, sourceLabel = directory,
|
|
|
474
565
|
}
|
|
475
566
|
const content = await handle.readFile().catch((error) => error);
|
|
476
567
|
await handle.close();
|
|
477
|
-
if (content instanceof Error) return
|
|
478
|
-
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");
|
|
479
570
|
if (signal?.aborted) return cancelled();
|
|
480
571
|
files.push({
|
|
481
572
|
path,
|
|
@@ -489,8 +580,10 @@ async function collectHostDirectory(directory, policy, sourceLabel = directory,
|
|
|
489
580
|
if (walked._tag === "Err") return walked;
|
|
490
581
|
if (files.length === 0) return unavailable("Source directory has no usable files.");
|
|
491
582
|
files.sort((left, right) => left.path.localeCompare(right.path));
|
|
583
|
+
skippedFiles.sort((left, right) => left.path.localeCompare(right.path));
|
|
492
584
|
return ok({
|
|
493
585
|
files,
|
|
586
|
+
skippedFiles,
|
|
494
587
|
attempts: [{
|
|
495
588
|
source: sourceLabel,
|
|
496
589
|
status: "used"
|
|
@@ -558,6 +651,7 @@ async function extractArchive(compressed, policy, layout) {
|
|
|
558
651
|
source.on("error", (error) => gunzip.destroy(error));
|
|
559
652
|
source.pipe(gunzip);
|
|
560
653
|
const files = [];
|
|
654
|
+
const skippedFiles = [];
|
|
561
655
|
const seen = /* @__PURE__ */ new Set();
|
|
562
656
|
const maxArchiveBytes = policy.maxSourceBytes + (policy.maxSourceFiles + 2) * blockBytes * 2;
|
|
563
657
|
let archiveBytes = 0;
|
|
@@ -645,7 +739,7 @@ async function extractArchive(compressed, policy, layout) {
|
|
|
645
739
|
};
|
|
646
740
|
continue;
|
|
647
741
|
}
|
|
648
|
-
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}.`);
|
|
649
743
|
const path = archivePath(nextPath ?? headerPath, layout);
|
|
650
744
|
nextPath = null;
|
|
651
745
|
if (path === null) return unavailable$1("Source archive contains an invalid path.");
|
|
@@ -654,6 +748,10 @@ async function extractArchive(compressed, policy, layout) {
|
|
|
654
748
|
fileEntries += 1;
|
|
655
749
|
if (fileEntries > policy.maxSourceFiles) return unavailable$1("Source archive contains too many files.");
|
|
656
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
|
+
});
|
|
657
755
|
entry = {
|
|
658
756
|
kind: size > policy.maxSourceFileBytes ? "skip" : "file",
|
|
659
757
|
path,
|
|
@@ -671,7 +769,11 @@ async function extractArchive(compressed, policy, layout) {
|
|
|
671
769
|
if (entry !== null || pending.some((byte) => byte !== 0) || !ended) return unavailable$1("Source archive ended before its final entry.");
|
|
672
770
|
if (files.length === 0) return unavailable$1("Source archive has no usable files.");
|
|
673
771
|
files.sort((left, right) => left.path.localeCompare(right.path));
|
|
674
|
-
|
|
772
|
+
skippedFiles.sort((left, right) => left.path.localeCompare(right.path));
|
|
773
|
+
return ok({
|
|
774
|
+
files,
|
|
775
|
+
skippedFiles
|
|
776
|
+
});
|
|
675
777
|
}
|
|
676
778
|
const packageSpecPattern = /^(@[^/@]+\/[^/@]+|[^/@]+)(?:@(\S+))?$/;
|
|
677
779
|
const exactVersionPattern = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[\da-z-]+(?:\.[\da-z-]+)*)?(?:\+[\da-z-]+(?:\.[\da-z-]+)*)?$/i;
|
|
@@ -949,7 +1051,8 @@ async function prepareNpmPackage(spec, policy, fetchClient, signal) {
|
|
|
949
1051
|
return unavailable(message, [...attempts, skipped(version.dist.tarball, message)]);
|
|
950
1052
|
}
|
|
951
1053
|
attempts.push(used(version.dist.tarball));
|
|
952
|
-
let files = [...extracted.value];
|
|
1054
|
+
let files = [...extracted.value.files];
|
|
1055
|
+
let skippedFiles = [...extracted.value.skippedFiles];
|
|
953
1056
|
const repositoryArchive = githubArchiveUrl(version.repository ?? repository, version.gitHead);
|
|
954
1057
|
const available = remainingPolicy(policy, files);
|
|
955
1058
|
if (repositoryArchive !== null && available === null) attempts.push(skipped(repositoryArchive, "Prepared package files reached the source limit."));
|
|
@@ -964,7 +1067,11 @@ async function prepareNpmPackage(spec, policy, fetchClient, signal) {
|
|
|
964
1067
|
const repositoryFiles = await extractResponse(fetchedRepository.value, available, "github");
|
|
965
1068
|
if (repositoryFiles._tag === "Err") attempts.push(skipped(repositoryArchive, repositoryFiles.error.message));
|
|
966
1069
|
else {
|
|
967
|
-
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) => ({
|
|
968
1075
|
...file,
|
|
969
1076
|
path: posix.join("repository", file.path)
|
|
970
1077
|
}))];
|
|
@@ -973,8 +1080,10 @@ async function prepareNpmPackage(spec, policy, fetchClient, signal) {
|
|
|
973
1080
|
}
|
|
974
1081
|
}
|
|
975
1082
|
files.sort((left, right) => left.path.localeCompare(right.path));
|
|
1083
|
+
skippedFiles.sort((left, right) => left.path.localeCompare(right.path));
|
|
976
1084
|
return ok({
|
|
977
1085
|
files,
|
|
1086
|
+
skippedFiles,
|
|
978
1087
|
attempts,
|
|
979
1088
|
npmResolution: {
|
|
980
1089
|
package: parsed.name,
|
|
@@ -982,6 +1091,61 @@ async function prepareNpmPackage(spec, policy, fetchClient, signal) {
|
|
|
982
1091
|
}
|
|
983
1092
|
});
|
|
984
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
|
+
}
|
|
985
1149
|
function cancelled() {
|
|
986
1150
|
return err({
|
|
987
1151
|
_tag: "Cancelled",
|
|
@@ -1040,6 +1204,7 @@ async function prepareRun(input, policy, fetchClient, signal) {
|
|
|
1040
1204
|
skillName: "generate-project-skill",
|
|
1041
1205
|
outputName: input.destination.name,
|
|
1042
1206
|
source: source.value,
|
|
1207
|
+
sourceDir: "",
|
|
1043
1208
|
current: current.value,
|
|
1044
1209
|
destination: input.destination
|
|
1045
1210
|
});
|
|
@@ -1049,7 +1214,8 @@ async function prepareRun(input, policy, fetchClient, signal) {
|
|
|
1049
1214
|
return source._tag === "Err" ? source : ok({
|
|
1050
1215
|
skillName: "review-skill",
|
|
1051
1216
|
outputName: "review",
|
|
1052
|
-
source: source.value
|
|
1217
|
+
source: source.value,
|
|
1218
|
+
sourceDir: basename(input.skillDir)
|
|
1053
1219
|
});
|
|
1054
1220
|
}
|
|
1055
1221
|
if (input.source._tag === "NpmPackage") {
|
|
@@ -1062,6 +1228,7 @@ async function prepareRun(input, policy, fetchClient, signal) {
|
|
|
1062
1228
|
skillName: "generate-package-skill",
|
|
1063
1229
|
outputName: input.destination.name,
|
|
1064
1230
|
source: source.value,
|
|
1231
|
+
sourceDir: "",
|
|
1065
1232
|
current: current.value,
|
|
1066
1233
|
destination: input.destination
|
|
1067
1234
|
});
|
|
@@ -1086,6 +1253,7 @@ async function prepareRun(input, policy, fetchClient, signal) {
|
|
|
1086
1253
|
skillName: "generate-package-skill",
|
|
1087
1254
|
outputName: input.destination.name,
|
|
1088
1255
|
source: source.value,
|
|
1256
|
+
sourceDir: "",
|
|
1089
1257
|
current: current.value,
|
|
1090
1258
|
destination: input.destination
|
|
1091
1259
|
});
|
|
@@ -1098,8 +1266,11 @@ function requestContent(skill) {
|
|
|
1098
1266
|
function renderRequest(template, sourcePath, currentSkillPath, outputPath, skillName) {
|
|
1099
1267
|
return template.replaceAll("{{SOURCE_PATH}}", sourcePath).replaceAll("{{CURRENT_SKILL_PATH}}", currentSkillPath).replaceAll("{{OUTPUT_PATH}}", outputPath).replaceAll("{{SKILL_NAME}}", skillName);
|
|
1100
1268
|
}
|
|
1269
|
+
function sourcePathOf(active, prepared) {
|
|
1270
|
+
return posix.join(active.workDir, "input/source", prepared.sourceDir);
|
|
1271
|
+
}
|
|
1101
1272
|
async function writePreparedSource(active, prepared, signal) {
|
|
1102
|
-
const sourcePath =
|
|
1273
|
+
const sourcePath = sourcePathOf(active, prepared);
|
|
1103
1274
|
const reset = await active.sandbox.run({
|
|
1104
1275
|
command: "rm -rf -- \"$SKILLD_INPUT\" \"$SKILLD_OUTPUT\" && mkdir -p -- \"$SKILLD_INPUT\"",
|
|
1105
1276
|
env: {
|
|
@@ -1123,6 +1294,7 @@ async function writePreparedSource(active, prepared, signal) {
|
|
|
1123
1294
|
path: posix.join(active.workDir, "input/source-manifest.json"),
|
|
1124
1295
|
content: `${JSON.stringify({
|
|
1125
1296
|
sourceAttempts: prepared.source.attempts,
|
|
1297
|
+
skippedFiles: prepared.source.skippedFiles,
|
|
1126
1298
|
npmResolution: prepared.source.npmResolution,
|
|
1127
1299
|
hasCurrentSkill: prepared.current !== void 0
|
|
1128
1300
|
}, null, 2)}\n`,
|
|
@@ -1145,17 +1317,40 @@ function createSkillHarness(options) {
|
|
|
1145
1317
|
const sandboxConfig = { ...options.sandboxConfig };
|
|
1146
1318
|
const harness = options.harness;
|
|
1147
1319
|
const sandbox = options.sandbox;
|
|
1148
|
-
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) {
|
|
1149
1336
|
const parsed = parseSkillRun(input);
|
|
1150
1337
|
if (parsed._tag === "Err") return parsed;
|
|
1151
1338
|
const prepared = await prepareRun(parsed.value, policy, fetchClient, runOptions.signal);
|
|
1152
1339
|
if (prepared._tag === "Err") return prepared;
|
|
1340
|
+
for (const warning of skippedFileWarnings(prepared.value.source.skippedFiles, policy)) log.warn(warning);
|
|
1153
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) {
|
|
1154
1350
|
let active;
|
|
1155
1351
|
const userOnSession = sandboxConfig.onSession;
|
|
1156
1352
|
const agent = new HarnessAgent({
|
|
1157
1353
|
harness,
|
|
1158
|
-
sandbox,
|
|
1159
1354
|
skills: [skill],
|
|
1160
1355
|
permissionMode: "allow-all",
|
|
1161
1356
|
sandboxConfig: {
|
|
@@ -1165,44 +1360,79 @@ function createSkillHarness(options) {
|
|
|
1165
1360
|
sandbox: sessionOptions.session,
|
|
1166
1361
|
workDir: sessionOptions.sessionWorkDir
|
|
1167
1362
|
};
|
|
1168
|
-
await writePreparedSource(active, prepared
|
|
1363
|
+
await writePreparedSource(active, prepared, signal);
|
|
1169
1364
|
await userOnSession?.(sessionOptions);
|
|
1170
1365
|
}
|
|
1171
1366
|
}
|
|
1172
1367
|
});
|
|
1173
|
-
const sessionResult = await agent.createSession({
|
|
1174
|
-
|
|
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);
|
|
1175
1373
|
const session = sessionResult.value;
|
|
1176
1374
|
try {
|
|
1177
1375
|
if (!active) return err({
|
|
1178
1376
|
_tag: "AgentFailed",
|
|
1179
1377
|
message: "Harness did not provide its sandbox session."
|
|
1180
1378
|
});
|
|
1181
|
-
const sourcePath =
|
|
1379
|
+
const sourcePath = sourcePathOf(active, prepared);
|
|
1182
1380
|
const currentSkillPath = posix.join(active.workDir, "input/current-skill");
|
|
1183
|
-
const outputPath = posix.join(active.workDir, "skilld-output", prepared.
|
|
1184
|
-
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;
|
|
1185
1384
|
const generated = await agent.generate({
|
|
1186
1385
|
session,
|
|
1187
1386
|
prompt,
|
|
1188
|
-
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
|
+
}
|
|
1189
1412
|
}).then(ok, (cause) => err(cause));
|
|
1190
|
-
if (generated._tag === "Err") return toAgentError(generated.error,
|
|
1191
|
-
|
|
1192
|
-
|
|
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);
|
|
1193
1417
|
if (collected._tag === "Err") return collected;
|
|
1194
|
-
if (prepared.
|
|
1195
|
-
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" });
|
|
1196
1423
|
if (validated._tag === "Err") return validated;
|
|
1197
|
-
if (!prepared.
|
|
1424
|
+
if (!prepared.destination) return err({
|
|
1198
1425
|
_tag: "InvalidInput",
|
|
1199
1426
|
message: "Skill destination is required."
|
|
1200
1427
|
});
|
|
1201
|
-
|
|
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);
|
|
1202
1432
|
} finally {
|
|
1203
1433
|
await session.destroy();
|
|
1204
1434
|
}
|
|
1205
|
-
}
|
|
1435
|
+
}
|
|
1206
1436
|
}
|
|
1207
1437
|
export { createSkillHarness };
|
|
1208
1438
|
|