diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index ba1722be..63fd99d9 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -141,6 +141,11 @@ jobs:
OpenWorkGraph is local-first, open-source context infrastructure for how human and AI-agent work actually happens.
+ ### First useful reconstruction
+ The local dashboard now has an evidence-driven first-value layer designed to become useful during the first real work session. It watches only the existing privacy-hardened local evidence surfaces and, once enough activity exists, offers a deterministic **Your last few minutes** reconstruction. This is a presentation layer over canonical evidence, not a new inference or capture pipeline.
+
+ The activation layer adds no sensor, OS/browser permission, AI permission, retention permission, Gateway sharing, MCP tool, screenshot capture, filesystem watcher, prompt/response capture or content telemetry. It never auto-enables AI access or saved history. Empty first-run placeholders are suppressed until their underlying features have useful data, while Evidence, History, Agents, Connect, Organization and Export remain available unchanged.
+
### Local-first remains the default
A normal OpenWorkGraph install captures to local SQLite, provides local dashboard/export/API/MCP access, and requires no OpenWorkGraph account or OpenWorkGraph-hosted evidence store. Capture continues locally if an optional customer-controlled Gateway or network is unavailable.
@@ -156,7 +161,7 @@ jobs:
The dashboard now separates giving an AI access to OpenWorkGraph context from observing an agent's own execution. It provides reviewable setup material for Claude Code lifecycle hooks, Codex trace export, OpenAI Agents tracing and generic OpenTelemetry/custom structural adapters. OpenWorkGraph does not silently edit third-party configuration files. An integration is shown as active only when telemetry actually observed by the local evidence store supports that status.
### Custom harnesses
- Arbitrary self-built or third-party agent harnesses can now connect in either or both directions. Python and Node/TypeScript helpers, OTLP/HTTP JSON and raw structural HTTP can send privacy-safe execution telemetry through the dedicated write-only agent credential. Any MCP-capable harness can separately read only the OpenWorkGraph context the user has authorized. The setup flow keeps telemetry write permission and context/history read permission explicitly separate.
+ Arbitrary self-built or third-party agent harnesses can connect in either or both directions. Python and Node/TypeScript helpers, OTLP/HTTP JSON and raw structural HTTP can send privacy-safe execution telemetry through the dedicated write-only agent credential. Any MCP-capable harness can separately read only the OpenWorkGraph context the user has authorized. The setup flow keeps telemetry write permission and context/history read permission explicitly separate.
The standalone helpers do not accept or serialize prompt text, model responses, tool arguments/results, returned values, exception text or hidden reasoning. OpenWorkGraph observer failures remain fail-open for the agent. This release publishes **OpenWorkGraph-Agent-Python.py**, **OpenWorkGraph-Agent-Node.mjs** and **OpenWorkGraph-Agent-Node.d.ts** as standalone release assets.
diff --git a/VERSION b/VERSION
index 5f8cbfdb..13bb6285 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-0.95.0
+0.96.0
diff --git a/dashboard/first_value_activation.js b/dashboard/first_value_activation.js
new file mode 100644
index 00000000..0fcf607e
--- /dev/null
+++ b/dashboard/first_value_activation.js
@@ -0,0 +1,281 @@
+(() => {
+ 'use strict';
+
+ const DISMISSED_KEY = 'owg_first_value_dismissed_v1';
+ const VIEWED_KEY = 'owg_first_value_reconstruction_viewed_at';
+ const POLL_MS = 15000;
+ const AGENT_POLL_MS = 30000;
+ let latest = null;
+ let busy = false;
+ let agentCache = {executions:[]};
+ let agentCacheAt = 0;
+
+ const esc = value => String(value ?? '').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
+ const n = value => Number.isFinite(Number(value)) ? Number(value) : 0;
+ const safeArray = value => Array.isArray(value) ? value : [];
+
+ function dismissed(){
+ try{return localStorage.getItem(DISMISSED_KEY)==='1';}catch(_){return false;}
+ }
+ function markDismissed(){
+ try{localStorage.setItem(DISMISSED_KEY,'1');}catch(_){}
+ document.querySelector('#firstValueCard')?.remove();
+ }
+ function milestone(name){
+ try{const key=`owg_first_value_${name}_at`;if(!localStorage.getItem(key))localStorage.setItem(key,new Date().toISOString());}catch(_){}
+ }
+ function markViewed(){
+ try{if(!localStorage.getItem(VIEWED_KEY))localStorage.setItem(VIEWED_KEY,new Date().toISOString());}catch(_){}
+ }
+
+ async function getJson(url){
+ await window.__owgAuthReady;
+ const response = await fetch(url,{cache:'no-store'});
+ if(!response.ok)throw new Error(`GET ${url} failed`);
+ return response.json();
+ }
+
+ function surfaceOf(item){
+ const host=String(item?.hostname||'').trim().toLowerCase();
+ if(host){
+ const parts=host.replace(/^www\./,'').split('.');
+ if(parts.length>1)return parts.slice(0,-1).join('.');
+ return host;
+ }
+ return String(item?.app||'Unknown surface').trim()||'Unknown surface';
+ }
+
+ function actionOf(item){
+ const label=String(item?.label||'').trim();
+ const action=String(item?.action||'').trim().replace(/[_-]+/g,' ');
+ if(label&&label.toLowerCase()!==surfaceOf(item).toLowerCase())return label.slice(0,120);
+ return action ? action.charAt(0).toUpperCase()+action.slice(1) : 'Observed activity';
+ }
+
+ function recentWindow(summary){
+ const rows=safeArray(summary?.recent_evidence).slice().reverse();
+ const cutoff=Date.now()-(15*60*1000);
+ const filtered=rows.filter(row=>{
+ const t=Date.parse(String(row?.observed_at||''));
+ return !Number.isFinite(t)||t>=cutoff;
+ });
+ return filtered.length?filtered:rows.slice(-30);
+ }
+
+ function collapsedEvidence(summary){
+ const rows=recentWindow(summary);
+ const output=[];
+ for(const row of rows){
+ const surface=surfaceOf(row),action=actionOf(row),observed_at=row?.observed_at||'';
+ const prior=output[output.length-1];
+ if(prior&&prior.kind==='human'&&prior.surface===surface&&prior.action===action){
+ prior.count+=1;prior.observed_at=observed_at||prior.observed_at;continue;
+ }
+ output.push({kind:'human',surface,action,observed_at,count:1,source:String(row?.source||'')});
+ }
+ return output;
+ }
+
+ function transitionFallback(summary){
+ return safeArray(summary?.transitions).slice(0,8).map(row=>({
+ kind:'human',
+ surface:`${String(row?.from||'Work surface')} → ${String(row?.to||'Work surface')}`,
+ action:`Observed transition${n(row?.count)>1?` · ${n(row?.count)} times`:''}`,
+ observed_at:'',count:1,source:'summary-transition'
+ }));
+ }
+
+ function agentItems(agentPayload){
+ const result=[];
+ for(const run of safeArray(agentPayload?.executions)){
+ const agent=run?.agent||{};
+ const name=agent.framework||agent.provider||agent.name||'Agent';
+ const operations=run?.operation_counts||{};
+ const tools=n(operations.tool_call);
+ const models=n(operations.model_call);
+ const approvals=n(run?.approval_request_count);
+ const failures=n(run?.failure_count)+n(operations.error);
+ const status=String(run?.outcome_status||'observed');
+ const details=[];
+ if(models)details.push(`${models} model ${models===1?'call':'calls'}`);
+ if(tools)details.push(`${tools} tool ${tools===1?'call':'calls'}`);
+ if(approvals)details.push(`${approvals} approval ${approvals===1?'request':'requests'}`);
+ if(failures)details.push(`${failures} observed ${failures===1?'failure':'failures'}`);
+ details.push(status);
+ result.push({
+ kind:'agent',surface:String(name).slice(0,80),action:details.join(' · '),
+ observed_at:run?.ended_at||run?.started_at||'',count:1,
+ observation_level:String(run?.observation_level||agent?.observation_level||'partial observation')
+ });
+ }
+ return result;
+ }
+
+ function reconstruction(summary,agentPayload){
+ let human=collapsedEvidence(summary);
+ if(!human.length)human=transitionFallback(summary);
+ const combined=[...human,...agentItems(agentPayload)];
+ combined.sort((a,b)=>{
+ const ta=Date.parse(String(a.observed_at||'')),tb=Date.parse(String(b.observed_at||''));
+ if(!Number.isFinite(ta)&&!Number.isFinite(tb))return 0;
+ if(!Number.isFinite(ta))return -1;if(!Number.isFinite(tb))return 1;return ta-tb;
+ });
+ const compact=[];
+ for(const item of combined){
+ const prior=compact[compact.length-1];
+ if(prior&&item.kind==='human'&&prior.kind==='human'&&prior.surface===item.surface){
+ if(prior.action!==item.action)prior.action=`${prior.action} → ${item.action}`.slice(0,180);
+ prior.count+=item.count||1;prior.observed_at=item.observed_at||prior.observed_at;continue;
+ }
+ compact.push({...item});
+ }
+ return compact.slice(-12);
+ }
+
+ function stateFrom(summary,agentPayload){
+ const evidence=recentWindow(summary),recentSurfaces=new Set(evidence.map(surfaceOf).filter(Boolean));
+ const summarySurfaceCount=safeArray(summary?.surfaces).length;
+ let recentTransitions=0,last='';
+ for(const row of evidence){const s=surfaceOf(row);if(last&&s&&s!==last)recentTransitions+=1;if(s)last=s;}
+ const summarizedTransitions=safeArray(summary?.transitions).length;
+ const surfaceCount=Math.max(recentSurfaces.size,summarySurfaceCount);
+ const transitions=Math.max(recentTransitions,summarizedTransitions);
+ const totalEvents=Math.max(evidence.length,n(summary?.events));
+ const runs=safeArray(agentPayload?.executions);
+ const agentEvents=runs.reduce((total,run)=>total+n(run?.event_count_total),0);
+ const ready=(totalEvents>=8&&surfaceCount>=2)||(transitions>=2&&totalEvents>=5)||agentEvents>=3;
+ const apps=safeArray(summary?.apps).map(x=>String(x?.app||'').toLowerCase());
+ const browserHeavy=apps.some(x=>/(chrome|edge|safari|firefox|arc|brave)/.test(x));
+ const browserConnected=Boolean(summary?.browser_sensor?.status==='connected'||summary?.browser_sensor?.connected||summary?.browser_sensor?.paired||summary?.browser_sensor?.active);
+ const shallowAgent=runs.some(run=>['os_observed','outcome_only'].includes(String(run?.observation_level||run?.agent?.observation_level||'')));
+ return {evidence_count:totalEvents,surface_count:surfaceCount,transitions,agent_runs:runs.length,agent_events:agentEvents,ready,browserHeavy,browserConnected,shallowAgent};
+ }
+
+ function aiEnabled(payload){return Boolean(payload?.enabled??payload?.ai_access_enabled??payload?.access?.enabled);}
+
+ function ensureStyle(){
+ if(document.querySelector('#first-value-style'))return;
+ const style=document.createElement('style');style.id='first-value-style';
+ style.textContent=`
+ .first-value-card{border:2px solid #285e42;background:linear-gradient(135deg,#ffffff,#f5faf6)}
+ .first-value-head{display:flex;justify-content:space-between;gap:14px;align-items:flex-start}.first-value-head button{min-height:32px;padding:4px 8px}
+ .first-value-progress{display:flex;gap:8px;flex-wrap:wrap;margin:13px 0}.first-value-progress span{border:1px solid #d8e5dc;background:#fff;border-radius:999px;padding:6px 9px;font-size:12px}
+ .first-value-ready{padding:10px 12px;border-radius:11px;background:#edf7f0;color:#285e42;font-weight:750;margin:10px 0}
+ .first-value-actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:12px}.first-value-actions button{min-height:38px}
+ .first-value-trace{margin:10px 0 0;padding:0;list-style:none}.first-value-trace li{display:grid;grid-template-columns:18px minmax(0,1fr);gap:8px;padding:8px 0;border-bottom:1px solid #eceee8}.first-value-trace li:last-child{border-bottom:0}.first-value-node{width:9px;height:9px;border-radius:50%;background:#285e42;margin-top:5px}.first-value-node.agent{background:#3159a5}.first-value-trace strong{font-size:13px}.first-value-trace .muted{margin-top:2px}
+ `;
+ document.head.appendChild(style);
+ }
+
+ function ensureCard(){
+ if(dismissed())return null;
+ const overview=document.querySelector('#panel-overview');if(!overview)return null;
+ let card=document.querySelector('#firstValueCard');
+ if(card)return card;
+ card=document.createElement('div');card.id='firstValueCard';card.className='card first-value-card';
+ card.innerHTML=`
FIRST VALUE
See what OpenWorkGraph understands
Keep working normally. OpenWorkGraph will use the evidence already being captured on this computer to show you a factual reconstruction.
Waiting for enough observed activity to form a useful reconstruction.
`;
+ const onboarding=document.querySelector('#historyOnboarding');
+ if(onboarding&&onboarding.parentNode===overview)onboarding.insertAdjacentElement('afterend',card);else overview.insertBefore(card,overview.firstChild);
+ card.querySelector('#firstValueDismiss').onclick=markDismissed;
+ return card;
+ }
+
+ function hideStaticTimelinePlaceholder(){
+ const overview=document.querySelector('#panel-overview');if(!overview)return;
+ for(const card of overview.querySelectorAll('.card')){
+ const text=card.textContent||'';
+ if(text.includes('Timeline lanes will activate in the stacked capture/timeline PR. Existing evidence collection is unchanged.'))card.style.display='none';
+ }
+ }
+
+ function hideEmptyPlaceholders(summary){
+ hideStaticTimelinePlaceholder();
+ const patternList=document.querySelector('#patternList');
+ if(patternList){
+ const card=patternList.closest('.card');
+ if(card)card.style.display=n(summary?.repeated_task_pattern_count)>0?'':'none';
+ }
+ }
+
+ function render(state,summary,agentPayload,aiPayload){
+ hideEmptyPlaceholders(summary);
+ if(state.evidence_count>0)milestone('first_event');
+ if(state.surface_count>1)milestone('first_cross_surface');
+ if(state.agent_runs>0)milestone('first_agent_run');
+ if(state.ready)milestone('ready');
+ const card=ensureCard();if(!card)return;
+ card.querySelector('#firstValueProgress').innerHTML=`${state.evidence_count} observed events${state.surface_count} work ${state.surface_count===1?'surface':'surfaces'}${state.transitions} observed ${state.transitions===1?'transition':'transitions'}${state.agent_runs} agent ${state.agent_runs===1?'run':'runs'}`;
+ const status=card.querySelector('#firstValueStatus'),actions=card.querySelector('#firstValueActions');
+ if(state.ready){
+ status.className='first-value-ready';status.textContent='OpenWorkGraph has enough observed activity to show a reconstruction. No AI interpretation is required for this view.';
+ actions.innerHTML='';
+ card.querySelector('#firstValueSee').onclick=()=>openReconstruction(summary,agentPayload,aiPayload,state);
+ }else{
+ status.className='note';
+ status.textContent=state.evidence_count===0?'No activity from this run has reached the local evidence store yet. Keep working normally.':'Evidence is arriving. A few more actions or a cross-tool transition will make the reconstruction more useful.';
+ actions.innerHTML='';
+ card.querySelector('#firstValueEvidence').onclick=()=>window.activateTab?.('evidence');
+ }
+ }
+
+ function traceHtml(items){
+ if(!items.length)return '
There is not enough bounded recent evidence to render a trace yet.
`).join('')}`;
+ }
+
+ function openReconstruction(summary,agentPayload,aiPayload,state){
+ markViewed();
+ const items=reconstruction(summary,agentPayload),enabled=aiEnabled(aiPayload);
+ const prompt='Using OpenWorkGraph, reconstruct what I was doing during the last 10 minutes. Distinguish observed facts from inference.';
+ let next='';
+ if(enabled){
+ next=`
Let your AI inspect the same work
AI access is currently on. Ask it to use OpenWorkGraph rather than relying on chat context alone.
${esc(prompt)}
`;
+ }else{
+ next='
Let your AI understand this too
AI access is still off. Connecting an AI is optional and does not change capture or retention.
Browser context can be richer. Your work includes a browser, but no active browser sensor was detected. The browser sensor is optional.
';
+ if(state.shallowAgent)next+='
Agent internals are only partially observed. You can add native/OTel telemetry for deeper structural traces without capturing prompts or responses.
';
+ const body=`
Observed evidence only. This reconstruction is assembled from the current session's existing privacy-hardened evidence. It does not infer intent or read hidden reasoning.
${traceHtml(items)}${next}`;
+ if(typeof window.openModal==='function')window.openModal('Your last few minutes','Observed reconstruction',body);else alert(items.map(x=>`${x.surface}: ${x.action}`).join('\n'));
+ setTimeout(()=>{
+ const copy=document.querySelector('#firstValueCopyPrompt');if(copy)copy.onclick=async()=>{try{await navigator.clipboard.writeText(prompt);copy.textContent='Copied';}catch(_){window.prompt('Copy this:',prompt);}};
+ const connect=document.querySelector('#firstValueConnectAI');if(connect)connect.onclick=()=>{window.closeModal?.();window.activateTab?.('connect');};
+ const browser=document.querySelector('#firstValueBrowser');if(browser)browser.onclick=()=>{window.closeModal?.();window.activateTab?.('organization');};
+ const agent=document.querySelector('#firstValueAgent');if(agent)agent.onclick=()=>{window.closeModal?.();window.activateTab?.('connect');setTimeout(()=>document.querySelector('#agent-observation-setup')?.scrollIntoView({behavior:'smooth',block:'start'}),0);};
+ },0);
+ }
+
+ function shouldPoll(force){
+ if(dismissed())return false;
+ if(force)return true;
+ const overview=document.querySelector('#tab-overview');
+ return !document.hidden&&(!overview||overview.getAttribute('aria-selected')==='true');
+ }
+
+ async function refresh(force=false){
+ if(busy||!shouldPoll(force))return;busy=true;
+ try{
+ const now=Date.now();
+ const agentPromise=(now-agentCacheAt>=AGENT_POLL_MS)
+ ? getJson('/v1/agent-execution-traces?limit=10&evidence_limit=3000&max_events_per_execution=20')
+ : Promise.resolve(agentCache);
+ const [summaryResult,agentResult,aiResult]=await Promise.allSettled([
+ getJson('/v1/summary?scope=current&limit=500'),agentPromise,getJson('/v1/ai-access')
+ ]);
+ if(summaryResult.status!=='fulfilled')return;
+ if(agentResult.status==='fulfilled'){agentCache=agentResult.value;agentCacheAt=now;}
+ const summary=summaryResult.value,agentPayload=agentCache,aiPayload=aiResult.status==='fulfilled'?aiResult.value:{};
+ const state=stateFrom(summary,agentPayload);latest={summary,agentPayload,aiPayload,state};render(state,summary,agentPayload,aiPayload);
+ }finally{busy=false;}
+ }
+
+ function install(){
+ ensureStyle();hideStaticTimelinePlaceholder();refresh(true);
+ document.querySelector('#tab-overview')?.addEventListener('click',()=>setTimeout(()=>refresh(true),0));
+ setInterval(()=>refresh(false),POLL_MS);
+ }
+
+ window.refreshFirstValue=()=>refresh(true);
+ window.firstValueReconstruction=()=>latest?openReconstruction(latest.summary,latest.agentPayload,latest.aiPayload,latest.state):refresh(true);
+ if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',install);else install();
+})();
diff --git a/docs/CHANGELOG_V096.md b/docs/CHANGELOG_V096.md
new file mode 100644
index 00000000..d6a7cf91
--- /dev/null
+++ b/docs/CHANGELOG_V096.md
@@ -0,0 +1,22 @@
+# OpenWorkGraph v0.96 — first useful reconstruction
+
+v0.96 adds a thin first-run activation layer over the existing local evidence stack. It is intended to make OpenWorkGraph understandable during the first real work session without changing what the product captures or what an AI may access.
+
+## What changes
+
+- Overview shows a dismissible **See what OpenWorkGraph understands** card.
+- Progress is evidence-driven rather than a countdown: observed events, work surfaces, transitions and agent runs.
+- Once enough current-session evidence exists, **Your last few minutes** presents a deterministic reconstruction from existing privacy-hardened dashboard evidence and structural agent-run reports.
+- If interaction-level evidence is sparse, already-derived observed surface transitions provide a factual fallback rather than inventing task intent.
+- The next action is contextual: Connect AI only when the user chooses, optional browser-sensor setup when browser context is shallow, and optional native/OTel agent telemetry when an agent is only surface-observed.
+- The old unfinished timeline placeholder and an empty repeated-workflows card are suppressed on first run until they have useful content. Their underlying DOM/data paths remain intact.
+
+## What does not change
+
+v0.96 adds no capture sensor, screenshot capture, filesystem watcher, prompt/response capture, clipboard-content capture, browser/OS permission, database schema, retention rule, AI permission, saved-history lease, Gateway behavior, agent-ingest permission, export format or MCP tool.
+
+The first-value layer performs GET requests only against existing authenticated local endpoints. It cannot enable AI access, save history, synchronize evidence or mutate canonical evidence.
+
+## Compatibility goal
+
+Evidence, History, Agents, Connect, Organization and Export remain the established advanced surfaces. Dismissing the first-value card leaves the ordinary dashboard behavior intact. Existing installations and existing MCP configurations keep their prior semantics.
diff --git a/mcpb/manifest.json b/mcpb/manifest.json
index fb515277..661049db 100644
--- a/mcpb/manifest.json
+++ b/mcpb/manifest.json
@@ -2,9 +2,9 @@
"manifest_version": "0.3",
"name": "openworkgraph-local",
"display_name": "OpenWorkGraph",
- "version": "0.95.0",
+ "version": "0.96.0",
"description": "Connect Claude Desktop to the compact local OpenWorkGraph context surface.",
- "long_description": "Uses the OpenWorkGraph installation already running on this computer. v0.95 adds a framework-neutral custom-harness setup flow plus standalone Python and Node helpers for privacy-safe structural agent telemetry; arbitrary MCP-capable harnesses can separately read authorized OpenWorkGraph context. v0.94 added explicit local history retention, separate saved-history AI access, lightweight history navigation, and structural browser-agent lifecycle observation. Canonical workflow evidence remains primary; Context Pulse provides incremental factual updates. Retained history is separately user-controlled: list_history can navigate saved human and agent sessions only while a time-limited saved-history lease is active. The legacy 24-tool MCP entrypoint remains available for existing configurations while new connections use this compact surface. Prompts, model responses, tool arguments/results, typed text, clipboard contents, exception text, returned values, and hidden reasoning are not captured by the custom agent helpers.",
+ "long_description": "Uses the OpenWorkGraph installation already running on this computer. v0.96 adds an evidence-driven first-value dashboard layer that reconstructs the current session from existing privacy-hardened evidence without adding sensors, permissions, AI access, retention, or MCP capabilities. v0.95 added a framework-neutral custom-harness setup flow plus standalone Python and Node helpers for privacy-safe structural agent telemetry; arbitrary MCP-capable harnesses can separately read authorized OpenWorkGraph context. v0.94 added explicit local history retention, separate saved-history AI access, lightweight history navigation, and structural browser-agent lifecycle observation. Canonical workflow evidence remains primary; Context Pulse provides incremental factual updates. Retained history is separately user-controlled: list_history can navigate saved human and agent sessions only while a time-limited saved-history lease is active. The legacy 24-tool MCP entrypoint remains available for existing configurations while new connections use this compact surface. Prompts, model responses, tool arguments/results, typed text, clipboard contents, exception text, returned values, and hidden reasoning are not captured by the custom agent helpers.",
"author": {"name": "Koyar Afrasyab / Kinvectum"},
"repository": {"type": "git", "url": "https://github.com/KAVentures/openworkgraph"},
"server": {"type": "node", "entry_point": "server/index.js", "mcp_config": {"command": "node", "args": ["${__dirname}/server/index.js"], "env": {}}},
diff --git a/pyproject.toml b/pyproject.toml
index ea4d63cb..293e28c6 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "workflow-observer"
-version = "0.95.0"
+version = "0.96.0"
description = "Local-first work evidence, self-hosted organizational context gateway, REST API, and MCP access."
requires-python = ">=3.11"
license = {file = "LICENSE"}
diff --git a/sdk/python/pyproject.toml b/sdk/python/pyproject.toml
index 0e39a70d..f077d263 100644
--- a/sdk/python/pyproject.toml
+++ b/sdk/python/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "openworkgraph-agent"
-version = "0.95.0"
+version = "0.96.0"
description = "Dependency-free structural telemetry helper for custom OpenWorkGraph agent harnesses"
requires-python = ">=3.10"
license = {text = "Apache-2.0"}
diff --git a/sdk/typescript/package.json b/sdk/typescript/package.json
index 8efd3b82..ab9db49d 100644
--- a/sdk/typescript/package.json
+++ b/sdk/typescript/package.json
@@ -1,6 +1,6 @@
{
"name": "@openworkgraph/agent",
- "version": "0.95.0",
+ "version": "0.96.0",
"description": "Dependency-free structural telemetry helper for custom OpenWorkGraph agent harnesses",
"type": "module",
"exports": {
diff --git a/server/enterprise_runner.py b/server/enterprise_runner.py
index 11a70392..a96f36ae 100644
--- a/server/enterprise_runner.py
+++ b/server/enterprise_runner.py
@@ -25,6 +25,7 @@ def main() -> None:
import server.browser_agent_projection # noqa: F401
import server.agent_dashboard_control_plane # noqa: F401
import server.custom_harness_control_plane # noqa: F401
+ import server.first_value_activation # noqa: F401
import server.org_join_routes as org_join_routes
import server.dashboard_privacy # noqa: F401
diff --git a/server/first_value_activation.py b/server/first_value_activation.py
new file mode 100644
index 00000000..b24d2832
--- /dev/null
+++ b/server/first_value_activation.py
@@ -0,0 +1,55 @@
+from __future__ import annotations
+
+from fastapi import Request, Response
+from fastapi.responses import HTMLResponse
+
+from .main import ROOT
+from .secure_app import app
+
+
+SCRIPT_TAG = ''
+
+
+@app.get("/first-value-activation.js", include_in_schema=False)
+def first_value_activation_javascript() -> Response:
+ """Serve the additive first-value dashboard layer.
+
+ The script reads only existing authenticated local endpoints. It adds no
+ capture sensor, data store, AI permission, retention permission, or MCP
+ capability.
+ """
+ path = ROOT / "dashboard" / "first_value_activation.js"
+ return Response(
+ path.read_text(encoding="utf-8"),
+ media_type="application/javascript",
+ headers={"Cache-Control": "no-store", "X-Content-Type-Options": "nosniff"},
+ )
+
+
+@app.middleware("http")
+async def inject_first_value_activation(request: Request, call_next):
+ response = await call_next(request)
+ if request.method.upper() != "GET" or request.url.path != "/" or response.status_code != 200:
+ return response
+ if "text/html" not in str(response.headers.get("content-type") or "").lower():
+ return response
+ try:
+ if hasattr(response, "body_iterator"):
+ chunks = [chunk async for chunk in response.body_iterator]
+ body = b"".join(
+ chunk if isinstance(chunk, bytes) else str(chunk).encode("utf-8")
+ for chunk in chunks
+ )
+ else:
+ body = bytes(getattr(response, "body", b""))
+ text = body.decode("utf-8")
+ except Exception:
+ return response
+ if SCRIPT_TAG not in text:
+ text = text.replace("