kdeps-templates-knowledge
v2.2.1kdeps template: knowledge
set templates / shelf workflow
PurposeProject scaffolds for kdeps new, and the knowledge vault's files (routing table, schema, graph page).
LLM context: none - kdeps uses it without sending it to the model
Where it runs
Part of the templates set, used in workflow and agent mode. Project scaffolds for kdeps new, and the knowledge vault's files (routing table, schema, graph page).
- kdeps new, or the knowledge vault's first use
- the template is renderedtemplates/knowledge
- the project or vault files are written
Loaded by: kdeps (agent loop), kdeps new, kdeps run, kdeps knowledge build
In the code: kdeps newtemplates.(*Generator).GenerateProject
Install
A kdeps asset: installed with the first-run profile that includes its set, or the first time a command needs it, and updated without a new release.
Pin a version with kdeps install templates/knowledge@<version>.
Contents
KDEPS.md
# Vault routing
This vault is the long-term memory. Read this table first, then the
project card, then only the notes the knowledge recall names. Never read the
whole vault.
## Where everything lives
Every place kdeps runs is a project with one folder, created and filled in by
kdeps. Nothing here needs to be set up by hand.
| What | Home | Written by |
|---|---|---|
| Project status and next step | `projects/<name>/<name>.md` (the card) | kdeps on first run; `knowledge_card_update` |
| Session history (focus, built, mistakes, decisions, wins) | `projects/<name>/sessions/` | kdeps, every turn |
| Saved facts (`[MEMORY: key] value`) | `projects/<name>/memory/` | kdeps, one note per key |
| Decisions and their rationale | `projects/<name>/decisions/` | kdeps from session summaries; `knowledge_note_save` with type decision |
| Ideas, gotchas, concepts for one project | `projects/<name>/notes/` | `knowledge_note_save`, or by hand |
| Knowledge across projects | `shared/` | `knowledge_note_save` with scope shared, or by hand |
| Unsorted notes | `inbox/` | by hand |
| Finished projects | `archive/<name>/` | kdeps moves the folder when the card says `status: archived` |
| Connections between notes | `~/.kdeps/knowledge/<vault-id>/` (outside this vault) | kdeps |
| How to write cards and notes | `schema.md` | |
## Rules
- One home per fact. Status lives in the card, the story in notes, the
connections in the graph. A fact written in two places goes stale in one.
- Cards stay short: frontmatter and a links table. They point; they do not
tell the story.
- Update a card with `knowledge_card_update`, never by copying status into a
note.
- `kdeps knowledge check` lists structural duplicates (the same summary, or the
same `canonical` or `fact_id`), dangling links and notes without a summary.
## Status overview
kdeps lists every active project's status and next step on each turn, read
from the cards themselves. Do not copy status into this file.
report/graph.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Knowledge graph</title>
<style>
:root { --bg: #0d1117; --fg: #c9d1d9; --dim: #8b949e; --line: rgba(255,255,255,0.08); --accent: #00e5ff; }
html, body { margin: 0; height: 100%; background: var(--bg); color: var(--fg); font: 13px system-ui, sans-serif; }
#bar { position: fixed; top: 12px; left: 12px; right: 12px; display: flex; gap: 12px; align-items: center; z-index: 2; }
#bar input { flex: 0 1 320px; background: #161b22; color: var(--fg); border: 1px solid var(--line); border-radius: 2px; padding: 6px 8px; }
#bar input:focus { outline: 1px solid var(--accent); }
#stats { color: var(--dim); }
#tip { position: fixed; pointer-events: none; background: #161b22; border: 1px solid var(--line); border-radius: 2px;
padding: 6px 8px; max-width: 360px; display: none; z-index: 3; }
#tip b { color: var(--fg); } #tip div { color: var(--dim); margin-top: 2px; }
canvas { display: block; width: 100%; height: 100%; cursor: grab; }
#legend { position: fixed; top: 56px; right: 12px; width: 280px; max-height: calc(100% - 80px); overflow-y: auto;
background: rgba(22,27,34,0.92); border: 1px solid var(--line); border-radius: 2px; padding: 8px; z-index: 2; }
#legend h2 { font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: var(--dim); margin: 0 0 6px; }
.com { display: flex; gap: 8px; align-items: flex-start; width: 100%; text-align: left; background: none; border: 1px solid transparent;
color: var(--fg); font: inherit; padding: 4px; border-radius: 2px; cursor: pointer; transition: border-color .15s ease; }
.com:hover, .com:focus-visible { border-color: var(--line); outline: none; }
.com.on { border-color: var(--accent); }
.sw { flex: 0 0 10px; height: 10px; border-radius: 50%; margin-top: 3px; }
.com small { display: block; color: var(--dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 240px; }
</style>
</head>
<body>
<div id="bar"><input id="q" placeholder="Find a note..." aria-label="Find a note"><span id="stats"></span></div>
<div id="tip"></div>
<div id="legend" aria-label="Communities"><h2>Communities</h2></div>
<canvas id="c"></canvas>
<script>
const G = "__GRAPH_DATA__";
const MAX = 1500;
const deg = {};
for (const e of G.edges) { deg[e.from] = (deg[e.from] || 0) + 1; deg[e.to] = (deg[e.to] || 0) + 1; }
let nodes = G.nodes.slice().sort((a, b) => (deg[b.id] || 0) - (deg[a.id] || 0)).slice(0, MAX);
const byId = {};
nodes.forEach((n, i) => { n.x = Math.cos(i) * (50 + i); n.y = Math.sin(i) * (50 + i); n.vx = 0; n.vy = 0; byId[n.id] = n; });
const edges = G.edges.filter(e => byId[e.from] && byId[e.to]);
document.getElementById('stats').textContent =
`${G.nodes.length} nodes, ${G.edges.length} edges, ${G.communities} communities` + (G.nodes.length > MAX ? ` (showing the ${MAX} most linked)` : '');
const color = c => `hsl(${(c * 137.508) % 360}, 65%, 60%)`;
// short is a node's display name: the last two parts of a package path.
const short = n => n.kind === 'code' ? n.label.split('/').slice(-2).join('/') : n.label;
// Communities: members by degree, and an anchor on a ring, largest first.
const coms = {};
for (const n of nodes) (coms[n.community] = coms[n.community] || []).push(n);
const comIds = Object.keys(coms).map(Number).sort((a, b) => coms[b].length - coms[a].length);
comIds.forEach(c => coms[c].sort((a, b) => (deg[b.id] || 0) - (deg[a.id] || 0)));
const anchor = {};
const ring = 120 + 25 * Math.sqrt(nodes.length);
comIds.forEach((c, i) => {
const r = i === 0 ? 0 : ring * (1 + Math.floor((i - 1) / 8) * 0.6), t = (i - 1) * Math.PI / 4 + (i > 8 ? 0.4 : 0);
anchor[c] = [Math.cos(t) * r, Math.sin(t) * r];
});
nodes.forEach((n, i) => { const [ax, ay] = anchor[n.community]; n.x = ax + Math.cos(i) * 30; n.y = ay + Math.sin(i) * 30; });
let focus = null;
const legend = document.getElementById('legend');
for (const c of comIds) {
const btn = document.createElement('button');
btn.className = 'com';
btn.setAttribute('aria-pressed', 'false');
const sw = document.createElement('span'); sw.className = 'sw'; sw.style.background = color(c);
const txt = document.createElement('span');
const head = document.createElement('span'); head.textContent = `#${c} ${coms[c].length} nodes`;
const top = document.createElement('small'); top.textContent = coms[c].slice(0, 3).map(short).join(', ');
top.title = coms[c].slice(0, 8).map(n => n.label).join('\n');
txt.append(head, top); btn.append(sw, txt);
btn.addEventListener('click', () => {
focus = focus === c ? null : c;
for (const b of legend.querySelectorAll('.com')) { b.classList.remove('on'); b.setAttribute('aria-pressed', 'false'); }
if (focus !== null) { btn.classList.add('on'); btn.setAttribute('aria-pressed', 'true'); }
});
legend.appendChild(btn);
}
const cv = document.getElementById('c'), ctx = cv.getContext('2d');
let W, H, scale = 1, ox = 0, oy = 0, query = '', hover = null;
function resize() { W = cv.width = innerWidth * devicePixelRatio; H = cv.height = innerHeight * devicePixelRatio; }
addEventListener('resize', resize); resize();
scale = Math.min(1, (innerHeight - 80) / (2.4 * ring + 200));
function step() {
for (let i = 0; i < nodes.length; i++) {
const a = nodes[i];
for (let j = i + 1; j < nodes.length; j++) {
const b = nodes[j], dx = a.x - b.x, dy = a.y - b.y, d2 = dx * dx + dy * dy + 0.01;
if (d2 > 90000) continue;
const f = 80 / d2; a.vx += dx * f; a.vy += dy * f; b.vx -= dx * f; b.vy -= dy * f;
}
}
for (const e of edges) {
const a = byId[e.from], b = byId[e.to], dx = b.x - a.x, dy = b.y - a.y;
const k = (a.community === b.community ? 0.02 : 0.0015) * (e.provenance === 'extracted' ? 1 : 0.5);
a.vx += dx * k; a.vy += dy * k; b.vx -= dx * k; b.vy -= dy * k;
}
for (const n of nodes) {
const [ax, ay] = anchor[n.community];
n.vx += (ax - n.x) * 0.01; n.vy += (ay - n.y) * 0.01; // each community gathers at its anchor
n.vx *= 0.85; n.vy *= 0.85; n.x += n.vx; n.y += n.vy;
}
}
function radius(n) { return 2 + Math.sqrt(deg[n.id] || 0) * 1.5; }
function toScreen(n) { return [W / 2 + (n.x + ox) * scale * devicePixelRatio, H / 2 + (n.y + oy) * scale * devicePixelRatio]; }
function dimmed(n) { return (query && !n.label.toLowerCase().includes(query)) || (focus !== null && n.community !== focus); }
function draw() {
ctx.clearRect(0, 0, W, H);
ctx.lineWidth = devicePixelRatio * 0.6;
for (const e of edges) {
const a = byId[e.from], b = byId[e.to];
if (focus !== null && a.community !== focus && b.community !== focus) continue;
const [x1, y1] = toScreen(a), [x2, y2] = toScreen(b);
ctx.strokeStyle = e.provenance === 'extracted' ? 'rgba(255,255,255,0.10)' : 'rgba(255,214,10,0.25)';
ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke();
}
for (const n of nodes) {
const [x, y] = toScreen(n), match = query && n.label.toLowerCase().includes(query);
ctx.fillStyle = dimmed(n) ? 'rgba(139,148,158,0.18)' : color(n.community);
ctx.beginPath(); ctx.arc(x, y, radius(n) * devicePixelRatio * (match ? 1.8 : 1), 0, 7); ctx.fill();
const lead = coms[n.community][0] === n;
if (match || n === hover || (!dimmed(n) && (lead || (focus !== null && (deg[n.id] || 0) > 3)))) {
ctx.fillStyle = '#c9d1d9'; ctx.font = `${11 * devicePixelRatio}px system-ui`;
ctx.fillText(short(n), x + 6 * devicePixelRatio, y - 6 * devicePixelRatio);
}
}
}
let ticks = 0;
for (; ticks < 250; ticks++) step(); // settle before the first frame
(function loop() { if (ticks++ < 500) step(); draw(); requestAnimationFrame(loop); })();
const tip = document.getElementById('tip');
let drag = null;
cv.addEventListener('mousedown', e => { drag = [e.clientX, e.clientY, ox, oy]; });
addEventListener('mouseup', () => { drag = null; });
cv.addEventListener('mousemove', e => {
if (drag) { ox = drag[2] + (e.clientX - drag[0]) / scale; oy = drag[3] + (e.clientY - drag[1]) / scale; return; }
hover = null;
for (const n of nodes) {
const [x, y] = toScreen(n), dx = x / devicePixelRatio - e.clientX, dy = y / devicePixelRatio - e.clientY;
if (dx * dx + dy * dy < 64) { hover = n; break; }
}
if (!hover) { tip.style.display = 'none'; return; }
tip.style.display = 'block'; tip.style.left = (e.clientX + 12) + 'px'; tip.style.top = (e.clientY + 12) + 'px';
tip.innerHTML = '';
const b = document.createElement('b'); b.textContent = hover.label; tip.appendChild(b);
const d = document.createElement('div');
d.textContent = [hover.kind, 'community ' + hover.community, hover.note || '', hover.summary || ''].filter(Boolean).join(' - ');
tip.appendChild(d);
});
cv.addEventListener('wheel', e => { e.preventDefault(); scale *= e.deltaY < 0 ? 1.1 : 0.9; }, { passive: false });
document.getElementById('q').addEventListener('input', e => { query = e.target.value.trim().toLowerCase(); });
</script>
</body>
</html>
schema.md
# Schema: cards and notes
## Card (`projects/<name>/<name>.md`)
kdeps writes this the first time it runs in a place: in a git repository the
project is named after the repository's root folder, else after the folder;
a workflow's project is named after the workflow (`metadata.name`) and shared by every place it runs from.
```markdown
---
type: project # project, client, area, research, bot - any word
status: active # active | paused | done | archived (archived moves the folder to archive/)
next: the one next step
updated: 2026-10-15
---
| Source | Path |
|---|---|
| repo | ~/code/my-project |
| folder | ~/Documents/my-project-notes |
| workflow | ~/agents/invoices |
```
A card has no prose. A `repo`, `folder` or `workflow` row ties a place on
disk to the project: kdeps runs in that place (or below it) use this project.
Add rows to make several places one project. kdeps adds an `index` row for a place's search index.
## Note
```markdown
---
type: note # session | decision | gotcha | concept | tool | note
status: draft # the note's own state: draft | evergreen | outdated
tags: [kdeps, graph]
created: 2026-10-15
summary: one line saying what this note holds
canonical: ship-monthly # optional stable id; fact_id is the same field
links: ["[[Other note]]"]
---
```
- One idea per note: `projects/<name>/notes/` for one project, `shared/` for
knowledge across projects. Two notes with the same `canonical` or `fact_id`
are one fact in two homes; `kdeps knowledge check` reports that.
- `summary` is what the knowledge recall shows before anyone opens the note.
Write it so the reader can decide whether to open it.
- Link with `[[Note title]]`, or the full path `[[projects/app/notes/Title]]`.
Links written in a note are facts; the graph marks them as extracted.
kdeps writes every link with its full vault path: sessions and decisions
link their card as `[[projects/<name>/<name>]]`.
## Written by kdeps
- Session note (`sessions/`): `## Focus`, `## What was built`, `## Mistakes`,
`## Decisions`, `## Wins`. Updated after every turn; rewritten from the
model's summary at compaction and session end.
- Decision note (`decisions/`): one per item of a summary's Key Decisions,
titled by its bold lead.
- Memory note (`memory/`): one per `[MEMORY: key]`; the frontmatter keeps the
key, type and dates.
- Workflow run note (`sessions/<date> <name> runs.md`): one line per chat
call, when `knowledge.workflow_notes: true`.
version: 2.2.1
files:
KDEPS.md: |
# Vault routing
This vault is the long-term memory. Read this table first, then the
project card, then only the notes the knowledge recall names. Never read the
whole vault.
## Where everything lives
Every place kdeps runs is a project with one folder, created and filled in by
kdeps. Nothing here needs to be set up by hand.
| What | Home | Written by |
|---|---|---|
| Project status and next step | `projects/<name>/<name>.md` (the card) | kdeps on first run; `knowledge_card_update` |
| Session history (focus, built, mistakes, decisions, wins) | `projects/<name>/sessions/` | kdeps, every turn |
| Saved facts (`[MEMORY: key] value`) | `projects/<name>/memory/` | kdeps, one note per key |
| Decisions and their rationale | `projects/<name>/decisions/` | kdeps from session summaries; `knowledge_note_save` with type decision |
| Ideas, gotchas, concepts for one project | `projects/<name>/notes/` | `knowledge_note_save`, or by hand |
| Knowledge across projects | `shared/` | `knowledge_note_save` with scope shared, or by hand |
| Unsorted notes | `inbox/` | by hand |
| Finished projects | `archive/<name>/` | kdeps moves the folder when the card says `status: archived` |
| Connections between notes | `~/.kdeps/knowledge/<vault-id>/` (outside this vault) | kdeps |
| How to write cards and notes | `schema.md` | |
## Rules
- One home per fact. Status lives in the card, the story in notes, the
connections in the graph. A fact written in two places goes stale in one.
- Cards stay short: frontmatter and a links table. They point; they do not
tell the story.
- Update a card with `knowledge_card_update`, never by copying status into a
note.
- `kdeps knowledge check` lists structural duplicates (the same summary, or the
same `canonical` or `fact_id`), dangling links and notes without a summary.
## Status overview
kdeps lists every active project's status and next step on each turn, read
from the cards themselves. Do not copy status into this file.
report/graph.html: |
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Knowledge graph</title>
<style>
:root { --bg: #0d1117; --fg: #c9d1d9; --dim: #8b949e; --line: rgba(255,255,255,0.08); --accent: #00e5ff; }
html, body { margin: 0; height: 100%; background: var(--bg); color: var(--fg); font: 13px system-ui, sans-serif; }
#bar { position: fixed; top: 12px; left: 12px; right: 12px; display: flex; gap: 12px; align-items: center; z-index: 2; }
#bar input { flex: 0 1 320px; background: #161b22; color: var(--fg); border: 1px solid var(--line); border-radius: 2px; padding: 6px 8px; }
#bar input:focus { outline: 1px solid var(--accent); }
#stats { color: var(--dim); }
#tip { position: fixed; pointer-events: none; background: #161b22; border: 1px solid var(--line); border-radius: 2px;
padding: 6px 8px; max-width: 360px; display: none; z-index: 3; }
#tip b { color: var(--fg); } #tip div { color: var(--dim); margin-top: 2px; }
canvas { display: block; width: 100%; height: 100%; cursor: grab; }
#legend { position: fixed; top: 56px; right: 12px; width: 280px; max-height: calc(100% - 80px); overflow-y: auto;
background: rgba(22,27,34,0.92); border: 1px solid var(--line); border-radius: 2px; padding: 8px; z-index: 2; }
#legend h2 { font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: var(--dim); margin: 0 0 6px; }
.com { display: flex; gap: 8px; align-items: flex-start; width: 100%; text-align: left; background: none; border: 1px solid transparent;
color: var(--fg); font: inherit; padding: 4px; border-radius: 2px; cursor: pointer; transition: border-color .15s ease; }
.com:hover, .com:focus-visible { border-color: var(--line); outline: none; }
.com.on { border-color: var(--accent); }
.sw { flex: 0 0 10px; height: 10px; border-radius: 50%; margin-top: 3px; }
.com small { display: block; color: var(--dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 240px; }
</style>
</head>
<body>
<div id="bar"><input id="q" placeholder="Find a note..." aria-label="Find a note"><span id="stats"></span></div>
<div id="tip"></div>
<div id="legend" aria-label="Communities"><h2>Communities</h2></div>
<canvas id="c"></canvas>
<script>
const G = "__GRAPH_DATA__";
const MAX = 1500;
const deg = {};
for (const e of G.edges) { deg[e.from] = (deg[e.from] || 0) + 1; deg[e.to] = (deg[e.to] || 0) + 1; }
let nodes = G.nodes.slice().sort((a, b) => (deg[b.id] || 0) - (deg[a.id] || 0)).slice(0, MAX);
const byId = {};
nodes.forEach((n, i) => { n.x = Math.cos(i) * (50 + i); n.y = Math.sin(i) * (50 + i); n.vx = 0; n.vy = 0; byId[n.id] = n; });
const edges = G.edges.filter(e => byId[e.from] && byId[e.to]);
document.getElementById('stats').textContent =
`${G.nodes.length} nodes, ${G.edges.length} edges, ${G.communities} communities` + (G.nodes.length > MAX ? ` (showing the ${MAX} most linked)` : '');
const color = c => `hsl(${(c * 137.508) % 360}, 65%, 60%)`;
// short is a node's display name: the last two parts of a package path.
const short = n => n.kind === 'code' ? n.label.split('/').slice(-2).join('/') : n.label;
// Communities: members by degree, and an anchor on a ring, largest first.
const coms = {};
for (const n of nodes) (coms[n.community] = coms[n.community] || []).push(n);
const comIds = Object.keys(coms).map(Number).sort((a, b) => coms[b].length - coms[a].length);
comIds.forEach(c => coms[c].sort((a, b) => (deg[b.id] || 0) - (deg[a.id] || 0)));
const anchor = {};
const ring = 120 + 25 * Math.sqrt(nodes.length);
comIds.forEach((c, i) => {
const r = i === 0 ? 0 : ring * (1 + Math.floor((i - 1) / 8) * 0.6), t = (i - 1) * Math.PI / 4 + (i > 8 ? 0.4 : 0);
anchor[c] = [Math.cos(t) * r, Math.sin(t) * r];
});
nodes.forEach((n, i) => { const [ax, ay] = anchor[n.community]; n.x = ax + Math.cos(i) * 30; n.y = ay + Math.sin(i) * 30; });
let focus = null;
const legend = document.getElementById('legend');
for (const c of comIds) {
const btn = document.createElement('button');
btn.className = 'com';
btn.setAttribute('aria-pressed', 'false');
const sw = document.createElement('span'); sw.className = 'sw'; sw.style.background = color(c);
const txt = document.createElement('span');
const head = document.createElement('span'); head.textContent = `#${c} ${coms[c].length} nodes`;
const top = document.createElement('small'); top.textContent = coms[c].slice(0, 3).map(short).join(', ');
top.title = coms[c].slice(0, 8).map(n => n.label).join('\n');
txt.append(head, top); btn.append(sw, txt);
btn.addEventListener('click', () => {
focus = focus === c ? null : c;
for (const b of legend.querySelectorAll('.com')) { b.classList.remove('on'); b.setAttribute('aria-pressed', 'false'); }
if (focus !== null) { btn.classList.add('on'); btn.setAttribute('aria-pressed', 'true'); }
});
legend.appendChild(btn);
}
const cv = document.getElementById('c'), ctx = cv.getContext('2d');
let W, H, scale = 1, ox = 0, oy = 0, query = '', hover = null;
function resize() { W = cv.width = innerWidth * devicePixelRatio; H = cv.height = innerHeight * devicePixelRatio; }
addEventListener('resize', resize); resize();
scale = Math.min(1, (innerHeight - 80) / (2.4 * ring + 200));
function step() {
for (let i = 0; i < nodes.length; i++) {
const a = nodes[i];
for (let j = i + 1; j < nodes.length; j++) {
const b = nodes[j], dx = a.x - b.x, dy = a.y - b.y, d2 = dx * dx + dy * dy + 0.01;
if (d2 > 90000) continue;
const f = 80 / d2; a.vx += dx * f; a.vy += dy * f; b.vx -= dx * f; b.vy -= dy * f;
}
}
for (const e of edges) {
const a = byId[e.from], b = byId[e.to], dx = b.x - a.x, dy = b.y - a.y;
const k = (a.community === b.community ? 0.02 : 0.0015) * (e.provenance === 'extracted' ? 1 : 0.5);
a.vx += dx * k; a.vy += dy * k; b.vx -= dx * k; b.vy -= dy * k;
}
for (const n of nodes) {
const [ax, ay] = anchor[n.community];
n.vx += (ax - n.x) * 0.01; n.vy += (ay - n.y) * 0.01; // each community gathers at its anchor
n.vx *= 0.85; n.vy *= 0.85; n.x += n.vx; n.y += n.vy;
}
}
function radius(n) { return 2 + Math.sqrt(deg[n.id] || 0) * 1.5; }
function toScreen(n) { return [W / 2 + (n.x + ox) * scale * devicePixelRatio, H / 2 + (n.y + oy) * scale * devicePixelRatio]; }
function dimmed(n) { return (query && !n.label.toLowerCase().includes(query)) || (focus !== null && n.community !== focus); }
function draw() {
ctx.clearRect(0, 0, W, H);
ctx.lineWidth = devicePixelRatio * 0.6;
for (const e of edges) {
const a = byId[e.from], b = byId[e.to];
if (focus !== null && a.community !== focus && b.community !== focus) continue;
const [x1, y1] = toScreen(a), [x2, y2] = toScreen(b);
ctx.strokeStyle = e.provenance === 'extracted' ? 'rgba(255,255,255,0.10)' : 'rgba(255,214,10,0.25)';
ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke();
}
for (const n of nodes) {
const [x, y] = toScreen(n), match = query && n.label.toLowerCase().includes(query);
ctx.fillStyle = dimmed(n) ? 'rgba(139,148,158,0.18)' : color(n.community);
ctx.beginPath(); ctx.arc(x, y, radius(n) * devicePixelRatio * (match ? 1.8 : 1), 0, 7); ctx.fill();
const lead = coms[n.community][0] === n;
if (match || n === hover || (!dimmed(n) && (lead || (focus !== null && (deg[n.id] || 0) > 3)))) {
ctx.fillStyle = '#c9d1d9'; ctx.font = `${11 * devicePixelRatio}px system-ui`;
ctx.fillText(short(n), x + 6 * devicePixelRatio, y - 6 * devicePixelRatio);
}
}
}
let ticks = 0;
for (; ticks < 250; ticks++) step(); // settle before the first frame
(function loop() { if (ticks++ < 500) step(); draw(); requestAnimationFrame(loop); })();
const tip = document.getElementById('tip');
let drag = null;
cv.addEventListener('mousedown', e => { drag = [e.clientX, e.clientY, ox, oy]; });
addEventListener('mouseup', () => { drag = null; });
cv.addEventListener('mousemove', e => {
if (drag) { ox = drag[2] + (e.clientX - drag[0]) / scale; oy = drag[3] + (e.clientY - drag[1]) / scale; return; }
hover = null;
for (const n of nodes) {
const [x, y] = toScreen(n), dx = x / devicePixelRatio - e.clientX, dy = y / devicePixelRatio - e.clientY;
if (dx * dx + dy * dy < 64) { hover = n; break; }
}
if (!hover) { tip.style.display = 'none'; return; }
tip.style.display = 'block'; tip.style.left = (e.clientX + 12) + 'px'; tip.style.top = (e.clientY + 12) + 'px';
tip.innerHTML = '';
const b = document.createElement('b'); b.textContent = hover.label; tip.appendChild(b);
const d = document.createElement('div');
d.textContent = [hover.kind, 'community ' + hover.community, hover.note || '', hover.summary || ''].filter(Boolean).join(' - ');
tip.appendChild(d);
});
cv.addEventListener('wheel', e => { e.preventDefault(); scale *= e.deltaY < 0 ? 1.1 : 0.9; }, { passive: false });
document.getElementById('q').addEventListener('input', e => { query = e.target.value.trim().toLowerCase(); });
</script>
</body>
</html>
schema.md: |
# Schema: cards and notes
## Card (`projects/<name>/<name>.md`)
kdeps writes this the first time it runs in a place: in a git repository the
project is named after the repository's root folder, else after the folder;
a workflow's project is named after the workflow (`metadata.name`) and shared by every place it runs from.
```markdown
---
type: project # project, client, area, research, bot - any word
status: active # active | paused | done | archived (archived moves the folder to archive/)
next: the one next step
updated: 2026-10-15
---
| Source | Path |
|---|---|
| repo | ~/code/my-project |
| folder | ~/Documents/my-project-notes |
| workflow | ~/agents/invoices |
```
A card has no prose. A `repo`, `folder` or `workflow` row ties a place on
disk to the project: kdeps runs in that place (or below it) use this project.
Add rows to make several places one project. kdeps adds an `index` row for a place's search index.
## Note
```markdown
---
type: note # session | decision | gotcha | concept | tool | note
status: draft # the note's own state: draft | evergreen | outdated
tags: [kdeps, graph]
created: 2026-10-15
summary: one line saying what this note holds
canonical: ship-monthly # optional stable id; fact_id is the same field
links: ["[[Other note]]"]
---
```
- One idea per note: `projects/<name>/notes/` for one project, `shared/` for
knowledge across projects. Two notes with the same `canonical` or `fact_id`
are one fact in two homes; `kdeps knowledge check` reports that.
- `summary` is what the knowledge recall shows before anyone opens the note.
Write it so the reader can decide whether to open it.
- Link with `[[Note title]]`, or the full path `[[projects/app/notes/Title]]`.
Links written in a note are facts; the graph marks them as extracted.
kdeps writes every link with its full vault path: sessions and decisions
link their card as `[[projects/<name>/<name>]]`.
## Written by kdeps
- Session note (`sessions/`): `## Focus`, `## What was built`, `## Mistakes`,
`## Decisions`, `## Wins`. Updated after every turn; rewritten from the
model's summary at compaction and session end.
- Decision note (`decisions/`): one per item of a summary's Key Decisions,
titled by its bold lead.
- Memory note (`memory/`): one per `[MEMORY: key]`; the frontmatter keeps the
key, type and dates.
- Workflow run note (`sessions/<date> <name> runs.md`): one line per chat
call, when `knowledge.workflow_notes: true`.
Versions
| Version | Published | Status |
|---|---|---|
| 2.2.1 | 10/27/2026 | active |
| 2.2.0 | 10/16/2026 | active |
| 2.1.0 | 10/16/2026 | active |
| 2.0.0 | 10/15/2026 | active |
| 1.0.0 | 10/15/2026 | active |
Details
- Author
- kdeps
- License
- Apache-2.0
- Latest Version
- 2.2.1
- Published
- 10/15/2026