Skip to content

Tags: systeminit/swamp

Tags

v20260424.234336.0-sha.0022d1ca

Toggle v20260424.234336.0-sha.0022d1ca's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
feat(doctor): add swamp doctor audit preflight diagnostic (swamp-club…

…#156) (#1227)

## Summary

Adds `swamp doctor audit [--tool <name>]` — a new preflight diagnostic
that verifies the AI-tool audit integration configured in the repo is
healthy end-to-end. Non-zero exit on any fail, safe to gate in CI.

`doctor` is registered as a parent namespace so future diagnostics
(`doctor datastore`, `doctor vault`, ...) plug in as siblings.
Implements swamp-club#156.

## Five checks

| Check | Scope | What it catches |
|---|---|---|
| `binary-on-path` | all 4 tools | AI tool binary missing from PATH |
| `swamp-binary-on-path` | all 4 tools (+ Kiro baked-path) | swamp
missing from PATH, or kiro hook's baked absolute path orphaned by `brew
upgrade` |
| `agent-config-loadable` | per-tool parser | config missing, malformed,
or broken (e.g. Kiro `tools: ["*"]`) |
| `default-agent-set` | Kiro only | `.kiro/settings/cli.json` not
pointing `chat.defaultAgent` at `swamp` |
| `recording-smoke-test` | all 4 tools | **upstream normalizer drift** —
synthetic `postToolUse` payload piped through `swamp audit record
--from-hook` must land as a row in today's audit JSONL |

## The headline verification

Temporarily removing `"shell"` from `KIRO_SHELL_TOOL_NAMES` in
`hook_input.ts`, recompiling, and running doctor against a
freshly-init'd kiro repo produces:

```
✗ recording-smoke-test  synthetic kiro payload did NOT land in today's audit JSONL
    hint: The hook normalizer in src/domain/audit/hook_input.ts may have drifted from the upstream contract...

4 passed, 1 failed, 0 skipped — OVERALL: FAIL
```

Reverted before this commit. This is the load-bearing proof that the
feature protects against future upstream drift instead of being theatre.

## Name override

The issue body says "please NOT `doctor`". The team picked `doctor`
anyway — the command is a namespace and will grow; the name fits that
shape. Acknowledging here for reviewer context.

## Scope note on the three cited Kiro bugs

Issue 156 cites three concrete bugs as motivation. Their status in this
PR:

- **`tools: ["*"]` in kiro agent config** → `agent-config-loadable`
flags it with a hint; NOT fixed here.
- **Missing `chat.defaultAgent: "swamp"`** → `default-agent-set` flags
it; NOT fixed here.
- **Hard-coded `tool_name === "execute_bash"` in Kiro normalizer** →
**already fixed** upstream of this PR.
`src/domain/audit/hook_input.ts:146-150` has `KIRO_SHELL_TOOL_NAMES =
new Set(["execute_bash", "shell", "execute_cmd"])`. This PR's diagnostic
catches the class of bug, it does not fix that specific one.

## Ancillary changes

- **Extract `todaysAuditFilePath` helper.**
`src/infrastructure/persistence/jsonl_audit_repository.ts` had two
duplicate copies of the `commands-YYYY-MM-DD.jsonl` format string; moved
to `src/domain/audit/audit_path.ts` and consumed by both the writer and
the doctor smoke-test reader. Single source of truth for the filename
format.
- **Reserve `echo swamp-doctor-smoke-test` as a sentinel command
prefix.** The smoke-test writes a real audit row with this prefix; the
audit timeline filters it out of the default view. Opt in with `swamp
audit --include-diagnostic`.
- **Add `parseAiToolOrThrow`** in `src/cli/ai_tool_parser.ts` — central
validator for `--tool` flags, so bogus values fail with a usage error
listing valid names.
- **Docs:** `design/audit.md`, `design/audit-doctor.md`, and a short
"verify the audit integration" block in the `swamp-repo` skill.

## Follow-ups

- UAT coverage tracked in
[systeminit/swamp-uat#160](systeminit/swamp-uat#160)
(CLI) and
[systeminit/swamp-uat#161](systeminit/swamp-uat#161)
(adversarial).
- swamp-club has GitHub issues disabled —
`content/manual/reference/doctor.md` and a "verify the integration"
section for each `content/manual/how-to/use-swamp-with-*.md` need to be
routed through the team's docs process.
- POSIX-only PATH resolution — documented; Windows support deferred.

## Test Plan

- [x] `deno check` — clean
- [x] `deno lint` — clean
- [x] `deno fmt --check` — clean
- [x] `deno run test` — **4922 passed, 0 failed, 1 ignored**
- [x] `deno run compile` — produces working binary
- [x] Manual smoke-run on freshly-init'd kiro / claude / cursor /
opencode repos — all pass
- [x] Regression injection (remove `"shell"` from
`KIRO_SHELL_TOOL_NAMES`, recompile, rerun) — smoke-test correctly FAILS
on kiro
- [x] Real-repo test against `~/code/kiro-audit` (existing audit
history) — passes; sentinel filter confirmed with `swamp audit
--include-diagnostic`
- [x] CWD-independence adversarial test — ran from CWD=`/` with
`--repo-dir /tmp/...`, synthetic row landed in the target repo, not the
CWD

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>

v20260424.230602.0-sha.46b67446

Toggle v20260424.230602.0-sha.46b67446's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
docs(skills): document markDirty in swamp-extension-datastore (#1226)

## Summary

Pick up the \`markDirty()\` addition from #1225 in the skill that guides
authors building custom datastore providers.

- \`references/api.md\` — interface block includes \`markDirty():
Promise<void>\`,
plus a section describing when it's load-bearing (fast-path
implementations
that cache a clean/dirty watermark) versus when a no-op is correct (sync
  services that unconditionally walk the cache).
- \`references/examples.md\` — the working sync service example returns
a
no-op \`markDirty\` with a comment pointing at the fast-path pattern in
  \`design/datastores.md\`.

No code changes, docs only — keeps guidance for extension authors in
lock-step with the core interface change that already shipped.

## Test Plan

- [x] \`deno fmt\` clean per the skill-files rule in CLAUDE.md.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>

v20260424.224539.0-sha.ce117565

Toggle v20260424.224539.0-sha.ce117565's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
fix(datastore): signal cache writes to sync service fast path (#1225)

## Summary

The s3/gcs zero-diff fast path in `DatastoreSyncService` maintains a
local-dirty watermark that it flips during its own
`pushFile`-equivalent.
Swamp core writes into the cache directly from the persistence
repositories (`UnifiedDataRepository`, `YamlOutputRepository`,
`YamlWorkflowRunRepository`, `YamlEvaluatedDefinitionRepository`), which
bypasses that hook — the next `pushChanged` short-circuits past the
write and the data lives only in the local cache until eviction. Single
writer setups against a low-traffic bucket are most exposed; high
traffic masks the bug because unrelated remote mutations invalidate the
sidecar.

### Fix

- Add a required `markDirty()` method to `DatastoreSyncService`
(mirrored
  in `packages/testing/datastore_types.ts`).
- Every public mutation on the four datastore-tier repositories calls
  `notifyDirty()` before any cache write begins, so a crash mid-write
  still leaves the watermark dirty — per-method (not per-atomicWrite)
  keeps the repositories clean and the hook is idempotent anyway.
- Single sync service instance threads through
  `requireInitializedRepoUnlocked` → `acquireModelLocks` and through the
  serve context (`OpenServerState`, `ConnectionContext`,
  `WebhookServiceDeps`), so an implementation that caches the sidecar
  flag in memory stays coherent across the `markDirty` hook and the
  pull/push fast-path read. The \`acquireModelLocks\` param is optional
  for backward compatibility; every caller in the repo now passes one.
- Document the contract under the Zero-Diff Fast Path section in
  \`design/datastores.md\`.
- Filesystem datastores wire no service; \`notifyDirty\` is a no-op for
  them.

### Extension follow-up

The required interface change surfaces as a compile-time signal to
extension authors (\`@swamp/s3-datastore\`, \`@swamp/gcs-datastore\`,
swamp-club/166) that they need to implement \`markDirty()\` — trivial:
it's the same primitive as their internal \`pushFile\` hook's
dirty-flip.

## Test Plan

- [x] New repository contract tests pin \`markDirty\` invocation on
every
      public mutation, including the negative cases (reads, dry-run GC,
      no-op delete) where it must not fire.
- [x] Existing \`datastore_sync_service_test.ts\` still passes; 12 stub
      sync services across tests updated to include the new field.
- [x] \`testing_package_compat_test.ts\` structurally verifies the
      \`@systeminit/swamp-testing\` mirror still matches core.
- [x] \`deno check\`, \`deno lint\`, \`deno fmt --check\`,
      \`deno run test\` (4854 passed, 0 failed, 1 ignored),
      \`deno run compile\` all clean.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>

v20260424.194520.0-sha.bd275a89

Toggle v20260424.194520.0-sha.bd275a89's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
fix(extensions): complete legacy layout migration on repo upgrade (sw…

…amp-club#160) (#1224)

## Summary

Closes [swamp-club#160](https://swamp.club/lab/issues/160).

`swamp repo upgrade` was silently deleting user working-tree files when
it detected extensions tracked at a legacy on-disk layout. The two-phase
migration added in #1186 renamed gen-1 paths to gen-2, then deleted the
gen-2 files expecting the user to run `swamp extension install`
afterward to restore them. No warning, no prompt, no automatic
follow-up.

The fix collapses the split between the two primitives that owned "make
extension state correct":

- `extensionInstall` now treats legacy-layout entries as needing re-pull
(not just missing files), installs them into the per-extension subtree,
and sweeps the original legacy paths only after a successful install.
- `repo upgrade` invokes `extensionInstall` directly as its final step.
- `migrateExtensionLayout` and its two phase methods are deleted (~280
LOC) along with `ExtensionLayoutMigrationSummary`, `pruneEmptyDirsUp`,
and the `extensionMigration` result field.

Net LOC: **−170** despite adding the install-pass wiring, a test seam, a
shared factory, and several new tests.

## User-visible behavior

- **`swamp repo upgrade` now completes the migration end-to-end.** No
manual `swamp extension install` follow-up required for the common case.
- **On install failure** (network down, extension yanked, auth missing),
legacy files are preserved and the error names the failed extensions
with a recovery command.
- **`swamp extension install` also migrates legacy-layout repos.**
Previously a silent no-op on legacy state — positive side effect of
unifying the two primitives.

## Latent bug caught during verification

Running an end-to-end manual test against `@swamp/aws/s3` surfaced a
second bug: empty `_lib/` directories were being stranded after the
sweep. Root cause: `resolveRepoDir` returns `"."` by default, which
propagated through `join(".", "path") → "path"` (relative), and
`cleanupEmptyParentDirs`'s `startsWith(stopAtDir)` check silently failed
against relative paths. The factory now absolutizes `repoDir` up front
so downstream filesystem helpers always see absolute paths. Added a
regression test asserting nested parent dirs are pruned.

This one wouldn't have been caught by unit tests alone — my initial unit
tests used absolute `tmpDir` from `Deno.makeTempDir`, which masked the
relative-path issue. Running against a real extension with a nested
`_lib/` found it immediately.

## Test plan

- [x] `deno check` — clean
- [x] `deno lint` — clean (1081 files)
- [x] `deno fmt` — clean (1199 files)
- [x] `deno run test` — 4728 pass, 0 fail, 1 ignored (added 8 new tests)
- [x] `deno run compile` — binary produced
- [x] **Manual E2E happy path:** `@swamp/aws/s3` seeded as gen-1 legacy,
`swamp repo upgrade` run; all 11 model files migrated to
`.swamp/pulled-extensions/@swamp/aws/s3/models/…`, `filesChecksum`
anchor matches original pull byte-for-byte (`a9378bf483…`), legacy paths
(including nested `_lib/`) fully swept.
- [x] **Manual E2E failure path:** lockfile referencing
`@me/launchd-daemon` (not in registry); `swamp repo upgrade` exits
non-zero with clear error naming the extension + recovery command; all 4
legacy files preserved on disk.
- [x] **Manual E2E via `extension install`:** same legacy seed; `swamp
extension install` also migrates successfully (positive side effect
verified).

## Notes for reviewers

- The `swamp extension install` behavior change (no-op → auto-migrate on
legacy repos) is intentional; documented in the `swamp-repo` skill.
- There is no `CHANGELOG.md` in this repo, so the user-visible release
notes live in this PR description.
- I skipped the integration test against a real/fake registry as out of
scope for this PR; unit coverage + manual verification cover the
critical paths. A follow-up would be reasonable.
- Private/auth-gated extensions are not directly verified end-to-end —
the factory uses the same `ExtensionApiClient` as `swamp extension
install`, so coupling should hold, but worth spot-checking if a reviewer
has a private extension in a test lockfile.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>

v20260424.185624.0-sha.633e3ae7

Toggle v20260424.185624.0-sha.633e3ae7's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
fix(datastore): clarify --timeout scope in SyncTimeoutError message (#…

…1223)

## Summary

Follow-up to #1222 addressing the two actionable findings from review:

### 1. CLI UX Review — `--timeout` remedy was misleading for implicit
syncs

The timeout fires from `flushDatastoreSync()` after *every* command, not
just `swamp datastore sync`. The previous message led with `• Rerun with
--timeout <seconds>`, which would fail for a user whose timeout fired
from e.g. `swamp model run` (the flag only exists on `swamp datastore
sync`).

**Fix:** reorder remedies so the env var (universal escape hatch) is
listed first and the `--timeout` flag is explicitly scoped to `swamp
datastore sync`:

\`\`\`
Datastore push to "@swamp/s3-datastore" timed out after 300000ms. Try
one of:
• Set SWAMP_DATASTORE_SYNC_TIMEOUT_MS for the duration of your shell
session — applies to every command that triggers sync, including the
implicit pull/push around write commands.
• If the timeout fired on an explicit 'swamp datastore sync', rerun with
--timeout <seconds> (e.g. --timeout 1800 for large one-off syncs). The
flag is not available on other commands; use the env var above for
those.
• If you are on @swamp/s3-datastore or @swamp/gcs-datastore at scale,
update to the latest extension — the fingerprint fast path
short-circuits zero-diff syncs.
• If a prior process crashed without releasing the lock, run 'swamp
datastore lock release --force' (add --model <type>/<id> for a specific
model lock).
\`\`\`

New scope-clarification test pins the contract: the message MUST name
`swamp datastore sync` explicitly and MUST mention the env var covers
implicit syncs. That prevents a future well-meaning reword from
reintroducing the same trap.

### 2. Code Review + Adversarial Review —
`CreateDatastoreSyncDepsOptions` not re-exported from barrel

The new interface lived at the internal path
`src/libswamp/datastores/sync.ts`. Any consumer outside the CLI wanting
to type an options variable would have had to violate CLAUDE.md's
barrel-import convention. One-line fix: add `type
CreateDatastoreSyncDepsOptions` to the export from
`src/libswamp/mod.ts`.

## What this does NOT change

- The precedence chain (CLI flag → config → env var → default) is
unchanged.
- The structured fields of `SyncTimeoutError` (`label`, `direction`,
`timeoutMs`, `cause`) are unchanged.
- No behavior change for users who don't hit the timeout.

## Test plan

- [x] `deno check` — clean
- [x] `deno lint` — clean (1094 files)
- [x] `deno fmt` — clean
- [x] `deno run test` — 4843 passed / 0 failed
- [x] `deno run compile` — binary built
- [x] New test `SyncTimeoutError: --timeout remedy is scoped to 'swamp
datastore sync'` passes

## Links

- Primary PR: #1222
- Lab issue: https://swamp.club/lab/164

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>

v20260424.184437.0-sha.44543d36

Toggle v20260424.184437.0-sha.44543d36's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
feat(datastore): add --timeout flag and enrich SyncTimeoutError (swam…

…p-club#164) (#1222)

## Summary

Companion PR to the `@swamp/s3-datastore` fingerprint fast-path fix for
[lab/164](https://swamp.club/lab/164) (minutes-slow `swamp datastore
sync` at 4k-file scale, even on zero-diff). This PR adds the swamp-core
ergonomics: a `--timeout` CLI flag for one-off sync overrides, and a
richer `SyncTimeoutError` message so users get actionable next steps
when the timeout does fire.

Either PR may land first — the enriched error uses version-free wording,
so there is no ordering dependency.

## What changed

### `--timeout <seconds>` flag on `swamp datastore sync`

- Per-invocation override; wins over
`CustomDatastoreConfig.syncTimeoutMs` and
`SWAMP_DATASTORE_SYNC_TIMEOUT_MS`.
- Validated at the CLI boundary: positive integer, rejected ≤0, capped
at **21600s (6h)** — generous enough for initial seeding from a slow
link, bulk recovery after an outage, or 100k-file migrations, while
rejecting fat-finger misconfigs. The env var stays uncapped as the
shell-session escape hatch.
- Scoped to `swamp datastore sync` only. Write commands that trigger
implicit sync keep their existing escape hatch (env var). Adding
`--timeout` everywhere would inflate the CLI surface without
commensurate benefit.
- `--help` clearly describes precedence so users reach for the right
knob.

### Enriched `SyncTimeoutError` message

Before:
\`\`\`
Datastore push to "@swamp/s3-datastore" timed out after 300000ms. If a
prior process crashed without releasing the lock, run 'swamp datastore
lock release --force' (add --model <type>/<id> for a specific model
lock).
\`\`\`

After:
\`\`\`
Datastore push to "@swamp/s3-datastore" timed out after 300000ms. Try
one of:
• Rerun with --timeout <seconds> (e.g. --timeout 1800 for large one-off
syncs).
• Set SWAMP_DATASTORE_SYNC_TIMEOUT_MS for the duration of your shell
session.
• If you are on @swamp/s3-datastore or @swamp/gcs-datastore at scale,
update to the latest extension — the fingerprint fast path
short-circuits zero-diff syncs.
• If a prior process crashed without releasing the lock, run 'swamp
datastore lock release --force' (add --model <type>/<id> for a specific
model lock).
\`\`\`

Version-free wording keeps the message stable across extension releases.
No changes to the error's structured fields (`label`, `direction`,
`timeoutMs`, `cause`).

### Internal plumbing

- `resolveSyncTimeoutMs(config, overrideMs?)` — new optional parameter
at highest precedence. Non-positive overrides silently fall through to
the next source.
- `createDatastoreSyncDeps(repoDir, resolver, options?)` — new
`CreateDatastoreSyncDepsOptions` interface with `syncTimeoutMsOverride`.

### Docs

`design/datastores.md` — expanded "Sync Timeout" section with the 4-tier
precedence chain and reference to the enriched error. New "Zero-Diff
Fast Path (Extension Guidance)" subsection with backend-agnostic wording
(fingerprint + dirty-flag sidecar pattern), so future extension authors
have a breadcrumb. Notes that sync is not a content-integrity tool — use
`DatastoreVerifier` for that.

## What this does NOT do

- Does not fix the 5-minute zero-diff sync itself — that's the
`@swamp/s3-datastore` fingerprint fast-path PR. This PR is the
ergonomics layer.
- Does not change default behavior for anyone who doesn't pass
`--timeout` or hit the timeout.
- Does not close the AbortSignal plumbing gap in the s3 extension.

## Test plan

- [x] `deno check` — clean
- [x] `deno lint` — clean (1091 files)
- [x] `deno fmt` — clean
- [x] `deno run test` — 4822 passed / 0 failed (19 new tests)
- [x] `deno run compile` — binary built
- [x] `./swamp datastore sync --help` — `--timeout` flag documented
correctly
- [ ] Reporter runs `swamp datastore sync --timeout 60` against their
4k-file repo and confirms: (a) validation rejects 0/negative/>21600, (b)
enriched error message renders all four remedies, (c) flag override wins
over env var when both are set. Tracked on lab/164 as the verification
gate.

### New tests

| File | What it covers |
|---|---|
| `src/domain/datastore/datastore_sync_service_test.ts` | Enriched
message contents; version-free extension hint (regex guards against
calendar-version and semver); structured field preservation; multi-line
JSON round-trip. |
| `src/domain/datastore/datastore_config_test.ts` |
`resolveSyncTimeoutMs` precedence: override wins over
config/env/default; non-positive override falls through; undefined
preserves existing precedence. |
| `src/cli/commands/datastore_sync_test.ts` | `parseTimeoutFlag`
validation: seconds→ms conversion, upper-bound acceptance,
zero/negative/over-cap/non-integer/non-number rejection. |

## Links

- Lab issue: https://swamp.club/lab/164
- Companion PR: `@swamp/s3-datastore` fingerprint fast-path (in
`systeminit/swamp-extensions`, being opened in parallel)
- Follow-up issue: https://swamp.club/lab/166 (gcs mirror)

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>

v20260424.182054.0-sha.e40c597d

Toggle v20260424.182054.0-sha.e40c597d's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
feat(drivers): finish repo-level defaultDriver — direct path + defini…

…tion tier (swamp-club#165) (#1221)

## Summary

Closes swamp-club#165. Completes the cleanup gaps left by #1219 so the
repo-level `defaultDriver` priority chain now applies uniformly to both
workflow and direct `swamp model method run` paths. Also fixes a
concurrent-read race on the marker load and un-breaks the
silently-dropped `definition.driver` tier.

Three deliverables, one commit:

- **Direct `swamp model method run` now honors `defaultDriver`.**
`src/libswamp/models/run.ts` routes through `resolveDriverConfig` with
cli/definition/repo tiers. Serve/WS (`src/serve/connection.ts`) and HTTP
SSE (`src/serve/open/http.ts`) paths inherit for free. A new
`loadRepoMarker` dep on `ModelMethodRunDeps` is built via the extracted
`createRepoMarkerLoader` helper.
- **Promise-memoize the marker read.** Previously
`WorkflowExecutionService.loadRepoMarker` cached the resolved value —
two concurrent callers both saw `undefined` and both hit the file. Now
`this.markerPromise ??= markerRepo.read(...)`; `.swamp.yaml` is read at
most once per service instance. The primitive is extracted to
`src/infrastructure/persistence/repo_marker_loader.ts` and reused by all
four factories (workflow service, CLI, serve/WS, HTTP SSE).
- **Make the `definition` tier live.**
`src/domain/workflows/execution_service.ts:523` used to read `ctx.driver
?? evaluatedDefinition.driver` — dead code, because `ctx.driver` was
always populated by the old upstream `resolveDriverConfig` call (with
`"raw"` as its fallback), so the RHS never fired and `definition.driver`
was silently dropped. `StepExecutionContext` now carries unresolved
`driverTiers: { cli, step, job, workflow, repo }` and final resolution
happens inside `DefaultStepExecutor.executeModelMethod` where
`evaluatedDefinition` is in scope.

## Behavior changes

- `swamp model method run <m> <meth>` in a repo with `defaultDriver:
docker` in `.swamp.yaml` now runs via docker without `--driver`.
`--driver raw` override still wins.
- Model definitions with `driver:` set at the definition level are now
honored during workflow execution. **Internal blast radius: zero** —
grep of `extensions/**/manifest.yaml` and `extensions/**/*.yaml`
returned no hits. Third-party extensions that set `driver:` at the
definition level will now apply it (this was the documented design from
day one but was never wired).
- Malformed `.swamp.yaml` now aborts every direct method run with a YAML
parse error (previously direct runs didn't read the marker at all).
Consistent with workflow behavior and documented in
`design/execution-drivers.md`.

## Tests added

- `src/infrastructure/persistence/repo_marker_loader_test.ts` —
concurrency primitive: two simultaneous calls through a
`Promise.withResolvers` latch, assert exactly one underlying `.read()`
call. Covers null-result caching too.
- `src/domain/models/method_execution_service_test.ts` — pre-flight
check receives the resolved `driver`/`driverConfig` from MethodContext
(propagation audit).
- `src/libswamp/models/run_test.ts` — four direct-path resolution tests:
defaultDriver from marker wins; CLI override wins; raw fallback;
definition tier applies when no higher tier set.
- `src/domain/workflows/execution_service_test.ts` — existing three
workflow-path resolution tests updated to assert on the new
`driverTiers` shape.
- `integration/driver_resolution_test.ts` — end-to-end CLI smoke (no
docker required): defaultDriver honored, CLI override wins, malformed
YAML aborts loudly.

## Docs

- `design/execution-drivers.md` — added a short note under "Resolution
Priority" stating that the priority chain applies uniformly to workflow
and direct paths and that malformed `.swamp.yaml` aborts at
start-of-run.
- **Follow-up docs gap (not in this PR):** the swamp-club manual
reference at `content/manual/reference/repository-configuration.md`
still does not document `defaultDriver` / `defaultDriverConfig` —
pre-existing gap from #1219. `systeminit/swamp-club` has GitHub issues
disabled; flagging here so the gap can be routed to the swamp-club lab.

## Out of scope (per issue body, separate follow-ups)

- Named-fields refactor of `resolveDriverConfig` (positional
`DriverSource | undefined` params).
- `WorkflowExecutionService.execute()` option forwarding (currently
drops `driver`).
- Runtime schema validation of `RepoMarkerData` (the `parseYaml(...) as
RepoMarkerData` cast).
- UAT coverage for `--driver` / `defaultDriver` in
`systeminit/swamp-uat`.

## Test plan

- [x] `deno check` — passes
- [x] `deno lint` — passes
- [x] `deno fmt --check` — passes
- [x] `deno run test` — 4823/4823 pass
- [x] `deno run compile` — binary produced
- [x] Local smoke: scratch repo with `defaultDriver: raw` → honored;
`--driver raw` beats `defaultDriver: docker`; malformed YAML aborts with
exit code 1.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>

v20260424.180844.0-sha.03130a3d

Toggle v20260424.180844.0-sha.03130a3d's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
docs(skills): document bare-specifier vs scorer hermeticity (swamp-cl…

…ub#161) (#1220)

## Summary

Closes swamp-club#161. Following #1218 shipping `swamp extension
quality`, authors now discover locally that the scorer strips the
tarball's `deno.json` and writes a controlled one with no imports map —
so bare `"zod"` imports fail with `deno doc --json failed: Import "zod"
not a dependency` before factor scoring even begins. This PR teaches
that pattern to authors through the two skills that previously led them
into the trap.

**Three files, +50/-7 lines:**

- **`swamp-extension-quality/references/templates.md`** — the JSDoc
entrypoint skeleton was showing bare `"zod"`; switched to `"npm:zod@4"`
with a rationale bullet explaining hermeticity. An author who copies the
template literally now produces an extension that scores.
- **`swamp-extension-publish/references/publishing.md`** — the Import
Rules had two contradictory bullets (`"npm:zod@4"` is "always required"
vs. bare specifiers "can be used" with `deno.json`). Rewritten into one
coherent paragraph that carries both load-bearing signals: hermeticity
at score time AND zod-specific externalization (why zod in particular is
called out, not merely a consequence of hermeticity).
- **`swamp-extension-quality/SKILL.md`** — added a gotcha block under
"Essential mechanics authors get wrong" that names the failure mode
explicitly, calls out the Deno `no-import-prefix` default that activates
when a `deno.json` with `imports` is present (which is every swamp
repo's default state, creating a catch-22), and gives concrete
mitigations: either global `"lint": { "rules": { "exclude":
["no-import-prefix"] } }` in `deno.json`, or per-import `//
deno-lint-ignore no-import-prefix`. Also added a one-sentence cross-link
from the new "Self-check your score locally" section and a symptom→fix
row to "Common patterns" so a reader landing anywhere in the skill finds
the fix.

## Test Plan

Three-layer verification:

- [x] `deno fmt --check` clean on all three files
- [x] `deno check` clean
- [x] `deno lint` clean (1088 files)
- [x] `deno run test` — **4803 passed, 0 failed**
- [x] Executable round-trip in `/tmp/swamp-issue-161-verify`:
- Bare `"zod"` import → `swamp extension quality` throws the exact error
documented in the gotcha (`deno doc --json failed: Warning Import "zod"
not a dependency`)
- Switch to `"npm:zod@4"` → scorer runs and reports 11/12 (91%) factor
results
- The test also surfaced the Deno `no-import-prefix` default rule firing
as predicted, and the documented mitigation (`lint.rules.exclude`)
unblocked the round-trip — confirming both halves of the gotcha match
the tool's actual behaviour.

## Follow-up (not in this PR)

The deadlock between the scorer (strips `deno.json`) and Deno's default
lint rules (forbid `npm:` prefix when an `imports` map is present) is
ultimately a swamp-club server-side concern. A future follow-up issue
could make the analyzer honour the tarball's `files/deno.json` with
appropriate hardening — the README/LICENSE promotion logic already does
similar. Until then, the gotcha documents the author-side workaround.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>

v20260424.171017.0-sha.76b653b9

Toggle v20260424.171017.0-sha.76b653b9's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
feat(drivers): repo-level defaultDriver in .swamp.yaml (swamp-club#15…

…9) (#1219)

## Summary

Closes swamp-club#159. Adds a repo-level `defaultDriver` (and
`defaultDriverConfig`) to `.swamp.yaml` so a repo can declare a baseline
execution driver once, without touching every workflow or passing
`--driver` on every CLI invocation.

New resolution chain for workflow steps:

```
cli > step > job > workflow > definition > repo > "raw"
```

- `RepoMarkerData` gains two optional fields (`defaultDriver`,
`defaultDriverConfig`) — same shape as the existing `datastore` field.
- `resolveDriverConfig` gains an optional 6th `repo` tier, positioned
below `definition` and above the `"raw"` fallback.
- `WorkflowExecutionService` loads `.swamp.yaml` once per `run()`
(memoized on the instance so nested workflow calls don't re-read) and
passes the repo tier into `resolveDriverConfig` at the step-context
build site.
- `design/execution-drivers.md` updated to document the new tier with an
example.

**Scope:** workflow execution only. Direct `swamp model method run`
invocations go through a separate code path that doesn't call
`resolveDriverConfig` today and will still default to `"raw"` when no
CLI override is supplied. Flagged in the adversarial review as ADV-1;
can be a follow-up if desired.

## Test Plan

- [x] `deno check` — passes
- [x] `deno lint` — passes
- [x] `deno fmt --check` — passes
- [x] `deno run test` — 4732 passed / 0 failed / 1 ignored
- [x] `deno run compile` — produces a working binary

New tests added:
- 5 cases in `driver_resolution_test.ts` covering repo-tier precedence
(wins over default, loses to workflow/job/step/cli, skipped when
`driver` undefined but `driverConfig` set)
- 1 roundtrip in `repo_marker_repository_test.ts` with `defaultDriver` +
`defaultDriverConfig`
- 3 service-level tests in `execution_service_test.ts` with a real
`.swamp.yaml` on disk: default applied, CLI overrides, no default → raw

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>

v20260424.164547.0-sha.31006b67

Toggle v20260424.164547.0-sha.31006b67's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
feat(extensions): add quality command and opportunistic package cache…

… (swamp-club#163) (#1218)

## Summary

Implements swamp-club#163 — a new `swamp extension quality <manifest>`
CLI that scores an extension against the Swamp Club rubric locally,
mirroring the server's scorecard pipeline so local results match the
score the registry will compute after publish.

- **New CLI command:** `swamp extension quality manifest.yaml [--json]`.
Exits non-zero if any factor misses; suitable for CI self-checks.
- **Server-mirrored scorer:** `extension_rubric_scorer.ts` mirrors
[`analysis-factors.ts`](https://github.com/systeminit/swamp-club/blob/main/lib/domain/scorecard/analysis-factors.ts)
and the rubric-v2 composition from
[`score.ts`](https://github.com/systeminit/swamp-club/blob/main/lib/domain/scorecard/score.ts)
— same slow-type code set, same JSDoc coverage shape, same
README/LICENSE lookup in the extracted tarball, same 12-point weighting.
- **Parity tests:** 24 tests marked `[parity]` port the upstream
swamp-club test suite so logic drift is caught before release.
- **Opportunistic package cache** at `.swamp/cache/packages/<hash>/`:
keyed on source inputs (manifest + resolved file contents + deno
config). `extension quality` writes the packaged tarball; `extension
push` checks the cache before packaging and reuses the bytes on a hit.
Any source change invalidates the entry by construction — authors who
run quality before push don't pay twice, and the bytes scored equal the
bytes uploaded.

### Opt-in by design
`quality` is uniformly opt-in: push's behaviour is unchanged, no new
flags, no mandatory gate. The `swamp-extension-publish` and
`swamp-extension-quality` skills gain brief pointers at the new CLI
between states 6 and 7 of the publish flow.

### Known caveat
`repository-verified` is a structural check on the CLI side (HTTPS +
allowlisted host). The server additionally performs an HTTP HEAD to
confirm the repo is public — so a local "earned" can come back "missing"
from the registry if the repo URL is malformed or private. The log
renderer surfaces this caveat. Follow-up issue: narrow the gap (HTTP
HEAD from the CLI, or a shared scoring package between this repo and
swamp-club).

### Smoke test
Ran against `systeminit/swamp-extensions/datastore/s3` — reports **12/12
points, 100%, all factors earned**, matching the registry.

## Test Plan

- [x] 10 unit tests for `extension_package_cache.ts` (hit/miss,
determinism, cache invalidation)
- [x] 39 unit tests for `extension_rubric_scorer.ts` covering every
factor and edge case
- [x] 24 `[parity]` tests ported from swamp-club's
`analysis-factors_test.ts` and `scorecard_test.ts`
- [x] 6 integration tests in `integration/extension_quality_test.ts`
covering the full CLI pipeline (package → cache → extract → deno doc →
compose), including cache hit/miss, source mutation invalidating cache,
log and JSON modes
- [x] Smoke test against real `@si/s3-datastore` extension — 12/12
(100%), matches registry
- [x] `deno check` clean
- [x] `deno lint` clean
- [x] `deno fmt --check` clean
- [x] `deno run test` (4,802 passed, 1 pre-existing flaky unrelated to
this change, 1 ignored)
- [x] `deno run compile` succeeds

## Follow-ups (not in this PR)

- File issue in `systeminit/swamp-uat` for CLI UAT coverage
(`tests/cli/extension/quality/`)
- File issue in `systeminit/swamp-club` to document the new command in
`content/manual/how-to`
- Consider a shared scoring package between `systeminit/swamp` and
`systeminit/swamp-club` to eliminate the current ~100 lines of mirrored
logic

🤖 Generated with [Claude Code](https://claude.com/claude-code)