Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,7 @@ The skilld-maintained Skills:

- [`skilld`](./skills/skilld): search, run, and install Skills with the CLI
- [`generate-package-skill`](./skills/generate-package-skill): draft a Skill for a package you maintain
- [`update-package-skill`](./skills/update-package-skill): update that Skill after a release, testing only what changed
- [`generate-project-skill`](./skills/generate-project-skill): draft a Skill from a project you maintain
- [`review-skill`](./skills/review-skill): review a Skill before you publish it

Expand Down
1 change: 1 addition & 0 deletions packages/harness/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const result = await skillHarness.run({
```

Every run returns a tagged `Ok` or `Err` value.
A `PackageSkill` run whose destination already holds the Skill runs `update-package-skill`, which tests only what the release changed. Without one it runs `generate-package-skill`.
The input tag sets the `Ok` value: `PackageSkill` and `ProjectSkill` return a `GeneratedSkill`, and `ReviewSkill` returns a `SkillReview`.
Both `Ok` and `Err` carry a `report`, so a failed run still reports its cost.
`report.usage` holds input, cached input, and output tokens.
Expand Down
11 changes: 8 additions & 3 deletions packages/harness/src/harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import { prepareNpmPackage } from './internal/source/npm.ts'
import { DEFAULT_OUTPUT_POLICY, loadSkilldMaintainedSkill } from './skills.ts'

interface PreparedRun {
readonly skillName: 'generate-package-skill' | 'generate-project-skill' | 'review-skill'
readonly skillName: 'generate-package-skill' | 'update-package-skill' | 'generate-project-skill' | 'review-skill'
readonly outputName: string
readonly source: PreparedSource
// Path under `input/source`. A reviewed Skill keeps its directory name, so
Expand Down Expand Up @@ -200,7 +200,7 @@ async function prepareRun(input: SkillRun, policy: SkillOutputPolicy, fetchClien
return err({ _tag: 'Cancelled', message: 'Skill run was cancelled.' })
return source._tag === 'Err'
? source
: ok({ skillName: 'generate-package-skill', outputName: input.destination.name, source: source.value, sourceDir: '', current: current.value, destination: input.destination })
: ok({ skillName: packageSkillName(current.value), outputName: input.destination.name, source: source.value, sourceDir: '', current: current.value, destination: input.destination })
}

const packageDir = resolveWithin(input.source.rootDir, input.source.packageDir)
Expand All @@ -218,7 +218,12 @@ async function prepareRun(input: SkillRun, policy: SkillOutputPolicy, fetchClien
const source = await collectHostDirectory(packageDir, policy, packageDir, signal)
return source._tag === 'Err'
? source
: ok({ skillName: 'generate-package-skill', outputName: input.destination.name, source: source.value, sourceDir: '', current: current.value, destination: input.destination })
: ok({ skillName: packageSkillName(current.value), outputName: input.destination.name, source: source.value, sourceDir: '', current: current.value, destination: input.destination })
}

/** A destination that already holds the Skill gets an update run, which tests only what the release changed. */
function packageSkillName(current: PreparedSource | undefined): 'generate-package-skill' | 'update-package-skill' {
return current === undefined ? 'generate-package-skill' : 'update-package-skill'
}

function requestContent(skill: HarnessV1Skill): string {
Expand Down
22 changes: 22 additions & 0 deletions packages/harness/test/e2e/harness.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,28 @@ describe('createSkillHarness', () => {
await expect(readFile(join(currentDir, 'references/old.md'), 'utf8')).rejects.toMatchObject({ code: 'ENOENT' })
})

it('runs update-package-skill when the destination already holds the package Skill', async () => {
const packageDir = await makePackage()
const destinationRoot = await mkdtemp(join(tmpdir(), 'skilld-output-'))
await mkdir(join(destinationRoot, 'example-package'), { recursive: true })
await writeFile(join(destinationRoot, 'example-package/SKILL.md'), skillSource('example-package'))
const fake = createFakeHarness({
async onPrompt({ sandbox, workDir }) {
await sandbox.writeTextFile({ path: join(workDir, 'skilld-output/example-package/SKILL.md'), content: skillSource('example-package') })
},
})

const result = await createSkillHarness({ harness: fake.harness, sandbox: createFakeSandboxProvider() }).run({
_tag: 'PackageSkill',
source: { _tag: 'LocalPackage', rootDir: packageDir, packageDir: '.' },
destination: { rootDir: destinationRoot, name: 'example-package' },
})

expect(result._tag).toBe('Ok')
expect(fake.capture.prompts[0]?.skills.map(skill => skill.name)).toEqual(['update-package-skill'])
expect(promptText(fake.capture.prompts[0]?.prompt)).toMatch(/The current Skill is at `\S*\/input\/current-skill`/)
})

it('creates the project .skilld root for a project Skill', async () => {
const projectDir = await makePackage()
const fake = createFakeHarness({
Expand Down
2 changes: 2 additions & 0 deletions packages/harness/test/unit/skills.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ describe('skilld-maintained Skills', () => {
it('loads the runnable Harness Skills', async () => {
await expect(harnessSkillNames()).resolves.toEqual([
'generate-package-skill',
'update-package-skill',
'generate-project-skill',
'review-skill',
])
Expand Down Expand Up @@ -65,6 +66,7 @@ describe('skilld-maintained Skills', () => {
it('lists every published skilld-maintained Skill', async () => {
await expect(skilldMaintainedSkillNames()).resolves.toEqual([
'generate-package-skill',
'update-package-skill',
'generate-project-skill',
'review-skill',
'skilld',
Expand Down
4 changes: 2 additions & 2 deletions skills/generate-package-skill/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Skip an internal package, whose users are the repository's own packages, such as
5. Read the current official docs and examples, and release notes only for breaking changes and removed APIs.
When docs cover several frameworks, read the consumer's framework pages first, then the core pages they link.

Test an existing Skill's claims; do not copy its layout.
To update an existing Skill after a release, use `update-package-skill`. It tests only what the release changed.

## 2. Test the examples

Expand Down Expand Up @@ -57,7 +57,7 @@ Observed behaviour beats documentation.
For a binary, use `--fetch-raw PATH --out DIR`. `DIR/responses.json` lists each status, content type, and response headers.
For an HTTP client package, start a fake target on port 0 inside the test, and assert on the requests it receives.
Without `--fetch`, it holds the server until SIGTERM. Background it with your tool's option; `&` and `nohup` die with the shell call.
Never kill by port or with `pkill -f`: another Agent can own that process.
Start servers from your own test script and record each process ID you start; stop only those. Never kill by port or with `pkill -f`: another Agent can own that process.
In a browser, set a desktop user agent; a package can treat `HeadlessChrome` as a bot.
4. If documentation and behaviour disagree, write the behaviour and report the mismatch.
5. Keep an unrunnable example only when the types prove it; report it untested.
Expand Down
3 changes: 1 addition & 2 deletions skills/generate-package-skill/assets/harness-request.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# Harness request

Use the prepared package source at `{{SOURCE_PATH}}`.
If it exists, use the current Skill at `{{CURRENT_SKILL_PATH}}` as the update baseline.
Generate the package Skill at `{{OUTPUT_PATH}}`.
The Skill directory name must be `{{SKILL_NAME}}`.

Expand All @@ -13,4 +12,4 @@ List each documentation and behaviour mismatch in your final message.
Write no files outside `{{OUTPUT_PATH}}`. Build fixtures in a scratch directory outside it.
Write only `SKILL.md` and Markdown files under `references/`: at most 9 files and 64 KiB in total. Skillgen rejects other output.
Finish only after checking every output rule in this Skill.
After you finish, the Harness checks the output. It returns each failed check to you; fix them in place.
Do not install or run `skilld`. After you finish, the Harness checks the output and returns each failed check to you; fix them in place.
1 change: 1 addition & 0 deletions skills/harness-skills.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[
"generate-package-skill",
"update-package-skill",
"generate-project-skill",
"review-skill"
]
1 change: 1 addition & 0 deletions skills/skilld-maintained-skills.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[
"generate-package-skill",
"update-package-skill",
"generate-project-skill",
"review-skill",
"skilld"
Expand Down
63 changes: 63 additions & 0 deletions skills/update-package-skill/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
---
name: update-package-skill
description: Updates an existing package Skill for a new release of its npm or local package by testing only what changed since the version the Skill names. Use when a maintainer asks to refresh a package Skill after a release, or when a SKILL.md names an older tested version than the package.
---

# Update a package Skill

Bring an existing package Skill to a new package version with the smallest correct edit.
The current Skill was tested against an earlier version. Trust what the release did not touch.
To write a Skill from nothing, use `generate-package-skill`.

## Inputs

- The current Skill directory, and the package at the new version: a name and version, a directory, or prepared source.
- The tested version is the version the Skill's first paragraph names. If it names none, stop and report that the Skill needs a full rewrite.
- If the tested version equals the new version, change nothing and report that.

## 1. Copy, then diff

1. Copy the current Skill to the destination unchanged. Edit only the copy.
2. Diff the two published versions: `npm diff --diff=PACKAGE@OLD --diff=PACKAGE@NEW`. For a local package, diff the old tarball against the packed build.
3. Read the changelog or release notes between the two versions.
4. List what changed for a consumer: exports, types, options and their defaults, config keys, error messages, CLI flags, peer ranges, engines, and behaviour the notes describe.

## 2. Map changes to the Skill

For each change, find the Skill lines it affects: a claim, an example, a trap, a version limit.
A change is relevant when it affects a Skill line, or when it adds or breaks something an Agent would get wrong: a new trap, a renamed call, a new required setup step.
Internals, refactors, and fixes the Skill never mentioned are not relevant.

If no change is relevant, leave the copy byte-identical and stop. Report "no change needed" with a one-line diff summary.

## 3. Test the affected lines

1. Create one consumer fixture outside the package source, with its own `pnpm-workspace.yaml`. Install the new version and the documented peers.
2. Run each affected example and claim, and each example you add. Compare output with the claim: return values, type errors, build logs, exit codes.
3. Run each with one failure input as well, such as a bad option or an unreachable URL.
4. Do not re-run untouched examples. The earlier version's tests cover them.
5. Wrap every run in `timeout 120`. Start servers from your own script, record each process ID you start, and stop only those. Never kill by port or with `pkill -f`.
6. Spend about 20 tool calls on testing. Report an affected claim you could not test as untested; never claim it passed.

## 4. Edit

- Change only affected lines, and add what the release needs: a new trap, an old call β†’ new call row, a new common task.
- Delete lines the release made false, and version limits it fixed.
- Update the tested version in the first paragraph. Other version literals go stale; avoid them.
- Keep the wording, order, and formatting of untouched lines exactly. The diff must show only what the release changed.
- Update the description only when a trigger changed, such as a renamed export or a new config key.

Keep the format rules:

- The frontmatter contains only `name` and `description`. The name matches the directory.
- The description is one plain YAML line without double quotes, backticks, or `%`.
- `SKILL.md` stays under 500 lines. Write at most eight reference files, each linked from `SKILL.md`.

## 5. Report

- The tested version, old β†’ new, and the files you changed.
- Each changed line with the diff hunk or release note behind it.
- Each affected claim left untested.
- Each mismatch between the new docs and observed behaviour: example, documented result, observed result. These are package bugs.

For a direct run, show the Skill diff for review. A direct run has no Harness checks; never claim it passed them.
13 changes: 13 additions & 0 deletions skills/update-package-skill/assets/harness-request.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Harness request

The current Skill is at `{{CURRENT_SKILL_PATH}}`.
The prepared package source for the new version is at `{{SOURCE_PATH}}`.
Write the updated Skill at `{{OUTPUT_PATH}}`. The Skill directory name must be `{{SKILL_NAME}}`.

Read this Skill fully before writing files.
Copy the current Skill to `{{OUTPUT_PATH}}` first. If no change is relevant, leave it byte-identical and finish.
The network allows the npm registry and GitHub source archives only. Fetch the tested version from npm for the diff.
Build fixtures in a scratch directory. Write no other files outside `{{OUTPUT_PATH}}`.
Write only `SKILL.md` and Markdown files under `references/`: at most 9 files and 64 KiB in total. Skillgen rejects other output.
Do not install or run `skilld`. After you finish, the Harness checks the output and returns each failed check to you.
List each untested affected claim and each documentation mismatch in your final message.