PUBLISHING COMMANDS
swamp provides commands for releasing and promoting extensions: fmt formats
and lints the source, quality scores it against the rubric, push validates
and uploads it, and promote moves a published version to a higher release
channel. fmt, quality, and push take a manifest and resolve the files it
references. For the consumer-side commands (pull, install, list, search,
update, outdated, rm, info, version), see
Management.
Manifest argument
The <manifest-path> argument of fmt, quality, and push is a manifest
file or an extension directory. A directory means the manifest.yaml,
manifest.yml, or manifest.json inside it, in that order. An absolute path is
used as given. A relative path is tried against each of these, and the first
match wins:
--extensions-dir, when set.- The current directory.
- The repository directory (
--repo-dir/SWAMP_REPO_DIR).
| Condition | Error |
|---|---|
| A directory with no manifest | No manifest.yaml found in <dir>, followed by Pass the extension directory that holds manifest.yaml, or the manifest file itself. |
| No candidate exists | Manifest file not found: <arg> (looked in <candidates>) |
A .ts or .js file |
Expected a manifest path but got a TypeScript/JavaScript file: <path> |
Entries in the manifest resolve from the extensions root: the directory that
contains extensions/. --extensions-dir sets the root explicitly. Without the
flag, Swamp uses the repository directory, unless no entry resolves there; it
then uses the nearest ancestor of the manifest that holds extensions/ or
.swamp.yaml, never the home directory. An entry found under both the
repository directory and that ancestor is refused, naming both paths. Models,
vaults, datastores, reports, workflows, and skills all resolve from the
extensions root. With paths.base: manifest, entries resolve next to the
manifest instead; see
Manifest § Path Resolution.
Every not-found error lists the paths it looked in and names --extensions-dir
or paths.base: manifest as the fix.
An additionalFiles, binaries, or include entry must name a regular file. A
directory, or any other entry that is not a regular file, stops all three
commands before they read any file, with a validation error that names the entry
and its field:
Error: Additional file is a directory: docs (at .../extensions/models/docs). additionalFiles lists files, not directories; list each file under docs/ individually.The binaries and include forms begin Binary file is a directory: and
Include file is a directory:. With --json, the same message is the error
field of the stderr document.
fmt, quality, and push use the same project deno.json: the first one
found walking up from the manifest's directory. For a manifest inside the
repository directory the walk stops at the repository directory; for one outside
it, at the extensions root that contains the manifest, or at the manifest's own
directory when neither contains it.
All three commands accept the global flags --json, --log, --quiet,
--verbose, --no-color, --no-telemetry, and --log-level <level>. The
command-specific flags are listed below.
fmt
Formats and lints the TypeScript files referenced by the manifest. Resolves each
model, vault, datastore, and report entry point plus its local imports, then
runs deno fmt and deno lint --fix over them under the project deno.json
(see Manifest argument), the same configuration push
checks against, so fmt --check and push give the same answer. The
no-import-prefix lint rule is never applied to extension code.
Flags
| Flag | Description |
|---|---|
--check |
Check only; do not auto-fix. Exits non-zero on issues. |
--repo-dir <dir> |
Repository directory (env: SWAMP_REPO_DIR). |
--extensions-dir <dir> |
Extensions root: the directory that contains extensions/ (models, workflows and skills resolve from it; env: SWAMP_EXTENSIONS_DIR). |
Output
... [INF] extension·fmt: Formatted 1 TypeScript files.
... [INF] extension·fmt: "Checked 1 file"
... [INF] extension·fmt: "Checked 1 file"In --check mode, with nothing to fix:
... [INF] extension·fmt: All quality checks passed.In --check mode, formatting or lint issues are reported under
Quality checks failed: and the command exits non-zero. Unfixable lint issues
also exit non-zero without --check. push runs the equivalent of --check
before uploading and is blocked if it fails.
quality
Packages the extension, scores it against the quality rubric, and runs the local
checks a push runs: content collectives, the additionalFiles allowlist,
safety, dependency trust, skills, formatting and lint, the upgrade chain, review
errors, and archive size. Scoring does not gate publishing — the score is
informational.
A failed check does not stop scoring. quality prints the rubric, then each
failed check under Checks that would block a push: with its details, then the
files a check rejected, which the scored archive leaves out, and exits non-zero.
See Output (failed checks).
When every check passes, quality writes the packaged tarball to
.swamp/cache/packages/<hash>/ for reuse by push. A cache entry is reused
only by the Swamp version that wrote it; a different version packages afresh. A
run with a failed check writes no cache entry.
The quality command also checks for bare import specifiers (from "zod" instead
of from "npm:zod@4") in the extension's source files. The registry's scorer
cannot resolve bare specifiers, so the extension would publish unscored.
quality still prints the rubric, which resolves the import map locally, and
reports a bare-specifiers finding that names each bare import's target from
the import map. It then exits non-zero; with --json, registryScorable is
false. See Output (bare specifiers).
Flags
| Flag | Description |
|---|---|
--repo-dir <dir> |
Repository directory (env: SWAMP_REPO_DIR). |
--extensions-dir <dir> |
Extensions root: the directory that contains extensions/ (models, workflows and skills resolve from it; env: SWAMP_EXTENSIONS_DIR). |
Output
... [INF] extension·quality: Packaging extension for quality scoring...
... [INF] extension·quality: Scoring extension against Swamp Club quality rubric...
... [INF] extension·quality: Rubric v3 — 12/12 points (100% (+2 pending server verification), all factors earned)
... [INF] extension·quality: "✓" "has-readme" ["2/2"] — "Has README or module doc"
... [INF] extension·quality: "✓" "readme-example" ["1/1"] — "README has a code example"
... [INF] extension·quality: "✓" "rich-readme" ["1/1"] — "README is substantive"
... [INF] extension·quality: "✓" "symbols-docs" ["1/1"] — "Most symbols documented"
... [INF] extension·quality: "✓" "fast-check" ["1/1"] — "No slow types (deprecated)"
... [INF] extension·quality: ℹ This factor is deprecated and will be removed in a future release.
... [INF] extension·quality: "✓" "description" ["1/1"] — "Has description"
... [INF] extension·quality: "✓" "platforms" ["2/2"] — "Platform support declared (or universal)"
... [INF] extension·quality: "✓" "has-license" ["1/1"] — "License declared"
... [INF] extension·quality: "~" "repository-verified" ["0/2"] — "Verified public repository (server confirms on publish)"
... [INF] extension·quality: "✓" "dependency-trust" ["2/2"] — "Dependencies pass trust gates"
... [INF] extension·quality: No npm/jsr dependencies to audit
... [INF] extension·quality: For next time:
... [INF] extension·quality: testing-completeness — text_entropy.ts: No sibling `_test.ts` found — cover both success and failure paths with unit tests before publishing.
... [INF] extension·quality: fix: Add a sibling <name>_test.ts covering the success and failure paths.
... [INF] extension·quality: or accept it, at the top of text_entropy.ts:
... [INF] extension·quality: // swamp-quality-ignore testing-completeness: <reason>
... [INF] extension·quality: Packaged archive: 2027 bytesA cache hit replaces the first line with
Cache hit — reusing previously packaged tarball ("<hash prefix>") and runs
every check a push's cache hit runs.
quality also runs the review rules and declared acceptances on every run,
including a cache hit, and ends with the Accepted, with reasons and
For next time blocks; see Declared acceptances.
Output (failed checks)
An extension that lists a hidden file in additionalFiles fails the safety
check. The rubric is printed first, scored without the rejected file:
... [INF] extension·quality: Packaged archive: 2028 bytes
... [ERR] extension·quality: Checks that would block a push:
... [ERR] extension·quality: "safety": "Extension has safety errors that must be resolved before pushing."
... [ERR] extension·quality: ".../extensions/models/.notes.txt: Hidden files are not allowed in extensions."
... [WRN] extension·quality: The rubric above scores the archive without the 1 file(s) a check rejected:
... [WRN] extension·quality: ".../extensions/models/.notes.txt"
Error: Extension quality failed: 1 check(s) would block a push (safety).The checks run, and are named, in this order: content-collectives,
additional-files, safety, dependency-trust, skills, fmt-lint,
upgrade-chain, review, and archive-size.
Output (bare specifiers)
... [INF] extension·quality: For next time:
...
... [INF] extension·quality: bare-specifiers — (multiple files): Extension uses bare import specifiers, which the registry's scorer cannot resolve, so it would publish unscored. Replace "zod" with "npm:zod@4".
... [INF] extension·quality: fix: Replace each bare name with its import-map target from deno.json (an explicit npm: or jsr: specifier such as npm:zod@4) so the registry scorer can resolve imports. Swamp's lint never applies no-import-prefix, so deno.json needs no exclude for it.
... [INF] extension·quality: Packaged archive: 2025 bytes
... [ERR] extension·quality: The registry cannot score this extension: it uses bare import specifiers, so it would publish unscored. The rubric above resolves the import map locally; the registry does not.
Error: Extension quality failed: the registry cannot score this extension (it uses bare import specifiers).JSON output
With --json, quality prints one document on stdout, including when it fails:
| Field | Type |
|---|---|
status |
"passed", or "failed" when a check failed or the registry cannot score the extension |
allPassed |
boolean; the rubric's own result, independent of status |
rubricVersion, earnedPoints, maxEarnablePoints, maxClientEarnablePoints, provisionalPoints, percentage |
number |
factors |
array of { id, label, earnedPoints, maxPoints, status } |
dependencyTrust |
object; the dependency-trust audit |
registryScorable |
boolean; false when the extension uses bare import specifiers |
gateFailures |
array of { gate, message, details? }, one per failed check |
excludedFromArchive |
array of absolute paths a check rejected |
cacheHash |
string; the content hash a dry run reports as contentHash |
cacheHit |
boolean |
archiveSize |
number (bytes) |
warnings, reviewRuleWarnings |
arrays of safety and review findings |
declaredAcceptances, unresolvedWarnings |
present when any acceptance applied or a warning remains; see Declared acceptances |
The failed-check example above prints:
{
"status": "failed",
"allPassed": true,
"registryScorable": true,
"gateFailures": [
{
"gate": "safety",
"message": "Extension has safety errors that must be resolved before pushing.",
"details": {
"safetyErrors": [
{
"ruleId": "hidden-file",
"file": ".../extensions/models/.notes.txt",
"message": "Hidden files are not allowed in extensions."
}
]
}
}
],
"excludedFromArchive": [".../extensions/models/.notes.txt"],
"cacheHit": false
}The document is trimmed to the fields that changed; the rubric fields are
present as in a passing run. A validation error that stops quality before
packaging, such as a directory entry, prints {"error": "..."} on stderr and no
stdout document.
repository-verified is provisional locally when the URL is well-formed and on
an allowlisted host, and missing otherwise. The registry awards it at publish
time, after its public-reachability check or, for a private extension, through
the collective's
private source hosting.
See the Scorecard Rubric for
every factor and its points.
push
Validates and uploads the extension to the registry. Without --dry-run, the
upload is a three-phase exchange: initiate, upload archive, confirm.
Flags
| Flag | Description |
|---|---|
--dry-run |
Build the archive and run the registry checks read-only, without pushing. |
-y, --yes |
Skip confirmation prompts; the summary records any warnings this waived. |
-f, --force |
Skip confirmation prompts (alias for --yes). |
--visibility <visibility> |
Publication visibility: public (registry default) or private; overrides manifest visibility. Omit to use the manifest or registry default. |
--release-notes <text> |
Per-version release notes (max 5000 chars). |
--channel <channel> |
Release channel: beta, rc, or stable (default: stable). |
--version-suffix <type> |
Override the version's micro segment: epoch uses the Unix timestamp (e.g. 2026.06.18.1750263600). |
--skip-upgrade-check |
Suppress the warning when a model version is bumped without upgrade entries. |
--repo-dir <dir> |
Repository directory (env: SWAMP_REPO_DIR). |
--extensions-dir <dir> |
Extensions root: the directory that contains extensions/ (models, workflows and skills resolve from it; env: SWAMP_EXTENSIONS_DIR). |
Stages
- Parse the manifest and validate required fields.
- Run the registry identity checks: authentication, reserved collective, collective membership, and, for private intent, private entitlement.
- Resolve entry points, their local imports,
includefiles, workflows, and skills, and detect the project'sdeno.json(orpackage.json). - Validate
quality.yamlbeside the manifest, when present. - Run the safety analysis over all files.
- Run the dependency-trust audit.
- Run the formatting, lint, and upgrade-chain checks.
- Run the review rules and apply declared acceptances.
- Bundle each entry point and build the
tar.gzarchive, or reuse the archivequalitycached for the same content hash with the same Swamp version. - Check that the version is not published on any channel.
- Run the version-drift check (advisory).
- Upload (initiate, upload, confirm) — skipped under
--dry-run.
A real push stops at the first failed registry check. A dry run runs every check, reports each one, and never prompts or writes to the registry.
Registry checks
| Check | Log label | Passes when |
|---|---|---|
authentication |
authentication |
The registry accepts the stored credentials. |
reserved-collective |
reserved collective |
The collective is not reserved, or the registry verified membership of it. |
collective-membership |
collective membership |
The manifest's collective is one of the account's collectives. |
private-entitlement |
private entitlement |
The collective's plan allows private extensions. Runs for private intent only. |
version-exists |
version exists |
The manifest version is not published on any channel. |
The checks run and are reported in the order above. private-entitlement is
omitted when collective-membership did not pass. Each check ends in one of
three statuses:
| Status | Meaning |
|---|---|
passed |
The check ran and the push would proceed. |
failed |
The check ran and the push would stop, with the message a real push fails with. |
not-run |
The check could not run. cause names why. |
| Cause | Meaning | Dry-run exit |
|---|---|---|
registry-unavailable |
The registry did not answer the request the check needs. | Non-zero |
entitlement-undecided |
The registry answered, but reported no entitlement for the collective, or a trial state the CLI does not know. | Zero |
authentication-failed |
The registry rejected the credentials; authentication itself is failed. |
Non-zero |
membership-unverified |
Membership of a reserved collective could not be verified; reserved-collective failed. |
Non-zero |
no-credentials |
No credentials were available, so the registry was not asked. | Zero |
A dry run prints its whole summary first and then exits non-zero on any failed
check or any check not run for registry-unavailable. The error line repeats
the failed check's message, or reads
Registry check "<label>" could not run: <message>.
When the registry does not answer the sign-in request, a dry run reports
authentication as not run and exits non-zero; a real push in the same
situation checks membership against the signed-in username and continues.
A passed authentication check names the credential:
| Credential | Message |
|---|---|
Personal key, signed in or in SWAMP_API_KEY |
Signed in as <username>. |
| Collective API token | Signed in with an API token for @<collective> (fingerprint <fp>). |
| Collective API token whose collective is not reported | Signed in with an API token (fingerprint <fp>). |
The fingerprint is the one swamp auth whoami prints. With a collective API
token in SWAMP_API_KEY:
... [INF] extension·push: authentication: passed — Signed in with an API token for @skunk-ape (fingerprint e83f6430f3d7a0d0).When the registry does not report the caller's collectives and the credential
has no username, collective-membership is not run (registry-unavailable):
Could not check that @<collective> is one of your collectives: the registry did not report your collectives, and this credential has no username to check against.
A real push in that case is refused with the same message and
Try again when <server> is reachable.
Version already published
A version is unique across channels. When the manifest version is already
published, the outcome depends on the channel it is on relative to the requested
--channel:
| Published on | version-exists message |
|---|---|
| The requested channel | Version <v> already exists for <name>. |
| A lower channel | Version <v> already exists for <name> on channel '<existing>'. To move it to '<requested>' without re-publishing, run: swamp extension promote <name> <v> --channel <requested> |
| A higher channel | Version <v> already exists for <name> on channel '<existing>', above '<requested>'. A version cannot move down a channel; publish a new version instead. |
A dry run reports the message as a failed check and exits non-zero. So does a
non-interactive push (--json, --yes, --force, or no terminal); with the
same channel, the error adds
Use a different version or let the CLI bump it interactively. --yes never
promotes.
An interactive push asks instead. On a lower channel it offers three choices, with no default; Enter asks again:
Version 2026.10.06.1 of @swampadmin/text-entropy already exists on channel 'beta'; you asked for 'stable'.
1. Promote: move the published 'beta' build of @swampadmin/text-entropy@2026.10.06.1 to 'stable' (your local files are not uploaded)
2. Bump: publish your local files as 2026.10.06.2 to 'stable'
3. Stop: exit; nothing is pushed
Choose [1/2/3]:Promote runs swamp extension promote in-process and exits without publishing:
... [INF] extension·push: Running: "swamp extension promote @swampadmin/text-entropy 2026.10.06.1 --channel stable"
... [INF] extension·promote: Promoting...
... [INF] extension·promote: Promoted "@swampadmin/text-entropy"@"2026.10.06.1" from "beta" to "stable"On the same or a higher channel it offers to bump the version's micro segment:
Version 2026.10.06.1 of @swampadmin/text-entropy already exists on channel 'stable'; you asked for 'beta', and a version cannot move down a channel.
y = publish your local files as 2026.10.06.2 to 'beta'; a review report for 2026.10.06.1 does not carry over to 2026.10.06.2
N = exit; nothing is pushed
Bump to 2026.10.06.2 and publish it to 'beta'? [y/N]On the same channel the first line reads
Version <v> of <name> already exists on channel '<channel>'.
Confirmation prompts
An interactive push asks up to two questions. When warnings remain after declared acceptances:
Continue with push despite warnings? [y/N]Then, before uploading, it names the channel, including the default:
y = publish @swampadmin/text-entropy@2026.10.06.2 to stable now
N = exit; nothing is pushed
Push @swampadmin/text-entropy@2026.10.06.2 to registry (channel: stable)? [y/N]Answering N to either prints Push cancelled. and exits 0. --yes and
--force answer both. A dry run never prompts.
API calls
A dry run records every HTTP request it makes before the upload: the registry lookups, and the OSV.dev and npm requests of the dependency-trust audit. Each record holds the service, method, URL, outcome, and HTTP status. Headers are never recorded, and credentials in a URL are removed. A retried request is listed once per attempt.
| Field | Values |
|---|---|
service |
registry, osv, npm, or other, from the request's host. |
method |
The HTTP method. |
url |
The request URL. |
outcome |
ok (2xx), not-found (404), or error (any other status, or no response). |
status |
The HTTP status. Absent when there was no response. |
The log summary prints API calls made (<n>): followed by one line per request,
or No API calls were made. when there were none.
Content hash
The dry run prints the archive's content hash. It covers the manifest, every
packaged source, workflow, skill, include, additional, and binary file, the
Deno configuration, and quality.yaml when present. File paths enter the hash
relative to the extensions root (see Manifest argument),
so the hash does not depend on which Swamp repository runs the dry run: from
inside the extension directory, or from a sibling repository that infers the
root from the manifest or names it with --extensions-dir, the hash is the
same. Any byte change in a packaged file, or a version change, changes the hash.
The adversarial-review report path is keyed by it. quality --json reports the
same hash as cacheHash.
To compare a local hash with the one CI reports, see Create and Publish § Match CI's content hash.
Output (dry run)
$ swamp extension push extensions/models/manifest.yaml --dry-run... [INF] extension·push: Extension: "@skunk-ape/text-entropy@2026.10.07.1"
... [INF] extension·push: Requested visibility: default (registry decides)
... [INF] extension·push: Description: "Measure the Shannon entropy of a piece of text"
... [INF] extension·push: Repository: "https://github.com/skunk-ape/text-entropy"
... [INF] extension·push: Models (1):
... [INF] extension·push: "@skunk-ape/text-entropy" ("extensions/models/text_entropy.ts")
... [INF] extension·push: Global Arguments:
... [INF] extension·push: "text": "string"
... [INF] extension·push: Additional files (2):
... [INF] extension·push: "extensions/models/README.md"
... [INF] extension·push: "extensions/models/LICENSE.md"
... [INF] extension·push: Labels: "text, analysis"
...
... [INF] extension·push: Dry run complete for "@skunk-ape/text-entropy@2026.10.07.1"
... [INF] extension·push: Archive size: "2.0KB"
... [INF] extension·push: Content hash: "bf925572727082765765218abc0b4226c2233d2bd0884c6541ee610af83e529c"
... [INF] extension·push: Registry checks:
... [INF] extension·push: authentication: passed — Signed in as skunk-ape.
... [INF] extension·push: reserved collective: passed — Collective "@skunk-ape" is not reserved.
... [INF] extension·push: collective membership: passed — Collective "@skunk-ape" is one of your collectives.
... [INF] extension·push: version exists: passed — Version 2026.10.07.1 is not published for @skunk-ape/text-entropy.
... [INF] extension·push: API calls made (3):
... [INF] extension·push: registry: GET https://swamp-club.com/api/whoami ok (200)
... [INF] extension·push: registry: GET https://swamp-club.com/api/v1/extensions/%40skunk-ape%2Ftext-entropy/versions?channel=stable&channel=rc&channel=beta&perPage=100&page=1 not-found (404)
... [INF] extension·push: registry: GET https://swamp-club.com/api/v1/extensions/%40skunk-ape%2Ftext-entropy/latest not-found (404)
...A printed path opens from the directory the command ran in: a file under that
directory prints relative to it, pushed content outside it prints relative (for
example ../text-entropy/...) unless the two share only /, and anything else,
such as the review report under the temporary directory, prints absolute.
A dry run that hits a failed check prints the same summary, then the failure:
...
... [INF] extension·push: Dry run complete for "@swamp/ssh@2026.09.28.1"
... [INF] extension·push: Archive size: "2.0KB"
... [INF] extension·push: Content hash: "8c4a87f11e3806a9c92828aa8da16ade791327ccddb891cd6e5d4356ca0d5140"
... [INF] extension·push: Registry checks:
... [INF] extension·push: authentication: passed — Signed in as skunk-ape.
... [INF] extension·push: reserved collective: passed — Collective "@swamp" is reserved; membership verified by the registry.
... [INF] extension·push: collective membership: passed — Collective "@swamp" is one of your collectives.
... [ERR] extension·push: version exists: failed — Version 2026.09.28.1 already exists for @swamp/ssh.
... [INF] extension·push: API calls made (3):
... [INF] extension·push: registry: GET https://swamp-club.com/api/whoami ok (200)
... [INF] extension·push: registry: GET https://swamp-club.com/api/v1/extensions/%40swamp%2Fssh/versions?channel=stable&channel=rc&channel=beta&perPage=100&page=1 ok (200)
... [INF] extension·push: registry: GET https://swamp-club.com/api/v1/extensions/%40swamp%2Fssh/latest ok (200)
...
Error: Version 2026.09.28.1 already exists for @swamp/ssh.When the registry does not answer:
...
... [WRN] extension·push: Version drift warnings (non-blocking):
... [WRN] extension·push: unable to check for version drift — registry lookup failed: Could not connect to https://swamp-club.com: fetch failed; the version-bump upgrade check was skipped too
... [INF] extension·push: Dry run complete for "@skunk-ape/text-entropy@2026.10.07.1"
... [INF] extension·push: Archive size: "2.0KB"
... [INF] extension·push: Content hash: "bf925572727082765765218abc0b4226c2233d2bd0884c6541ee610af83e529c"
... [INF] extension·push: Registry checks:
... [WRN] extension·push: authentication: not run — registry did not answer: Could not connect to https://swamp-club.com: fetch failed
... [INF] extension·push: reserved collective: passed — Collective "@skunk-ape" is not reserved.
... [INF] extension·push: collective membership: passed — Collective "@skunk-ape" is one of your collectives.
... [WRN] extension·push: version exists: not run — registry lookup failed: Could not connect to https://swamp-club.com: fetch failed
... [INF] extension·push: API calls made (3):
... [INF] extension·push: registry: GET https://swamp-club.com/api/whoami error
... [INF] extension·push: registry: GET https://swamp-club.com/api/v1/extensions/%40skunk-ape%2Ftext-entropy/versions?channel=stable&channel=rc&channel=beta&perPage=100&page=1 error
... [INF] extension·push: registry: GET https://swamp-club.com/api/v1/extensions/%40skunk-ape%2Ftext-entropy/latest error
...
Error: Registry check "authentication" could not run: registry did not answer: Could not connect to https://swamp-club.com: fetch failedVersion drift
The version-drift check looks up the latest published version. It is advisory
and never changes the exit code. When it cannot compare, its warning says why,
and with --json the entry under warnings.versionDrift carries a cause:
| Cause | When | Warning |
|---|---|---|
never-published |
The registry answered 404 or 410. | unable to check for version drift — no previously published version found in the registry (this is expected on first publish) |
no-credentials |
No credentials were stored. | Names the missing credentials; the version-bump upgrade check was skipped too. |
authentication-failed |
The registry answered 401 or 403. | Names the rejection; the version-bump upgrade check was skipped too. |
registry-unavailable |
No network, a 5xx, or any other error. | unable to check for version drift — registry lookup failed: <reason>; the version-bump upgrade check was skipped too |
A drift warning that found a real difference has no cause.
JSON output (dry run)
With --json, a run prints one document on stdout. Its status says how the
run ended:
status |
When |
|---|---|
dry_run |
A dry run finished, including one with failed registry checks. |
pushed |
A push completed. |
blocked |
A local check blocked the push; errors is keyed by the check. |
failed |
The run failed for any other reason; the error is on stderr. |
cancelled |
A prompt was declined. |
A dry run's document carries:
| Field | Type |
|---|---|
status |
"dry_run" |
resolved |
object; the resolved manifest, with every file name absolute |
warnings |
object; present when any warning remains, with the arrays safety, review, dependencyTrust, versionDrift, and versionBumpUpgrade, each only when non-empty |
name |
string |
version |
string |
archiveSize |
number (bytes) |
visibility |
"public", "private", or "default" (the registry decides) |
contentHash |
string |
registryChecks |
array of { name, status, message, cause?, credential? }; a failed version-exists row adds existingChannel and requestedChannel |
apiCalls |
array of { service, method, url, outcome, status? } |
acceptedWarnings |
object; present when --yes or --force waived a warning. See Waived warnings. |
declaredAcceptances |
object; present when any acceptance applied. See Declared acceptances. |
unresolvedWarnings |
array; present when a warning remains. See Declared acceptances. |
A passed authentication row carries credential, with the field names of
swamp auth whoami --json: { username, fingerprint } for a personal key, or
{ collectiveToken: true, collectiveSlug, fingerprint } for a collective API
token.
The example below is trimmed: empty arrays under resolved, the
adversarial-review-report entries, and the last two API calls are left out.
{
"status": "dry_run",
"resolved": {
"name": "@skunk-ape/text-entropy",
"version": "2026.10.07.1",
"visibility": "default",
"description": "Measure the Shannon entropy of a piece of text",
"repository": "https://github.com/skunk-ape/text-entropy",
"models": [
{
"type": "@skunk-ape/text-entropy",
"fileName": ".../extensions/models/text_entropy.ts",
"globalArguments": [
{
"name": "text",
"type": "string",
"description": "The text to measure entropy for",
"required": true
}
]
}
],
"additionalFiles": [
".../extensions/models/README.md",
".../extensions/models/LICENSE.md"
],
"labels": ["text", "analysis"]
},
"warnings": {
"review": [
{
"ruleId": "testing-completeness",
"dimension": "Testing Completeness",
"severity": "medium",
"file": ".../extensions/models/text_entropy.ts",
"message": "No sibling `_test.ts` found — cover both success and failure paths with unit tests before publishing.",
"remediation": "Add a sibling <name>_test.ts covering the success and failure paths.",
"acceptance": {
"form": "comment",
"file": ".../extensions/models/text_entropy.ts",
"line": 1,
"position": "file-header",
"text": "// swamp-quality-ignore testing-completeness"
}
}
],
"versionDrift": [
{
"check": "version-drift",
"output": "unable to check for version drift — no previously published version found in the registry (this is expected on first publish)",
"cause": "never-published"
}
]
},
"name": "@skunk-ape/text-entropy",
"version": "2026.10.07.1",
"archiveSize": 2027,
"visibility": "default",
"contentHash": "bf925572727082765765218abc0b4226c2233d2bd0884c6541ee610af83e529c",
"registryChecks": [
{
"name": "authentication",
"status": "passed",
"message": "Signed in as skunk-ape.",
"credential": {
"username": "skunk-ape",
"fingerprint": "08c75ce7fa23fa22"
}
},
{
"name": "reserved-collective",
"status": "passed",
"message": "Collective \"@skunk-ape\" is not reserved."
},
{
"name": "collective-membership",
"status": "passed",
"message": "Collective \"@skunk-ape\" is one of your collectives."
},
{
"name": "version-exists",
"status": "passed",
"message": "Version 2026.10.07.1 is not published for @skunk-ape/text-entropy."
}
],
"apiCalls": [
{
"service": "registry",
"method": "GET",
"url": "https://swamp-club.com/api/whoami",
"outcome": "ok",
"status": 200
}
],
"unresolvedWarnings": [
{
"ruleId": "testing-completeness",
"file": ".../extensions/models/text_entropy.ts",
"message": "No sibling `_test.ts` found — cover both success and failure paths with unit tests before publishing.",
"remediation": "Add a sibling <name>_test.ts covering the success and failure paths.",
"acceptance": {
"form": "comment",
"file": ".../extensions/models/text_entropy.ts",
"line": 1,
"position": "file-header",
"text": "// swamp-quality-ignore testing-completeness"
}
}
]
}A blocked run carries errors, keyed by the check that blocked it, beside any
resolved and warnings recorded before the block: safety, review,
dependencyTrust, collective, quality (formatting and lint),
upgradeChain, or compilation:
{
"status": "blocked",
"errors": {
"safety": [
{
"ruleId": "hidden-file",
"file": ".../extensions/models/.notes.txt",
"message": "Hidden files are not allowed in extensions."
}
]
}
}A run that fails for any other reason, such as a missing manifest or a version
that already exists in a non-interactive push, prints {"status": "failed"} on
stdout and the usual {"error": ...} document on stderr.
Output (successful push)
... [INF] extension·push: Pushed "@stack72/text-entropy@2026.05.31.1"
... [INF] extension·push: Channel: "stable"
... [INF] extension·push: Visibility: "public"
... [INF] extension·push: Extension ID: "bbc4f071-..."
... [INF] extension·push: Archive size: "3.5KB"
... [INF] extension·push: "Models: 1, Workflows: 0, Vaults: 0, Bundles: 1"With --json, the completed push's document has status "pushed", the
resolved manifest and any warnings, then name, version, extensionId,
archiveSize, the content counts (modelCount, workflowCount, bundleCount,
vaultCount, datastoreCount, reportCount, webhookCount, skillCount),
channel, and the applied visibility, followed by acceptedWarnings,
declaredAcceptances, and unresolvedWarnings when present. It has no
registryChecks, apiCalls, or contentHash.
Visibility
When a publish request omits visibility:
- A new extension in a public collective is public.
- A new extension in a private collective is private, including the existing exception to the private-extension entitlement gate.
- Subsequent publishes preserve the extension's own visibility, even if its collective's visibility has changed.
The publication intent comes from --visibility, then the manifest's
visibility field, then the registry default. public selects the registry
default, so --visibility public overrides a private manifest; it does not make
an already-private extension public. Only private intent is sent to the
registry. A dry run reports the intent as public, private, or default; a
completed push reports the applied visibility. See
Registry publication API for the wire contract.
Release channels are independent of visibility.
Private entitlement
For private intent, the push checks the collective's entitlement from the same
sign-in answer it uses for collective membership. A dry run reports the result
as the private-entitlement registry check; a real push stops on a failure.
Nothing about the plan is stored locally.
| The registry reported | Status | Message |
|---|---|---|
| A paid plan | passed |
Collective "@<slug>" is on the <plan> plan, which allows private extensions. |
| A free plan with an active trial | passed |
Collective "@<slug>" is on the <plan> plan with an active trial (<n> days left, ends <date>), which allows private extensions. |
| A free plan whose trial ended | failed |
Collective "@<slug>" is on the <plan> plan and its trial ended on <date>. Private publication requires a paid plan; upgrade at <registry>/o/<slug>/billing. |
| A free plan with no trial | failed |
Collective "@<slug>" is on the <plan> plan and has no trial. Private publication requires a paid plan; upgrade at <registry>/o/<slug>/billing. |
| A trial state the CLI does not know | not-run (entitlement-undecided) |
the registry reported a trial state for "@<slug>" that this client does not know; private publication is decided at publish |
| No entitlement for the collective | not-run (entitlement-undecided) |
the registry did not report entitlement for "@<slug>"; private publication is decided at publish |
A trial starts only when its collective is created, never at publish. A failed
check makes the dry run exit non-zero, and a real push is refused before upload
with the same message. An undecided check leaves the dry run green; the registry
decides at publish.
... [ERR] extension·push: private entitlement: failed — Collective "@ended-trial" is on the Free plan and its trial ended on 2026-09-21. Private publication requires a paid plan; upgrade at https://swamp-club.com/o/ended-trial/billing.When the registry refuses a private publication, the CLI prints the registry's reason followed by what the registry had reported for the collective at sign-in, and the billing page when the plan is free:
Error: <registry reason>. At sign-in the registry reported "@<slug>" on the <plan> plan with no trial reported. Private publication requires a paid plan; upgrade at <registry>/o/<slug>/billing.| The registry reported at sign-in | The sentence after the registry's reason |
|---|---|
| A paid plan | At sign-in the registry reported "@<slug>" on the <plan> plan. |
| A free plan with an active trial | At sign-in the registry reported "@<slug>" on the <plan> plan with an active trial (<n> days left, ends <date>). |
| A free plan whose trial ended | At sign-in the registry reported "@<slug>" on the <plan> plan; its trial ended on <date>. and the upgrade sentence |
| Any other free plan | At sign-in the registry reported "@<slug>" on the <plan> plan with no trial reported. and the upgrade sentence |
| No entitlement for the collective | At sign-in the registry did not report entitlement for "@<slug>". |
An authorized operative can use the registry UI to make an already-published extension public. This is a visibility change, not a manifest setting.
Private visibility covers every public surface: the extension is absent from
registry listings and search, its page returns 404 for anyone outside the owning
collective, swamp extension pull and the registry API return 404, and it has
no social card — the page emits no description or Open Graph image, and
/og/x/<name> returns 404. Owners and admins of the owning collective see the
full page as normal.
Private extensions include an additional warning line:
warn extension·push "@stack72/text-entropy" is private — only members of "@stack72" can pull itBlocking errors
The push is refused when any of the following is found. The safety and
dependency-trust checks also run during quality.
Safety errors (see
Manifest § Push Checks for
the full table): eval() or new Function(), symlinks, hidden files,
disallowed file extensions, files over 1 MB, total size over 10 MB, more than
150 files.
Only .ts, .json, .md, .yaml, .yml, and .txt files are allowed. Files
listed in binaries are exempt, and so are the extensionless legal files
LICENSE, COPYING, COPYING-EXCEPTION, NOTICE, PATENTS, AUTHORS, and
CONTRIBUTORS, so a license may ship as bare LICENSE. Any other file is
rejected:
... [ERR] extension·push: Safety errors (push blocked):
... [ERR] extension·push: "extensions/models/helper.sh": 'File extension ".sh" is not allowed. Allowed: .ts, .json, .md, .yaml, .yml, .txt'An additionalFiles entry with a disallowed extension is refused before the
safety analysis, with the binaries field named as the place for executables:
Error: File "extensions/models/deploy.sh" in additionalFiles has extension ".sh" which is not allowed. Allowed extensions for additionalFiles: .ts, .json, .md, .yaml, .yml, .txt. If this is an executable or binary file, move it to the `binaries` field in manifest.yaml instead.Dependency-trust errors: any npm: import that is deprecated, or carries a
HIGH, CRITICAL, or unknown-severity OSV.dev advisory, blocks the push:
ERR extension·push Dependency trust errors (push blocked):
ERR extension·push "lodash": "vulnerability GHSA-jf85-cpcp-j695 (CRITICAL)"
ERR extension·push "lodash": "vulnerability GHSA-35jh-r3h4-6jhm (HIGH)"Lower-severity advisories, unusual licenses, and low download counts reduce the
dependency-trust score but do not block the push. jsr: imports are not
audited. See the
Scorecard Rubric § Dependency trust audit.
Safety warnings
Subprocess spawning (Deno.Command()), lines with 500+ non-whitespace
characters, and base64-looking blobs in .ts files, and IPv4 address literals
in .md and .txt files, are warnings, not errors. Addresses in the RFC 5737
documentation, loopback, and link-local ranges do not warn. Each finding names
its rule, file, and line. A warning can be accepted where it occurs; see
Declared acceptances. A warning that is not accepted
makes the push prompt for confirmation. --yes waives it; see
Waived warnings.
Review warnings
Review warnings surface after the safety and formatting checks. They are
warnings, not blocking errors: the push prompts for confirmation when any remain
after declared acceptances, and --yes waives them.
Two categories of review rule produce warnings:
Static rules check for common oversights:
| Rule ID | Dimension | Condition |
|---|---|---|
testing-completeness |
Testing Completeness | A model file has no sibling _test.ts file. |
credentials-sensitive-field |
Credentials & Secrets | A schema field name looks like a secret but is not marked .meta({ sensitive: true }). |
schema-strictness |
Schema strictness | A model uses z.object({}).passthrough() with no declared properties. |
bare-specifiers |
scoring | Source files use bare import specifiers, which the registry's scorer cannot resolve, so the extension would publish unscored. push warns; quality fails. |
uncatalogued-model reports each model entry point the registry catalog would
leave out, and what could not be read: a non-plain export, a missing literal
type, or a missing literal version. It appears in push, including
--dry-run and --json, and in quality, and changes no exit code. See
Model catalog.
Adversarial-review report checks for a content-hash-bound JSON report at a
deterministic path. The report is invalidated when the extension source changes.
A missing, stale, or incomplete report (or one with any "issue" verdict)
produces a warning. It cannot be accepted.
Output
... [WRN] extension·push: Extension review warnings:
... [WRN] extension·push: ["medium"] "Testing Completeness" — "extensions/models/text_entropy.ts": "No sibling `_test.ts` found — cover both success and failure paths with unit tests before publishing."
... [WRN] extension·push: ["medium"] "Adversarial review" — "/tmp/swamp-extension-review/_skunk-ape_text-entropy-bf925572727082765765218abc0b4226c2233d2bd0884c6541ee610af83e529c.json": "No adversarial review recorded for the current code — perform the review and write the report here (run with --dry-run --json for the fill-in skeleton, or see the swamp skill)."JSON output (--json)
With --json, review warnings are the warnings.review array of the run's
document:
{
"status": "dry_run",
"warnings": {
"review": [
{
"ruleId": "testing-completeness",
"dimension": "Testing Completeness",
"severity": "medium",
"file": ".../extensions/models/text_entropy.ts",
"message": "No sibling `_test.ts` found — cover both success and failure paths with unit tests before publishing.",
"remediation": "Add a sibling <name>_test.ts covering the success and failure paths.",
"acceptance": {
"form": "comment",
"file": ".../extensions/models/text_entropy.ts",
"line": 1,
"position": "file-header",
"text": "// swamp-quality-ignore testing-completeness"
}
},
{
"ruleId": "adversarial-review-report",
"dimension": "Adversarial review",
"severity": "medium",
"file": "/tmp/swamp-extension-review/_skunk-ape_text-entropy-bf925572727082765765218abc0b4226c2233d2bd0884c6541ee610af83e529c.json",
"message": "No adversarial review recorded for the current code — perform the review and write the report here (run with --dry-run --json for the fill-in skeleton, or see the swamp skill).",
"remediation": "Run the adversarial review and record its report at the path the finding names.",
"acceptance": null,
"skeleton": {
"extension": "@skunk-ape/text-entropy",
"version": "2026.10.07.1",
"reviewedAt": "<ISO-8601 timestamp>",
"dimensions": [
{ "id": "credentials-secrets", "verdict": "pending", "note": "" }
]
}
}
]
}
}The example is trimmed to the review warnings and the skeleton's first
dimension. remediation says how to fix the finding, and acceptance says
where to accept it — the same object as in
unresolvedWarnings — and is null when the rule
cannot be accepted. The skeleton field is present only on the
adversarial-review-report rule. It is an object with every review dimension
pre-filled as "verdict": "pending". Write this skeleton to the path in file,
fill in reviewedAt and each dimension's verdict ("pass", "issue", or
"not-applicable") and note, and re-run the push to clear the warning.
Model catalog
The registry catalog reads a model's type and version as string literals
from a plain export const model = { ... } object:
export const model = {
...definition,
type: "@acme/thing",
version: "2026.09.25.1",
};- A model built by a shared factory and spread into the export repeats both literals in the export.
- The export carries no type annotation; use
satisfiesafter the object instead.
A model that does not meet this installs and runs, but the registry does not
list it: it is missing from swamp extension search --content-type models and
from contentMetadata.models in swamp extension info. push and quality
report it as an uncatalogued-model warning.
Waived warnings
--yes and --force answer both confirmation prompts, so they waive every
safety and review warning that remains after declared acceptances. They do not
waive errors. When a flag waived at least one warning, the summary records each
one:
... [WRN] extension·push: Accepted 2 warnings with --yes:
... [WRN] extension·push: ["medium"] "Testing Completeness" — "extensions/models/text_entropy.ts": "No sibling `_test.ts` found — cover both success and failure paths with unit tests before publishing."
... [WRN] extension·push: ["medium"] "Adversarial review" — "/tmp/swamp-extension-review/_skunk-ape_text-entropy-bf925572727082765765218abc0b4226c2233d2bd0884c6541ee610af83e529c.json": "No adversarial review recorded for the current code — perform the review and write the report here (run with --dry-run --json for the fill-in skeleton, or see the swamp skill)."With --json, the run's document carries the same record as acceptedWarnings:
| Field | Type |
|---|---|
safety |
array of { ruleId, file, line?, message } |
review |
array of { ruleId, dimension, severity, file, line?, message } |
A run in log mode without a terminal and without --yes fails at the first
prompt with
stdin is not a terminal — use --yes (-y) to skip confirmation prompts non-interactively.
A --json run never prompts.
Declared acceptances
An author accepts a warning-level finding where it occurs, with a reason, so the
acceptance is reviewed with the code. A declared acceptance silences that
finding in quality and push, and is listed with its reason in the summary,
in --json, and in the published extension's metadata. Errors cannot be
accepted. --yes is unaffected.
Comment form
// swamp-quality-ignore <rule-id>: <reason>In Markdown:
<!-- swamp-quality-ignore <rule-id>: <reason> -->The comment form applies to .ts, .js, .tsx, .jsx, and .md files. A
Markdown directive must close on the same line. Directives inside a Markdown
fenced code block or a multi-line /* */ comment are ignored.
| Rule scope | Placement |
|---|---|
| Site | On the finding's line, or alone on the line above it. One blank line between is allowed; stacked directives share a target. |
| File | Anywhere in the file. |
| Extension | Not accepted by comment; use quality.yaml. |
quality.yaml
quality.yaml sits beside manifest.yaml. It is found by location and is never
named in the manifest, so it survives a regenerated manifest. It is packaged at
the archive root and enters the content hash.
version: 1
accept:
- rule: ipv4-address-literals
file: hosts.txt
reason: documentation addresses from a private range| Key | Type | Notes |
|---|---|---|
version |
1 |
Required. |
accept |
array, at most 50 entries | Optional. |
accept[].rule |
string, 1–100 characters | Required. A rule ID from the table below. |
accept[].reason |
string, 1–200 characters | Required. |
accept[].file |
relative path, at most 512 characters | With file: a site-scoped rule in a file with no comment form, such as a .txt file; it accepts the rule for the whole file. Without file: an extension-scoped rule. |
generated |
{ by, source, commit } |
Optional. Declares generated code; accepts testing-completeness for the whole extension. Each value is 1–200 characters. |
Unknown keys are rejected, and the file is capped at 64 KiB. An invalid
quality.yaml stops quality and push before the safety and review checks
with quality.yaml beside the manifest is invalid: followed by each problem.
Rules
| Rule ID | Level | Accepted by |
|---|---|---|
long-line |
Warning | Comment on the line |
base64-run |
Warning | Comment on the line |
deno-command |
Warning | Comment on the line |
ipv4-address-literals |
Warning | Comment on the line, or quality.yaml with file |
schema-strictness |
Warning | Comment on the line |
credentials-sensitive-field |
Warning | Comment on the line |
testing-completeness |
Warning | Comment anywhere in the file, or generated in quality.yaml |
bare-specifiers |
Warning | Not acceptable |
uncatalogued-model |
Warning | Not acceptable |
adversarial-review-report |
Warning | Not acceptable |
adversarial-review-dimension-issue |
Warning | Not acceptable |
stale-acceptance |
Warning | Not acceptable |
file-count, total-size, hidden-file, file-type, unreadable-file, symlink, file-size, fmt, lint, upgrade-chain, dynamic-code, dynamic-import, invalid-acceptance |
Error | Not acceptable |
Invalid and stale acceptances
An acceptance that names an unknown or error-level rule, has no reason, keeps
the <reason> placeholder, or uses the wrong form is an invalid-acceptance
error, which blocks quality and push:
... [ERR] extension·quality: Checks that would block a push:
... [ERR] extension·quality: "review": "Extension review found issues that must be resolved before pushing."
... [ERR] extension·quality: '[high] invalid-acceptance — .../extensions/models/text_entropy.ts:1: Acceptance "// swamp-quality-ignore fmt: formatted by hand" is invalid: "fmt" is an error-level rule and cannot be accepted.'push reports the same error under
Extension review rule errors (push blocked):. bare-specifiers is a warning
that cannot be accepted: declaring it, in quality.yaml or inline, is an
invalid-acceptance error, although push without the declaration still only
warns on the finding:
... [ERR] extension·push: Extension review rule errors (push blocked):
... [ERR] extension·push: ["high"] "Declared acceptances" — "extensions/models/quality.yaml:1": `Acceptance "accept[0]: bare-specifiers" is invalid: "bare-specifiers" is not a rule that can be accepted. Replace each bare name with its import-map target from deno.json (an explicit npm: or jsr: specifier such as npm:zod@4) so the registry scorer can resolve imports. Swamp's lint never applies no-import-prefix, so deno.json needs no exclude for it.`An acceptance whose rule no longer fires there is a stale-acceptance warning:
... [INF] extension·quality: stale-acceptance — text_entropy.ts:11: Acceptance of long-line matches nothing: the rule does not fire on line 12. Remove it.
... [INF] extension·quality: fix: Remove the acceptance; the finding it named no longer fires there.Report
quality and push end with two blocks: Accepted warnings:, the acceptances
that applied, and Unresolved warnings:, each warning that remains with its fix
and where to accept it.
... [INF] extension·quality: Accepted warnings:
... [INF] extension·quality: testing-completeness — extensions/models/plain.ts: a fixture exercised by the factory test
... [INF] extension·quality: Unresolved warnings:
... [INF] extension·quality: ipv4-address-literals — extensions/models/hosts.txt:2: Line 2 contains IPv4 address literals (10.0.0.12) that may be real infrastructure identifiers. Use RFC 5737 documentation ranges (192.0.2.x, 198.51.100.x, 203.0.113.x) or example.com for examples.
... [INF] extension·quality: fix: Use RFC 5737 documentation ranges (192.0.2.x, 198.51.100.x, 203.0.113.x) or example.com in examples.
... [INF] extension·quality: or accept in quality.yaml: { rule: ipv4-address-literals, file: hosts.txt }Each Accepted warnings: line is the rule, an em dash, the location, and
: <reason> when one was given. The location is file:line, or the file alone
for a file-scoped rule, or (extension) for an extension-scoped rule. A
generated declaration adds a generated by <by> from <source> at <commit>
line first.
With --json, the summary carries declaredAcceptances and
unresolvedWarnings, each omitted when empty:
| Field | Type |
|---|---|
declaredAcceptances.accepted |
array of { ruleId, file?, archivePath?, line?, reason, source, message }; source is inline, sidecar, or generated |
declaredAcceptances.generated |
{ by, source, commit }, when declared |
unresolvedWarnings |
array of { ruleId, file, line?, message, remediation?, acceptance? } |
acceptance is { form: "comment", file, line, position, text } for a comment,
or { form: "sidecar", file, entry } for a quality.yaml entry, where file
is the file to edit.
"declaredAcceptances": {
"accepted": [
{
"ruleId": "ipv4-address-literals",
"file": "hosts.txt",
"archivePath": "files/hosts.txt",
"line": 2,
"reason": "documentation addresses from a private range",
"source": "sidecar",
"message": "Line 2 contains IPv4 address literals (10.0.0.12) that may be real infrastructure identifiers. Use RFC 5737 documentation ranges (192.0.2.x, 198.51.100.x, 203.0.113.x) or example.com for examples."
},
...
]
}Registry
A push sends the acceptances to the registry in the extension's
contentMetadata.acceptances, with each file as its archive path. The
registry stores them with the version and lists them on the extension page in a
collapsed Accepted warnings section below the stats, headed by their count
and grouped by rule. The version detail
(GET /api/v1/extensions/{name}@{version}), latestVersionDetail on
GET /api/v1/extensions/{name}/latest, and each entry of the versions list
carry them as acceptances, which is null for a version that declared none:
| Field | Type |
|---|---|
acceptances.accepted |
array of { rule, file, line, reason, source }; file, line and reason are null when the push omitted them |
acceptances.generated |
{ by, source, commit }, or null |
acceptances.total |
number of entries the push sent |
The registry keeps the first 500 entries; total counts every entry sent. A
malformed acceptances field is dropped and the confirmation returns a warning
beginning contentMetadata.acceptances was rejected; the rest of the listing is
kept and the push succeeds.
swamp extension info prints the latest version's acceptances; see
Management § info.
Registry publication API
Publication uses three requests:
POST /api/v1/extensions/pushreturnsuploadUrl,s3Key, andextensionIdwith status200.PUTtouploadUrluploads the gzip archive directly to storage.POST /api/v1/extensions/confirmpersists the extension and version with status201.
Both POST endpoints require name and version and accept an optional
visibility field. Its only supported explicit value is "private".
"public", null, empty strings, and non-string values are invalid.
swamp extension push sends it for private intent from --visibility private
or the manifest's visibility: private.
{
"name": "@public-collective/internal-infrastructure",
"version": "2026.09.16.1",
"visibility": "private"
}Omission and confirmation
| Push visibility | Confirm visibility | Applied behavior |
|---|---|---|
| Omitted | Omitted | Existing behavior, unless an earlier request reserved private visibility for this name and version. |
"private" |
Omitted | Private; confirmation recovers the stored choice. |
| Omitted | "private" |
Private, subject to authorization at confirmation. |
"private" |
"private" |
Private. |
| Any | Unsupported value | Rejected; the stored choice is not replaced. |
Existing behavior means:
- A new extension in a public collective defaults to public.
- A new extension in a private collective defaults to private, with the existing exception to the private-extension entitlement gate.
- An existing extension keeps its own visibility, even if the collective's visibility has changed.
Older clients need no additional fields, headers, or tokens. Release channels
(stable, rc, and beta) are independent of visibility.
A private choice is reserved for the normalized extension name and version and
bound to the initiating operative or collective-token identity. A different
operative cannot confirm that reservation. A replacement token for the same
collective can confirm it if its scope and authorization remain valid.
Reservations survive failed requests, server restarts, and upload-URL expiry. An
omitted retry does not erase them. A conflicting claim after confirmation has
started returns 409; the caller must use a new version where necessary.
License
POST /api/v1/extensions/confirm accepts an optional license string, an SPDX
expression checked by the same rules as the manifest field. The CLI sends the
manifest's license: on push. A license is never required.
license |
Result |
|---|---|
| Omitted | The version publishes with no license. |
| Valid SPDX | The version records it. |
| Invalid SPDX | The version publishes with no license, and the response's warnings array explains why. |
Every version returned by GET /api/v1/extensions/<name>/versions and /latest
carries license: the expression, or null when the version has none. The
extension-level license is the latest version's.
Authorization
Both endpoints retain namespace membership checks. Collective tokens must match
the namespace and have extensions:push permission.
An explicit private choice, including one recovered during confirmation, also requires private-extension entitlement: a paid plan or an active trial on the owning collective. Personal keys and collective tokens are checked the same way at initiation and confirmation, and neither starts a trial. Site administrators retain the billing bypass, but must still satisfy the publication namespace checks.
A personal namespace is entitled when any collective its owner belongs to is. A collective that has ever paid is judged on its plan alone; its trial does not reopen the feature.
Authorization is checked again at confirmation. Losing membership or entitlement does not change a private request into a public publication. Explicit private republication of an already-private extension is entitlement-checked; omitted visibility keeps the legacy behavior.
Publishing with visibility: "private" does not convert an already-public
extension. That request returns 409; use the existing extension visibility
action before republishing.
Confirmation response
The response retains extensionId, name, and version and adds the applied
visibility, which is either "public" or "private":
{
"extensionId": "...",
"name": "@public-collective/internal-infrastructure",
"version": "2026.09.16.1",
"visibility": "private"
}Privacy is persisted before discovery or publication events. Unauthorized readers cannot retrieve private metadata, archives, or checksums. Private publication events are routed privately or held when no private destination is configured.
Errors and rollout
| Status | Meaning |
|---|---|
400 |
Invalid JSON or unsupported explicit visibility. |
401 |
Missing authentication. |
403 |
Namespace, token scope, or explicit-private entitlement denied. For the entitlement refusal, swamp extension push adds what the registry reported for the collective at sign-in. |
409 |
Published version conflict, incompatible publish reservation, existing-public visibility conflict, or concurrent publication conflict. |
422 |
Missing required fields, invalid extension metadata, or archive not uploaded. |
Private-aware clients must wait for the service rollout to finish on all
replicas before sending the new field. Older servers ignore it. One successful
response from an upgraded replica does not establish rollout completion.
Private-aware clients must require visibility: "private" in the confirmation
response; a missing field is not evidence of privacy. Rolling back to an older
server requires pausing explicit-private publication.
promote
Promotes a published extension version to a higher release channel. Valid transitions: beta to rc, beta to stable, rc to stable. The archive content does not change — promotion is a metadata-only operation.
Arguments
<extension-or-manifest> — an extension name (e.g. @myorg/ext), or a manifest
path or extension directory, resolved like the
manifest argument of push.
[version] — the exact version to promote (e.g. 2026.06.10.1). Required with
an extension name; omitted with a manifest, whose name and version are used.
Flags
| Flag | Description |
|---|---|
--channel <channel> |
Target channel to promote to: rc or stable. Required. |
--from-channel <channel> |
Source channel: beta or rc. Skips direction validation if omitted. Not allowed with a manifest. |
--repo-dir <dir> |
Repository directory a relative manifest path resolves against (env: SWAMP_REPO_DIR). |
--extensions-dir <dir> |
Extensions root a relative manifest path resolves against (env: SWAMP_EXTENSIONS_DIR). |
Output
... [INF] extension·promote: Promoting...
... [INF] extension·promote: Promoted "@swampadmin/text-entropy"@"2026.10.06.1" from "beta" to "stable"From a manifest, the registry reports which channel the version is on:
... [INF] extension·promote: Promoting...
... [INF] extension·promote: Found "@swampadmin/text-entropy"@"2026.10.06.2" on channel "beta"; promoting "beta" → "stable"
... [INF] extension·promote: Promoted "@swampadmin/text-entropy"@"2026.10.06.2" from "beta" to "stable"Errors (manifest)
| Condition | Error |
|---|---|
| The version is on no channel | Nothing to promote: <name>@<v> is not published on any channel. |
| The version is already on the target | Nothing to promote: <name>@<v> is already on '<channel>'. |
| The version is on a higher channel | Nothing to promote: <name>@<v> is on channel '<existing>', which is not below '<channel>'. |
--from-channel with a manifest |
--from-channel cannot be used with a manifest: the registry reports which channel the version is on. |
Each exits 1.
Examples
swamp extension promote @myorg/ext 2026.06.10.1 --channel rc # beta → rc
swamp extension promote @myorg/ext 2026.06.10.1 --channel stable # rc → stable
swamp extension promote extensions/models/my-ext/manifest.yaml --channel stableyank
Yanks an extension or a specific version from the registry. Yanked extensions
cannot be pulled by consumers. Use swamp extension unyank to restore
availability.
Arguments
| Argument | Required | Description |
|---|---|---|
extension |
Yes | Extension name (e.g. @myorg/ext) |
version |
No | Specific version to yank. Omit to yank the entire extension. |
Flags
| Flag | Description |
|---|---|
--reason <text> |
Reason for yanking (required). |
--channel <channel> |
Channel to yank from: stable, beta, or rc. Default: all channels. |
-y, --yes |
Skip confirmation prompt. |
Output
extension·yank: Yanked "@myorg/ext"@"2026.06.10.1" — reason: "security vulnerability in v1"Examples
swamp extension yank @myorg/ext 2026.06.10.1 --reason "critical bug"
swamp extension yank @myorg/ext --reason "deprecated, use @myorg/ext-v2"
swamp extension yank @myorg/ext 2026.06.10.1 --reason "rc recalled" --channel rcunyank
Restores availability of a previously yanked extension or version. Consumers can pull the extension again after unyanking.
Arguments
| Argument | Required | Description |
|---|---|---|
extension |
Yes | Extension name (e.g. @myorg/ext) |
version |
No | Specific version to unyank. Omit to unyank the entire extension. |
Flags
| Flag | Description |
|---|---|
--reason <text> |
Reason for unyanking (optional, audit log only). |
--channel <channel> |
Channel to unyank: stable, beta, or rc. Default: all channels. |
-y, --yes |
Skip confirmation prompt. |
Output
extension·unyank: Restored "@myorg/ext"@"2026.06.10.1"Examples
swamp extension unyank @myorg/ext 2026.06.10.1
swamp extension unyank @myorg/ext 2026.06.10.1 --reason "patch applied"deprecate
Deprecates an extension in the registry. Deprecated extensions can still be
pulled but display a deprecation notice. Use swamp extension undeprecate to
remove the deprecation status.
Arguments
| Argument | Required | Description |
|---|---|---|
extension |
Yes | Extension name (e.g. @myorg/ext) |
Flags
| Flag | Description |
|---|---|
--reason <text> |
Reason for deprecation (required). |
--superseded-by <ext> |
Extension that supersedes this one (e.g. @myorg/ext-v2). |
-y, --yes |
Skip confirmation prompt. |
Output
extension·deprecate: Deprecated "@myorg/ext" — reason: "use @myorg/ext-v2 instead"Examples
swamp extension deprecate @myorg/ext --reason "use @myorg/ext-v2"
swamp extension deprecate @myorg/ext --reason "replaced" --superseded-by @myorg/ext-v2undeprecate
Removes deprecation status from an extension in the registry.
Arguments
| Argument | Required | Description |
|---|---|---|
extension |
Yes | Extension name (e.g. @myorg/ext) |
Flags
| Flag | Description |
|---|---|
-y, --yes |
Skip confirmation prompt. |
Output
extension·undeprecate: Removed deprecation from "@myorg/ext"Examples
swamp extension undeprecate @myorg/ext
swamp extension undeprecate @myorg/ext -yRelated
- Manifest — the YAML schema and full push-check tables.
- Scorecard Rubric — quality factors and the dependency-trust audit.
- Management — the consumer-side commands.
- About the Extension Publishing Lifecycle — why publishing is gated, including why review warnings are advisory rather than blocking.