Skip to content
Closed
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
75 changes: 75 additions & 0 deletions docs/project-notes/yc-demo-runbook.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# cfgit YC-style demo runbook

Goal: record a 2-3 minute founder demo that makes the product obvious fast:
runtime config changed in the database, cfgit catches it, shows the exact
behavior delta, reasons about impact, then proves the same workflow is scriptable.

## Setup

Use only the synthetic demo database:

```bash
.venv/bin/cfg --config-file /private/tmp/cfgit-llm-demo.toml --env dev --author demo.user@example.com
```

The local UI is at:

```text
http://127.0.0.1:8777/
```

For the recorded demo, start the UI with an explicit impact provider/model:

```bash
CFGIT_UI_IMPACT_PROVIDER=gemini CFGIT_UI_IMPACT_MODEL=gemini-2.5-flash \
.venv/bin/cfg --config-file /private/tmp/cfgit-llm-demo.toml --env dev \
--author demo.user@example.com ui --port 8777 --no-open
```

Prepared state:

- `Drift 5`
- hero drift: `agent_configs:refund_resolution`
- review branch: `support-orchestrator-review`
- open PR: use the current id from `cfg pr list --status open`

## Screen Flow

1. Start on overview.
Voiceover: "A lot of AI products keep live behavior in Mongo: prompts, routing,
policies, eval gates, rollout controls. The app still reads that database.
cfgit sits beside it and adds git-shaped control."

2. Click `Drift 5`, then `agent_configs:refund_resolution`.
Voiceover: "This refund agent was edited outside cfgit. cfgit did not block
the admin-console edit. It just refuses to pretend nothing changed."

3. Pause on the diff.
Call out: model changed, threshold dropped, refund cap moved from `250` to
`500`, and enterprise courtesy-credit behavior was added.
Voiceover: "This is the actual runtime behavior delta, not a stale config
file or a prompt-registry copy."

4. Click `Impact`.
Voiceover: "Now cfgit maps the blast radius across related agents, policies,
evals, tools, routing, and rollout controls. This is why live config changes
need review."

5. Select branch `support-orchestrator-review`, click `Diff`.
Voiceover: "Branches are sparse draft timelines. They do not checkout the
database. Runtime only changes on merge."

6. Click `PR`.
Voiceover: "A PR is review metadata in cfgit's sidecar store. Humans and
agents can use the same semantics around records already living in the
datastore."

7. Switch to terminal and run `/tmp/cfgit_demo_terminal.sh`.
Voiceover: "And because this is CLI-first, the same flow works for scripts,
CI, and MCP clients."

## Tight Closing Line

"cfgit is git-shaped safety for live datastore records: history, drift, diff,
impact, rollback, branches, and PRs, without moving the data or putting a gateway
in your runtime path."
9 changes: 6 additions & 3 deletions plugins/cfg_impact/cfg_impact/overview.py
Original file line number Diff line number Diff line change
Expand Up @@ -422,9 +422,12 @@ def _parse_jsonish(text: str) -> dict[str, Any] | None:
if not raw:
return None
if raw.startswith("```"):
raw = raw.strip("`")
if raw.startswith("json"):
raw = raw[4:]
raw = raw[3:].strip()
if raw.lower().startswith("json"):
raw = raw[4:].strip()
fence_end = raw.rfind("```")
if fence_end != -1:
raw = raw[:fence_end].strip()
try:
data = json.loads(raw)
except json.JSONDecodeError:
Expand Down
37 changes: 35 additions & 2 deletions src/cfg/ui/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from http import HTTPStatus
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import json
import os
import socket
from typing import Any
from urllib.parse import parse_qs, urlparse
Expand Down Expand Up @@ -107,6 +108,11 @@ def _schema(self, params: dict[str, list[str]]) -> dict[str, Any]:
for item in project.collections
],
"connections": actions.to_json(project.connections),
"impact": {
"provider": os.getenv("CFGIT_UI_IMPACT_PROVIDER")
or project.connections.ai_provider,
"model": os.getenv("CFGIT_UI_IMPACT_MODEL") or None,
},
},
}
except Exception as exc:
Expand Down Expand Up @@ -641,7 +647,7 @@ def find_free_port(host: str = DEFAULT_HOST) -> int:
<div class="mbg" id="mbg"><div class="modal" id="modal"></div></div>

<script>
const S={records:[],recent:[],branches:[],prs:[],filter:"all",q:"",sel:null,against:new Set(),hist:[],who:null,open:{}};
const S={records:[],recent:[],branches:[],prs:[],filter:"all",q:"",sel:null,against:new Set(),hist:[],who:null,open:{},impact:{}};
const $=id=>document.getElementById(id);
const esc=v=>String(v==null?"":v).replace(/[&<>"']/g,c=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"}[c]));
// inline markdown for LLM narration: escape first (XSS-safe), then render the small
Expand Down Expand Up @@ -678,6 +684,7 @@ def find_free_port(host: str = DEFAULT_HOST) -> int:
const sc=await fetch("/api/schema?"+qs()).then(r=>r.json()).catch(()=>null);
if(sc&&sc.data&&Array.isArray(sc.data.envs)&&sc.data.envs.length){
const cur=$("env").value; $("env").innerHTML=sc.data.envs.map(e=>`<option ${e===cur?"selected":""}>${esc(e)}</option>`).join("");}
if(sc&&sc.data&&sc.data.impact)S.impact=sc.data.impact;
S.records=(st.data&&st.data.status)?st.data.status:[];
S.recent=(st.data&&st.data.recent_history)?st.data.recent_history:[];
S.branches=(st.data&&st.data.branches)?st.data.branches:[];
Expand Down Expand Up @@ -869,6 +876,8 @@ def find_free_port(host: str = DEFAULT_HOST) -> int:
const against=[...S.against].filter(k=>k!==S.sel);
const btn=$("aImpact"); if(btn){btn.disabled=true;btn.textContent="Analyzing…";}
const payload={record:S.sel,a:S.diffCtx.a,b:S.diffCtx.b,use_llm:true};
if(S.impact&&S.impact.provider)payload.provider=S.impact.provider;
if(S.impact&&S.impact.model)payload.model=S.impact.model;
if(against.length)payload.against=against;
const r=await api("impact",payload);
if(btn){btn.disabled=false;}
Expand Down Expand Up @@ -1119,11 +1128,35 @@ def find_free_port(host: str = DEFAULT_HOST) -> int:
$("dTitle").innerHTML=`Branch diff · <b>main..${esc(br)}</b>`;
$("dActs").innerHTML="";
if(!rows.length){$("diff").innerHTML=`<div class="paper"><div class="nodiff">No draft changes on this branch.</div></div>`;return;}
$("diff").innerHTML=`<div class="paper doconly"><div class="paper-h"><div>branch draft changes</div></div><div class="docbody">${esc(JSON.stringify(rows,null,2))}</div></div>`;
if(rows.length===1){
const row=rows[0];
const rec=`${row.collection}:${row.record_id}`;
$("dTitle").innerHTML=`Branch diff · <b>main..${esc(br)}</b> · ${esc(rec)}`;
renderDiff({data:{changes:row.changes||[]}},"main",br,"No field-level draft changes.");
return;
}
const cards=rows.map(row=>`<div class="frow"><div class="fname">${esc(row.collection+":"+row.record_id)}</div>
<div class="docbody">${esc(JSON.stringify(row.changes||[],null,2))}</div></div>`).join("");
$("diff").innerHTML=`<div class="paper doconly"><div class="paper-h"><div>branch draft changes</div></div>${cards}</div>`;
}
function openPrModal(){
const br=selectedBranch();
if(br==="main"){toast("select a non-main branch",true);return;}
const prs=S.prs.filter(p=>p.head_branch===br&&p.status==="open");
if(prs.length){
const body=prs.map(p=>`<div class="desc">
<b>${esc(p.id)}</b><br>
${esc(p.message||"review draft")}<br>
<span class="mono">${esc(p.base_branch||"main")} ← ${esc(p.head_branch||br)}</span><br>
${(p.records||[]).map(r=>`<span class="cat">${esc(r.collection+":"+r.record_id)}</span>`).join(" ")}
</div>`).join("");
modal(`<h3>Open PR</h3><div class="b">
${body}
<div class="desc">This is review metadata in cfgit's sidecar store. Runtime remains unchanged until the separate merge action.</div>
</div>
<div class="f"><button class="btn go" onclick="closeModal()">Close</button></div>`);
return;
}
modal(`<h3>Open PR</h3><div class="b">
<div class="desc">Open a review object for <b>${esc(br)}</b>. Runtime is unchanged until merge.</div>
<div><label>Message</label><input id="mMsg" value="merge ${esc(br)}" autocomplete="off"></div></div>
Expand Down
10 changes: 10 additions & 0 deletions tests/test_impact_boundary.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,16 @@ def test_impact_payload_includes_actual_field_diff_values() -> None:
assert "changes" not in payload


def test_parse_jsonish_strips_fenced_json() -> None:
sys.path.insert(0, str(ROOT / "plugins" / "cfg_impact"))
try:
from cfg_impact.overview import _parse_jsonish
finally:
sys.path.pop(0)

assert _parse_jsonish('```json\n{"summary":"ok"}\n```') == {"summary": "ok"}


def test_changed_string_values_reads_before_after_keys() -> None:
sys.path.insert(0, str(ROOT / "plugins" / "cfg_impact"))
try:
Expand Down
Loading