mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
Sélecteur Chat|Code par discussion dans le pied du composeur ; en mode chat, une détection serveur suggère la bascule (puce ignorable, jamais automatique). Conception reprise d'OpenFox (MIT), réécrite en Go — voir NOTICE.md. - Outils fichiers : read (lignes numérotées, borné), grep, glob, et le tracker « lu avant d'écrire » qui refuse write/edit sur un fichier non lu ou modifié depuis la lecture ; edit préserve les fins de ligne CRLF. - Politique d'exécution : commandes catastrophiques refusées (rm -rf /, mkfs, reboot…), chemins bornés au dossier de la discussion en mode code, mutex par fichier. - Critères d'acceptation : contrat posé via l'outil criteria (éditable dans l'UI), passe de vérification indépendante sur contexte isolé — seule habilitée à marquer « passed » — puis corrections plafonnées. - Rôles embarqués (agents/*.md) : builder, planner, verifier, explorer, code-reviewer ; badge de rôle dans le fil. - LSP : gopls / typescript-language-server / pyright (inclus dans l'image), diagnostics injectés dans le retour de write/edit. - Git natif : git_status, git_diff, git_clone (borné à la discussion). - Jobs d'arrière-plan bash_bg/bash_tail (serveur de dev, build long). - Auto-retry : un appel d'outil écrit en texte (default_api:…, <tool_call>…) relance le tour une fois avec consigne corrective. - Outil ask : question à choix rendue en carte à boutons. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
206 lines
5.7 KiB
Go
206 lines
5.7 KiB
Go
package loki
|
|
|
|
// code_criteria.go — critères d'acceptation du mode code (contrat d'exécution,
|
|
// repris du session-metadata/criteria d'OpenFox, réécrit en Go — voir
|
|
// NOTICE.md). Les critères sont posés au début d'une tâche (par le modèle via
|
|
// l'outil `criteria`, ou à la main dans l'UI), puis la passe de vérification
|
|
// (code_verify.go) les fait passer à `passed` ou `failed`. Le tour de build ne
|
|
// se termine « vraiment » que quand tout est passed — c'est le contrat.
|
|
//
|
|
// Rangés par discussion dans bbolt (bucket chat, clé crit:<id>) : ils suivent
|
|
// la discussion, sa suppression les emporte.
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type Criterion struct {
|
|
ID int `json:"id"`
|
|
Text string `json:"text"`
|
|
Status string `json:"status"` // pending | passed | failed
|
|
Note string `json:"note,omitempty"`
|
|
}
|
|
|
|
const critMaxCount = 20
|
|
|
|
func critKey(convID string) string { return "crit:" + convID }
|
|
|
|
func critList(convID string) []Criterion {
|
|
var list []Criterion
|
|
if b := getBytes(bkChat, critKey(convID)); len(b) > 0 {
|
|
_ = json.Unmarshal(b, &list)
|
|
}
|
|
return list
|
|
}
|
|
|
|
func critSave(convID string, list []Criterion) {
|
|
if len(list) == 0 {
|
|
_ = putBytes(bkChat, critKey(convID), nil) // nil supprime la clé
|
|
return
|
|
}
|
|
b, err := json.Marshal(list)
|
|
if err != nil {
|
|
return
|
|
}
|
|
_ = putBytes(bkChat, critKey(convID), b)
|
|
}
|
|
|
|
func critDrop(convID string) { _ = putBytes(bkChat, critKey(convID), nil) }
|
|
|
|
// critAllPassed : le contrat est rempli. Une liste vide ne compte pas comme
|
|
// remplie — il n'y a simplement pas de contrat.
|
|
func critAllPassed(list []Criterion) bool {
|
|
if len(list) == 0 {
|
|
return false
|
|
}
|
|
for _, c := range list {
|
|
if c.Status != "passed" {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func critPending(list []Criterion) int {
|
|
n := 0
|
|
for _, c := range list {
|
|
if c.Status != "passed" {
|
|
n++
|
|
}
|
|
}
|
|
return n
|
|
}
|
|
|
|
// critRender : la liste formatée pour un prompt (builder ou verifier).
|
|
func critRender(list []Criterion) string {
|
|
var b strings.Builder
|
|
for _, c := range list {
|
|
mark := "[ ]"
|
|
switch c.Status {
|
|
case "passed":
|
|
mark = "[✓]"
|
|
case "failed":
|
|
mark = "[✗]"
|
|
}
|
|
fmt.Fprintf(&b, "%s #%d %s", mark, c.ID, c.Text)
|
|
if c.Note != "" {
|
|
fmt.Fprintf(&b, " — %s", c.Note)
|
|
}
|
|
b.WriteString("\n")
|
|
}
|
|
return strings.TrimRight(b.String(), "\n")
|
|
}
|
|
|
|
func criteriaTool() Tool {
|
|
return Tool{
|
|
Type: "function",
|
|
Function: ToolFunction{
|
|
Name: "criteria",
|
|
Description: "Manage the acceptance criteria of the current task (the contract that defines DONE). " +
|
|
"action=add posits new criteria (texts[]), action=set updates one (id + status passed|failed|pending, optional note), " +
|
|
"action=list shows them, action=clear removes them all. Set criteria BEFORE building; only the verification pass may mark passed.",
|
|
Parameters: map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"action": map[string]any{"type": "string", "enum": []string{"add", "set", "list", "clear"}},
|
|
"texts": map[string]any{"type": "array", "items": map[string]any{"type": "string"}, "description": "add: one entry per criterion, short and testable"},
|
|
"id": map[string]any{"type": "integer", "description": "set: criterion id"},
|
|
"status": map[string]any{"type": "string", "enum": []string{"pending", "passed", "failed"}},
|
|
"note": map[string]any{"type": "string", "description": "set: why it failed / how it was verified"},
|
|
},
|
|
"required": []string{"action"},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// toolCriteria exécute l'outil. allowPass : seul le passage de VÉRIFICATION a
|
|
// le droit de marquer `passed` (sinon le builder s'auto-valide et le contrat ne
|
|
// contraint plus rien).
|
|
func toolCriteria(args map[string]any, allowPass bool) string {
|
|
convID := convEnsureActive()
|
|
action, _ := args["action"].(string)
|
|
list := critList(convID)
|
|
switch action {
|
|
case "list", "":
|
|
if len(list) == 0 {
|
|
return "[aucun critère]"
|
|
}
|
|
return critRender(list)
|
|
case "add":
|
|
raw, _ := args["texts"].([]any)
|
|
next := 1
|
|
for _, c := range list {
|
|
if c.ID >= next {
|
|
next = c.ID + 1
|
|
}
|
|
}
|
|
added := 0
|
|
for _, r := range raw {
|
|
s, ok := r.(string)
|
|
if !ok || strings.TrimSpace(s) == "" {
|
|
continue
|
|
}
|
|
if len(list) >= critMaxCount {
|
|
break
|
|
}
|
|
list = append(list, Criterion{ID: next, Text: strings.TrimSpace(s), Status: "pending"})
|
|
next++
|
|
added++
|
|
}
|
|
if added == 0 {
|
|
return "[erreur] aucun critère fourni (texts)"
|
|
}
|
|
critSave(convID, list)
|
|
critNotify(list)
|
|
return fmt.Sprintf("[ok] %d critère(s) ajouté(s)\n%s", added, critRender(list))
|
|
case "set":
|
|
idf, _ := args["id"].(float64)
|
|
id := int(idf)
|
|
status, _ := args["status"].(string)
|
|
note, _ := args["note"].(string)
|
|
if status == "passed" && !allowPass {
|
|
return "[refusé] Seule la passe de vérification peut marquer un critère passed. Termine ton implémentation ; la vérification suivra."
|
|
}
|
|
for i := range list {
|
|
if list[i].ID != id {
|
|
continue
|
|
}
|
|
if status != "" {
|
|
list[i].Status = status
|
|
}
|
|
if note != "" {
|
|
list[i].Note = strings.TrimSpace(note)
|
|
}
|
|
critSave(convID, list)
|
|
critNotify(list)
|
|
return "[ok] critère #" + fmt.Sprint(id) + " → " + list[i].Status
|
|
}
|
|
return fmt.Sprintf("[erreur] critère #%d inconnu", id)
|
|
case "clear":
|
|
critDrop(convID)
|
|
critNotify(nil)
|
|
return "[ok] critères effacés"
|
|
}
|
|
return "[erreur] action inconnue : " + action
|
|
}
|
|
|
|
// critNotify pousse la liste à l'UI (panneau critères) via le journal de la
|
|
// conversation. Best-effort : hors génération il n'y a pas d'epoch en cours,
|
|
// on publie sur l'epoch courant.
|
|
func critNotify(list []Criterion) {
|
|
if conv == nil {
|
|
return
|
|
}
|
|
conv.mu.Lock()
|
|
epoch := conv.epoch
|
|
conv.mu.Unlock()
|
|
if list == nil {
|
|
list = []Criterion{}
|
|
}
|
|
conv.appendDelta(epoch, map[string]any{"criteria": list, "ts": time.Now().UnixMilli()})
|
|
}
|