Skip to main content
← Back to list
01Issue
BugShippedSwamp CLI
Assigneesstack72

additionalFiles flatten to basenames on push and lack a runtime access API, creating a source-vs-pulled layout mismatch

Opened by martinwhite1617 · 4/22/2026· Shipped 4/23/2026

Context

I'm building an extension (@hivemq/adversarial-review) that ships LLM prompts as markdown alongside its TypeScript models. The models need to read those prompts at runtime. The additionalFiles manifest field looks like the right mechanism, but the round-trip has some sharp edges that make it hard to use in practice.

Current behavior

  1. Push flattens directory structure to basenames. In src/libswamp/extensions/push.ts:1115-1118:

    for (const af of input.additionalFilePaths) {
      const destPath = join(extDir, "files", basename(af));
      await Deno.copyFile(af, destPath);
    }

    An entry like extensions/models/adversarial-review/prompts/review.md gets packed as files/review.md. Any directory structure under the source path is lost.

  2. Basename collisions silently overwrite. Because the files/ dir is flat, two additionalFiles entries with the same basename (e.g. prompts/review.md and templates/review.md) produce a collision. Deno.copyFile overwrites; nothing validates uniqueness before packing.

  3. Source-mode and pulled-mode layouts diverge. Pulled consumers find the file at .swamp/pulled-extensions/<name>/files/<basename> (src/libswamp/extensions/pull.ts:848, 985-990). But during local extension development (swamp extension source add <path>), the same model runs against extensions/models/<name>/prompts/<basename> in the source repo. A model that does Deno.readTextFile must handle both paths, or its smoke tests pass while pulled consumers break.

  4. No runtime context API for locating additionalFiles. MethodContext exposes repoDir (the consumer's repo), but there's no context.extensionFilesDir or context.readExtensionFile(name). Models have to hardcode the pulled-extensions path layout, which means they're coupled to the storage convention rather than a documented API.

Impact

The combination makes "ship an asset alongside the code that needs it" harder than expected:

  • Authors can't organize assets into subdirectories (e.g. prompts/reviewer/, prompts/judge/) — they all flatten together.
  • Authors have to implement runtime path detection for source vs pulled mode, or they'll have a smoke-test blind spot.
  • Authors who want to avoid all of that fall back to inlining assets as TypeScript string constants at build time, which loses the benefit of additionalFiles entirely.

Proposed direction

Not prescriptive — open to whatever fits swamp's model. Some combination of:

  1. Preserve directory structure on push, using the relative path from the manifest rather than basename(af). The archive's files/ dir would mirror the source layout; pull.ts's existing recursive copyDir already preserves that on the consumer side.
  2. Reject basename collisions at push time with a clear validation error, regardless of whether (1) lands.
  3. Add a MethodContext helper like context.extensionFile(relativePath: string): string that returns the correct absolute path whether the extension is source-loaded or pulled. This would let models do await Deno.readTextFile(context.extensionFile("prompts/review.md")) portably.

Workaround in use

Generating a prompts.ts from .md files at build time and importing strings from it. Works, but means the .md files never actually ship with the extension — the additionalFiles mechanism stays unused.

  • Push flattening: src/libswamp/extensions/push.ts:1115-1118
  • Pull unpack location: src/libswamp/extensions/pull.ts:848, 985-990
02Bog Flow
OPENTRIAGEDIN PROGRESSSHIPPED+ 1 MOREASSIGNED+ 8 MOREREVIEW+ 4 MOREPR_MERGEDSHIPPED

Shipped

4/23/2026, 12:46:17 AM

Click a lifecycle step above to view its details.

03Sludge Pulse
stack72 assigned stack724/22/2026, 2:40:07 PM

Sign in to post a ripple.