diff --git a/README.md b/README.md index 38d20ea..9b5688f 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,17 @@ go test ./... ## Status +M6 (knowledge port): the planning coach now sharpens intents against who you +actually are. A single profile file (`~/.antidrift/knowledge.md`, overridable +via `ANTIDRIFT_KNOWLEDGE_FILE`) holds your standing context — priorities, +values, what counts as good work — and the daemon loads it asynchronously on +entering planning, mirroring the tasks fetch. The cached text grounds the AI +coach prompt only; the drift judge and nudge are untouched, and the profile text +never crosses the wire. A subtle planning-screen indicator shows whether the +profile loaded and from where, with a "change" affordance to repoint at another +file. It degrades gracefully — a missing, blank, or unreadable file just leaves +the coach ungrounded, and planning still works. + M3.5 (semantic nudge): the drift interceptor catches the *wrong app*, but not the *wrong work inside a right app*. M3.5 adds a third, ambient AI role that — only while you are on-task in an allowed app — periodically reads your recent diff --git a/internal/web/static/app.css b/internal/web/static/app.css index e7d9c95..d46e41f 100644 --- a/internal/web/static/app.css +++ b/internal/web/static/app.css @@ -116,3 +116,10 @@ input:focus { outline: 0; border-color: var(--accent); } cursor: pointer; text-align: left; } .task-chip:hover { border-color: var(--accent); color: var(--accent); } + +/* Planning: standing-profile grounding indicator */ +.knowline { opacity: 0.85; } +.link { + background: none; border: none; padding: 0; font: inherit; font-size: 12px; + color: var(--accent); cursor: pointer; text-decoration: underline; +} diff --git a/internal/web/static/app.js b/internal/web/static/app.js index 001df47..42376f3 100644 --- a/internal/web/static/app.js +++ b/internal/web/static/app.js @@ -157,12 +157,35 @@ function updatePlanningTasks(tasks) { }); } +// updatePlanningKnowledge renders the standing-profile grounding indicator and +// a small "change" affordance to repoint the file. idle/nil renders nothing; +// pending/ready/absent/error each show one quiet line. The profile text never +// reaches the browser — only status, path, and size. +function updatePlanningKnowledge(k) { + const el = document.getElementById('knowBand'); + if (!el) return; + if (!k || k.status === 'idle') { el.innerHTML = ''; return; } + const base = (k.path || '').split('/').pop() || k.path || ''; + let line; + if (k.status === 'pending') line = 'loading profile…'; + else if (k.status === 'ready') line = `grounded by ${base} · ${k.chars} chars`; + else if (k.status === 'absent') line = `no profile (${k.path || 'unset'})`; + else line = 'profile unreadable'; + el.innerHTML = + `${line} `; + document.getElementById('knowChange').onclick = () => { + const next = prompt('Profile file path (blank = default):', k.path || ''); + if (next !== null) post('/knowledge/path', { path: next.trim() }); + }; +} + function render(state) { setStateAttr(state); const rs = state.runtime_state; if (rs === 'planning' && renderedState === 'planning') { updatePlanningCoach(state.coach); updatePlanningTasks(state.tasks); + updatePlanningKnowledge(state.knowledge); return; } if (rs === 'active' && renderedState === 'active') { @@ -189,6 +212,7 @@ function render(state) {
+
@@ -214,6 +238,7 @@ function render(state) { }; updatePlanningCoach(state.coach); updatePlanningTasks(state.tasks); + updatePlanningKnowledge(state.knowledge); } else if (rs === 'active') { const c = state.commitment || {};