Skip to content

Commit 3b4f68f

Browse files
committed
chore: merge main into perf/warm-run-shortcut
#510 lets a ready build serve only a CLI that reads what it holds, so the warm run passes the CLI's linked-files capability to findReusableBuild.
2 parents c7b2f3a + d591cb1 commit 3b4f68f

96 files changed

Lines changed: 6780 additions & 1434 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎GLOSSARY.md‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ Shared terms match the skilld CLI glossary (`skilld-dev/skilld/GLOSSARY.md`). Th
1212
| Agent | the user's tool | Agent 1—N Agent target | "your agent" |
1313
| Repository | GitHub | Repository 1—N Skill | "repository" |
1414
| Artifact | skilld.dev API | Skill commit 1—1 Artifact | not shown |
15+
| linked file | Artifact attestation | Artifact 1—N linked file | not shown |
16+
| symbolic link | Skill folder, `symbolic-links` check result | Skill folder 1—N symbolic link | not shown |
1517
| source status | lockfile, skilld.dev API | Artifact 1—1 source status | "Verified", "Unverified", "Local" |
1618
| run | skilld CLI | Skill 1—1 run command | "run" |
1719
| demo | Skill page, homepage | Skill 1—N demo | "demo" |
@@ -262,6 +264,24 @@ Existing route paths, table names, and protocol fields keep their identifiers.
262264

263265
**Never:** signature (bare), certificate, receipt.
264266

267+
### linked file
268+
269+
**Is:** a Skill file the Artifact attestation lists by path, size, and Git blob SHA, without packing its bytes. The skilld CLI reads it from GitHub at the attested commit and checks it against that SHA. Only a CLI that sends `Skilld-Capabilities: linked-files` gets one. See ADR-0013.
270+
271+
**Never:** external file, remote asset, download, attachment.
272+
273+
### omitted file
274+
275+
**Is:** a Skill file left out of an Artifact for a size limit. The `omitted-files` check result names each one. A CLI that reads linked files gets a large file as a linked file instead.
276+
277+
**Never:** skipped file, excluded file, dropped file.
278+
279+
### symbolic link
280+
281+
**Is:** a Git tree entry of mode `120000` whose blob holds a path. A build packs the files a link in the Skill folder names at the link path, or leaves the link out. The `symbolic-links` check result lists each one. See ADR-0014.
282+
283+
**Never:** linked file (that is a file the CLI reads from GitHub), alias, shortcut, symlink in prose.
284+
265285
### check result
266286

267287
**Is:** one named check, its version, its finding, and its outcome for one Artifact.

‎VISION.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ The filter for skilld-scoped work, precise enough to hold a proposal next to it
9595

9696
### 1. Provenance is the quality signal
9797

98-
A Skill is trustworthy because you can see who wrote it and read it before you run it. Every surface that shows a Skill names the author. The Skill page links the exact source file in its repository; other surfaces link the Skill page, which is where a visitor inspects a Skill. Registry identity is frozen at admission (ADR-0002) so provenance URLs never rot. Machine-generated metadata is labeled as such and never dressed as authorship.
98+
A Skill is trustworthy because you can see who wrote it and read it before you run it. Every surface that shows a Skill names the author. The Skill page links the exact source file in its repository; other surfaces link the Skill page, which is where a visitor inspects a Skill. When GitHub moves a Repository, its Skills move to the new name and every old URL answers 301 (ADR-0015), so provenance URLs never rot. Machine-generated metadata is labeled as such and never dressed as authorship.
9999

100100
**How to apply:** no Skill card, search result, or digest entry ships without its author and a link to its Skill page or its source. A layout that drops provenance to save space fails review. Trust signals (official repository, owner-verified) are earned facts, never editorial favors.
101101

‎app/components/DemoStage.vue‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ const liveUrl = computed(() => live && !demo.video ? demo.liveUrl : null)
5656
:src="liveUrl"
5757
sandbox="allow-scripts"
5858
:title="`Live output of /${demo.name}`"
59+
loading="lazy"
5960
class="demo-stage__frame"
6061
/>
6162
<DemoMedia v-else :demo :eager play="visible" />

‎app/components/WeeklyEmailPreview.vue‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@ import type { DigestDemoResponse } from '#layers/identity/server/api/digest/demo
88
99
// A failed demo costs the section its picture, never its copy or its CTA:
1010
// `data` stays null on error and `hasDemo` gates the whole frame.
11-
const { data: demo } = await useFetch<DigestDemoResponse>('/api/digest/demo', {
11+
// Only the card and its row count reach the payload. The answer also carries
12+
// the whole email as `html`, which this preview never renders.
13+
const { data: demo } = await useFetch('/api/digest/demo', {
1214
key: 'home-digest-demo-v1',
15+
transform: ({ card, rowCount }: DigestDemoResponse) => ({ card, rowCount }),
1316
})
1417
1518
const hasDemo = computed(() => !!demo.value?.card && (demo.value?.rowCount ?? 0) > 0)

‎app/pages/index.vue‎

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -91,9 +91,10 @@ const [
9191
query: { limit: 24 },
9292
// The board ships only its head. The rest of the named Skills, with their
9393
// posts and star series, serve `/skills/trending` and would ride in this
94-
// page's payload unread.
94+
// page's payload unread. The texture reads only Skill names, so the
95+
// repository cards stay out too: they were 47 kB of the payload.
9596
transform: (feed: TrendingFeedResponse) => ({
96-
items: feed.items,
97+
textureNames: feed.items.flatMap(repo => repo.skills.map(skill => `${repo.owner}/${repo.repo}/${skill.name}`)),
9798
board: feedBoardRows(feed).slice(0, HOME_BOARD_ROWS),
9899
}),
99100
})),
@@ -110,8 +111,6 @@ const { data: demosData } = useLazyFetch<{ items: HomeDemoItem[] }>('/api/skill-
110111
const recentUpdates = computed(() => updatesData.value?.items ?? [])
111112
const recentPublishes = computed(() => publishesData.value?.items ?? [])
112113
113-
const trendingRepos = computed(() => trendingData.value?.items ?? [])
114-
115114
/** Owners whose avatar failed, which means the GitHub account is gone; see `/skills/trending`. */
116115
const missingAvatars = ref(new Set<string>())
117116
function onAvatarError(owner: string) {
@@ -132,9 +131,7 @@ const showTrending = computed(() => (trendingData.value?.board.length ?? 0) >= H
132131
const weekBoardPath = trendingRangeMeta('week').path
133132
134133
/** Real Skill names from this week's trending repositories, for the hero texture. */
135-
const heroTextureNames = computed(() =>
136-
trendingRepos.value.flatMap(repo => repo.skills.map(skill => `${repo.owner}/${repo.repo}/${skill.name}`)),
137-
)
134+
const heroTextureNames = computed(() => trendingData.value?.textureNames ?? [])
138135
139136
/**
140137
* Track order, measured 2026-09-04 rather than assumed.
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
# ADR-0013: Artifact size limits and linked files
2+
3+
Date: 2026-10-07
4+
5+
Amends [ADR-0005](0005-v3-cli-harness-artifact-delivery.md).
6+
7+
## Context
8+
9+
A public build read every Skill file into memory, then packed a second copy as the archive.
10+
Its limits were 10 MiB an Artifact, 2 MiB a file, and 900 files.
11+
Only the 900 had a recorded reason: one blob read per file, against 1,000 subrequests.
12+
13+
#487 kept Skills over those limits running by leaving files out.
14+
`latent-spaces/brag` then ran without the four music files its films use.
15+
`oaustegard/claude-skills/tree-sitting` ran without eight of its parsers.
16+
17+
Each limit now names the constraint that sets it, measured on 2026-10-07.
18+
19+
## Constraints
20+
21+
| Constraint | Real limit | Old limit | New limit |
22+
|---|---|---|---|
23+
| Worker memory | 128 MB per isolate, shared with site requests | 10 MiB of files plus their 10 MiB archive, held at once | None from size. A build holds one network chunk, one inflate write of at most 16 KiB, SKILL.md, and the archive when it is at most 16 MiB |
24+
| Worker CPU | 300 s for the queue consumer | Not measured | 1 GiB of archive per pass. Inflating and walking costs 4 to 5 ms per MiB, so about 5 s |
25+
| Wall time | 15 min for the consumer. The skilld CLI waits 60 s for one Resolution | Not measured | A pass stops after the last Skill file. Cold codeload served PostHog/posthog (191 MiB gzipped) in 26 s |
26+
| Subrequests | 1,000 per invocation | 900 files, one blob read each | No file limit. One or two archive reads, and at most 400 single-file reads for files the archive lacks |
27+
| R2 put | 5 GiB in one part | Bytes in memory | A stream of known length in one part. R2 checks it against the SHA-256 the scan found |
28+
| D1 row | 2,000,000 bytes | Not measured | A file list of 448 KiB. The Resolution row holds about 3.34 statements plus the check results |
29+
| Signer | 128 MB per isolate | 10 MiB, read whole | 64 MiB, hashed as it streams |
30+
| skilld CLI 3.0 to 3.6.2 | 64 MiB download, 2,000 files, 6 MiB statement. Before 3.6.2 a download gets 30 s, so 64 MiB needs 2.2 MB/s | 10 MiB, 900 files | 64 MiB and 2,000 files. These two now bind |
31+
| USTAR | A path of at most 256 bytes | Checked after packing | Refused before any byte is read |
32+
| [GitHub](https://github.com) | 100 MiB a file | Not applicable | Linked files of at most 256 MiB in all |
33+
34+
A private build keeps 10 MiB, 2 MiB a file, and 900 files.
35+
It still reads one blob per request and encrypts the archive in memory.
36+
37+
## Decision
38+
39+
### A build streams the Repository archive
40+
41+
A public build reads `https://codeload.github.com/{owner}/{repository}/tar.gz/{commit}`.
42+
That is one subrequest and spends no REST quota.
43+
It inflates the archive in 16 KiB writes and keeps only the Skill folder's entries.
44+
It stops reading after the last Skill file.
45+
46+
The Git tree still names every file, its mode, its size, and its blob SHA.
47+
Each file's bytes are hashed as they pass and must match that blob SHA.
48+
The archive is only a byte source, as before.
49+
50+
A file the archive lacks, or holds with another size, is read from `raw.githubusercontent.com` at the same commit.
51+
Git LFS pointers, `export-ignore`, and `export-subst` produce those cases.
52+
A file whose archive bytes keep their size and fail the digest is read the same way on a second pass.
53+
54+
Artifact order is the UTF-8 byte order of paths.
55+
It is the order a Git archive lists a tree in.
56+
All 487 archives of the 2026-10-07 sweep listed their files in that order.
57+
58+
The first pass learns the archive digest and size, the file inventory, and the check results.
59+
An archive of at most 16 MiB stays in memory, and R2 stores it from there.
60+
A larger one is read from GitHub a second time and streams into R2 under the digest from the first pass.
61+
R2 refuses the put if one byte differs.
62+
The store happens before the checks are recorded.
63+
The CLI gives each stage after the checks 15 seconds.
64+
65+
### Limits
66+
67+
A public Artifact holds at most 64 MiB and 2,000 files.
68+
A file has no limit of its own.
69+
70+
When the files exceed a limit, the largest leave the archive first.
71+
For a CLI without linked files, only files the Skill does not read leave, and the `omitted-files` check lists them.
72+
That is the #487 rule at the new limits.
73+
More files than 2,000 leave the same way, so linked files do not change the file count.
74+
75+
### Linked files
76+
77+
A skilld CLI that sends `Skilld-Capabilities: linked-files` on `POST /api/v1/resolutions` may get linked files.
78+
The Resolution stores that as `linked_files`, and the request fingerprint names it.
79+
80+
When the archive would pass 64 MiB, the largest files other than SKILL.md become linked files, up to 256 MiB.
81+
The signed statement lists each one:
82+
83+
```json
84+
{
85+
"linkedFiles": [
86+
{ "path": "scripts/KimiXlsx", "mode": 420, "size": 77001601, "gitBlobSha": "<40 hex characters>" }
87+
]
88+
}
89+
```
90+
91+
The CLI verifies the attestation first.
92+
It then reads each linked file from `https://raw.githubusercontent.com/{owner}/{repository}/{commitSha}/{skillPath}/{path}`, each path segment percent-encoded.
93+
It refuses the Skill unless every file has the declared size and Git blob SHA.
94+
It installs the file with the declared mode, beside the archive's files.
95+
The bytes never pass through skilld.dev, and the signature still covers them through the blob SHA.
96+
97+
The field is present only when it is not empty.
98+
A CLI without linked files never receives it, because it refuses a statement field it does not know.
99+
A build with linked files serves only a CLI that reads them.
100+
A build that left nothing out serves both kinds.
101+
102+
## Consequences
103+
104+
`latent-spaces/brag` runs with its music: 289 files, 16.03 MiB, nothing left out.
105+
`tt-a1i/archify` runs whole at 10.36 MiB.
106+
`thedivergentai/gd-agentic-skills/godot-master` runs with 1,728 files.
107+
`thvroyal/kimi-skills/kimi-xlsx` runs its 73 MiB binary as a linked file for a CLI that reads one.
108+
109+
A first run of a Skill late in a large archive can take longer than the CLI's 60-second wait.
110+
The build still finishes, and the next run reuses it.
111+
112+
The previous policy `2026-10-07.2` stays signable. This change closes no safety gap.
113+
A ready build under that policy that left nothing out packs the same files, so it is checked again from its bytes.
114+
A stored archive over 16 MiB is never read back whole. It loads from GitHub.
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# ADR-0014: Symbolic links in a Skill folder
2+
3+
Date: 2026-10-07
4+
5+
Amends [ADR-0013](0013-artifact-size-limits-and-linked-files.md).
6+
7+
## Context
8+
9+
A build refused every Skill folder that held a symbolic link, with `INVALID_SOURCE: The Skill source layout was rejected.`
10+
The 2026-10-07 sweep found 108 such Skills, the largest class still refused.
11+
12+
- 103 are `simota/agent-skills/*`. Each Skill folder links the shared `_common` folder at the Repository root, and some link `_templates` too.
13+
- `austintgriffith/ethskills` links `AGENTS.md`, `CLAUDE.md`, and `llms.txt` to `SKILL.md`.
14+
- `garrytan/gstack` links `connect-chrome` to `open-gstack-browser`.
15+
- The two `twostraws` Skills link `skills/<name>/references` to the Skill's own `references`.
16+
- `oaustegard/claude-skills/agent-routing` links `agent-routing` to `../../agent-routing`, outside the Repository.
17+
18+
`tar --dereference` cannot pack simota/agent-skills.
19+
`_common` links every Skill folder back, so tar recursed 5,800 folders deep and crashed.
20+
21+
## Decision
22+
23+
A build follows each symbolic link in the Skill folder at the same commit.
24+
The skilld CLI receives regular files, so the attestation, the Resolution answer, and the CLI are unchanged.
25+
26+
### Resolution
27+
28+
The link blob holds a path. The build reads it through the REST API and checks it against the blob SHA.
29+
It resolves the path from the link's own folder by POSIX rules: `..` leaves the folder, and a link on the way resolves in turn.
30+
31+
- A file becomes one file at the link path.
32+
- A folder becomes every file in it, under the link path.
33+
34+
Each file keeps its own Git blob SHA from the tree, so the build checks its bytes like any other file.
35+
The files count against the limits of ADR-0013. A file a link put in the Skill is never a linked file: [GitHub](https://github.com) serves no file at the link path.
36+
37+
### Refused
38+
39+
These links refuse the Skill, with the existing summary and one finding each:
40+
41+
| Link | Finding |
42+
|---|---|
43+
| An absolute path | `the target is an absolute path` |
44+
| `..` past the Repository root | `the target is outside the Repository` |
45+
| A path through `.git` | `the target is inside .git` |
46+
| A path that does not exist at the commit | `the target does not exist at this commit` |
47+
| A path into a Git submodule | `the target is in a Git submodule` |
48+
| A chain that comes back to a link it passed | `the link loops` |
49+
| A chain through more than 8 links | `the target passes through more than 8 links` |
50+
| A folder above the Skill folder that holds it | `the target folder holds the Skill folder` |
51+
| A blob that is not a UTF-8 path of at most 4,096 bytes | `the link does not hold a valid path` |
52+
53+
A Skill folder may hold 64 links, and following them may cost 128 GitHub reads.
54+
55+
### Left out
56+
57+
Two kinds of link are left out. Following them would repeat the same files without end.
58+
59+
- A link inside a folder that a link names. The build does not read it.
60+
- A link to a folder inside the Skill that holds the link, such as `atlas/reference/atlas -> ../../atlas`. The Skill already packs every file in it.
61+
62+
Refusing the second kind would keep 22 of the 103 simota Skills refused.
63+
64+
### The check result
65+
66+
The `symbolic-links` check is not required, so a released CLI accepts it.
67+
It warns when the Skill folder holds a link, and lists each one: `_common -> ../_common` when followed, or the link and the reason when left out.
68+
A stored build that followed a link is not checked again from its bytes, because the bytes do not show which files a link put there.
69+
70+
### The archive
71+
72+
The bytes of a file a link put in the Skill sit at the target path in the Repository archive, not at the link path.
73+
So the archive no longer lists every file in Artifact order.
74+
75+
- An entry that passes before its turn stays in memory, up to 8 MiB in one pass.
76+
- A file whose target comes later waits for it, when the files it holds back fit in that memory.
77+
- Any other file reads its bytes from `raw.githubusercontent.com` at the target path.
78+
79+
A Skill without links streams as before and holds nothing.
80+
81+
### Private builds
82+
83+
A private build follows links by the same rules. It reads one blob per file, so the reads the links cost come out of its 900-file limit.
84+
85+
## Consequences
86+
87+
A sweep built the 108 Skills with this change on 2026-10-07:
88+
89+
| | Before | After |
90+
|---|---|---|
91+
| Ready | 0 | 107 |
92+
| Refused | 108 | 1, `oaustegard/claude-skills/agent-routing`, whose only link leaves the Repository |
93+
| Files read from `raw.githubusercontent.com` | | 2, both in `garrytan/gstack` |
94+
| Slowest build | | 1.1 s |
95+
| Most memory one build added | | 11.5 MiB, `garrytan/gstack` |
96+
97+
The policy is `2026-10-07.4`. The previous policy `2026-10-07.3` stays signable, because it refused every link.
98+
A ready build under it held no link, so it is checked again from its bytes.
99+
100+
The skilld CLI reads an installed copy back as a local Skill on `skilld update`, and refuses one of more than 512 files.
101+
`simota/agent-skills/hone` packs 527 files and `garrytan/gstack` 2,000. A run is not affected.

0 commit comments

Comments
 (0)