Skip to content

Agent/Command Embedding & CLI Distribution #91

@jmgilman

Description

@jmgilman

Work Unit 003: Agent/Command Embedding & CLI Distribution

1. Behavioral Goal

As a sow repository owner using Claude Code web,
I need all sow agents, commands, prompts, and installation scripts automatically available after cloning,
So that new contributors can start working in ephemeral web VMs without manual setup or plugin installation.

Success Criteria

Users can verify this work unit is complete when:

  1. Embedded artifacts accessible: Running sow claude init in any repository creates .claude/agents/, .claude/commands/, .sow/scripts/ with all necessary files
  2. Idempotent initialization: Running sow claude init multiple times doesn't duplicate files or overwrite customizations
  3. SessionStart hook configured: .claude/settings.json is created or merged with existing hooks preserved
  4. Binary self-contained: The sow CLI binary contains all files needed for web VM operation (no external downloads during init)
  5. Repository-committed: All distributed files can be committed to git and survive VM recreation
  6. Minimal size impact: Total embedded content adds <100KB to binary size

2. Existing Code Context

This work unit leverages the proven embed.FS pattern already used successfully in multiple places in the sow codebase:

Prompt Template Embedding (cli/internal/prompts/prompts.go:31-32):
The prompts package demonstrates the exact pattern we'll replicate. It uses //go:embed templates to bundle markdown files into the binary and exposes them via a public FS variable. The templates package then renders these using templates.Render(prompts.FS, "templates/guidance/research.md", nil). This pattern is clean, type-safe, and proven in production.

Schema Embedding (cli/schemas/embed.go:9-10):
The schemas package shows how to embed multiple file types and subdirectories using //go:embed *.cue project/*.cue cue.mod/module.cue. It demonstrates selective file inclusion and directory traversal.

Repository Initialization (cli/internal/sow/sow.go:38-134):
The sow.Init() function provides the blueprint for our claude.Init() implementation. It shows the established pattern for:

  • Directory creation with os.MkdirAll()
  • File writing with os.WriteFile()
  • Error handling with wrapped errors
  • Version tracking
  • Idempotency checking (lines 46-48 check if already initialized)

Command Structure (cli/cmd/init.go, cli/cmd/project/project.go):
The existing command organization demonstrates the cobra pattern for both simple commands (init.go:12-38) and command groups (project/project.go). New commands should follow this structure: use cmdutil.GetContext(cmd.Context()) to access the sow context, implement business logic in an internal package, and provide clear error messages.

Existing Agents and Commands (.claude/agents/*.md, .claude/commands/*.md):
These 5 agent files (implementer, planner, reviewer, researcher, decomposer) and 9 command files already exist in the repository. We'll copy them into cli/internal/claude/ to embed them, then distribute them back to repositories via sow claude init.

Reference List

Key Files to Study:

  • cli/internal/prompts/prompts.go:31-32 - Embedding pattern with //go:embed
  • cli/internal/sow/sow.go:38-134 - Init function pattern (directory creation, file writing)
  • cli/schemas/embed.go:9-10 - Multi-file embedding pattern
  • cli/cmd/init.go:12-38 - Simple command structure
  • cli/cmd/project/project.go:8-38 - Command group structure
  • .claude/agents/ - 5 agent files to embed (implementer.md, planner.md, reviewer.md, researcher.md, decomposer.md)
  • .claude/commands/ - 9 command files to embed (bootstrap.md, create-adr.md, design-doc.md, fix-bug.md, implement-feature.md, index.md, project-new.md, project.md, sow-greet.md)

Rendering Infrastructure (exists, will be reused):

  • cli/internal/templates/renderer.go:54-91 - Render(embedFS embed.FS, path string, data interface{}) function

3. Design Context

Reference: .sow/knowledge/designs/claude-code-web-integration.md section 2 (Agent Distribution)

The design document specifies that all files needed for Claude Code web integration must be embedded in the CLI binary to achieve a zero-dependency distribution model. This work unit implements the "Agent Distribution" component (design section 2) which has two key requirements:

  1. Everything embedded: Agents, commands, prompts, scripts, and hook templates must all be embedded using //go:embed directives in a new cli/internal/claude package
  2. Single command setup: The sow claude init command must create all necessary repository structure (.claude/agents/, .claude/commands/, .sow/scripts/) and merge hook configuration idempotently

The design emphasizes idempotency (section 2, "Key Design Decisions"): running sow claude init multiple times must be safe. This means:

  • Skip files that already exist (don't overwrite user customizations)
  • Merge .claude/settings.json hooks array (preserve existing hooks)
  • Update only what's missing

The design also specifies minimal size impact (<5MB total for all web VM features, section "Performance Targets"). This work unit contributes <100KB of embedded markdown and shell script files.

4. Implementation Approach

Package Structure

Create a new package at cli/internal/claude/ with this structure:

cli/internal/claude/
├── claude.go              # Init() function and embed directives
├── agents/                # Copied from .claude/agents/
│   ├── implementer.md
│   ├── planner.md
│   ├── reviewer.md
│   ├── researcher.md
│   └── decomposer.md
├── commands/              # Copied from .claude/commands/
│   ├── bootstrap.md
│   ├── create-adr.md
│   ├── design-doc.md
│   ├── fix-bug.md
│   ├── implement-feature.md
│   ├── index.md
│   ├── project-new.md
│   ├── project.md
│   └── sow-greet.md
├── prompts/               # New templates (TBD in work unit 005)
│   └── general.md
├── scripts/               # New scripts (TBD in work unit 006)
│   ├── install-sow.sh
│   └── session-start-prompt.sh
└── config/                # New configuration (TBD in work unit 006)
    └── settings.json

Embedding Pattern

Follow the established pattern from prompts.go:

// cli/internal/claude/claude.go
package claude

import (
    "embed"
    "encoding/json"
    "fmt"
    "io/fs"
    "os"
    "path/filepath"
)

//go:embed agents commands prompts scripts config
var FS embed.FS

func Init(repoRoot string) error {
    // Similar structure to sow.Init()
    // 1. Create directories
    // 2. Copy files from FS to repository
    // 3. Merge settings.json
    // 4. Handle idempotency
}

Init Function Logic

The claude.Init() function should:

  1. Create directory structure:

    • .claude/agents/
    • .claude/commands/
    • .sow/scripts/
  2. Copy embedded files (if they don't exist):

    • Iterate through FS using fs.WalkDir
    • For each file, check if destination exists
    • Skip if exists (idempotency)
    • Otherwise, copy from embedded FS to repository
  3. Merge .claude/settings.json:

    • Read existing settings.json if it exists
    • Parse JSON
    • Append SessionStart hook if not present
    • Write merged result
    • Handle case where settings.json doesn't exist (create new)
  4. Return clear errors for edge cases:

    • Not in git repository
    • Permission errors
    • Invalid existing settings.json

CLI Command

Create cli/cmd/claude.go with command group:

func NewClaudeCmd() *cobra.Command {
    cmd := &cobra.Command{
        Use:   "claude",
        Short: "Manage Claude Code integration",
    }

    cmd.AddCommand(newInitCmd())
    return cmd
}

func newInitCmd() *cobra.Command {
    // Similar to existing init.go pattern
    // Call claude.Init(ctx.RepoRoot())
}

Register in root.go via rootCmd.AddCommand(NewClaudeCmd()).

Settings.json Merge Strategy

The SessionStart hook configuration must be merged carefully:

type Settings struct {
    Hooks map[string][]Hook `json:"hooks,omitempty"`
    // ... other fields preserved
}

type Hook struct {
    Matcher string     `json:"matcher"`
    Hooks   []HookItem `json:"hooks"`
}

type HookItem struct {
    Type    string `json:"type"`
    Command string `json:"command"`
}

Merge algorithm:

  1. Load existing settings.json (or create empty struct)
  2. Check if SessionStart hook with matcher "startup" exists
  3. If not, append our hook
  4. Preserve all other hooks and settings
  5. Marshal back to JSON with indentation

5. Dependencies

None - This is an independent work unit.

While the design document describes a complete system with 5 components (installation, agent distribution, Claude Code integration, GitHub integration, project initialization), this work unit focuses solely on the embedding infrastructure and sow claude init command. The embedded files (install scripts, prompt templates) will be created in later work units but can have placeholder files initially.

6. Acceptance Criteria

Functional Requirements

The following behaviors must be demonstrable:

  1. Embedding works:

    • Run go build successfully
    • Binary size increases by <100KB
    • Embedded FS accessible programmatically (unit test can read files)
  2. Init creates structure:

    cd /path/to/repository
    sow claude init
    # Verify directories created:
    ls .claude/agents/      # 5 agent files
    ls .claude/commands/    # 9 command files
    ls .sow/scripts/        # 2 script files (or placeholders)
  3. Idempotency verified:

    sow claude init          # First run
    sow claude init          # Second run - no errors, no duplicates
    git status               # No changes
  4. Settings merge works:

    # Create settings with existing hook
    echo '{"hooks":{"OtherHook":[...]}}' > .claude/settings.json
    sow claude init
    # Verify both hooks present in settings.json
    cat .claude/settings.json  # Should have OtherHook + SessionStart
  5. Error handling:

    cd /tmp  # Not a git repo
    sow claude init
    # Should error clearly: "not in git repository"

Non-Functional Requirements

  • Performance: sow claude init completes in <1 second
  • Size: Embedded content adds <100KB to binary
  • Compatibility: Existing sow workflows unchanged (no breaking changes)

7. Testing Strategy

Test-Driven Development Approach

Write tests before implementation in this order:

Phase 1: Embedding Tests

// cli/internal/claude/claude_test.go
func TestFS_ContainsAgents(t *testing.T)
func TestFS_ContainsCommands(t *testing.T)
func TestFS_FileReadable(t *testing.T)

Phase 2: Init Function Tests

func TestInit_CreatesDirectories(t *testing.T)
func TestInit_CopiesAgentFiles(t *testing.T)
func TestInit_CopiesCommandFiles(t *testing.T)
func TestInit_Idempotent(t *testing.T)
func TestInit_NotInGitRepo_ReturnsError(t *testing.T)

Phase 3: Settings Merge Tests

func TestInit_CreatesSettingsJSON(t *testing.T)
func TestInit_MergesExistingSettings(t *testing.T)
func TestInit_PreservesExistingHooks(t *testing.T)
func TestInit_InvalidJSON_ReturnsError(t *testing.T)

Phase 4: CLI Command Tests

// cli/cmd/claude_test.go
func TestClaudeInit_Success(t *testing.T)
func TestClaudeInit_OutputMessage(t *testing.T)

Test Patterns

Follow existing test conventions:

  • Table-driven tests where appropriate (see cli/cmd/worktree_test.go)
  • Use t.TempDir() for filesystem tests (see cli/internal/sow/sow.go tests)
  • Mock git repositories with .git directory
  • Use testdata/ for fixture files if needed

Integration Validation

Manual verification steps (not automated):

  1. Build sow CLI: go build -o sow ./cli
  2. Create test repository: mkdir test-repo && cd test-repo && git init
  3. Run init: ../sow claude init
  4. Verify structure: tree .claude .sow/scripts
  5. Run again: ../sow claude init (verify no errors)
  6. Commit and verify: git status (all files tracked)

8. Implementation Notes

Binary Size Monitoring

The design document specifies <5MB total increase for all web VM features. This work unit should contribute <100KB. To monitor:

# Before changes
go build -o sow-before ./cli
ls -lh sow-before

# After changes
go build -o sow-after ./cli
ls -lh sow-after

# Compare
du -h sow-before sow-after

If size exceeds 100KB, investigate:

  • Are test files being embedded? (exclude _test.go)
  • Are hidden files being embedded? (exclude .DS_Store)
  • Use //go:embed selectively to exclude unnecessary files

Idempotency Implementation

Two approaches for file copying idempotency:

Option 1: Check before copy

destPath := filepath.Join(repoRoot, ".claude/agents", filename)
if _, err := os.Stat(destPath); err == nil {
    continue // File exists, skip
}
// Copy file

Option 2: Use OpenFile with O_EXCL

f, err := os.OpenFile(destPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644)
if err != nil {
    if os.IsExist(err) {
        continue // File exists, skip
    }
    return err
}
// Write to f

Recommendation: Option 1 (simpler, clearer intent)

Settings.json Merge Algorithm

Use the standard library encoding/json package:

// Minimal example (expand with error handling)
existing := Settings{}
data, err := os.ReadFile(settingsPath)
if err == nil {
    json.Unmarshal(data, &existing)
}

// Add SessionStart hook if not present
if !hasSessionStartHook(existing) {
    existing.Hooks["SessionStart"] = append(
        existing.Hooks["SessionStart"],
        sessionStartHook,
    )
}

// Write back with indentation
data, _ = json.MarshalIndent(existing, "", "  ")
os.WriteFile(settingsPath, data, 0644)

Handle edge cases:

  • Missing hooks field (initialize map)
  • Missing SessionStart array (initialize slice)
  • Malformed JSON (return clear error)

Directory Permissions

Follow existing sow conventions:

  • Directories: 0755 (rwxr-xr-x)
  • Files: 0644 (rw-r--r--)
  • No executable scripts yet (work unit 006 will handle install-sow.sh)

Error Messages

Provide actionable error messages following existing sow patterns:

// Good (existing pattern from sow.Init)
return fmt.Errorf("failed to create .claude directory: %w", err)

// Good (from github.go)
return fmt.Errorf("not in git repository")

// Avoid
return err  // No context

Placeholder Files

For files that will be implemented in later work units, create minimal placeholders:

cli/internal/claude/prompts/general.md:

# General Mode Operator Prompt

(Implementation pending - Work Unit 005)

cli/internal/claude/scripts/install-sow.sh:

#!/bin/bash
# Installation script
# (Implementation pending - Work Unit 006)
exit 0

This allows the embedding and distribution infrastructure to be tested before the actual content is finalized.

Code Organization

Keep concerns separated:

  • claude.go: Init function, file operations, settings merge
  • claude_test.go: All tests for Init function
  • claude/: Only data files (markdown, scripts, JSON)
  • cmd/claude.go: CLI command definition only (thin wrapper)

Avoid putting business logic in cmd/ - follow the pattern where cmd/init.go calls sow.Init().


Summary

This work unit establishes the foundation for Claude Code web integration by embedding all necessary files in the sow CLI binary and providing a single command (sow claude init) to distribute them to repositories. The implementation follows proven patterns from the existing codebase (embed.FS, Init function structure) and maintains backward compatibility. Future work units will build on this foundation by implementing the actual scripts and prompts, but the distribution infrastructure will be complete and tested.

Estimated Effort: 2-3 days (includes TDD, testing, documentation)
Risk Level: Low (proven patterns, no breaking changes)
Value: High (enables all other web VM features)

Metadata

Metadata

Assignees

No one assigned

    Labels

    sowIssues managed by sow breakdown workflow

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions