Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,12 @@ Created: .repl/runtime/
Initialized: execution-state.json
Initialized: task-progress.json
Initialized: execution-log.json
Copied: .repl/agent.md
Copied: AGENTS.md
```

> If `AGENTS.md` already exists in the project root, the template content (excluding the `# AGENTS.md` heading) is **prepended** to the existing file instead of overwriting it.

### 2. Start Runtime Session

```bash
Expand Down Expand Up @@ -353,7 +357,12 @@ repl-cli/
├── internal/
│ └── runtime/
│ └── manager.go
├── templates/
│ ├── .repl/
│ │ └── agent.md ← copied to .repl/agent.md on init
│ └── AGENTS.md ← copied (or prepended) to AGENTS.md on init
├── .repl/
│ ├── agent.md
│ ├── product.md
│ ├── framework.md
│ ├── architecture.md
Expand Down
17 changes: 17 additions & 0 deletions cmd/repl/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,22 @@ func runInit() error {
fmt.Println("Initialized: task-progress.json")
fmt.Println("Initialized: execution-log.json")

// Copy templates/.repl/agent.md → .repl/agent.md
if err := rt.CopyAgentMD(); err != nil {
return fmt.Errorf("failed to copy agent.md: %w", err)
}
fmt.Println("Copied: .repl/agent.md")

// Copy templates/AGENTS.md → AGENTS.md (prepend if already exists)
prepended, err := rt.CopyOrPrependAgentsMD()
if err != nil {
return fmt.Errorf("failed to copy AGENTS.md: %w", err)
}
if prepended {
fmt.Println("Prepended: AGENTS.md")
} else {
fmt.Println("Copied: AGENTS.md")
}

return nil
}
25 changes: 23 additions & 2 deletions cmd/repl/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,20 @@ func TestInitCommand(t *testing.T) {
}

func TestInitCommandExecution(t *testing.T) {
// Clean up any existing .repl directory
// Clean up any existing .repl directory and generated files
_ = os.RemoveAll(".repl")
defer func() { _ = os.RemoveAll(".repl") }()
_ = os.Remove("AGENTS.md")
defer func() {
_ = os.RemoveAll(".repl")
_ = os.Remove("AGENTS.md")
_ = os.RemoveAll("templates")
}()

// Set up template fixtures required by init
_ = os.MkdirAll("templates/.repl", 0755)
_ = os.WriteFile("templates/.repl/agent.md", []byte("# agent.md\n"), 0644)
_ = os.MkdirAll("templates", 0755)
_ = os.WriteFile("templates/AGENTS.md", []byte("# AGENTS.md\n\n## Rule\n\nread agent.md\n"), 0644)

// Execute the init command
cmd := newInitCmd()
Expand Down Expand Up @@ -70,6 +81,16 @@ func TestInitCommandExecution(t *testing.T) {
if _, err := os.Stat(".repl/runtime/execution-log.json"); os.IsNotExist(err) {
t.Error("Expected execution-log.json to be created")
}

// Verify .repl/agent.md was copied
if _, err := os.Stat(".repl/agent.md"); os.IsNotExist(err) {
t.Error("Expected .repl/agent.md to be created")
}

// Verify AGENTS.md was copied
if _, err := os.Stat("AGENTS.md"); os.IsNotExist(err) {
t.Error("Expected AGENTS.md to be created")
}
}

func TestInitCommandAlreadyInitialized(t *testing.T) {
Expand Down
68 changes: 68 additions & 0 deletions internal/runtime/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,74 @@ func Reset() error {
return Init()
}

// CopyAgentMD copies templates/.repl/agent.md to .repl/agent.md.
func CopyAgentMD() error {
const src = "templates/.repl/agent.md"
const dst = ".repl/agent.md"

data, err := os.ReadFile(src)
if err != nil {
return fmt.Errorf("failed to read template %s: %w", src, err)
}

if err := os.WriteFile(dst, data, 0644); err != nil {
return fmt.Errorf("failed to write %s: %w", dst, err)
}

return nil
}

// CopyOrPrependAgentsMD copies templates/AGENTS.md to AGENTS.md.
// If AGENTS.md already exists, the template content (excluding the first heading line)
// is prepended to the existing file.
func CopyOrPrependAgentsMD() (bool, error) {
const src = "templates/AGENTS.md"
const dst = "AGENTS.md"

templateData, err := os.ReadFile(src)
if err != nil {
return false, fmt.Errorf("failed to read template %s: %w", src, err)
}

// Check if AGENTS.md already exists
existingData, err := os.ReadFile(dst)
if err != nil && !os.IsNotExist(err) {
return false, fmt.Errorf("failed to read %s: %w", dst, err)
}

if os.IsNotExist(err) {
// File does not exist: plain copy
if err := os.WriteFile(dst, templateData, 0644); err != nil {
return false, fmt.Errorf("failed to write %s: %w", dst, err)
}
return false, nil
}

// File exists: strip the first heading line from the template, then prepend
templateLines := strings.Split(string(templateData), "\n")
var contentLines []string
for _, line := range templateLines {
if strings.HasPrefix(strings.TrimSpace(line), "# AGENTS") {
continue
}
contentLines = append(contentLines, line)
}

// Remove leading blank lines after heading removal
for len(contentLines) > 0 && strings.TrimSpace(contentLines[0]) == "" {
contentLines = contentLines[1:]
}

prependContent := strings.Join(contentLines, "\n")
merged := prependContent + "\n" + string(existingData)

if err := os.WriteFile(dst, []byte(merged), 0644); err != nil {
return false, fmt.Errorf("failed to write %s: %w", dst, err)
}

return true, nil
}

func Validate() error {
// Check if .repl directory exists
if !Exists() {
Expand Down
132 changes: 132 additions & 0 deletions internal/runtime/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package runtime

import (
"os"
"strings"
"testing"
)

Expand Down Expand Up @@ -329,3 +330,134 @@ func TestValidate(t *testing.T) {
t.Error("Validate() should fail when state file is corrupted")
}
}

func TestCopyAgentMD(t *testing.T) {
// Set up a temp working dir so paths are isolated
origDir, _ := os.Getwd()
tmpDir := t.TempDir()
if err := os.Chdir(tmpDir); err != nil {
t.Fatalf("Chdir failed: %v", err)
}
defer func() { _ = os.Chdir(origDir) }()

// Create template source
_ = os.MkdirAll("templates/.repl", 0755)
_ = os.MkdirAll(".repl", 0755)
const agentContent = "# agent.md\n\ntest content\n"
_ = os.WriteFile("templates/.repl/agent.md", []byte(agentContent), 0644)

if err := CopyAgentMD(); err != nil {
t.Fatalf("CopyAgentMD() failed: %v", err)
}

got, err := os.ReadFile(".repl/agent.md")
if err != nil {
t.Fatalf("failed to read .repl/agent.md: %v", err)
}
if string(got) != agentContent {
t.Errorf("expected:\n%s\ngot:\n%s", agentContent, string(got))
}
}

func TestCopyAgentMD_MissingTemplate(t *testing.T) {
origDir, _ := os.Getwd()
tmpDir := t.TempDir()
if err := os.Chdir(tmpDir); err != nil {
t.Fatalf("Chdir failed: %v", err)
}
defer func() { _ = os.Chdir(origDir) }()

// No template file — should return error
if err := CopyAgentMD(); err == nil {
t.Error("CopyAgentMD() should fail when template is missing")
}
}

func TestCopyOrPrependAgentsMD_NoPrior(t *testing.T) {
origDir, _ := os.Getwd()
tmpDir := t.TempDir()
if err := os.Chdir(tmpDir); err != nil {
t.Fatalf("Chdir failed: %v", err)
}
defer func() { _ = os.Chdir(origDir) }()

// Create template
_ = os.MkdirAll("templates", 0755)
const tmpl = "# AGENTS.md\n\n## Rule\n\nread agent.md\n"
_ = os.WriteFile("templates/AGENTS.md", []byte(tmpl), 0644)

prepended, err := CopyOrPrependAgentsMD()
if err != nil {
t.Fatalf("CopyOrPrependAgentsMD() failed: %v", err)
}
if prepended {
t.Error("prepended should be false when AGENTS.md did not exist")
}

got, err := os.ReadFile("AGENTS.md")
if err != nil {
t.Fatalf("failed to read AGENTS.md: %v", err)
}
if string(got) != tmpl {
t.Errorf("expected:\n%s\ngot:\n%s", tmpl, string(got))
}
}

func TestCopyOrPrependAgentsMD_Prepend(t *testing.T) {
origDir, _ := os.Getwd()
tmpDir := t.TempDir()
if err := os.Chdir(tmpDir); err != nil {
t.Fatalf("Chdir failed: %v", err)
}
defer func() { _ = os.Chdir(origDir) }()

// Create template (heading will be stripped)
_ = os.MkdirAll("templates", 0755)
const tmpl = "# AGENTS.md\n\n## Rule\n\nread agent.md\n"
_ = os.WriteFile("templates/AGENTS.md", []byte(tmpl), 0644)

// Create existing AGENTS.md
const existing = "# My existing rules\n\nsome content\n"
_ = os.WriteFile("AGENTS.md", []byte(existing), 0644)

prepended, err := CopyOrPrependAgentsMD()
if err != nil {
t.Fatalf("CopyOrPrependAgentsMD() failed: %v", err)
}
if !prepended {
t.Error("prepended should be true when AGENTS.md already existed")
}

got, err := os.ReadFile("AGENTS.md")
if err != nil {
t.Fatalf("failed to read AGENTS.md: %v", err)
}

content := string(got)

// heading line must NOT appear
if contains(content, "# AGENTS.md") {
t.Error("heading '# AGENTS.md' must be stripped from prepended content")
}

// template body must appear before existing content
ruleIdx := indexOf(content, "## Rule")
existingIdx := indexOf(content, "# My existing rules")
if ruleIdx < 0 {
t.Error("template body '## Rule' not found in output")
}
if existingIdx < 0 {
t.Error("existing content not found in output")
}
if ruleIdx > existingIdx {
t.Error("template body should appear before existing content")
}
}

func contains(s, sub string) bool {
return strings.Contains(s, sub)
}

func indexOf(s, sub string) int {
return strings.Index(s, sub)
}
Loading