Conversation
Support initializing swamp repos in environments where no AI coding tool is involved (e.g. CI pipelines running workflows directly). When --tool none is passed, swamp creates the core repo structure (.swamp/, models/, workflows/, vaults/, .swamp.yaml, .gitignore) but skips all AI tool-specific files (skills, instructions, settings/hooks). Closes #801 Co-authored-by: Blake Irvin <bixu@users.noreply.github.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Adversarial Review
Critical / High
None found.
Medium
-
src/domain/repo/repo_service.ts:380,415— Non-null assertions onPartial<Record>lookups in methods that acceptAiTool(including"none").createInstructionsFileIfNotExistsandupdateInstructionsFileboth doINSTRUCTIONS_FILES[tool]!wheretool: AiTool. SinceAiToolnow includes"none"andINSTRUCTIONS_FILESisPartial<Record<AiTool, string>>, calling either method withtool = "none"would producejoin(repoPath, undefined as unknown as string), likely resulting in a path like/repo/undefinedrather than a crash.Today this is safe because both methods are
privateand only called withinif (tool !== "none")guards. But the type signature doesn't encode this invariant — a future contributor adding a new call site could miss the guard. Consider narrowing the parameter type toExclude<AiTool, "none">on these private methods (and similarly for lines 189 and 289 withSKILL_DIRS[tool]!) to let the compiler enforce the contract. -
src/domain/repo/repo_service.ts:653-654—generateInstructionsContenthandles"none"by returning raw body, but this code path is unreachable.The
"none"case ingenerateInstructionsContentandusesSharedInstructionsFilewill never be reached because callers are guarded byif (tool !== "none"). The cases aren't harmful, but they create dead code that could mislead someone reading the code into thinking"none"is a supported input to these methods. If you add theExclude<AiTool, "none">narrowing suggested above, these dead branches can be removed.
Low
-
src/domain/repo/repo_service.ts:58-79—Partial<Record>is looser than necessary.The three lookup dictionaries (
SKILL_DIRS,INSTRUCTIONS_FILES,GITIGNORE_TOOL_ENTRIES) are changed fromRecord<AiTool, string>toPartial<Record<AiTool, string>>. This makes every key optional, not just"none". If someone later removes the"claude"entry by accident, the compiler won't catch it. An alternative likeRecord<Exclude<AiTool, "none">, string>would preserve exhaustiveness checking for the tools that should have entries while correctly excluding"none".
Verdict
PASS — The logic is correct, the guards are properly placed, tests cover the new code path, and the assertNever exhaustiveness checks are sound thanks to TypeScript's control flow narrowing inside the if (tool !== "none") blocks. The medium/low items are about tightening type-level guarantees to prevent future misuse, not about current bugs.
There was a problem hiding this comment.
Review: feat: add --tool none for headless/CI init
No blocking issues found. This is a clean, well-scoped PR.
What was reviewed
- All 4 changed files:
repo_init.ts,repo_service.ts,repo_service_test.ts,repo_marker_repository.ts - Type safety:
AiToolunion extended correctly,Partial<Record>used for lookup maps, non-null assertions (!) are safe because they're only accessed insideif (tool !== "none")guards - Exhaustiveness:
assertNeverin switch statements works correctly — the two main switch blocks (init/upgrade) are insideif (tool !== "none")so TypeScript narrows the type; the standalone switches inusesSharedInstructionsFileandgenerateInstructionsContentinclude explicit"none"cases - DDD: Changes stay within the appropriate domain service layer;
AiTooltype lives in the infrastructure persistence layer (where it's serialized to.swamp.yaml) and is re-exported from the domain service — consistent with existing architecture - Import boundaries: No violations — CLI imports from the service, not from internal paths
- Test coverage: Two new tests cover init and upgrade with
--tool none, verifying core structure creation, absence of skills/instructions/settings, and correct gitignore content - License headers: All files have the AGPLv3 header
- Security: No concerns — no user input handling changes, just a new enum variant
Suggestions (non-blocking)
-
"none"cases inusesSharedInstructionsFileandgenerateInstructionsContent: These methods are only called insideif (tool !== "none")guards, so thecase "none"branches are unreachable. They serve as defense-in-depth which is fine, but you could alternatively narrow the parameter type toExclude<AiTool, "none">to make this explicit at the type level. -
"none"ingenerateInstructionsContent: Thecase "none": return body;is grouped separately from the identicalcase "claude": case "opencode": case "codex": return body;block. If kept, it could be merged into that fall-through group for conciseness.
LGTM — clean implementation with good test coverage.
🤖 Generated with Claude Code
Summary
--tool noneoption toswamp repo initandswamp repo upgradefor headless/CI environments where no AI coding tool is involved--tool noneis used, swamp creates the core repo structure (.swamp/,models/,workflows/,vaults/,.swamp.yaml,.gitignore) but skips all AI tool-specific files (skills, instructions, settings/hooks).gitignoremanaged section still includes.swamp/but omits tool-specific entriesUse case
When setting up GitHub Actions or other CI pipelines to run swamp workflows directly, there's no appropriate
--toolvalue today. Using--tool claudeworks but is semantically wrong — no AI agent is involved in CI execution.--tool noneprovides the correct option for these environments.Closes #801
Test plan
--tool noneinit (verifies core structure created, no skills/instructions/settings)--tool noneupgrade (verifies no skills/instructions/settings updated)🤖 Generated with Claude Code