Score how often a real utterance reaches Hexis/Praxis (ecosystem reach fixture) #160

Merged
claude merged 21 commits from task/405-score-how-often-a-real-utterance-reaches into master 2026-08-04 18:50:49 +02:00
4 changed files with 127 additions and 120 deletions
Showing only changes of commit 2689715c2d - Show all commits
+24
View File
@@ -0,0 +1,24 @@
{{template "shellTop" "chat"}}
<h1>Chat</h1>
<section class=card>
{{if .Error}}<div class="msg msg-err">{{.Error}}</div>{{end}}
<div class="scroll chat-scroll" id=chatHistory>
{{range .Messages}}
<div class="chat-msg {{.Role}}"><strong>{{if eq .Role "user"}}you{{else}}maven{{end}}:</strong> {{.Text}}</div>
{{else}}
<div class=empty>
<svg class=icon width="20" height="20"><use href="/ethos-icons.svg#i-message"/></svg>
<div>start a conversation</div>
</div>
{{end}}
</div>
<form method=post action=/api/chat class=chat-form>
<input type=text name=text class=input-wide placeholder="type a message..." required autofocus>
<button class=btn>send</button>
</form>
</section>
<script>
var ch = document.getElementById('chatHistory');
if(ch) ch.scrollTop = ch.scrollHeight;
</script>
{{template "shellBottom"}}
+21 -120
View File
@@ -75,6 +75,15 @@ var morningHTML string
//go:embed events.html
var eventsHTML string
//go:embed tools.html
var toolsHTML string
//go:embed routines.html
var routinesHTML string
//go:embed chat.html
var chatPageHTML string
// shellHTML — the shell partial every page is wrapped in: "shellTop", the
// "sidebar" it calls, and "shellBottom". It used to be two Go string constants
// with the sidebar assembled by a strings.Builder, which is the one piece of
@@ -672,92 +681,6 @@ var toolsTmpl = template.Must(template.New("tools").Funcs(func() template.FuncMa
return m
}()).Parse(shellHTML + toolsHTML))
const toolsHTML = `{{template "shellTop" "tools"}}
<h1>Tools</h1>
<p class=hint>enabling requires step-up — <a href=/auth/passkey>assert a passkey</a> first.</p>
{{if .Msg}}<div class="msg msg-ok">{{.Msg}}</div>{{end}}
<section class=card>
<h2 class=card-title>proposed <span class=badge>{{len .Proposed}}</span></h2>
{{if .Proposed}}<p class=hint>maven drafted these from acts she couldn't run. Fill the command (argv, space-separated) and enable. A row in an <code>mcp:</code> scope came from an MCP server and already knows what it calls — check the command, then enable.</p>
<div class=scroll><table><tr><th>name</th><th>scope</th><th>from utterance</th><th>enable as</th></tr>
{{range .Proposed}}<tr>
<td><code>{{.Name}}</code></td><td><span class=badge>{{.Scope}}</span></td><td>{{.Utterance}}</td>
<td><form method=post action=/tools>
<input type=hidden name=name value="{{.Name}}">
<input type=hidden name=scope value="{{.Scope}}">
<input type=hidden name=action value=enable>
<input type=text name=cmd class=input-wide placeholder="systemctl restart" value="{{join .Cmd " "}}" required>
<label><input type=checkbox name=destructive {{if .Destructive}}checked{{end}}> destructive</label>
<button class=btn>enable</button></form>
<form method=post action=/tools class=inline-form>
<input type=hidden name=name value="{{.Name}}">
<input type=hidden name=action value=dismiss>
<button class="btn btn-muted">dismiss</button></form></td>
</tr>{{end}}</table></div>
{{else}}<div class=empty>
<svg class=icon width="20" height="20"><use href="/ethos-icons.svg#i-search"/></svg>
<div>no proposed tools</div>
<div class=hint>maven will propose tools here when she needs help running an action</div>
</div>{{end}}
</section>
<section class=card>
<h2 class=card-title>enabled <span class=badge>{{len .Enabled}}</span></h2>
{{if .Enabled}}<div class=scroll><table><tr><th>name</th><th>scope</th><th>command</th><th></th><th></th></tr>
{{range .Enabled}}<tr><td><code>{{.Name}}</code></td><td><span class=badge>{{.Scope}}</span></td><td><code>{{join .Cmd " "}}</code></td>
<td>{{if .Destructive}}<span class=red>destructive</span>{{end}}</td>
<td><form method=post action=/tools class=inline-form>
<input type=hidden name=name value="{{.Name}}">
<input type=hidden name=scope value="{{.Scope}}">
<input type=hidden name=action value=disable>
<button class=btn>disable</button></form></td></tr>{{end}}</table></div>
{{else}}<div class=empty>
<svg class=icon width="20" height="20"><use href="/ethos-icons.svg#i-settings"/></svg>
<div>no tools enabled</div>
<div class=hint>enable proposed tools above, or ask maven to configure one</div>
</div>{{end}}
</section>
<section class=card>
<h2 class=card-title>MCP servers <span class=badge>{{len .MCP}}</span></h2>
{{if .MCP}}<p class=hint>servers she connects OUT to. Their tools appear above as proposals — a configured server is a place she may look, not a capability she has. A <code>stdio</code> target is a process on this box; an <code>http</code> one on a loopback or LAN address is inside the network, so treat its tools accordingly.</p>
<div class=scroll><table><tr><th>name</th><th>transport</th><th>target</th><th>state</th><th>tools</th></tr>
{{range .MCP}}<tr><td><code>{{.Name}}</code></td><td><span class=badge>{{.Transport}}</span></td><td><code>{{.Target}}</code></td>
<td>{{if .Connected}}connected{{if .Server}}{{.Server}}{{end}}{{else}}<span class=red>down</span>{{if .Err}}{{.Err}}{{end}}{{end}}</td>
<td>{{.Tools}}</td></tr>{{end}}</table></div>
{{else}}<div class=empty>
<svg class=icon width="20" height="20"><use href="/ethos-icons.svg#i-settings"/></svg>
<div>no MCP servers configured</div>
<div class=hint>add an <code>mcp.servers</code> block to mavend.json to let her use an external tool server</div>
</div>{{end}}
</section>
{{template "shellBottom"}}`
// routinesHTML — proposed routine review surface. One row per thing maven
// noticed, in her words, with at most two actions: accept or dismiss.
const routinesHTML = `{{template "shellTop" "routines"}}
<h1>Routines</h1>
{{if .Msg}}<div class="msg msg-ok">{{.Msg}}</div>{{end}}
<section class=card>
<h2 class=card-title>noticed <span class=badge>{{len .Proposed}}</span></h2>
{{if .Proposed}}<div class=scroll><table><tr><th>maven noticed</th><th>when</th><th></th><th></th></tr>
{{range .Proposed}}<tr>
<td>{{.Phrase}}</td><td class=muted>{{.Noticed}}</td>
<td><form method=post action=/routines class=inline-form>
<input type=hidden name=id value="{{.ID}}">
<input type=hidden name=action value=accept>
<button class=btn>accept</button></form></td>
<td><form method=post action=/routines class=inline-form>
<input type=hidden name=id value="{{.ID}}">
<input type=hidden name=action value=dismiss>
<button class="btn btn-muted">dismiss</button></form></td>
</tr>{{end}}</table></div>
{{else}}<div class=empty>
<svg class=icon width="20" height="20"><use href="/ethos-icons.svg#i-wave"/></svg>
<div>no proposed routines</div>
<div class=hint>maven will propose routines here when she detects a recurring pattern</div>
</div>{{end}}
</section>
{{template "shellBottom"}}`
var historyTmpl = template.Must(template.New("history").Funcs(shellFuncs()).Parse(shellHTML + historyHTML))
var notificationsTmpl = template.Must(template.New("notifications").Funcs(shellFuncs()).Parse(shellHTML + notificationsHTML))
@@ -770,6 +693,8 @@ var voiceTmpl = template.Must(template.New("voice").Funcs(shellFuncs()).Parse(sh
var tasksTmpl = template.Must(template.New("tasks").Funcs(shellFuncs()).Parse(shellHTML + tasksHTML))
// routinesTmpl — the proposed-routine review surface. One row per thing maven
// noticed, in her words, with at most two actions: accept or dismiss.
var routinesTmpl = template.Must(template.New("routines").Funcs(shellFuncs()).Parse(shellHTML + routinesHTML))
var traceTmpl = template.Must(template.New("trace").Funcs(func() template.FuncMap {
@@ -1095,9 +1020,10 @@ func fmtTaskDate(t *time.Time) string {
return t.Local().Format("02 Jan")
}
// routineRow is one line on the page: what maven noticed, in her words, and
// how long ago she noticed it.
type routineRow struct {
// routineView is one line on the page: what maven noticed, in her words, and
// how long ago she noticed it. A view model, not a database row — the template
// never formats an interval or a timestamp itself.
type routineView struct {
ID int64
Phrase string
Noticed string
@@ -1159,23 +1085,23 @@ func handleRoutines(w http.ResponseWriter, r *http.Request, core ipc.CoreAPI, se
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := routinesTmpl.Execute(w, struct {
Msg string
Proposed []routineRow
}{msg, routineRows(proposed)}); err != nil {
Proposed []routineView
}{msg, toRoutineViews(proposed)}); err != nil {
log.Printf("routines render: %v", err)
}
}
// routineRows turns the wire rows into display rows. The phrase comes from
// toRoutineViews turns the wire rows into view models. The phrase comes from
// pattern.PhraseRoutine so the page says the same thing maven's voice says.
func routineRows(rs []ipc.ProposedRoutine) []routineRow {
out := make([]routineRow, 0, len(rs))
func toRoutineViews(rs []ipc.ProposedRoutine) []routineView {
out := make([]routineView, 0, len(rs))
for _, r := range rs {
p := pattern.ProposedRoutine{Action: r.Action, Object: r.Object, IntervalDays: r.IntervalDays}
noticed := "just now"
if r.CreatedTs > 0 {
noticed = time.Since(time.UnixMilli(r.CreatedTs)).Round(time.Minute).String() + " ago"
}
out = append(out, routineRow{ID: r.ID, Phrase: pattern.PhraseRoutine(&p), Noticed: noticed})
out = append(out, routineView{ID: r.ID, Phrase: pattern.PhraseRoutine(&p), Noticed: noticed})
}
return out
}
@@ -1574,31 +1500,6 @@ func handlePTT(w http.ResponseWriter, r *http.Request, voiceAddr string, session
// and the handler redirects back to /chat with the response.
var chatTmpl = template.Must(template.New("chat").Funcs(shellFuncs()).Parse(shellHTML + chatPageHTML))
const chatPageHTML = `{{template "shellTop" "chat"}}
<h1>Chat</h1>
<section class=card>
{{if .Error}}<div class="msg msg-err">{{.Error}}</div>{{end}}
<div class="scroll chat-scroll" id=chatHistory>
{{range .Messages}}
<div class="chat-msg {{.Role}}"><strong>{{if eq .Role "user"}}you{{else}}maven{{end}}:</strong> {{.Text}}</div>
{{else}}
<div class=empty>
<svg class=icon width="20" height="20"><use href="/ethos-icons.svg#i-message"/></svg>
<div>start a conversation</div>
</div>
{{end}}
</div>
<form method=post action=/api/chat class=chat-form>
<input type=text name=text class=input-wide placeholder="type a message..." required autofocus>
<button class=btn>send</button>
</form>
</section>
<script>
var ch = document.getElementById('chatHistory');
if(ch) ch.scrollTop = ch.scrollHeight;
</script>
{{template "shellBottom"}}`
// handleChatPage renders the chat conversation page.
func handleChatPage(w http.ResponseWriter, r *http.Request, core ipc.CoreAPI) {
if core == nil {
+24
View File
@@ -0,0 +1,24 @@
{{template "shellTop" "routines"}}
<h1>Routines</h1>
{{if .Msg}}<div class="msg msg-ok">{{.Msg}}</div>{{end}}
<section class=card>
<h2 class=card-title>noticed <span class=badge>{{len .Proposed}}</span></h2>
{{if .Proposed}}<div class=scroll><table><tr><th>maven noticed</th><th>when</th><th></th><th></th></tr>
{{range .Proposed}}<tr>
<td>{{.Phrase}}</td><td class=muted>{{.Noticed}}</td>
<td><form method=post action=/routines class=inline-form>
<input type=hidden name=id value="{{.ID}}">
<input type=hidden name=action value=accept>
<button class=btn>accept</button></form></td>
<td><form method=post action=/routines class=inline-form>
<input type=hidden name=id value="{{.ID}}">
<input type=hidden name=action value=dismiss>
<button class="btn btn-muted">dismiss</button></form></td>
</tr>{{end}}</table></div>
{{else}}<div class=empty>
<svg class=icon width="20" height="20"><use href="/ethos-icons.svg#i-wave"/></svg>
<div>no proposed routines</div>
<div class=hint>maven will propose routines here when she detects a recurring pattern</div>
</div>{{end}}
</section>
{{template "shellBottom"}}
+58
View File
@@ -0,0 +1,58 @@
{{template "shellTop" "tools"}}
<h1>Tools</h1>
<p class=hint>enabling requires step-up — <a href=/auth/passkey>assert a passkey</a> first.</p>
{{if .Msg}}<div class="msg msg-ok">{{.Msg}}</div>{{end}}
<section class=card>
<h2 class=card-title>proposed <span class=badge>{{len .Proposed}}</span></h2>
{{if .Proposed}}<p class=hint>maven drafted these from acts she couldn't run. Fill the command (argv, space-separated) and enable. A row in an <code>mcp:</code> scope came from an MCP server and already knows what it calls — check the command, then enable.</p>
<div class=scroll><table><tr><th>name</th><th>scope</th><th>from utterance</th><th>enable as</th></tr>
{{range .Proposed}}<tr>
<td><code>{{.Name}}</code></td><td><span class=badge>{{.Scope}}</span></td><td>{{.Utterance}}</td>
<td><form method=post action=/tools>
<input type=hidden name=name value="{{.Name}}">
<input type=hidden name=scope value="{{.Scope}}">
<input type=hidden name=action value=enable>
<input type=text name=cmd class=input-wide placeholder="systemctl restart" value="{{join .Cmd " "}}" required>
<label><input type=checkbox name=destructive {{if .Destructive}}checked{{end}}> destructive</label>
<button class=btn>enable</button></form>
<form method=post action=/tools class=inline-form>
<input type=hidden name=name value="{{.Name}}">
<input type=hidden name=action value=dismiss>
<button class="btn btn-muted">dismiss</button></form></td>
</tr>{{end}}</table></div>
{{else}}<div class=empty>
<svg class=icon width="20" height="20"><use href="/ethos-icons.svg#i-search"/></svg>
<div>no proposed tools</div>
<div class=hint>maven will propose tools here when she needs help running an action</div>
</div>{{end}}
</section>
<section class=card>
<h2 class=card-title>enabled <span class=badge>{{len .Enabled}}</span></h2>
{{if .Enabled}}<div class=scroll><table><tr><th>name</th><th>scope</th><th>command</th><th></th><th></th></tr>
{{range .Enabled}}<tr><td><code>{{.Name}}</code></td><td><span class=badge>{{.Scope}}</span></td><td><code>{{join .Cmd " "}}</code></td>
<td>{{if .Destructive}}<span class=red>destructive</span>{{end}}</td>
<td><form method=post action=/tools class=inline-form>
<input type=hidden name=name value="{{.Name}}">
<input type=hidden name=scope value="{{.Scope}}">
<input type=hidden name=action value=disable>
<button class=btn>disable</button></form></td></tr>{{end}}</table></div>
{{else}}<div class=empty>
<svg class=icon width="20" height="20"><use href="/ethos-icons.svg#i-settings"/></svg>
<div>no tools enabled</div>
<div class=hint>enable proposed tools above, or ask maven to configure one</div>
</div>{{end}}
</section>
<section class=card>
<h2 class=card-title>MCP servers <span class=badge>{{len .MCP}}</span></h2>
{{if .MCP}}<p class=hint>servers she connects OUT to. Their tools appear above as proposals — a configured server is a place she may look, not a capability she has. A <code>stdio</code> target is a process on this box; an <code>http</code> one on a loopback or LAN address is inside the network, so treat its tools accordingly.</p>
<div class=scroll><table><tr><th>name</th><th>transport</th><th>target</th><th>state</th><th>tools</th></tr>
{{range .MCP}}<tr><td><code>{{.Name}}</code></td><td><span class=badge>{{.Transport}}</span></td><td><code>{{.Target}}</code></td>
<td>{{if .Connected}}connected{{if .Server}} — {{.Server}}{{end}}{{else}}<span class=red>down</span>{{if .Err}} — {{.Err}}{{end}}{{end}}</td>
<td>{{.Tools}}</td></tr>{{end}}</table></div>
{{else}}<div class=empty>
<svg class=icon width="20" height="20"><use href="/ethos-icons.svg#i-settings"/></svg>
<div>no MCP servers configured</div>
<div class=hint>add an <code>mcp.servers</code> block to mavend.json to let her use an external tool server</div>
</div>{{end}}
</section>
{{template "shellBottom"}}