Skip to content

feat: add --tool none for headless/CI init#812

Merged
stack72 merged 1 commit intomainfrom
tool-none-init
Mar 21, 2026
Merged

feat: add --tool none for headless/CI init#812
stack72 merged 1 commit intomainfrom
tool-none-init

Conversation

@stack72
Copy link
Contributor

@stack72 stack72 commented Mar 21, 2026

Summary

  • Adds --tool none option to swamp repo init and swamp repo upgrade for headless/CI environments where no AI coding tool is involved
  • When --tool none is 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)
  • The .gitignore managed section still includes .swamp/ but omits tool-specific entries

Use case

When setting up GitHub Actions or other CI pipelines to run swamp workflows directly, there's no appropriate --tool value today. Using --tool claude works but is semantically wrong — no AI agent is involved in CI execution. --tool none provides the correct option for these environments.

Closes #801

Test plan

  • Added unit tests for --tool none init (verifies core structure created, no skills/instructions/settings)
  • Added unit tests for --tool none upgrade (verifies no skills/instructions/settings updated)
  • All 80 existing tests continue to pass
  • Type checking passes
  • Linting passes

🤖 Generated with Claude Code

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>
Copy link

@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.

Medium

  1. src/domain/repo/repo_service.ts:380,415 — Non-null assertions on Partial<Record> lookups in methods that accept AiTool (including "none").

    createInstructionsFileIfNotExists and updateInstructionsFile both do INSTRUCTIONS_FILES[tool]! where tool: AiTool. Since AiTool now includes "none" and INSTRUCTIONS_FILES is Partial<Record<AiTool, string>>, calling either method with tool = "none" would produce join(repoPath, undefined as unknown as string), likely resulting in a path like /repo/undefined rather than a crash.

    Today this is safe because both methods are private and only called within if (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 to Exclude<AiTool, "none"> on these private methods (and similarly for lines 189 and 289 with SKILL_DIRS[tool]!) to let the compiler enforce the contract.

  2. src/domain/repo/repo_service.ts:653-654generateInstructionsContent handles "none" by returning raw body, but this code path is unreachable.

    The "none" case in generateInstructionsContent and usesSharedInstructionsFile will never be reached because callers are guarded by if (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 the Exclude<AiTool, "none"> narrowing suggested above, these dead branches can be removed.

Low

  1. src/domain/repo/repo_service.ts:58-79Partial<Record> is looser than necessary.

    The three lookup dictionaries (SKILL_DIRS, INSTRUCTIONS_FILES, GITIGNORE_TOOL_ENTRIES) are changed from Record<AiTool, string> to Partial<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 like Record<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.

Copy link

@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.

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: AiTool union extended correctly, Partial<Record> used for lookup maps, non-null assertions (!) are safe because they're only accessed inside if (tool !== "none") guards
  • Exhaustiveness: assertNever in switch statements works correctly — the two main switch blocks (init/upgrade) are inside if (tool !== "none") so TypeScript narrows the type; the standalone switches in usesSharedInstructionsFile and generateInstructionsContent include explicit "none" cases
  • DDD: Changes stay within the appropriate domain service layer; AiTool type 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)

  1. "none" cases in usesSharedInstructionsFile and generateInstructionsContent: These methods are only called inside if (tool !== "none") guards, so the case "none" branches are unreachable. They serve as defense-in-depth which is fine, but you could alternatively narrow the parameter type to Exclude<AiTool, "none"> to make this explicit at the type level.

  2. "none" in generateInstructionsContent: The case "none": return body; is grouped separately from the identical case "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

@stack72 stack72 merged commit 08c7a7f into main Mar 21, 2026
9 checks passed
@stack72 stack72 deleted the tool-none-init branch March 21, 2026 01:23
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.

Support headless/CI init without requiring an AI tool name

1 participant