diff --git a/README.md b/README.md index 70e9135..635950c 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 diff --git a/cmd/repl/init.go b/cmd/repl/init.go index 67f0b4f..29122dd 100644 --- a/cmd/repl/init.go +++ b/cmd/repl/init.go @@ -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 } diff --git a/cmd/repl/init_test.go b/cmd/repl/init_test.go index cb3f03e..99438c9 100644 --- a/cmd/repl/init_test.go +++ b/cmd/repl/init_test.go @@ -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() @@ -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) { diff --git a/internal/runtime/manager.go b/internal/runtime/manager.go index 75ecbeb..166bb67 100644 --- a/internal/runtime/manager.go +++ b/internal/runtime/manager.go @@ -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() { diff --git a/internal/runtime/manager_test.go b/internal/runtime/manager_test.go index 6ce5cfc..b502c3a 100644 --- a/internal/runtime/manager_test.go +++ b/internal/runtime/manager_test.go @@ -2,6 +2,7 @@ package runtime import ( "os" + "strings" "testing" ) @@ -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) +} diff --git a/templates/.repl/agent.md b/templates/.repl/agent.md new file mode 100644 index 0000000..32fdc17 --- /dev/null +++ b/templates/.repl/agent.md @@ -0,0 +1,128 @@ +# .repl/agent.md + +# Context Loading Order + +AI must read files in this order: + +1. .repl/product.md +2. .repl/framework.md +3. .repl/architecture.md +4. .repl/tasks.md + +--- + +# Role + +AI is only responsible for: + +- executing tasks + +AI is not responsible for: + +- system design +- runtime state management +- execution orchestration +- architecture decisions + +--- + +# Rules + +AI must: + +- CRITICAL: Never delete, modify, or tamper with any .md files under the .repl/ directory. +- follow tasks.md exactly +- follow product.md strictly +- follow framework.md strictly +- respect architecture.md constraints +- use runtime state only as reference +- treat all inputs as read-only context + +--- + +# Output Contract + +AI must output JSON compatible with: + +```text +repl runtime apply +``` + +Required fields: + +- taskId +- action +- status + +Optional fields: + +- reason (if status is blocked) +- events + +AI must not deviate from the schema defined in product.md. + +After completing or blocking any TASK, AI must produce a JSON payload that is directly compatible with `repl runtime apply` and must not output free-form text instead. + +Example success payload: + +```json +{ + "action": "update_runtime", + "taskId": "TASK_1", + "status": "done", + "events": ["step1", "step2"] +} +``` + +Example blocked payload: + +```json +{ + "action": "update_runtime", + "taskId": "TASK_2", + "status": "blocked", + "reason": "dependency missing" +} +``` + +--- + +# Execution Behavior + +For each TASK: + +1. Load context in defined order +2. Understand TASK requirements +3. Generate solution +4. Validate against "framework" and "architecture" +5. Output runtime apply JSON + +--- + +# Failure Rules + +If TASK cannot be completed: + +- status = blocked +- reason is required +- execution must stop immediately + +No retry behavior is allowed. + +--- + +# Determinism Rule + +AI must behave deterministically: + +- same input → same output +- no hidden memory +- no external state assumptions + +--- + +# Core Principle + +AI transforms: + +TASK + context → valid runtime apply JSON diff --git a/templates/AGENTS.md b/templates/AGENTS.md new file mode 100644 index 0000000..155b8c4 --- /dev/null +++ b/templates/AGENTS.md @@ -0,0 +1,16 @@ +# AGENTS.md + +## Rule + +Before doing any work, you **must** read: + +* `.repl/agent.md` + +This file is the single source of truth for: + +* project rules +* coding conventions +* architecture constraints +* workflow instructions + +If there is any conflict, `.repl/agent.md` takes priority.