diff --git a/internal/loki/chat_conversation.go b/internal/loki/chat_conversation.go index b3aa038..9b0c928 100644 --- a/internal/loki/chat_conversation.go +++ b/internal/loki/chat_conversation.go @@ -769,6 +769,9 @@ func (c *Conversation) generate(ctx context.Context, caps Caps, temperature floa final := msgs if caps.Agent { final = append(projectSystemMessages(), msgs...) + if m, ok := codeInstructionsMessage(caps); ok { + final = append([]Message{m}, final...) + } if sp := readSysPrompt(); sp != "" { final = append([]Message{{Role: "system", Content: sp}}, final...) } diff --git a/internal/loki/code_instructions.go b/internal/loki/code_instructions.go new file mode 100644 index 0000000..ee56b62 --- /dev/null +++ b/internal/loki/code_instructions.go @@ -0,0 +1,55 @@ +package loki + +// code_instructions.go — consignes du dépôt (AGENTS.md, CLAUDE.md) en mode Code. +// Repris de context/instructions.ts d'OpenFox. +// +// Un dépôt qui documente ses commandes de build et de test, ses conventions, +// ses pièges, le fait dans ces fichiers. Sans eux, le modèle les redécouvrait +// à coups de read et de bash — quand il les redécouvrait. Le contenu part avec +// le contexte du projet : déplacé dans le premier message utilisateur par +// normalizeSystemMessages, il ne touche pas au préfixe système (cache). + +import ( + "os" + "path/filepath" + "strings" +) + +const codeInstructionsPrefix = "Repository instructions" + +// codeInstructionsMax : au-delà, on tronque (≈ 1k tokens) — ces fichiers +// peuvent être longs, et le contexte est compté. +const codeInstructionsMax = 4000 + +// codeInstructionFiles : par ordre de préférence ; le premier trouvé gagne. +var codeInstructionFiles = []string{"AGENTS.md", "CLAUDE.md", ".github/copilot-instructions.md"} + +// codeInstructionsMessage renvoie les consignes du dépôt de la discussion +// (racine, ou dépôt cloné détecté par gitRepoDir), ok=false s'il n'y en a pas. +func codeInstructionsMessage(caps Caps) (Message, bool) { + if !caps.Code { + return Message{}, false + } + dirs := []string{agentCwd()} + if d, err := gitRepoDir(""); err == nil && d != dirs[0] { + dirs = append([]string{d}, dirs...) + } + for _, dir := range dirs { + for _, name := range codeInstructionFiles { + b, err := os.ReadFile(filepath.Join(dir, name)) + if err != nil || len(strings.TrimSpace(string(b))) == 0 { + continue + } + s := strings.TrimSpace(string(b)) + if r := []rune(s); len(r) > codeInstructionsMax { + s = string(r[:codeInstructionsMax]) + "\n…[tronqué — lis le fichier pour la suite]" + } + rel := name + if r, err := filepath.Rel(agentCwd(), filepath.Join(dir, name)); err == nil { + rel = filepath.ToSlash(r) + } + return Message{Role: "system", Content: codeInstructionsPrefix + " (" + rel + ") — follow them:\n\n" + s}, true + } + } + return Message{}, false +} diff --git a/internal/loki/code_instructions_test.go b/internal/loki/code_instructions_test.go new file mode 100644 index 0000000..319cc21 --- /dev/null +++ b/internal/loki/code_instructions_test.go @@ -0,0 +1,34 @@ +package loki + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +// AGENTS.md du dépôt cloné part avec le contexte en mode Code, déplacé dans le +// premier message utilisateur (préfixe système intact). +func TestConsignesDuDepot(t *testing.T) { + withWorkspace(t) + if _, ok := codeInstructionsMessage(Caps{Code: true}); ok { + t.Fatal("consignes trouvées dans un dossier vide") + } + repo := filepath.Join(agentCwd(), "projet") + _ = os.MkdirAll(filepath.Join(repo, ".git"), 0o755) + if err := os.WriteFile(filepath.Join(repo, "AGENTS.md"), []byte("Build : make test\n"+strings.Repeat("x", 5000)), 0o644); err != nil { + t.Fatal(err) + } + if _, ok := codeInstructionsMessage(Caps{}); ok { + t.Fatal("consignes injectées hors mode Code") + } + m, ok := codeInstructionsMessage(Caps{Code: true}) + s := msgText(m) + if !ok || !strings.Contains(s, "make test") || !strings.Contains(s, "projet/AGENTS.md") || !strings.Contains(s, "tronqué") { + t.Fatalf("message = %.200q", s) + } + out := normalizeSystemMessages([]Message{{Role: "system", Content: "base"}, m, {Role: "user", Content: "fais-le"}}) + if msgText(out[0]) != "base" || !strings.Contains(msgText(out[1]), "make test") { + t.Fatalf("consignes restées dans le système : %+v", out) + } +} diff --git a/internal/loki/code_verify.go b/internal/loki/code_verify.go index c987165..ef04aed 100644 --- a/internal/loki/code_verify.go +++ b/internal/loki/code_verify.go @@ -152,9 +152,15 @@ func (c *Conversation) runBuilderTurn(ctx context.Context, caps Caps, temperatur msgs := append([]Message(nil), c.Messages...) c.mu.Unlock() - final := msgs + // Même contexte que le tour de build : projet (description, index mémoire, + // trackers) et consignes du dépôt. Sans eux, la reprise changeait le début + // du premier message utilisateur — tout le cache de prompt à refaire. + final := append(projectSystemMessages(), msgs...) + if m, ok := codeInstructionsMessage(caps); ok { + final = append([]Message{m}, final...) + } if sp := readSysPrompt(); sp != "" { - final = append([]Message{{Role: "system", Content: sp}}, msgs...) + final = append([]Message{{Role: "system", Content: sp}}, final...) } var content strings.Builder extra, _ := runChat(ctx, InjectSkills(final, caps), temperature, caps, func(ev StreamEvent) bool { diff --git a/internal/loki/llm_client.go b/internal/loki/llm_client.go index 67323f5..2185abb 100644 --- a/internal/loki/llm_client.go +++ b/internal/loki/llm_client.go @@ -359,7 +359,8 @@ func isProjectSystem(m Message) bool { return strings.HasPrefix(s, projectContextPrefix) || strings.HasPrefix(s, memIndexPrefix) || strings.HasPrefix(s, trackerIndexPrefix) || - strings.HasPrefix(s, memReminderPrefix) + strings.HasPrefix(s, memReminderPrefix) || + strings.HasPrefix(s, codeInstructionsPrefix) } // prependToFirstUser place ctx en tête du premier message user de msgs (modifié