feat: add mind model with versioned personality, templates and self-model

Phase 2. Minds are rows with a slug, status (active/disabled/archived),
cognitive configuration and a pointer to the current personality version.
Personalities are structured (identity, values, traits, communication,
behaviour, free text) with a deterministic Brief() used by prompts and the
editor preview. Every change appends a personality_versions row protected
by the append-only trigger; restore copies an old version forward.
Templates carry reusable personalities; minds record their overrides so a
template can be re-applied without losing mind-specific edits. The
self-model starts with structural limitations the mind cannot drop
(no policy changes, no influence over shutdown, no cross-mind access).

API, UI (list, overview, editor with sliders/diff/history/import/export,
template editor) and noemactl mind/template commands. All mutations are
audited with field-level diffs.
This commit is contained in:
Andy Dixon
2026-06-15 22:17:01 +01:00
parent a002a2c100
commit fb34f08f40
28 changed files with 2560 additions and 10 deletions
+19 -1
View File
@@ -17,6 +17,7 @@ import (
"github.com/andydixon/noema/internal/app"
"github.com/andydixon/noema/internal/auth"
"github.com/andydixon/noema/internal/minds"
"github.com/andydixon/noema/internal/version"
"github.com/andydixon/noema/web"
)
@@ -72,11 +73,27 @@ func (u *UI) parse(fsys fs.FS) error {
"bytes": humanBytes,
"pct": func(f float64) string { return fmt.Sprintf("%.0f%%", f*100) },
"conf": func(f float64) string { return fmt.Sprintf("%.2f", f) },
"inc": func(i int) int { return i + 1 },
"title": func(s string) string { s = strings.ReplaceAll(s, "_", " "); return strings.ToUpper(s[:1]) + s[1:] },
"valueKeys": func() []string { return minds.ValueKeys },
"traitKeys": func() []string { return minds.TraitKeys },
"dict": func(kv ...any) map[string]any {
m := map[string]any{}
for i := 0; i+1 < len(kv); i += 2 {
m[fmt.Sprint(kv[i])] = kv[i+1]
}
return m
},
}
base, err := template.New("base").Funcs(funcs).ParseFS(fsys, "layout.html")
if err != nil {
return fmt.Errorf("ui: parse layout: %w", err)
}
if partials, _ := fs.Glob(fsys, "partials/*.html"); len(partials) > 0 {
if _, err := base.ParseFS(fsys, partials...); err != nil {
return fmt.Errorf("ui: parse partials: %w", err)
}
}
pages, err := fs.Glob(fsys, "pages/*.html")
if err != nil {
return err
@@ -180,8 +197,9 @@ func (u *UI) Register(m *http.ServeMux) {
m.Handle("POST /admin/users", u.require(auth.RoleAdministrator, u.usersCreate))
m.Handle("POST /admin/users/{id}", u.require(auth.RoleAdministrator, u.usersUpdate))
u.mindRoutes(m)
for _, p := range []struct{ path, title, note string }{
{"/minds", "Minds", "Mind management arrives in phase 2."},
{"/approvals", "Approvals", "The approval centre arrives with the capability system."},
{"/llm", "LLM", "Provider configuration arrives in phase 7."},
{"/prompts", "Prompts", "The prompt manager arrives in phase 7."},