Chapter 1 of 7 · 10 min read
Six copies in Slack are not a shared artefact
On this page
Marta's svc-scaffold skill turned a one-line service description into a working repository skeleton: a FastAPI app, the company's standard health and readiness endpoints, a Dockerfile built on the platform team's base image, and a CI workflow with the right approval gates already wired in. She had been using it daily for two months and it had saved her most of a morning on each of the four services she had stood up in that time. On a Tuesday she zipped the directory — 41 KB, four files — and posted it in #eng-tooling with a note saying to unzip it into the skills directory and to pip install jinja2 httpx first if you did not already have them.
Six people did that within a day, and five of them reported it working. The sixth said the generated Dockerfile looked odd and then went quiet, which everyone read as the problem having resolved itself. Three weeks later two engineers filed complaints in the same thread: the scaffolds they were generating had an openapi.json with an empty paths object, which meant the contract tests in the generated CI workflow passed trivially and the service shipped without any of the checks the workflow existed to run. One of them had already merged such a service to main.
The failure sits in the step between Marta's laptop and everyone else's, and that step is the subject of this topic: distribution and reuse — getting something that works on one machine onto other people's machines in a form they can install, run, identify and update, and keeping it working there while it changes. Three kinds of thing travel that way in agent work. A skill is a folder of written instructions and supporting files that an agent loads when a request matches its description, of the sort 12.6 covered testing. An MCP server — Model Context Protocol, the standard interface through which an agent reaches tools and data that live outside it — is a process other people's agents call over that interface. And shared configuration is the settings, permissions and hooks deciding what an agent is allowed to do, which 12.10 covered for your own machine. Each of the three usually starts as something one engineer built for themselves, and each becomes a dependency of other people's work the first time a second person copies it.
The engineering that makes that survivable is packaging: turning a directory that happens to run on your laptop into an artefact with an identity separate from any machine — pinned dependencies, declared entry points, no absolute paths into your home directory, and no credentials it quietly borrows from your shell. You meet the need for it roughly a week after you build something good enough that a colleague asks for a copy. You meet its absence in the form Marta was about to: a report of broken output that cannot be tied to any particular revision, and a fix with no route to the people running the broken one.
The bug report that cannot be filed
Marta's first question was the ordinary one, and it had no answer. She asked which version they were on. There was no version, because there had never been a build, a tag or a commit — the zip was a snapshot of a working directory taken at whatever state it happened to be in on a Tuesday afternoon, and her own copy had moved on since. She asked people to send their copies back. Six directories came in, and the four files in them produced four distinct sets of checksums: two people had the Tuesday snapshot untouched, two had edited SKILL.md to add their team's service-naming convention, one had rewritten a template because the health-check path was wrong for their gateway, and one had a copy of a copy that a seventh person had forwarded from a DM, which turned out to predate the zip by a week.
The failure itself was mundane once she could see a copy. Her helper script shelled out to render-openapi, a CLI the platform team publishes, and passed it --out. She had 3.4.1 installed from a Homebrew tap eight months earlier. The two engineers reporting broken output had installed it fresh in the last month and got 4.0, which renamed the flag to --output and, rather than erroring, treated the unrecognised --out as a positional argument, wrote nothing, and exited 0. The script saw a zero exit code, found a stale openapi.json left by an earlier step, and carried on.
Compare that with what happens when a Postgres driver misbehaves. You say "psycopg 3.1.18", the maintainer checks out that tag, reproduces against the same pinned dependency tree, fixes it, cuts 3.1.19, and everyone affected moves by changing one line in a lockfile. Every step of that depends on the artefact having an identity separate from any particular machine. What went into #eng-tooling had no identity, so none of the steps were available: Marta could not reproduce a specific failure, could not describe the fix as "upgrade to X", and had no channel that would reach exactly the people running the broken revision. Her actual remedy was to post a second zip and hope, which reproduced the original problem with one more generation of copies in circulation.
Everything that was really on Marta's machine
The zip contained what Marta had written. What made it work was a much larger set of facts about her laptop, none of which travelled. Enumerating them is the whole of packaging, and the enumeration for this skill looked like this.
| What the skill relied on | Where it came from on Marta's machine | What the zip said | What that produced elsewhere |
|---|---|---|---|
jinja2, httpx | Global site-packages, whatever versions a 2023 project had installed | "pip install jinja2 httpx" in a Slack message | Worked, by luck: no incompatible major had shipped |
render-openapi CLI | Homebrew tap, 3.4.1, installed 8 months earlier | Nothing | 4.0 renamed a flag, exited 0, wrote no output |
| Template directory | /Users/marta/dev/scaffold-templates, a sibling checkout | Nothing | Script fell back to a minimal built-in template |
SCAFFOLD_TOKEN | Exported in .zshrc from a previous project | Nothing | Registry calls returned public defaults instead of the company's |
Three of those four rows are silent failures, which is the part worth sitting with. An undeclared dependency that is missing entirely announces itself with an ImportError on the first run, and someone fixes it in a minute. An undeclared dependency that is present at the wrong version, a path that has a fallback, or a credential that changes which data you get rather than whether you get any, all produce output that looks right. That is why the two reports took three weeks to arrive and why the sixth person's "looks odd" got dropped.
The offending part of the helper script showed all four problems in about fifteen lines:
import subprocess, os, json
from pathlib import Path
TEMPLATES = Path("/Users/marta/dev/scaffold-templates")
def build_spec(service: str, out_dir: Path) -> dict:
subprocess.run(
["render-openapi", "--out", str(out_dir / "openapi.json"), service],
check=True,
)
return json.loads((out_dir / "openapi.json").read_text())
def registry_defaults(service: str) -> dict:
token = os.environ.get("SCAFFOLD_TOKEN", "")
resp = httpx.get(f"https://registry.internal/defaults/{service}",
headers={"Authorization": f"Bearer {token}"})
return resp.json()check=True is doing nothing useful here because the failing tool exits 0. os.environ.get with a default of "" turns a missing credential into a different code path rather than a stop. The absolute path is the most obviously wrong line and the least harmful, because at least a colleague hitting it gets a template that is visibly wrong rather than a spec that is invisibly empty.
What a package has to declare
The corrected version is not more sophisticated; it is more explicit. The skill and its script move into a repository with a dependency manifest, the templates come inside the package instead of being read from a neighbouring checkout, the external CLI becomes a pinned Python dependency rather than an ambient binary, and the credential becomes a named requirement that the code refuses to run without.
svc-scaffold/
pyproject.toml
README.md
src/svc_scaffold/
__init__.py
__main__.py # entry point: python -m svc_scaffold
preflight.py
templates/ # packaged data, not a sibling checkout
skills/svc-scaffold/
SKILL.md[project]
name = "svc-scaffold"
version = "0.4.0"
requires-python = ">=3.11"
dependencies = [
"jinja2==3.1.4",
"httpx==0.27.2",
"render-openapi==3.4.1", # 4.0 renames --out to --output
]
[project.scripts]
svc-scaffold = "svc_scaffold.__main__:main"
[tool.setuptools.package-data]
"svc_scaffold" = ["templates/**"]The comment on the render-openapi pin is worth more than the pin. Six months from now somebody will want to relax it, and the line tells them what they are agreeing to check. Pinning without recording why is how a lockfile becomes a wall of numbers nobody dares to touch.
A manifest that pins direct dependencies still leaves the transitive set floating, and that gap catches most first attempts at this. httpx==0.27.2 says nothing about which h11, anyio or certifi a resolver will choose, so an install run today and the same install run in November can produce different trees from an identical manifest. For a script doing modest templating and one HTTP call that difference is unlikely to matter for a while; for anything that parses another tool's output, formats dates, or branches on a library's exception messages, it eventually does, and it does so on somebody else's machine rather than yours. The cheap discipline is to commit a lock file next to the manifest — uv.lock, poetry.lock, or a fully-resolved requirements.txt from pip-compile — and to have the documented install command install from the lock rather than resolve afresh. The manifest states the range the package intends to support, the lock states the tree it was actually tested against, and a package shipping only one of the two is either untestable or unupgradable.
Secrets get the opposite treatment from dependencies: dependencies are bundled, credentials are declared and refused. The package should know the name of every environment variable it needs, and should stop with a message naming the variable rather than continuing with an empty string.
REQUIRED_ENV = {
"SCAFFOLD_TOKEN": "Registry read token. Request via #platform-access; scope: registry:read",
}
def preflight() -> None:
missing = [f"{k}: {why}" for k, why in REQUIRED_ENV.items() if not os.environ.get(k)]
if missing:
raise SystemExit("svc-scaffold cannot run. Missing configuration:\n " + "\n ".join(missing))That is four lines of behaviour and one line of documentation that cannot drift out of date, because it lives next to the check that enforces it. 12.10 established the hygiene rules for your own configuration — the same discipline applied to a shared package means the package states what it needs rather than inheriting what happens to be exported.
Entry points deserve the same explicitness. "Run python helper.py" works only from the directory the file sits in, with the interpreter that happens to be first on the path. A declared console script or a python -m svc_scaffold invocation resolves against the installed package wherever the caller happens to be standing, which is what lets the SKILL.md body reference the command without also describing a directory layout. The same reasoning applies to every file the skill body points at: a line reading "see /Users/marta/dev/scaffold-templates/gateway.md for the health-check convention" is a local path wearing documentation's clothes, and it fails in the quietest possible way, because a model that cannot read the referenced file will usually proceed on what it can infer from the file name. References inside a package are relative to the package, and the packaging step is where that gets enforced rather than remembered.
What packaging does not do
Packaging is not distribution. A repository with a perfect pyproject.toml that nobody can find, that has no install instruction anyone has followed, and that lives under one engineer's personal namespace, is a well-built artefact with the same adoption as the zip. Distribution is a separate problem with its own machinery — a marketplace repository, an internal registry, a plugin manifest the team's tooling reads — and the official guide to creating and distributing a plugin marketplace is where to look when the artefact is ready and the question becomes how people get it.
Packaging is also not correctness. A pinned, path-free, secret-declaring package that generates the wrong Dockerfile is reproducibly wrong, which is an improvement over irreproducibly wrong but not the same as working. 12.6 covered how to test and iterate on the skill itself, and none of that work is displaced by anything here; packaging determines whether the thing you tested is the thing your colleague runs.
And packaging is not versioning. The version = "0.4.0" line in the manifest is a string until something enforces a relationship between the number and the behaviour, and a number that increments whenever someone remembers is barely better than no number at all. What a version has to earn is the ability for a consumer to tell a compatible release from an incompatible one, which is a question about the boundary the artefact exposes rather than about the file it is written in.
The immediate gap is smaller and more awkward than either of those. Marta could do everything in this chapter, post the repository, and get six colleagues to install it successfully — and still not know whether it is packaged, because every one of those six is running a machine that was already prepared. Their success is evidence about a population selected for having the missing pieces already installed. The test that would actually reveal an undeclared assumption has not been run yet.
Distribution and reuse — read the current version at https://daviddominguez.dev/learn/en/distribution-and-reuse/six-copies-in-slack-are-not-a-shared-artefact