Skip to content

Releases: systeminit/swamp

swamp 20260309.162343.0-sha.1e5e0c55

09 Mar 16:24
Immutable release. Only release title and notes can be modified.
1e5e0c5

Choose a tag to compare

What's Changed

  • fix: audit timeline misses today's entries in UTC+ timezones (#660)

Summary

Fixes #659

swamp audit failed to read the current day's audit file when the local timezone is east of UTC (e.g. CET/UTC+1). The root cause was a mismatch between how audit files are written vs read:

  • Write path (append): Uses the UTC date from the ISO timestamp (entry.timestamp.split("T")[0]), so files are named with UTC dates (e.g. commands-2026-03-09.jsonl).
  • Read path (findByTimeRange): Used setHours(0,0,0,0) (local time) then toISOString() (UTC) to generate filenames. In UTC+ timezones, local midnight converts to the previous UTC day, so today's file was never opened.

Fix

Changed three lines in findByTimeRange to use UTC methods consistently:

  • setHours(0,0,0,0)setUTCHours(0,0,0,0) — start of date range
  • setHours(23,59,59,999)setUTCHours(23,59,59,999) — end of date range
  • setDate(getDate()+1)setUTCDate(getUTCDate()+1) — date iteration step

This ensures the read path generates the same UTC-based date strings as the write path, regardless of the user's local timezone.

User Impact

Users in UTC+ timezones (CET, IST, JST, AEST, etc.) will now see today's audit entries when running swamp audit. Previously, today's entries were silently missing — only older days' data appeared.

Test Plan

  • Added regression test verifying findByTimeRange reads the correct UTC-dated file for same-day queries
  • Added test verifying multi-day UTC date spanning works correctly
  • All 2775 existing tests continue to pass
  • deno check, deno lint, deno fmt all pass

Co-authored-by: Blake Irvin blakeirvin@me.com


Installation

macOS (Apple Silicon):

curl -L https://github.com/systeminit/swamp/releases/download/v20260309.162343.0-sha.1e5e0c55/swamp-darwin-aarch64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/

macOS (Intel):

curl -L https://github.com/systeminit/swamp/releases/download/v20260309.162343.0-sha.1e5e0c55/swamp-darwin-x86_64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/

Linux (x86_64):

curl -L https://github.com/systeminit/swamp/releases/download/v20260309.162343.0-sha.1e5e0c55/swamp-linux-x86_64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/

Linux (aarch64):

curl -L https://github.com/systeminit/swamp/releases/download/v20260309.162343.0-sha.1e5e0c55/swamp-linux-aarch64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/

swamp 20260308.214522.0-sha.9aa5b71a

08 Mar 21:46
Immutable release. Only release title and notes can be modified.
9aa5b71

Choose a tag to compare

What's Changed

  • docs: add sync method pattern to extension model skill (#656)

Summary

Closes #627.

Adds sync as a standard CRUD lifecycle method in the swamp-extension-model skill. This teaches Claude to generate sync methods by default when creating CRUD models, enabling drift detection without any changes to swamp core.

Why model-level, not core

A generic swamp status command would need to know:

  • What the identifier field is called (VpcId vs id vs Name vs slug)
  • How to call the provider API to check existence
  • What "not found" looks like (404, empty response, error code)

The model already knows all of this. The model is the domain expert for its service — sync belongs there, not in a generic framework command.

What changed

  • SKILL.md — CRUD lifecycle section updated from create/update/delete to create/update/delete/sync. Explains the key UX distinction: unlike get (which requires the resource ID as an argument), sync reads the ID from stored state, making it zero-arg and suitable for automated drift detection workflows.
  • references/examples.md — Sync method added to the VPC CRUD example. New standalone "Sync Method" section with a generic pattern and a workflow example showing how to orchestrate sync across an entire stack.
  • references/scenarios.md — Sync method added to the S3 bucket CRUD scenario with the workflow updated to include a sync action.

Design decisions

  • sync is non-optional — unlike polling and idempotent creates (which are opt-in), every CRUD model gets sync by default
  • sync does not throw when the resource is gone — it writes a not_found marker, because the purpose is detection, not failure
  • No swamp core changes needed — all primitives already exist (context.dataRepository.getContent(), swamp data list, swamp data search)

Test plan

  • Verify skill files are valid markdown with correct internal links
  • Verify deno fmt --check passes
  • Test that Claude generates sync methods when asked to create a new CRUD model

🤖 Generated with Claude Code


Installation

macOS (Apple Silicon):

curl -L https://github.com/systeminit/swamp/releases/download/v20260308.214522.0-sha.9aa5b71a/swamp-darwin-aarch64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/

macOS (Intel):

curl -L https://github.com/systeminit/swamp/releases/download/v20260308.214522.0-sha.9aa5b71a/swamp-darwin-x86_64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/

Linux (x86_64):

curl -L https://github.com/systeminit/swamp/releases/download/v20260308.214522.0-sha.9aa5b71a/swamp-linux-x86_64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/

Linux (aarch64):

curl -L https://github.com/systeminit/swamp/releases/download/v20260308.214522.0-sha.9aa5b71a/swamp-linux-aarch64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/

swamp 20260308.213304.0-sha.517c0145

08 Mar 21:34
Immutable release. Only release title and notes can be modified.
517c014

Choose a tag to compare

What's Changed

  • fix: skip globalArgument expressions with missing inputs in workflow execution (#655)

Summary

Fixes #653.

The workflow execution path (evaluateDefinitionExpressions() in execution_service.ts) was evaluating all globalArgument CEL expressions unconditionally. When a factory model defines globalArguments referencing multiple inputs fields (e.g., instanceName, routeTableId, natGatewayId) but a workflow step only provides a subset (e.g., just instanceName for a delete operation), the CEL evaluator would fail with:

Invalid expression: No such key: cidrBlock

>    1 | inputs.cidrBlock
                ^

The CLI path (ExpressionEvaluationService.evaluateDefinition()) already had selective skipping logic that checks whether referenced inputs are actually provided before evaluating. This PR adds the same two-phase check to the workflow path:

  1. Skip expressions referencing unprovided inputs — builds a providedInputKeys set from context.inputs and skips any expression whose inputs.* references aren't in that set
  2. Skip expressions referencing unavailable model data — skips expressions referencing model.X.resource or model.X.file when that model data isn't in context

Skipped expressions stay as raw ${{ ... }} strings. The existing Proxy in method_execution_service.ts throws a clear error only if the method code actually tries to access an unresolved globalArgument at runtime — this is the correct design because it means expressions are evaluated lazily and errors only surface when a value is genuinely needed.

Design rationale

This selective skipping is the correct approach because:

  • Factory models reuse one definition for multiple lifecycle methods (create, delete, sync). Each method needs different inputs — delete typically only needs instanceName + identifier, not the full set of create-time parameters like cidrBlock or gatewayId.
  • Forcing all inputs defeats the purpose of factory models. Without this fix, delete workflow steps must duplicate all create-time parameters even though the delete method never accesses them.
  • Both code paths now behave identically. The CLI path (model method run) and the workflow path (workflow run) apply the same skipping logic, eliminating a confusing behavioral difference.

Testing

Automated tests

  • Added integration/workflow_partial_inputs_test.ts — creates a factory model with instanceName, cidrBlock, and gatewayId inputs in globalArguments, then runs a workflow providing only instanceName. Verifies the workflow succeeds.
  • All 2773 existing tests pass.

Manual verification

Created a test repo in /tmp/swamp-653-test with a factory model (partial-input-test) that has three inputs (instanceName, cidrBlock, gatewayId) wired into globalArguments, and a workflow that only provides instanceName.

Old binary (on PATH) — fails:

{
  "status": "failed",
  "error": "Invalid expression: No such key: cidrBlock\n\n>    1 | inputs.cidrBlock\n                ^"
}

Fixed binary (compiled) — succeeds:

{
  "status": "succeeded",
  "jobs": [{ "status": "succeeded", "steps": [{ "status": "succeeded" }] }]
}

Files changed

File Change
src/domain/workflows/execution_service.ts Add 2 imports + selective skipping in evaluateDefinitionExpressions()
integration/workflow_partial_inputs_test.ts New integration test for partial input evaluation in workflow path

Verification

  • deno check — passes
  • deno lint — passes
  • deno fmt — passes
  • deno run test — 2773 passed, 0 failed
  • deno run compile — binary compiles successfully

🤖 Generated with Claude Code


Installation

macOS (Apple Silicon):

curl -L https://github.com/systeminit/swamp/releases/download/v20260308.213304.0-sha.517c0145/swamp-darwin-aarch64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/

macOS (Intel):

curl -L https://github.com/systeminit/swamp/releases/download/v20260308.213304.0-sha.517c0145/swamp-darwin-x86_64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/

Linux (x86_64):

curl -L https://github.com/systeminit/swamp/releases/download/v20260308.213304.0-sha.517c0145/swamp-linux-x86_64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/

Linux (aarch64):

curl -L https://github.com/systeminit/swamp/releases/download/v20260308.213304.0-sha.517c0145/swamp-linux-aarch64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/

swamp 20260308.211941.0-sha.102d6392

08 Mar 21:20
Immutable release. Only release title and notes can be modified.
102d639

Choose a tag to compare

What's Changed

  • feat: add interactive secret prompt to vault put (#654)

Summary

  • When a user runs swamp vault put <vault> KEY in an interactive terminal (TTY, no =, no piped stdin), they are now prompted to enter the value with echo suppression — keeping secrets out of both shell history and the visible terminal
  • Extracted a shared readSecretFromTty() utility in src/infrastructure/io/stdin_reader.ts and refactored auth_login.ts to use it, removing duplicated raw-mode input code
  • Updated vault skill documentation (SKILL.md and references/examples.md) to describe all three input methods

Motivation

Previously, vault put only supported two ways to provide a secret value:

  1. Inline: swamp vault put my-vault API_KEY=secret — convenient but exposes the secret in shell history
  2. Piped stdin: echo "secret" | swamp vault put my-vault API_KEY — secure but less discoverable

Users who simply run swamp vault put my-vault API_KEY in a terminal got an error telling them to use one of the above formats. The interactive prompt is a natural third option that is both secure (hidden input) and ergonomic (no shell history exposure, no pipe gymnastics).

How it works

The resolution order in the command action is:

  1. Has =? → parse as KEY=VALUE (unchanged)
  2. No = + stdin is piped? → read value from stdin (unchanged)
  3. No = + stdin is TTY + log mode? → prompt with readSecretFromTty() (new)
  4. No = + JSON mode + no pipe? → error with usage hint (unchanged)

This means all existing behaviour is preserved exactly — the interactive prompt only activates when the user is at a TTY and doesn't provide a value through any other method.

Testing

All 16 existing unit tests pass (deno run test src/cli/commands/vault_put_test.ts).

Additionally, the compiled binary was tested end-to-end against a fresh repo:

# Setup
mkdir /tmp/swamp-test-interactive
cd /tmp/swamp-test-interactive
swamp repo init
swamp vault create local_encryption test-vault

# Test 1: Inline KEY=VALUE (existing behaviour) ✅
swamp vault put test-vault MY_KEY=inline-secret
# → Stored secret "MY_KEY" in vault "test-vault"

# Test 2: Piped stdin (existing behaviour) ✅
echo "piped-secret-value" | swamp vault put test-vault PIPED_KEY
# → Stored secret "PIPED_KEY" in vault "test-vault"

# Test 3: Overwrite protection with piped stdin (existing behaviour) ✅
echo "new-value" | swamp vault put test-vault MY_KEY
# → Error: Secret 'MY_KEY' already exists... Use --force (-f)

# Test 4: Overwrite with --force (existing behaviour) ✅
echo "new-value" | swamp vault put test-vault MY_KEY --force
# → Stored secret "MY_KEY" in vault "test-vault"

# Test 5: Verified both secrets stored correctly ✅
swamp vault list-keys test-vault
# → MY_KEY, PIPED_KEY (2 keys)

The interactive TTY prompt path (swamp vault put test-vault KEY with no value) cannot be tested in a non-TTY CI environment, but it uses the same readSecretFromTty function that powers auth login password input, which is proven in production.

Test plan

  • All 16 existing vault_put_test.ts unit tests pass
  • deno check passes
  • deno lint passes
  • deno fmt passes
  • Compiled binary tested: inline KEY=VALUE works
  • Compiled binary tested: piped stdin works
  • Compiled binary tested: overwrite protection works
  • Compiled binary tested: --force overwrite works
  • Manual test: interactive TTY prompt with hidden input

🤖 Generated with Claude Code


Installation

macOS (Apple Silicon):

curl -L https://github.com/systeminit/swamp/releases/download/v20260308.211941.0-sha.102d6392/swamp-darwin-aarch64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/

macOS (Intel):

curl -L https://github.com/systeminit/swamp/releases/download/v20260308.211941.0-sha.102d6392/swamp-darwin-x86_64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/

Linux (x86_64):

curl -L https://github.com/systeminit/swamp/releases/download/v20260308.211941.0-sha.102d6392/swamp-linux-x86_64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/

Linux (aarch64):

curl -L https://github.com/systeminit/swamp/releases/download/v20260308.211941.0-sha.102d6392/swamp-linux-aarch64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/

swamp 20260308.023116.0-sha.09e423d3

08 Mar 02:32
Immutable release. Only release title and notes can be modified.
09e423d

Choose a tag to compare

What's Changed

  • docs: document factory pattern for model reuse (#650) (#652)

Summary

  • Add Scenario 5: Factory Pattern for Model Reuse to swamp-model/references/scenarios.md — covers when to use factory vs separate models, model definition with inputs schema, the name ↔ data name connection, create/delete workflow snippets, and input requirements for delete
  • Add Factory Model Patterns section to swamp-workflow/references/data-chaining.md — covers calling one model multiple times, referencing factory instance data downstream, and delete step requirements with before/after examples
  • Add brief pointer in swamp-model/SKILL.md after the Model Inputs Schema section

Key insight documented: globalArgument expressions are selectively evaluated — inputs not provided are skipped, and a runtime error only occurs if the method code actually tries to access an unresolved globalArgument. Delete steps always need instanceName (to key the data instance) but other create-time inputs are only required if the delete method implementation accesses them.

Closes #650

Test plan

  • Documentation-only change, no code modified
  • Verified all anchor links resolve within the files
  • Verified line counts stay reasonable (SKILL.md: 550 lines)

🤖 Generated with Claude Code


Installation

macOS (Apple Silicon):

curl -L https://github.com/systeminit/swamp/releases/download/v20260308.023116.0-sha.09e423d3/swamp-darwin-aarch64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/

macOS (Intel):

curl -L https://github.com/systeminit/swamp/releases/download/v20260308.023116.0-sha.09e423d3/swamp-darwin-x86_64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/

Linux (x86_64):

curl -L https://github.com/systeminit/swamp/releases/download/v20260308.023116.0-sha.09e423d3/swamp-linux-x86_64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/

Linux (aarch64):

curl -L https://github.com/systeminit/swamp/releases/download/v20260308.023116.0-sha.09e423d3/swamp-linux-aarch64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/

swamp 20260308.021843.0-sha.ac378830

08 Mar 02:19
Immutable release. Only release title and notes can be modified.
ac37883

Choose a tag to compare

What's Changed

  • docs: add polling and idempotent-create patterns to extension model skill (#651)

Summary

Adds two opt-in patterns to the swamp-extension-model skill for extension models that manage real cloud resources:

  • Polling to completion — When a provider's API is async, poll until the resource is fully provisioned before returning from create/update/delete. Ensures writeResource() stores complete data so downstream CEL expressions resolve to real values (IPs, ARNs, endpoints), not placeholders like "pending".

  • Idempotent creates — Check context.dataRepository.getContent() for existing state before calling the provider's create API, then verify the resource still exists at the provider. Prevents duplicate resources when workflows are re-run after partial failures.

Why documentation, not framework changes

Both issues proposed framework-level solutions (waitFor config, createOrGet helpers, workflow-level wait steps). After working through the AWS CloudControl provider implementation, it's clear these are best handled at the model level:

  • Every provider has different readiness semantics — AWS uses request tokens and operation status polling, Hetzner has action IDs, DigitalOcean has action endpoints with status fields. A generic waitFor in swamp core would either be too simplistic or end up reimplementing what each model needs anyway.

  • Idempotency checks are inherently provider-specific — The model knows what field contains the resource identifier (id, VpcId, Arn, slug), how to verify existence (a provider-specific GET call), and whether the underlying API is already idempotent. A framework-level skip-if-data-exists would be fragile — stale data from a deleted resource would silently skip creation.

  • The primitives already existcontext.dataRepository.getContent() is available in every model method. No new framework code needed.

Both patterns are framed as opt-in — the skill tells Claude to ask the user whether they want polling/idempotency when creating a new extension model, rather than mandating them for every model.

What changed

File Change
.claude/skills/swamp-extension-model/SKILL.md Added "Optional Patterns for Cloud/API Models" subsection under CRUD Lifecycle Models with links to examples
.claude/skills/swamp-extension-model/references/examples.md Added "Polling to Completion" section with withRetry/pollUntilReady helpers and load balancer example; added "Idempotent Creates" section with droplet example using dataRepository.getContent()

SKILL.md stays at 470 lines (under the 500-line skill-creator guideline). No content duplication — SKILL.md has decision guidance, examples.md has the full patterns and code.

Both files are already in BUNDLED_SKILLS in skill_assets.ts, so updates ship automatically via swamp repo init and swamp repo upgrade.

Fixes #618
Fixes #625

🤖 Generated with Claude Code


Installation

macOS (Apple Silicon):

curl -L https://github.com/systeminit/swamp/releases/download/v20260308.021843.0-sha.ac378830/swamp-darwin-aarch64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/

macOS (Intel):

curl -L https://github.com/systeminit/swamp/releases/download/v20260308.021843.0-sha.ac378830/swamp-darwin-x86_64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/

Linux (x86_64):

curl -L https://github.com/systeminit/swamp/releases/download/v20260308.021843.0-sha.ac378830/swamp-linux-x86_64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/

Linux (aarch64):

curl -L https://github.com/systeminit/swamp/releases/download/v20260308.021843.0-sha.ac378830/swamp-linux-aarch64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/

swamp 20260308.001236.0-sha.c90aba3a

08 Mar 00:13
Immutable release. Only release title and notes can be modified.
c90aba3

Choose a tag to compare

What's Changed

  • fix: scope CRUD lifecycle deleted-resource check to declared resource data (#645) (#646)

Summary

Fixes #645. PR #640 introduced a deleted-resource lifecycle check (#637) that calls findAllForModel() and throws if any data entry has lifecycle: "deleted". This is too broad — models accumulate multiple data entries over time (old data names, VPC IDs as data names, etc.), and old deleted entries cause false positives that block read/update operations on active resources.

Why This Is the Right Fix

The root cause is that the lifecycle check operates on all data entries for a model, but it should only consider data that belongs to declared resource specs. The writeResource() function already auto-injects a specName tag on every resource write (line 493 in data_writer.ts), so the infrastructure to distinguish declared resource data from historical/untagged data already exists — it just wasn't being used.

This fix:

  1. Adds filterDeclaredResourceData() helper — filters data entries to only those with tags.type === "resource" AND tags.specName matching a key in the model's declared resources map.

  2. Scopes the read/update pre-check — instead of "fail if ANY data is deleted", the semantic is now "fail if ALL declared-resource data is deleted." Old historical entries without specName tags are excluded from the check entirely.

  3. Scopes deletion marker writing — the delete method now only writes deletion markers for declared resource data, not for untagged historical entries.

Why "all deleted" instead of "any deleted"

A model may have multiple declared resource specs. If one is deleted but another is active, blocking the entire model is too aggressive — the active resource should still be readable. The check now only blocks when every single declared resource data entry is deleted, which means the model genuinely has no active resources.

Backwards compatibility

Data created before PR #640 won't have specName tags. This is fine — those entries are excluded from the filter, so they can't trigger false positives. The lifecycle check only activates for data written by the current writeResource() which always injects specName.

User Impact

  • Bug fixed: Models that accumulate data entries over multiple workflow runs no longer get falsely blocked on read/update with "Resource was deleted" errors
  • No breaking changes: The #637 protection is preserved — genuinely deleted resources (all declared resource data marked deleted) still block read/update and require a create to re-create
  • No action required: Existing repos work without migration — pre-existing data without specName tags is simply excluded from the check

Files Changed

File Change
src/domain/models/method_execution_service.ts Add filterDeclaredResourceData() helper; scope read/update pre-check and deletion marker writing to declared resource data
src/domain/models/method_execution_service_test.ts Add specName tags to 5 existing test data entries; add 3 new tests for the scoped behavior

Testing

  • All 2772 tests pass (32 in method_execution_service_test.ts, including 3 new)
  • New tests cover:
    • "read succeeds when old data is deleted but current resource data is active" (the #645 bug scenario)
    • "deletion markers only written for declared resource data, not untagged data"
    • "read blocked when all declared resource data is deleted" (preserves #637)
  • deno check, deno lint, deno fmt all pass
  • deno run compile succeeds

🤖 Generated with Claude Code


Installation

macOS (Apple Silicon):

curl -L https://github.com/systeminit/swamp/releases/download/v20260308.001236.0-sha.c90aba3a/swamp-darwin-aarch64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/

macOS (Intel):

curl -L https://github.com/systeminit/swamp/releases/download/v20260308.001236.0-sha.c90aba3a/swamp-darwin-x86_64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/

Linux (x86_64):

curl -L https://github.com/systeminit/swamp/releases/download/v20260308.001236.0-sha.c90aba3a/swamp-linux-x86_64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/

Linux (aarch64):

curl -L https://github.com/systeminit/swamp/releases/download/v20260308.001236.0-sha.c90aba3a/swamp-linux-aarch64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/

swamp 20260307.224826.0-sha.df22793b

07 Mar 22:49
Immutable release. Only release title and notes can be modified.
df22793

Choose a tag to compare

What's Changed

  • fix: skip unresolvable cross-model resource expressions in globalArguments (#644)

Summary

Fixes #641. When a model's globalArguments contain CEL expressions referencing another model's resource or file data (e.g., ${{ model.ssh-key.resource.state.key.attributes.id }}), method execution crashes with No such key: resource if the referenced model has no data on disk. This prevents methods like update from running even when they don't need the unresolvable field.

What Changed

1. Expression evaluation skips unresolvable resource/file references

In ExpressionEvaluationService.evaluateDefinition(), expressions referencing model.*.resource or model.*.file are now skipped when the context proves the data doesn't exist (the referenced model was never executed or its data was cleaned up). This extends the existing pattern that already skips unresolved inputs.* references.

The raw ${{ ... }} expression is preserved in the definition — it is not silently discarded.

2. Broadened unresolved-expression detection in globalArgs validation

In DefaultMethodExecutionService.executeWorkflow(), the check for unresolved globalArgs fields now catches any remaining ${{ ... }} expression, not just inputs.* ones. This is safe because vault/env expressions are always resolved before executeWorkflow() is called (both in the CLI path via model_method_run.ts:282 and the workflow path via execution_service.ts:402).

3. Fixed pre-existing silent data corruption bug in globalArgs Proxy

The Proxy on context.globalArgs previously only threw for unresolved inputs.* expressions. If a globalArg contained an unresolved model.*.resource expression, the Proxy would silently return the raw ${{ ... }} string to the method. A model method could unknowingly send that literal string to an API. The Proxy now throws for any unresolved expression:

Unresolved expression in globalArguments.ssh_keys: ${{ model["ssh-key"].resource.state... }}

Design Rationale

Follows an established pattern. The codebase already skips unresolved inputs.* expressions in evaluateDefinition() (lines 214-229). The model.*.resource case is structurally identical — an expression references context data that doesn't exist yet. Extending the same skip logic is a natural, consistent extension.

The safety net already exists. The Proxy on context.globalArgs catches any attempt to use an unresolved expression at runtime. The flow is:

  1. Expression can't be resolved → skip it, leave the raw ${{ ... }} in the definition
  2. If the method actually needs that field → Proxy throws a clear error
  3. If the method doesn't need that field → everything works fine

buildContext() already loads data from disk. When dataRepo is provided (which it is in both CLI and workflow paths), ModelResolver.buildContext() eagerly populates model.*.resource from on-disk data. If the data exists, the expression resolves normally. The skip only triggers when data genuinely doesn't exist.

Alternative approaches are worse:

  • "Only evaluate fields the method schema needs" — requires the evaluation service to understand method schemas, a layer violation
  • "Make model.*.resource persistently available" — already happens; the problem is the referenced model has no data at all
  • "Allow data.latest() in definitions" — pushes complexity to users

User Impact

  • No breaking changes. Every expression that resolved before still resolves. The skip logic only triggers when the CEL evaluation would have crashed anyway.
  • What was broken now works. Models with cross-model resource references in globalArguments can run methods that don't need those fields (e.g., update doesn't need ssh_keys).
  • Better error messages. If a method accesses an unresolved field, the error is now Unresolved expression in globalArguments.ssh_keys: ${{ ... }} instead of the cryptic No such key: resource.
  • Fixes silent corruption. Unresolved non-input expressions can no longer silently leak through the Proxy as raw strings.

Files Changed

File Change
src/domain/expressions/expression_evaluation_service.ts Skip expressions with unresolvable model.*.resource/model.*.file deps
src/domain/models/method_execution_service.ts Broaden globalArgs unresolved detection + fix Proxy to catch all expressions
src/domain/models/method_execution_service_test.ts 3 new unit tests for Proxy and globalArgs validation behavior
integration/keeb_shell_model_test.ts Integration test: model method run with unresolvable cross-model resource ref
.claude/skills/swamp-model/references/troubleshooting.md Document new error message and updated behavior

Testing

  • All 2769 existing tests pass
  • 3 new unit tests covering Proxy throws, Proxy allows resolved fields, and executeWorkflow skips validation
  • 1 new integration test verifying standalone method run succeeds with unresolvable cross-model resource expression
  • Manually verified against reproduction case in /tmp/swamp-641-test

Installation

macOS (Apple Silicon):

curl -L https://github.com/systeminit/swamp/releases/download/v20260307.224826.0-sha.df22793b/swamp-darwin-aarch64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/

macOS (Intel):

curl -L https://github.com/systeminit/swamp/releases/download/v20260307.224826.0-sha.df22793b/swamp-darwin-x86_64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/

Linux (x86_64):

curl -L https://github.com/systeminit/swamp/releases/download/v20260307.224826.0-sha.df22793b/swamp-linux-x86_64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/

Linux (aarch64):

curl -L https://github.com/systeminit/swamp/releases/download/v20260307.224826.0-sha.df22793b/swamp-linux-aarch64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/

swamp 20260307.222421.0-sha.8ee10347

07 Mar 22:25
Immutable release. Only release title and notes can be modified.
8ee1034

Choose a tag to compare

What's Changed

  • fix: add --json to all non-interactive CLI examples in skill files (#643)

Summary

  • Added --json flag to all non-interactive CLI command examples across 11 skill files
  • Skill files are the primary way the agent interacts with the CLI — commands missing --json cause the agent to receive human-readable log output instead of structured JSON, making parsing unreliable
  • Interactive commands (edit, bare issue bug/feature) are intentionally left unchanged since the agent cannot use them

Files changed: swamp-data, swamp-repo, swamp-extension-model, swamp-issue, swamp-vault, and swamp-model skill files and their references.

Test Plan

  • Verified all non-interactive commands now include --json
  • Verified no --json was added to interactive edit commands
  • Ran deno fmt to ensure formatting is correct

🤖 Generated with Claude Code


Installation

macOS (Apple Silicon):

curl -L https://github.com/systeminit/swamp/releases/download/v20260307.222421.0-sha.8ee10347/swamp-darwin-aarch64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/

macOS (Intel):

curl -L https://github.com/systeminit/swamp/releases/download/v20260307.222421.0-sha.8ee10347/swamp-darwin-x86_64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/

Linux (x86_64):

curl -L https://github.com/systeminit/swamp/releases/download/v20260307.222421.0-sha.8ee10347/swamp-linux-x86_64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/

Linux (aarch64):

curl -L https://github.com/systeminit/swamp/releases/download/v20260307.222421.0-sha.8ee10347/swamp-linux-aarch64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/

swamp 20260307.015433.0-sha.94e4b27d

07 Mar 01:55
Immutable release. Only release title and notes can be modified.
94e4b27

Choose a tag to compare

What's Changed

  • fix: allow reserved collective members to push extensions (#642)

fix: allow reserved collective members to push extensions

Summary

  • Moves the reserved collective (@swamp, @si) check from manifest schema validation to the push command's authorization flow
  • Legitimate members of the swamp or si collectives (verified via swamp auth whoami) can now push extensions scoped to those collectives
  • Non-members are still rejected with a clear error message listing their available collectives

Problem

Running swamp extension push manifest.yaml with a @swamp/ or @si/ scoped extension was unconditionally rejected during Zod schema parsing — before the user's collective membership was ever checked. This made it impossible for actual collective members to publish official extensions.

What changed

Manifest validation (extension_manifest.ts)

  • Removed the .refine() that rejected reserved collectives at parse time
  • Removed the now-unused ModelType import
  • The manifest parser now only validates structure (format, required fields), not authorization

Push command (extension_push.ts)

  • Added ModelType.isReservedCollective() check in the existing collective membership validation
  • If the extension uses a reserved collective but the server can't be reached to verify membership, the push is rejected (no username fallback for reserved collectives)
  • If the server confirms membership, the push proceeds normally

Tests (extension_manifest_test.ts)

  • Replaced the two rejection tests with acceptance tests verifying @swamp/ and @si/ names are valid at the manifest level

Security boundaries preserved

  1. Server-side membership verification is required for reserved collectives — the username fallback path is explicitly blocked
  2. Non-members are still rejected — the existing isAllowed check validates collective membership via the API
  3. Network failures are safe — if the server can't be reached for a reserved collective, the push fails closed with a clear error
  4. All other validation unchanged — scoped name format, CalVer version, content collective matching, safety analysis, and quality checks remain in place

Test plan

  • deno check passes
  • deno lint passes
  • deno fmt passes
  • All 2733 tests pass (deno run test)
  • Manual: swamp extension push with @swamp/ extension as a collective member succeeds
  • Manual: swamp extension push with @swamp/ extension as a non-member is rejected

🤖 Generated with Claude Code


Installation

macOS (Apple Silicon):

curl -L https://github.com/systeminit/swamp/releases/download/v20260307.015433.0-sha.94e4b27d/swamp-darwin-aarch64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/

macOS (Intel):

curl -L https://github.com/systeminit/swamp/releases/download/v20260307.015433.0-sha.94e4b27d/swamp-darwin-x86_64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/

Linux (x86_64):

curl -L https://github.com/systeminit/swamp/releases/download/v20260307.015433.0-sha.94e4b27d/swamp-linux-x86_64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/

Linux (aarch64):

curl -L https://github.com/systeminit/swamp/releases/download/v20260307.015433.0-sha.94e4b27d/swamp-linux-aarch64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/