Skip to main content

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:

  1. --extensions-dir, when set.
  2. The current directory.
  3. 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 bytes

A 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

  1. Parse the manifest and validate required fields.
  2. Run the registry identity checks: authentication, reserved collective, collective membership, and, for private intent, private entitlement.
  3. Resolve entry points, their local imports, include files, workflows, and skills, and detect the project's deno.json (or package.json).
  4. Validate quality.yaml beside the manifest, when present.
  5. Run the safety analysis over all files.
  6. Run the dependency-trust audit.
  7. Run the formatting, lint, and upgrade-chain checks.
  8. Run the review rules and apply declared acceptances.
  9. Bundle each entry point and build the tar.gz archive, or reuse the archive quality cached for the same content hash with the same Swamp version.
  10. Check that the version is not published on any channel.
  11. Run the version-drift check (advisory).
  12. 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 failed

Version 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 it

Blocking 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 satisfies after 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:

  1. POST /api/v1/extensions/push returns uploadUrl, s3Key, and extensionId with status 200.
  2. PUT to uploadUrl uploads the gzip archive directly to storage.
  3. POST /api/v1/extensions/confirm persists the extension and version with status 201.

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 stable

yank

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 rc

unyank

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-v2

undeprecate

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 -y