feat(repo): custom agent tool definitions via swamp agent setup#1400
Conversation
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>
ae55750 to
a22124d
Compare
… 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>
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>
There was a problem hiding this comment.
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
-
No test file for
agent_setup.ts— The domain logic it delegates to is well-tested, butrepo_init_test.tsanddoctor_audit_test.tsset a precedent for CLI command tests. At minimum, testing thatagentListCommandandagentRemoveCommandwire up correctly (or that the--jsonguard onagentSetupCommandthrowsUserError) would be lightweight wins. -
Unused re-export in
ai_tool.ts—isBuiltInToolis re-exported fromai_tool.ts(line 39) but the only consumer (doctor_service.ts) imports fromcustom_tool.tsdirectly. The re-export is harmless but creates an odd dependency direction (ai_tool.ts→custom_tool.ts→ai_tool.tstype import). Consider removing it until a consumer actually needs it from that path. -
Fixed 1024-byte stdin buffer in
promptLine— Extremely unlikely to matter for tool names and paths, butDeno.stdin.read(buf)with a 1024-byte buffer silently truncates longer input. ATextLineStreamorreadlineapproach would be more robust if this wizard grows more prompts over time.
There was a problem hiding this comment.
CLI UX Review
Blocking
None.
Suggestions
-
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 --helpgives the user no sample invocation to copy. Consider adding even one example per command — e.g.swamp agent rm windsurf. -
Exit-code asymmetry in
agent rmfor missing tool (agent_setup.ts:298–301). In log mode,swamp agent rm nonexistentthrowsUserError→ non-zero exit. In JSON mode, the same call exits 0 with{ removed: false }. A script doingswamp agent rm --json nonexistent || echo "error"will never print "error". The JSON output does carryremoved: false, so callers who inspect the payload are fine — but the discrepancy with log mode is surprising. Consider throwingUserErrorin JSON mode too (or documenting the intended contract). -
Tab completion for
--tooldoesn'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 withswamp agent setup, pressing<tab>after--toolwon't suggest it. Async IO incomplete()is awkward, so this is likely an acceptable known limitation — worth a code comment so future contributors don't wonder. -
agent setuphas no--repo-dirflag (agent_setup.ts:89). Most commands that touch the repo accept--repo-dir(e.g.datastore setup).agent setupusesresolveRepoDir(undefined)without exposing the flag, so users running from outside the repo root mustcdfirst. 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.
There was a problem hiding this comment.
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
-
resolveSkillsDir(skill_dirs.ts:44-49) doesn't know about custom tools → wrongextensionsToReinstalladvisory during upgrade.
repo_service.ts:327callsresolveSkillsDir(repoPath.value, oldPrimary)to find pulled extensions. WhenoldPrimaryis a custom tool (e.g.,"windsurf"),resolveSkillsDirfalls through to.swamp/pulled-extensions/skills/instead of the custom tool's actual skills dir (e.g.,.windsurf/skills/). Same forrepo_service.ts:348. This meanslistPulledExtensionsscans the wrong directory andextensionsToReinstallis incorrect.
Breaking example: User runsswamp repo init --tool windsurf, pulls extensions, then runsswamp 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: UseToolResolver(orToolConfig) in the upgrade path to resolve the old primary's skills dir, rather than the staticSKILL_DIRSmap. -
Case-sensitive duplicate check in
addCustomToolallows filesystem collisions.
custom_tools_repository.ts:123checkst.name === tool.name(exact match), butvalidateCustomToolName(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 theaddCustomToolduplicate check, or invalidateCustomToolNamereject names that differ only by case from existing entries. -
No path traversal validation at tool definition time.
agent_setup.ts:221callsaddCustomTool(repoDir, def)without checkingassertPathContainedon the user-suppliedinstructionsPathorchosenSkillsDir. The traversal check only runs later inapplyToolScaffolding(repo_service.ts:452-460). A user could type../../etc/fooas 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 runningswamp repo init --tool <name>.
Suggested fix: CallassertPathContainedin theagentSetupCommandaction beforeaddCustomTool, so the wizard rejects traversal paths immediately.
Low
-
promptLinefixed 1024-byte buffer (agent_setup.ts:48-51). Input longer than 1024 bytes is silently truncated. Extremely unlikely in practice (paths are short), but areadAllpattern would be more robust. -
Tab completion omits custom tools (repo_init.ts:47-49).
ToolNameType.complete()only returns built-in names. Custom tools defined viaswamp agent setupwon't appear in shell completions. A future enhancement could read.swamp-custom-tools.yamlincomplete().
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.
Summary
swamp agent setupinteractive wizard for defining custom AI agent tools.swamp-custom-tools.yamlat repo rootswamp repo init --tool <custom-name>works once definedHow it works
swamp agent setupasks the tool name, optionally scans an existing repo for config patterns (.windsurf/rules/,AGENTS.md, etc.), then asks where the tool reads instructions from.<name>/skills/, instructions mode (shared vs owned) from the file location.swamp-custom-tools.yaml— committable, copyable between reposswamp repo init --tool <name>resolves custom tools viaToolResolver, copies skills, generates instructionsArchitecture
ToolConfigvalue object unifies built-in and custom tools at the scaffolding layerAiToolunion stays closed for built-in exhaustiveness checksRepoMarkerData.tools, init/upgrade options/results) widened tostring[]ToolResolverchecks built-in tools first (O(1)), then custom-tools.yaml (lazy-loaded)ToolResolveraccepts an injectedCustomToolLoaderto avoid domain→infrastructure importNew files
src/domain/repo/custom_tool.ts— types, factories, validation, detection, defaults derivationsrc/domain/repo/tool_resolver.ts— resolution layer (built-in + custom)src/infrastructure/persistence/custom_tools_repository.ts— YAML persistencesrc/cli/commands/agent_setup.ts—swamp agent setup/list/rmcommandsEcosystem 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 checkpassesdeno lintclean on new filesdeno fmtapplieddeno run test— 6007 passed, 0 faileddeno run compile— binary compilesswamp agent setup→ define custom tool →swamp repo init --tool <name>→ verify scaffoldingswamp agent list/swamp agent rmswamp repo init --tool nonexistent→ helpful error🤖 Generated with Claude Code