Tâches : scripts planifiés sans modèle, et l'IA planifie la sienne

Deux reprises d'AJEAN autour des tâches planifiées.

Dossier de scripts (/data/scripts) : un dossier durable À CÔTÉ de memory,
hors du workspace. Le workspace est jetable — supprimer une discussion
emporte ses fichiers — donc un script qu'on veut garder n'y avait pas sa
place. Le briefing machine l'annonce à l'IA, qui y écrit et y lance ses
scripts normalement.

Tâche « script seul » (Task.Kind/Script) : le planificateur lance le script
sans charger le modèle ni consommer un token, sa sortie devient le
compte-rendu, et l'UI l'affiche comme n'importe quelle tâche. Elle tourne
hors du verrou de génération — d'où un registre à part pour l'afficher « en
cours » et l'arrêter (/api/tasks/stop), et un « tester » qui n'attend ni le
verrou ni le moteur. Sélecteur Consigne IA / Script seul dans la modale.

Outils task_list/create/update/delete : l'IA se donne elle-même rappels et
veilles récurrentes, cloisonnés par projet (dans un projet, elle ne voit et
ne pilote que ses tâches). Le budget du préambule passe de 8600 à 11000
caractères, avec le palier documenté dans le test.

Dossier mémoire réservé à ses outils : bash, write et edit ne le touchent
plus (guardToolOnly*). Un `cat memory/…` contournait l'index MEMORY.md, et
c'est la condition d'un chiffrement de la mémoire à venir.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K6CAgoLJzufeA8rZTSpSpY
This commit is contained in:
Claude committed 2026-09-22 13:33:39 +00:00
1 parent 91d1796615
commit 74fb1fc781
17 files changed
+1043 -26

No files matched your search

+68 -8
View File
@@ -21,6 +21,14 @@ func handleTasks(w http.ResponseWriter, r *http.Request) {
conv.mu.Lock()
runningID := conv.runningTaskID
conv.mu.Unlock()
// Une tâche script tourne HORS du verrou de génération (aucune inférence) :
// conv ne la voit pas, on complète donc avec le registre des scripts en cours
// (tasks_script.go), sinon l'UI l'affiche à l'arrêt pendant qu'elle tourne.
if runningID == "" {
if sid, _ := scriptRunningAny(); sid != "" {
runningID = sid
}
}
// Presets (id + nom + actif) pour peupler le sélecteur du formulaire de tâche.
presets := []map[string]any{}
if list, err := ListPresets(); err == nil {
@@ -28,13 +36,20 @@ func handleTasks(w http.ResponseWriter, r *http.Request) {
presets = append(presets, map[string]any{"id": p.ID, "name": p.Name, "active": p.Active})
}
}
// Scripts durables disponibles, pour le sélecteur d'une tâche « script seul ».
scripts, _ := listScripts()
if scripts == nil {
scripts = []scriptInfo{}
}
sendJSON(w, 200, map[string]any{
"ok": true,
"tasks": tasks,
"paused": tasksPaused(),
"agent": agentEnabled(),
"running_id": runningID,
"presets": presets,
"ok": true,
"tasks": tasks,
"scripts": scripts,
"scripts_dir": scriptsDir(),
"paused": tasksPaused(),
"agent": agentEnabled(),
"running_id": runningID,
"presets": presets,
// Projets, pour le sélecteur « projet visé » du formulaire de tâche.
"projects": projectDTOs(),
// État global mémoire/web, pour proposer des défauts cohérents à la création.
@@ -54,6 +69,8 @@ func handleTaskSave(w http.ResponseWriter, r *http.Request) {
TZ string `json:"tz"`
Preset string `json:"preset"`
Project string `json:"project"`
Kind string `json:"kind"`
Script string `json:"script"`
NoMem bool `json:"no_mem"`
NoWeb bool `json:"no_web"`
Enabled bool `json:"enabled"`
@@ -65,8 +82,26 @@ func handleTaskSave(w http.ResponseWriter, r *http.Request) {
req.Name = strings.TrimSpace(req.Name)
req.Prompt = strings.TrimSpace(req.Prompt)
req.Schedule = strings.TrimSpace(req.Schedule)
if req.Name == "" || req.Prompt == "" {
sendJSON(w, 400, map[string]any{"ok": false, "error": "nom et consigne obligatoires"})
req.Kind = strings.TrimSpace(req.Kind)
req.Script = strings.TrimSpace(req.Script)
if req.Name == "" {
sendJSON(w, 400, map[string]any{"ok": false, "error": "nom obligatoire"})
return
}
// Une tâche script exige un script EXISTANT (le vérifier ici évite une tâche
// planifiée qui échouera silencieusement au premier tic) ; une tâche IA exige
// une consigne.
if req.Kind == "script" {
if req.Script == "" {
sendJSON(w, 400, map[string]any{"ok": false, "error": "script obligatoire pour une tâche script"})
return
}
if err := scriptExists(req.Script); err != nil {
sendJSON(w, 400, map[string]any{"ok": false, "error": err.Error()})
return
}
} else if req.Prompt == "" {
sendJSON(w, 400, map[string]any{"ok": false, "error": "consigne obligatoire"})
return
}
if err := validateSchedule(req.Schedule, req.TZ); err != nil {
@@ -87,6 +122,7 @@ func handleTaskSave(w http.ResponseWriter, r *http.Request) {
}
t.Name, t.Prompt, t.Enabled = req.Name, req.Prompt, req.Enabled
t.TZ, t.Preset = req.TZ, req.Preset
t.Kind, t.Script = req.Kind, req.Script
// Projet visé : vide = le projet actif au moment de l'enregistrement. Une tâche
// créée depuis un projet appartient à ce projet — c'est là que sa mémoire et
// ses trackers l'attendent quand elle tournera (voir runTask).
@@ -162,6 +198,22 @@ func handleTasksPause(w http.ResponseWriter, r *http.Request) {
sendJSON(w, 200, map[string]any{"ok": true, "paused": tasksPaused()})
}
// handleTaskStop arrête la tâche en cours. Une tâche SCRIPT ne passe pas par le
// verrou de génération : on l'annule via son registre (tasks_script.go). Sinon
// (tâche IA), on retombe sur conv.Stop, qui annule la génération autonome.
func handleTaskStop(w http.ResponseWriter, r *http.Request) {
var req struct {
ID string `json:"id"`
}
_ = json.NewDecoder(r.Body).Decode(&req)
if req.ID != "" && scriptRunStop(req.ID) {
sendJSON(w, 200, map[string]any{"ok": true})
return
}
conv.Stop()
sendJSON(w, 200, map[string]any{"ok": true})
}
// handleTaskRun lance une tâche MAINTENANT (bouton « tester »). Exécution
// détachée : on rend la main tout de suite et on rafraîchit l'état à la fin.
// 409 si une génération est déjà en cours.
@@ -175,6 +227,14 @@ func handleTaskRun(w http.ResponseWriter, r *http.Request) {
sendJSON(w, 404, map[string]any{"ok": false, "error": "tâche introuvable"})
return
}
// Tâche script : aucune inférence, donc ni verrou de génération ni moteur
// chargé à attendre — la refuser parce que le modèle répond ailleurs n'aurait
// aucun sens.
if t.Kind == "script" {
go runTask(t)
sendJSON(w, 200, map[string]any{"ok": true})
return
}
// Vérifie la disponibilité AVANT de détacher, pour pouvoir répondre 409/503.
conv.mu.Lock()
busy := conv.Generating