Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion assets/dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -749,7 +749,7 @@ async function initPrefs(){
tog.disabled=true; st.textContent='saving…';
try{
const r=await apiFetch('/api/preferences',{method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify({brain:prefsBrain,content:ta.value})});
body:JSON.stringify({brain:prefsBrain,content:ta.value,source:'you'})});
const j=await r.json();
if(r.ok){ st.textContent='saved · '+j.bytes+' bytes'; if(j.cap) PCAP=j.cap; showMeter(j.bytes); setMode(false); } // close only on success
else st.textContent='error: '+(j.error||r.status); // stay open; don't lose the edit
Expand Down
8 changes: 5 additions & 3 deletions assets/skills/distill/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,9 @@ may reword only bullets *you* added, never the user's.

One readable log carries everything you need: `$DATA/preferences/edits.md`, the **diff history**
of the page. Each entry is a timestamped, sourced diff — `· you` is a hand-edit (dashboard or by
hand), `· distill` is a refresh you wrote. The diff is **context-free** (zero unchanged lines):
hand in the dashboard editor), `· agent` is a write by an agent or script through the dashboard API
(the API's default label — treat those lines as revisable, like your own, never as the user's), and
`· distill` is a refresh you wrote. The diff is **context-free** (zero unchanged lines):
**every** line is a real change — a line starting `-` was removed, `+` was added, and the rest of
the line is the content verbatim. A removed bullet `- Foo` therefore appears as `-- Foo` (the
first `-` is the diff marker, `- Foo` is the line). Do not read it as two dashes of content:
Expand All @@ -398,8 +400,8 @@ Then:
parallel key ledger to keep in sync:
- **What the user removed** — any `-` line in a `· you` entry. NEVER re-add a steer the user
deleted; the removal is deliberate. (You just *see* it now, instead of matching keys.)
- **Which current bullets are yours** — ones you introduced in a `· distill` entry that the user
hasn't edited or removed since. Only these may you reword or collapse; treat every other
- **Which current bullets are yours** — ones you introduced in a `· distill` or `· agent` entry
that the user hasn't edited or removed since. Only these may you reword or collapse; treat every other
bullet as the user's and leave it untouched.
- **Whether the user edited since your last write** — if the newest `· you` entry is newer than
your newest `· distill` entry, the page is the user's this run: stay **strictly additive**,
Expand Down
41 changes: 41 additions & 0 deletions internal/dashboard/prefs_source_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package dashboard

import (
"os"
"path/filepath"
"strings"
"testing"
)

// The edit ledger must say who wrote the page: only the dashboard editor is the
// owner's hand ("· you"); any other API caller is an agent ("· agent"), so
// /distill never mistakes agent wording for the owner's protected lines.
func TestPreferencesEditSourceLabel(t *testing.T) {
srv, ts := newTestServer(t)
ledger := filepath.Join(srv.Q.Data, "preferences", "edits.md")
page := filepath.Join(srv.Q.Data, "preferences", "global.md")

if code, resp := postJSON(t, ts.URL+"/api/preferences", map[string]any{"content": "# P\n\n- one\n"}, nil); code != 200 {
t.Fatalf("unlabelled POST = %d %v", code, resp)
}
b, _ := os.ReadFile(ledger)
if !strings.Contains(string(b), " · agent\n") || strings.Contains(string(b), " · you\n") {
t.Errorf("a write without a source must be recorded as · agent, got:\n%s", b)
}

if code, resp := postJSON(t, ts.URL+"/api/preferences", map[string]any{"content": "# P\n\n- two\n", "source": "you"}, nil); code != 200 {
t.Fatalf("source=you POST = %d %v", code, resp)
}
b, _ = os.ReadFile(ledger)
if !strings.Contains(string(b), " · you\n") {
t.Errorf("the editor's write (source=you) must be recorded as · you, got:\n%s", b)
}

code, resp := postJSON(t, ts.URL+"/api/preferences", map[string]any{"content": "# P\n\n- three\n", "source": "distill"}, nil)
if code != 400 || !strings.Contains(resp["error"].(string), "source") {
t.Errorf("unknown source must be rejected with 400 naming source, got %d %v", code, resp)
}
if onDisk, _ := os.ReadFile(page); string(onDisk) != "# P\n\n- two\n" {
t.Errorf("rejected write must not touch the page, got %q", onDisk)
}
}
15 changes: 14 additions & 1 deletion internal/dashboard/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,19 @@ func (s *Server) doPOST(w http.ResponseWriter, r *http.Request) {
s.sendJSON(w, 400, map[string]any{"error": "content must be a string"})
return
}
// Who is writing decides the ledger label /distill reads: "you" is the
// owner typing in the dashboard editor (those lines are protected);
// anything else calling this endpoint — an agent, a script — is "agent",
// so its wording never gets mistaken for the owner's.
source := "agent"
if v, has := d["source"]; has {
src, ok := v.(string)
if !ok || (src != "you" && src != "agent") {
s.sendJSON(w, 400, map[string]any{"error": `source must be "you" (the dashboard editor) or "agent"`})
return
}
source = src
}
pdir := filepath.Join(s.Q.Data, "preferences")
if err := os.MkdirAll(pdir, 0o755); err != nil {
s.postErr(w, err)
Expand All @@ -419,7 +432,7 @@ func (s *Server) doPOST(w http.ResponseWriter, r *http.Request) {
// entry is a real change; nothing unchanged to mistake for an edit.
if diffBody := editLedgerDiff(splitPyLines(old), splitPyLines(content)); len(diffBody) > 0 {
ts := s.Q.Now().Format("2006-01-02T15:04:05") // local time, seconds
entry := "## " + ts + " · you\n\n```diff\n" + strings.Join(diffBody, "\n") + "\n```\n\n"
entry := "## " + ts + " · " + source + "\n\n```diff\n" + strings.Join(diffBody, "\n") + "\n```\n\n"
f, err := os.OpenFile(filepath.Join(pdir, "edits.md"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil {
s.postErr(w, err)
Expand Down
3 changes: 2 additions & 1 deletion internal/dashboard/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,8 @@ func TestPreferencesEditLedger(t *testing.T) {
t.Parallel()
srv, ts := newTestServer(t)
post := func(content string) {
code, _ := postJSON(t, ts.URL+"/api/preferences", map[string]any{"content": content}, nil)
// the dashboard editor identifies itself; see TestPreferencesEditSourceLabel for the API default
code, _ := postJSON(t, ts.URL+"/api/preferences", map[string]any{"content": content, "source": "you"}, nil)
if code != 200 {
t.Fatalf("preferences POST = %d", code)
}
Expand Down
Loading