Skip to content

feat(repo): custom agent tool definitions via swamp agent setup#1400

Merged
stack72 merged 5 commits into
mainfrom
worktree-tooling-discovery
May 26, 2026
Merged

feat(repo): custom agent tool definitions via swamp agent setup#1400
stack72 merged 5 commits into
mainfrom
worktree-tooling-discovery

Conversation

@stack72
Copy link
Copy Markdown
Contributor

@stack72 stack72 commented May 18, 2026

Summary

  • Adds swamp agent setup interactive wizard for defining custom AI agent tools
  • Custom tools get skills + instructions + gitignore scaffolding, stored in .swamp-custom-tools.yaml at repo root
  • swamp repo init --tool <custom-name> works once defined
  • Built-in tools (claude, cursor, kiro, etc.) retain full audit/hooks/doctor integration
  • Custom tools skip audit subsystems — promoted to built-in when adoption warrants

How it works

  1. swamp agent setup asks the tool name, optionally scans an existing repo for config patterns (.windsurf/rules/, AGENTS.md, etc.), then asks where the tool reads instructions from
  2. Defaults are derived: skills go to .<name>/skills/, instructions mode (shared vs owned) from the file location
  3. Definition saved to .swamp-custom-tools.yaml — committable, copyable between repos
  4. swamp repo init --tool <name> resolves custom tools via ToolResolver, copies skills, generates instructions

Architecture

  • ToolConfig value object unifies built-in and custom tools at the scaffolding layer
  • AiTool union stays closed for built-in exhaustiveness checks
  • Boundary types (RepoMarkerData.tools, init/upgrade options/results) widened to string[]
  • ToolResolver checks built-in tools first (O(1)), then custom-tools.yaml (lazy-loaded)
  • DDD: ToolResolver accepts an injected CustomToolLoader to avoid domain→infrastructure import

New files

  • src/domain/repo/custom_tool.ts — types, factories, validation, detection, defaults derivation
  • src/domain/repo/tool_resolver.ts — resolution layer (built-in + custom)
  • src/infrastructure/persistence/custom_tools_repository.ts — YAML persistence
  • src/cli/commands/agent_setup.tsswamp agent setup/list/rm commands
  • Tests for all new modules (41 tests)

Ecosystem research

Researched 12 AI coding tools (Windsurf, Zed, Amp, Aider, Cline, Roo Code, Kilo Code, Trae, Augment, Tabnine, PearAI, Pi). AGENTS.md is converging as the cross-tool standard. Most tools use .<toolname>/rules/ for config. Skills at .<toolname>/skills/ works for tools with native support (Pi, Kilo) and via instructions references for others.

Test plan

  • deno check passes
  • deno lint clean on new files
  • deno fmt applied
  • deno run test — 6007 passed, 0 failed
  • deno run compile — binary compiles
  • Manual: swamp agent setup → define custom tool → swamp repo init --tool <name> → verify scaffolding
  • Manual: swamp agent list / swamp agent rm
  • Manual: swamp repo init --tool nonexistent → helpful error

🤖 Generated with Claude Code

stack72 and others added 2 commits May 26, 2026 19:08
Allow users to define custom AI agent tools without code changes.
Custom tools get the same skills + instructions + gitignore scaffolding
as built-in tools, stored in `.swamp-custom-tools.yaml` at the repo
root.

The `swamp agent setup` interactive wizard scans an existing repo for
config patterns (e.g. `.windsurf/rules/`, `AGENTS.md`) and offers
informed choices. `swamp agent list` and `swamp agent rm` manage
definitions. `swamp repo init --tool <custom-name>` works once defined.

Built-in tools retain their full integration (audit hooks, harness
detection, doctor checks). Custom tools skip these subsystems — when a
tool gains enough traction it can be promoted to built-in with proper
hook normalization.

Type system: AiTool union stays closed for built-in exhaustiveness
checks. Boundary types (marker, init/upgrade options, results) widened
to string[] so custom names flow through. ToolConfig value object
unifies both at the scaffolding layer.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The agent setup wizard now lets users confirm or override the derived
skills directory, fixing tools like deepAgents that use a bare `skills/`
path instead of the conventional `.<toolname>/skills/`. Detection also
probes for an existing `skills/` directory during repo scanning.

Adds a preamble to the wizard listing the three things users need to
know about their tool before starting setup.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@stack72 stack72 force-pushed the worktree-tooling-discovery branch from ae55750 to a22124d Compare May 26, 2026 18:09
@stack72 stack72 marked this pull request as ready for review May 26, 2026 18:10
… widening

The custom tool PR widened boundary types from AiTool to string but left
behind unused AiTool imports in 10 files, dead INSTRUCTIONS_FILES and
GITIGNORE_TOOL_ENTRIES constants in repo_service, and stale SKILL_DIRS /
builtInToolConfig / isBuiltInTool imports.

Also fixes a regression where built-in tools (cursor, opencode, codex,
copilot, kiro) had their skills directories gitignored via ToolConfig —
the old code intentionally did not gitignore skills since they should be
checked in. Only claude retains tool-specific gitignore entries (for
config files, not skills).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
github-actions[bot]

This comment was marked as outdated.

github-actions[bot]

This comment was marked as outdated.

github-actions[bot]

This comment was marked as outdated.

stack72 and others added 2 commits May 26, 2026 19:42
Blockers:
- agent list emits JSON when --json is passed
- agent rm emits JSON and skips interactive confirmation in --json mode
- agent setup throws UserError in --json mode (interactive wizard)

Suggestions fixed:
- Path traversal: assertPathContained() validates custom tool skillsDir
  and instructionsFile stay within the repo root before scaffolding
- Field validation: toDefinition() now rejects YAML entries missing
  name, skillsDir, or instructionsFile with a clear UserError
- Error paths: agent setup throws UserError instead of console.error +
  Deno.exit; agent rm throws UserError for unknown tool (nonzero exit)
- Dead code: removed unused usesSharedInstructionsFile private method
- Path construction: detectToolConfig uses join() from @std/path for
  all filesystem paths instead of template literal concatenation
- DRY: exported BUILT_IN_TOOL_NAMES from custom_tool.ts, removed
  duplicate VALID_BUILT_IN_NAMES array from tool_resolver.ts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The path containment check used hardcoded "/" which fails on Windows
where resolve() returns backslash-separated paths. Use SEPARATOR from
@std/path and temp dirs in tests instead of hardcoded Unix paths.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Clean, well-designed PR. The DDD patterns are solid — ToolConfig as a value object, ToolResolver as a domain service with an injected CustomToolLoader port to avoid domain→infrastructure coupling, and exhaustive builtInToolConfig factory. Test coverage for the new domain and infrastructure modules is thorough (custom_tool_test.ts, tool_resolver_test.ts, custom_tools_repository_test.ts).

Blocking Issues

None.

Suggestions

  1. No test file for agent_setup.ts — The domain logic it delegates to is well-tested, but repo_init_test.ts and doctor_audit_test.ts set a precedent for CLI command tests. At minimum, testing that agentListCommand and agentRemoveCommand wire up correctly (or that the --json guard on agentSetupCommand throws UserError) would be lightweight wins.

  2. Unused re-export in ai_tool.tsisBuiltInTool is re-exported from ai_tool.ts (line 39) but the only consumer (doctor_service.ts) imports from custom_tool.ts directly. The re-export is harmless but creates an odd dependency direction (ai_tool.tscustom_tool.tsai_tool.ts type import). Consider removing it until a consumer actually needs it from that path.

  3. Fixed 1024-byte stdin buffer in promptLine — Extremely unlikely to matter for tool names and paths, but Deno.stdin.read(buf) with a 1024-byte buffer silently truncates longer input. A TextLineStream or readline approach would be more robust if this wizard grows more prompts over time.

Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CLI UX Review

Blocking

None.

Suggestions

  1. No .example() calls on new agent subcommands (src/cli/commands/agent_setup.ts). All three (setup, list, rm) have descriptions but no examples. Every other command with a comparable shape (e.g. repoInitCommand, datastoreSetupFilesystemCommand) shows examples in help. swamp agent setup --help gives the user no sample invocation to copy. Consider adding even one example per command — e.g. swamp agent rm windsurf.

  2. Exit-code asymmetry in agent rm for missing tool (agent_setup.ts:298–301). In log mode, swamp agent rm nonexistent throws UserError → non-zero exit. In JSON mode, the same call exits 0 with { removed: false }. A script doing swamp agent rm --json nonexistent || echo "error" will never print "error". The JSON output does carry removed: false, so callers who inspect the payload are fine — but the discrepancy with log mode is surprising. Consider throwing UserError in JSON mode too (or documenting the intended contract).

  3. Tab completion for --tool doesn't include custom tools (repo_init.ts:47–48). ToolNameType.complete() returns only the seven built-in names. After a user defines a custom tool with swamp agent setup, pressing <tab> after --tool won't suggest it. Async IO in complete() is awkward, so this is likely an acceptable known limitation — worth a code comment so future contributors don't wonder.

  4. agent setup has no --repo-dir flag (agent_setup.ts:89). Most commands that touch the repo accept --repo-dir (e.g. datastore setup). agent setup uses resolveRepoDir(undefined) without exposing the flag, so users running from outside the repo root must cd first. Low priority since this is an interactive wizard, but it's an inconsistency.

Verdict

PASS. The --tool help text update is clear, the ToolResolver error (Unknown tool "X". Available tools: …. Run \swamp agent setup` to define a custom tool.) is actionable, and both JSON and log modes work correctly for agent listandagent rm`. No blocking issues.

Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adversarial Review

Critical / High

None found. The new architecture is well-structured: ToolResolver with injected CustomToolLoader avoids domain→infrastructure import violations, path traversal is checked at scaffolding time via assertPathContained, YAML deserialization is validated through toDefinition with field-level checks, and the exhaustive builtInToolConfig switch keeps the AiTool union closed for built-in tools.

Medium

  1. resolveSkillsDir (skill_dirs.ts:44-49) doesn't know about custom tools → wrong extensionsToReinstall advisory during upgrade.
    repo_service.ts:327 calls resolveSkillsDir(repoPath.value, oldPrimary) to find pulled extensions. When oldPrimary is a custom tool (e.g., "windsurf"), resolveSkillsDir falls through to .swamp/pulled-extensions/skills/ instead of the custom tool's actual skills dir (e.g., .windsurf/skills/). Same for repo_service.ts:348. This means listPulledExtensions scans the wrong directory and extensionsToReinstall is incorrect.
    Breaking example: User runs swamp repo init --tool windsurf, pulls extensions, then runs swamp repo upgrade --tool windsurf --tool kiro. The upgrade won't detect pulled extensions in .windsurf/skills/ and won't advise reinstalling them for kiro.
    Suggested fix: Use ToolResolver (or ToolConfig) in the upgrade path to resolve the old primary's skills dir, rather than the static SKILL_DIRS map.

  2. Case-sensitive duplicate check in addCustomTool allows filesystem collisions.
    custom_tools_repository.ts:123 checks t.name === tool.name (exact match), but validateCustomToolName (custom_tool.ts:66) checks built-in conflicts case-insensitively (name.toLowerCase()). A user can add both "Windsurf" and "windsurf" as separate custom tools. Both would scaffold to .Windsurf/skills/ and .windsurf/skills/ respectively — which collide on case-insensitive filesystems (macOS HFS+, Windows NTFS).
    Suggested fix: Lowercase-normalize in the addCustomTool duplicate check, or in validateCustomToolName reject names that differ only by case from existing entries.

  3. No path traversal validation at tool definition time.
    agent_setup.ts:221 calls addCustomTool(repoDir, def) without checking assertPathContained on the user-supplied instructionsPath or chosenSkillsDir. The traversal check only runs later in applyToolScaffolding (repo_service.ts:452-460). A user could type ../../etc/foo as the skills dir during the wizard, and the invalid definition would be saved to .swamp-custom-tools.yaml. They'd only discover the error later when running swamp repo init --tool <name>.
    Suggested fix: Call assertPathContained in the agentSetupCommand action before addCustomTool, so the wizard rejects traversal paths immediately.

Low

  1. promptLine fixed 1024-byte buffer (agent_setup.ts:48-51). Input longer than 1024 bytes is silently truncated. Extremely unlikely in practice (paths are short), but a readAll pattern would be more robust.

  2. Tab completion omits custom tools (repo_init.ts:47-49). ToolNameType.complete() only returns built-in names. Custom tools defined via swamp agent setup won't appear in shell completions. A future enhancement could read .swamp-custom-tools.yaml in complete().

Verdict

PASS — The core architecture is solid: DDD boundaries are respected, the ToolResolver abstraction is clean, path traversal is caught at use-time, YAML validation is thorough, and the built-in/custom tool distinction is well-modeled. The medium findings are real but low-impact (advisory messages, UX friction, filesystem edge case). None cause data loss, corruption, or security vulnerabilities. Ship it and address the mediums in a follow-up.

@stack72 stack72 merged commit c890ba3 into main May 26, 2026
11 checks passed
@stack72 stack72 deleted the worktree-tooling-discovery branch May 26, 2026 19:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant