@kentwynn/kgraph 0.2.5 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -133,7 +133,7 @@ Agents can also report session activity so KGraph can estimate token waste:
133
133
  kgraph session start --agent codex
134
134
  kgraph session read src/auth.ts --agent codex
135
135
  kgraph session write src/auth.ts --agent codex
136
- kgraph session end --agent codex
136
+ kgraph session end --agent codex --conclude --topic "auth session work"
137
137
  kgraph session
138
138
  ```
139
139
 
@@ -204,10 +204,30 @@ kgraph session start --agent codex
204
204
  kgraph session read src/auth.ts --agent codex
205
205
  kgraph session write src/auth.ts --agent codex
206
206
  kgraph session end --agent codex
207
+ kgraph session end --agent codex --conclude --topic "auth token refresh"
207
208
  ```
208
209
 
209
210
  Track agent-reported read/write activity, repeated reads, and estimated token cost. Supported agents are `codex`, `claude-code`, `copilot`, `cursor`, `gemini`, `windsurf`, and `cline`.
210
- The text report now includes next actions, such as using `kgraph context "<topic>"` before repeated broad file inspection.
211
+ The text report now includes next actions, such as using `kgraph context "<topic>"` before repeated broad file inspection. Add `--conclude` to store a durable session summary with touched files attached as related cognition.
212
+
213
+ ```bash
214
+ kgraph conclude "auth refresh requires rotating the session cookie" \
215
+ --type gotcha \
216
+ --confidence high \
217
+ --domain auth \
218
+ --file src/auth.ts \
219
+ --symbol refreshSession \
220
+ --note "The refresh path must update both the access token and cookie expiry."
221
+ ```
222
+
223
+ Store durable engineering memory directly. Cognition is typed as `finding`, `decision`, `gotcha`, `summary`, or `relationship`, and confidence is `high`, `medium`, or `low`. Keep conclusions concise: preserve expensive-to-rediscover knowledge, not raw chain-of-thought, speculative exploration, or temporary reasoning.
224
+
225
+ ```bash
226
+ kgraph compact --dry-run
227
+ kgraph compact
228
+ ```
229
+
230
+ Merge duplicate cognition records and archive low-confidence stale entries. Compaction keeps memory inspectable under `.kgraph/cognition/` while reducing low-value noise in future context responses.
211
231
 
212
232
  ## Optional Step Commands
213
233
 
@@ -0,0 +1,2 @@
1
+ import type { Command } from 'commander';
2
+ export declare function registerCompactCommand(program: Command): void;
@@ -0,0 +1,27 @@
1
+ import { compactCognition } from '../../cognition/compact.js';
2
+ import { assertWorkspace } from '../../storage/kgraph-paths.js';
3
+ import { runCommand } from '../errors.js';
4
+ export function registerCompactCommand(program) {
5
+ program
6
+ .command('compact')
7
+ .description('Merge duplicate cognition and archive low-value stale entries')
8
+ .option('--dry-run', 'Preview compaction without changing files')
9
+ .option('--json', 'Print JSON output')
10
+ .action((options) => runCommand(async () => {
11
+ const workspace = await assertWorkspace(process.cwd());
12
+ const result = await compactCognition(workspace, Boolean(options.dryRun));
13
+ if (options.json) {
14
+ console.log(JSON.stringify(result, null, 2));
15
+ return;
16
+ }
17
+ console.log(options.dryRun ? 'KGraph Compact Preview' : 'KGraph Compact Complete');
18
+ console.log(`Merged duplicate groups: ${result.merged.length}`);
19
+ console.log(`Archived stale low-confidence notes: ${result.archived.length}`);
20
+ for (const item of result.merged) {
21
+ console.log(`- merged ${item.sourceIds.length} notes into ${item.title}`);
22
+ }
23
+ for (const item of result.archived) {
24
+ console.log(`- archived ${item.title}: ${item.reason}`);
25
+ }
26
+ }));
27
+ }
@@ -0,0 +1,5 @@
1
+ import type { Command } from 'commander';
2
+ import type { CognitionConfidence, CognitionKind } from '../../types/cognition.js';
3
+ export declare function registerConcludeCommand(program: Command): void;
4
+ export declare function normalizeKind(value: string | undefined): CognitionKind;
5
+ export declare function normalizeConfidence(value: string | undefined): CognitionConfidence;
@@ -0,0 +1,57 @@
1
+ import { concludeTopic } from '../../cognition/conclusion.js';
2
+ import { assertWorkspace } from '../../storage/kgraph-paths.js';
3
+ import { KGraphError, runCommand } from '../errors.js';
4
+ export function registerConcludeCommand(program) {
5
+ program
6
+ .command('conclude <topic>')
7
+ .description('Store durable typed engineering cognition for this repo')
8
+ .option('--type <type>', 'finding, decision, gotcha, summary, or relationship', 'summary')
9
+ .option('--confidence <level>', 'high, medium, or low', 'medium')
10
+ .option('--domain <name>', 'Domain name for this cognition')
11
+ .option('--tag <tag>', 'Tag to attach; repeatable', collect, [])
12
+ .option('--file <path>', 'Related repo file; repeatable', collect, [])
13
+ .option('--symbol <name>', 'Related symbol; repeatable', collect, [])
14
+ .option('--note <text>', 'Concise durable conclusion text')
15
+ .option('--json', 'Print JSON output')
16
+ .action((topic, options) => runCommand(async () => {
17
+ const workspace = await assertWorkspace(process.cwd());
18
+ const note = await concludeTopic(workspace, {
19
+ topic,
20
+ body: options.note,
21
+ kind: normalizeKind(options.type),
22
+ confidence: normalizeConfidence(options.confidence),
23
+ domain: options.domain,
24
+ tags: options.tag ?? [],
25
+ relatedFiles: options.file ?? [],
26
+ relatedSymbols: options.symbol ?? [],
27
+ source: 'conclude',
28
+ });
29
+ if (options.json) {
30
+ console.log(JSON.stringify(note, null, 2));
31
+ return;
32
+ }
33
+ console.log(`Stored ${note.kind} cognition: ${note.title}`);
34
+ console.log(`Confidence: ${note.confidence}`);
35
+ console.log(`Status: ${note.referencesStatus}`);
36
+ }));
37
+ }
38
+ export function normalizeKind(value) {
39
+ if (value === 'finding' ||
40
+ value === 'decision' ||
41
+ value === 'gotcha' ||
42
+ value === 'summary' ||
43
+ value === 'relationship') {
44
+ return value;
45
+ }
46
+ throw new KGraphError('--type must be finding, decision, gotcha, summary, or relationship.');
47
+ }
48
+ export function normalizeConfidence(value) {
49
+ if (value === 'high' || value === 'medium' || value === 'low') {
50
+ return value;
51
+ }
52
+ throw new KGraphError('--confidence must be high, medium, or low.');
53
+ }
54
+ function collect(value, previous) {
55
+ previous.push(value);
56
+ return previous;
57
+ }
@@ -49,7 +49,7 @@ export function renderContextMarkdown(response) {
49
49
  return `- ${s.name} (${kindInfo}) in ${s.filePath}${lineRange} because ${formatReasons(item.reasons)}`;
50
50
  })));
51
51
  lines.push('', '## Relevant Cognition', '');
52
- lines.push(...formatList(response.relevantCognition.map((item) => `- ${item.item.title} [${item.item.referencesStatus}] because ${formatReasons(item.reasons)}`)));
52
+ lines.push(...formatList(response.relevantCognition.map((item) => `- ${item.item.title} [${item.item.kind ?? 'summary'}, ${item.item.confidence ?? 'medium'}, ${item.item.referencesStatus}] because ${formatReasons(item.reasons)}`)));
53
53
  lines.push('', '## Relationships', '');
54
54
  lines.push(...formatGroupedRelationships(response.relationships, response.relationshipExplanations));
55
55
  lines.push('', '## Nearby Symbols (1-hop imports)', '');
@@ -1,7 +1,9 @@
1
+ import { buildActiveSessionConclusion, concludeTopic, } from '../../cognition/conclusion.js';
1
2
  import { assertSessionAgent, buildSessionReport, recordSessionEvent, resetSession, } from '../../session/session-store.js';
2
3
  import { assertWorkspace } from '../../storage/kgraph-paths.js';
3
4
  import { readMaps } from '../../storage/map-store.js';
4
5
  import { KGraphError, runCommand } from '../errors.js';
6
+ import { normalizeConfidence, normalizeKind } from './conclude.js';
5
7
  export function registerSessionCommand(program) {
6
8
  const session = program
7
9
  .command('session')
@@ -62,14 +64,32 @@ export function registerSessionCommand(program) {
62
64
  .command('end')
63
65
  .requiredOption('--agent <name>', 'KGraph integration agent name')
64
66
  .option('--source <source>', 'automatic, agent-reported, or manual', 'manual')
67
+ .option('--conclude', 'Store a durable typed summary for this session')
68
+ .option('--topic <topic>', 'Conclusion topic when using --conclude')
69
+ .option('--type <type>', 'finding, decision, gotcha, summary, or relationship', 'summary')
70
+ .option('--confidence <level>', 'high, medium, or low', 'medium')
71
+ .option('--note <text>', 'Concise durable conclusion text')
65
72
  .action((options) => runCommand(async () => {
66
73
  const workspace = await assertWorkspace(process.cwd());
74
+ let pendingConclusion;
75
+ if (options.conclude) {
76
+ pendingConclusion = await buildActiveSessionConclusion(workspace, requireAgent(options.agent), {
77
+ topic: options.topic ?? `${options.agent} session summary`,
78
+ body: options.note,
79
+ kind: normalizeKind(options.type),
80
+ confidence: normalizeConfidence(options.confidence),
81
+ });
82
+ }
67
83
  const event = await recordSessionEvent(workspace, {
68
84
  agent: requireAgent(options.agent),
69
85
  type: 'end',
70
86
  captureSource: normalizeSource(options.source),
71
87
  });
72
88
  console.log(`KGraph session ended for ${event.agent}.`);
89
+ if (pendingConclusion) {
90
+ const note = await concludeTopic(workspace, pendingConclusion);
91
+ console.log(`Stored session cognition: ${note.title}`);
92
+ }
73
93
  }));
74
94
  session
75
95
  .command('reset')
package/dist/cli/help.js CHANGED
@@ -29,6 +29,9 @@ export function renderRootHelp(useColor = supportsColor()) {
29
29
  command('scan', 'Optional: refresh only file, symbol, import, and relationship maps'),
30
30
  command('session', 'Show agent read/write activity and token estimates'),
31
31
  command('session read src/auth.ts --agent codex', 'Record an agent file read'),
32
+ command('session end --agent codex --conclude', 'End tracking and store a durable session summary'),
33
+ command('conclude "auth refresh gotcha"', 'Store typed engineering cognition'),
34
+ command('compact', 'Merge duplicate cognition and archive stale noise'),
32
35
  command('context "auth token refresh"', 'Optional: return context without scanning or updating'),
33
36
  command('impact "Button"', 'Show imports, callers, calls, cognition, and risk'),
34
37
  command('update', 'Optional: process only .kgraph/inbox Markdown cognition notes'),
package/dist/cli/index.js CHANGED
@@ -3,6 +3,8 @@ import { Command } from 'commander';
3
3
  import { realpathSync } from 'node:fs';
4
4
  import { createRequire } from 'node:module';
5
5
  import { fileURLToPath } from 'node:url';
6
+ import { registerCompactCommand } from './commands/compact.js';
7
+ import { registerConcludeCommand } from './commands/conclude.js';
6
8
  import { registerContextCommand } from './commands/context.js';
7
9
  import { registerDoctorCommand } from './commands/doctor.js';
8
10
  import { registerHistoryCommand } from './commands/history.js';
@@ -40,6 +42,8 @@ export function createProgram() {
40
42
  registerInitCommand(program);
41
43
  registerScanCommand(program);
42
44
  registerSessionCommand(program);
45
+ registerConcludeCommand(program);
46
+ registerCompactCommand(program);
43
47
  registerUpdateCommand(program);
44
48
  registerContextCommand(program);
45
49
  registerImpactCommand(program);
@@ -29,6 +29,7 @@ export async function updateCognition(workspace, currentMaps, dryRun = false) {
29
29
  .split(path.sep)
30
30
  .join('/'),
31
31
  createdAt: new Date().toISOString(),
32
+ source: 'inbox',
32
33
  referencesStatus: evaluateReferenceStatus(parsed.relatedFiles, parsed.relatedSymbols, currentMaps),
33
34
  };
34
35
  processed.push(note);
@@ -73,6 +74,7 @@ export async function refreshCognitionReferenceStatuses(workspace, currentMaps)
73
74
  await writeCognitionNote(workspace, {
74
75
  ...note,
75
76
  relatedSymbols,
77
+ updatedAt: new Date().toISOString(),
76
78
  referencesStatus: nextStatus,
77
79
  });
78
80
  }
@@ -0,0 +1,14 @@
1
+ import type { KGraphWorkspace } from '../types/config.js';
2
+ export interface CompactResult {
3
+ merged: Array<{
4
+ targetId: string;
5
+ sourceIds: string[];
6
+ title: string;
7
+ }>;
8
+ archived: Array<{
9
+ id: string;
10
+ title: string;
11
+ reason: string;
12
+ }>;
13
+ }
14
+ export declare function compactCognition(workspace: KGraphWorkspace, dryRun?: boolean): Promise<CompactResult>;
@@ -0,0 +1,167 @@
1
+ import { mkdir, rename } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { overwriteDomainRecord, readCognitionNotes, readDomainRecords, slugify, writeCognitionNote, } from '../storage/cognition-store.js';
4
+ import { pathExists } from '../storage/kgraph-paths.js';
5
+ import { readMaps } from '../storage/map-store.js';
6
+ import { evaluateReferenceStatus } from './cognition-updater.js';
7
+ export async function compactCognition(workspace, dryRun = false) {
8
+ const notes = await readCognitionNotes(workspace);
9
+ const maps = await readMaps(workspace);
10
+ const groups = groupDuplicates(notes);
11
+ const result = { merged: [], archived: [] };
12
+ const consumed = new Set();
13
+ const archived = new Set();
14
+ const mergedNotes = [];
15
+ for (const group of groups.filter((items) => items.length > 1)) {
16
+ const merged = mergeNotes(group, {
17
+ files: maps.fileMap.files,
18
+ symbols: maps.symbolMap.symbols,
19
+ });
20
+ const sourceIds = group.map((note) => note.id);
21
+ result.merged.push({
22
+ targetId: merged.id,
23
+ sourceIds,
24
+ title: merged.title,
25
+ });
26
+ sourceIds.forEach((id) => consumed.add(id));
27
+ mergedNotes.push(merged);
28
+ if (!dryRun) {
29
+ await writeCognitionNote(workspace, merged);
30
+ for (const note of group) {
31
+ await archiveNote(workspace, note, `superseded-by-${merged.id}`);
32
+ }
33
+ }
34
+ }
35
+ for (const note of notes) {
36
+ if (consumed.has(note.id))
37
+ continue;
38
+ if (note.confidence === 'low' &&
39
+ (note.referencesStatus === 'stale' || note.referencesStatus === 'unresolved')) {
40
+ result.archived.push({
41
+ id: note.id,
42
+ title: note.title,
43
+ reason: 'low-confidence stale cognition',
44
+ });
45
+ archived.add(note.id);
46
+ if (!dryRun) {
47
+ await archiveNote(workspace, note, 'low-confidence-stale');
48
+ }
49
+ }
50
+ }
51
+ if (!dryRun && (consumed.size > 0 || archived.size > 0)) {
52
+ const activeNotes = [
53
+ ...notes.filter((note) => !consumed.has(note.id) && !archived.has(note.id)),
54
+ ...mergedNotes,
55
+ ];
56
+ await rebuildDomainRecords(workspace, activeNotes, {
57
+ files: maps.fileMap.files,
58
+ symbols: maps.symbolMap.symbols,
59
+ });
60
+ }
61
+ return result;
62
+ }
63
+ function groupDuplicates(notes) {
64
+ const byKey = new Map();
65
+ for (const note of notes) {
66
+ if (note.supersededBy)
67
+ continue;
68
+ const key = [
69
+ note.kind ?? 'summary',
70
+ note.domain ?? 'general',
71
+ normalizeTitle(note.title),
72
+ normalizeText(note.summary ?? ''),
73
+ stableList(note.relatedFiles),
74
+ stableList(note.relatedSymbols),
75
+ ].join('\0');
76
+ const group = byKey.get(key) ?? [];
77
+ group.push(note);
78
+ byKey.set(key, group);
79
+ }
80
+ return [...byKey.values()];
81
+ }
82
+ function mergeNotes(notes, currentMaps) {
83
+ const sorted = [...notes].sort((left, right) => left.createdAt.localeCompare(right.createdAt));
84
+ const base = sorted[sorted.length - 1];
85
+ const now = new Date().toISOString();
86
+ const id = `${now.replace(/[:.]/g, '-')}-${slugify(base.title) || 'compacted'}`;
87
+ const summaries = unique(sorted
88
+ .map((note) => note.summary)
89
+ .filter((summary) => Boolean(summary?.trim())));
90
+ const relatedFiles = unique(sorted.flatMap((note) => note.relatedFiles));
91
+ const relatedSymbols = unique(sorted.flatMap((note) => note.relatedSymbols));
92
+ return {
93
+ ...base,
94
+ id,
95
+ source: 'compact',
96
+ createdAt: now,
97
+ updatedAt: now,
98
+ supersedes: sorted.map((note) => note.id),
99
+ supersededBy: undefined,
100
+ confidence: highestConfidence(sorted.map((note) => note.confidence)),
101
+ relatedFiles,
102
+ relatedSymbols,
103
+ tags: unique(sorted.flatMap((note) => note.tags)),
104
+ summary: summaries[0] ?? base.summary,
105
+ sections: {
106
+ Summary: summaries.map((summary) => `- ${summary}`).join('\n'),
107
+ 'Compacted From': sorted.map((note) => `- ${note.id}`).join('\n'),
108
+ },
109
+ warnings: unique(sorted.flatMap((note) => note.warnings)),
110
+ referencesStatus: evaluateReferenceStatus(relatedFiles, relatedSymbols, currentMaps),
111
+ };
112
+ }
113
+ async function rebuildDomainRecords(workspace, notes, currentMaps) {
114
+ const existingDomains = await readDomainRecords(workspace);
115
+ const existingByName = new Map(existingDomains.map((domain) => [domain.name, domain]));
116
+ const domainNames = new Set([
117
+ ...existingDomains.map((domain) => domain.name),
118
+ ...notes.map((note) => note.domain ?? 'general'),
119
+ ]);
120
+ const fileSet = new Set(currentMaps.files.map((file) => file.path));
121
+ const symbolSet = new Set(currentMaps.symbols.map((symbol) => symbol.name));
122
+ for (const name of domainNames) {
123
+ const relatedNotes = notes.filter((note) => (note.domain ?? 'general') === name);
124
+ const existing = existingByName.get(name);
125
+ const next = {
126
+ name,
127
+ description: existing?.description,
128
+ pathHints: unique(relatedNotes.flatMap((note) => note.relatedFiles)),
129
+ tags: unique(relatedNotes.flatMap((note) => note.tags)),
130
+ files: unique(relatedNotes
131
+ .flatMap((note) => note.relatedFiles)
132
+ .filter((file) => fileSet.has(file))),
133
+ symbols: unique(relatedNotes
134
+ .flatMap((note) => note.relatedSymbols)
135
+ .filter((symbol) => symbolSet.has(symbol))),
136
+ cognitionNotes: relatedNotes.map((note) => note.id),
137
+ };
138
+ await overwriteDomainRecord(workspace, next);
139
+ }
140
+ }
141
+ async function archiveNote(workspace, note, reason) {
142
+ const source = path.join(workspace.cognitionPath, `${note.id}.md`);
143
+ if (!(await pathExists(source)))
144
+ return;
145
+ const archivedDir = path.join(workspace.cognitionPath, 'archived');
146
+ await mkdir(archivedDir, { recursive: true });
147
+ await rename(source, path.join(archivedDir, `${reason}-${note.id}.md`));
148
+ }
149
+ function highestConfidence(values) {
150
+ if (values.includes('high'))
151
+ return 'high';
152
+ if (values.includes('medium'))
153
+ return 'medium';
154
+ return 'low';
155
+ }
156
+ function normalizeTitle(value) {
157
+ return normalizeText(value);
158
+ }
159
+ function normalizeText(value) {
160
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim();
161
+ }
162
+ function stableList(values) {
163
+ return [...values].sort().join('\0');
164
+ }
165
+ function unique(items) {
166
+ return [...new Set(items)];
167
+ }
@@ -0,0 +1,16 @@
1
+ import type { CognitionConfidence, CognitionKind, CognitionNote, CognitionSource } from '../types/cognition.js';
2
+ import type { KGraphWorkspace } from '../types/config.js';
3
+ export interface ConclusionInput {
4
+ topic: string;
5
+ body?: string;
6
+ kind?: CognitionKind;
7
+ confidence?: CognitionConfidence;
8
+ domain?: string;
9
+ tags?: string[];
10
+ relatedFiles?: string[];
11
+ relatedSymbols?: string[];
12
+ source: CognitionSource;
13
+ }
14
+ export declare function concludeTopic(workspace: KGraphWorkspace, input: ConclusionInput): Promise<CognitionNote>;
15
+ export declare function concludeActiveSession(workspace: KGraphWorkspace, agent: string, input: Omit<ConclusionInput, 'source' | 'relatedFiles' | 'relatedSymbols'>): Promise<CognitionNote>;
16
+ export declare function buildActiveSessionConclusion(workspace: KGraphWorkspace, agent: string, input: Omit<ConclusionInput, 'source' | 'relatedFiles' | 'relatedSymbols'>): Promise<ConclusionInput>;
@@ -0,0 +1,96 @@
1
+ import { readMaps } from '../storage/map-store.js';
2
+ import { slugify, writeCognitionNote, writeDomainRecord } from '../storage/cognition-store.js';
3
+ import { KGraphError } from '../cli/errors.js';
4
+ import { evaluateReferenceStatus } from './cognition-updater.js';
5
+ import { readSessionState } from '../session/session-store.js';
6
+ export async function concludeTopic(workspace, input) {
7
+ const maps = await readMaps(workspace);
8
+ const now = new Date().toISOString();
9
+ const timestamp = now.replace(/[:.]/g, '-');
10
+ const title = input.topic.trim();
11
+ const summary = normalizeBody(input.body) ?? title;
12
+ const note = {
13
+ title,
14
+ kind: input.kind ?? 'summary',
15
+ confidence: input.confidence ?? 'medium',
16
+ domain: input.domain,
17
+ tags: input.tags ?? [],
18
+ summary,
19
+ sections: {
20
+ Summary: summary,
21
+ ...(input.relatedFiles?.length ? { 'Related Files': input.relatedFiles.map((file) => `- ${file}`).join('\n') } : {}),
22
+ ...(input.relatedSymbols?.length ? { 'Key Symbols': input.relatedSymbols.map((symbol) => `- \`${symbol}\``).join('\n') } : {}),
23
+ },
24
+ relatedFiles: input.relatedFiles ?? [],
25
+ relatedSymbols: input.relatedSymbols ?? [],
26
+ warnings: [],
27
+ id: `${timestamp}-${slugify(title) || 'conclusion'}`,
28
+ sourceInboxPath: '',
29
+ processedPath: `.kgraph/cognition/${timestamp}-${slugify(title) || 'conclusion'}.md`,
30
+ createdAt: now,
31
+ source: input.source,
32
+ referencesStatus: evaluateReferenceStatus(input.relatedFiles ?? [], input.relatedSymbols ?? [], { files: maps.fileMap.files, symbols: maps.symbolMap.symbols }),
33
+ };
34
+ await writeCognitionNote(workspace, note);
35
+ await writeDomainRecord(workspace, toDomainRecord(note, {
36
+ files: maps.fileMap.files,
37
+ symbols: maps.symbolMap.symbols,
38
+ }));
39
+ return note;
40
+ }
41
+ export async function concludeActiveSession(workspace, agent, input) {
42
+ return concludeTopic(workspace, await buildActiveSessionConclusion(workspace, agent, input));
43
+ }
44
+ export async function buildActiveSessionConclusion(workspace, agent, input) {
45
+ const state = await readSessionState(workspace);
46
+ const active = state.active[agent];
47
+ if (!active) {
48
+ throw new KGraphError(`No active session for agent "${agent}".`);
49
+ }
50
+ const events = state.events.filter((event) => event.agent === agent && event.timestamp >= active.startedAt);
51
+ const touchedFiles = [
52
+ ...new Set(events
53
+ .filter((event) => event.type === 'read' || event.type === 'write')
54
+ .map((event) => event.path)
55
+ .filter((file) => Boolean(file))),
56
+ ];
57
+ const writtenFiles = [
58
+ ...new Set(events
59
+ .filter((event) => event.type === 'write')
60
+ .map((event) => event.path)
61
+ .filter((file) => Boolean(file))),
62
+ ];
63
+ const body = [
64
+ normalizeBody(input.body) ?? `Session concluded for ${input.topic}.`,
65
+ touchedFiles.length
66
+ ? `Touched files: ${touchedFiles.join(', ')}.`
67
+ : undefined,
68
+ writtenFiles.length
69
+ ? `Changed files: ${writtenFiles.join(', ')}.`
70
+ : undefined,
71
+ ]
72
+ .filter(Boolean)
73
+ .join('\n\n');
74
+ return {
75
+ ...input,
76
+ body,
77
+ source: 'session-conclude',
78
+ relatedFiles: touchedFiles,
79
+ };
80
+ }
81
+ function normalizeBody(value) {
82
+ const trimmed = value?.trim();
83
+ return trimmed ? trimmed : undefined;
84
+ }
85
+ function toDomainRecord(note, currentMaps) {
86
+ const fileSet = new Set(currentMaps.files.map((file) => file.path));
87
+ const symbolSet = new Set(currentMaps.symbols.map((symbol) => symbol.name));
88
+ return {
89
+ name: note.domain ?? 'general',
90
+ pathHints: note.relatedFiles,
91
+ tags: note.tags,
92
+ files: note.relatedFiles.filter((file) => fileSet.has(file)),
93
+ symbols: note.relatedSymbols.filter((symbol) => symbolSet.has(symbol)),
94
+ cognitionNotes: [note.id],
95
+ };
96
+ }
@@ -10,6 +10,8 @@ export function parseMarkdownNote(markdown) {
10
10
  const combined = Object.values(sections).join('\n');
11
11
  return {
12
12
  title,
13
+ kind: normalizeKind(frontmatter.type ?? frontmatter.kind, warnings),
14
+ confidence: normalizeConfidence(frontmatter.confidence, warnings),
13
15
  domain: typeof frontmatter.domain === 'string' ? frontmatter.domain : undefined,
14
16
  tags: Array.isArray(frontmatter.tags) ? frontmatter.tags.map(String) : [],
15
17
  summary: sections.Summary,
@@ -19,6 +21,28 @@ export function parseMarkdownNote(markdown) {
19
21
  warnings,
20
22
  };
21
23
  }
24
+ function normalizeKind(value, warnings) {
25
+ if (value === 'finding' ||
26
+ value === 'decision' ||
27
+ value === 'gotcha' ||
28
+ value === 'summary' ||
29
+ value === 'relationship') {
30
+ return value;
31
+ }
32
+ if (value !== undefined) {
33
+ warnings.push(`Unsupported cognition type "${String(value)}"; defaulted to summary.`);
34
+ }
35
+ return 'summary';
36
+ }
37
+ function normalizeConfidence(value, warnings) {
38
+ if (value === 'high' || value === 'medium' || value === 'low') {
39
+ return value;
40
+ }
41
+ if (value !== undefined) {
42
+ warnings.push(`Unsupported confidence "${String(value)}"; defaulted to medium.`);
43
+ }
44
+ return 'medium';
45
+ }
22
46
  function splitFrontmatter(markdown, warnings) {
23
47
  if (!markdown.startsWith('---\n')) {
24
48
  return { frontmatter: {}, body: markdown };
@@ -1,9 +1,14 @@
1
1
  import { getRecentlyCommittedFiles, getWorkingTreeChangesDetailed, isGitRepo, } from '../scanner/git-utils.js';
2
2
  import { readCognitionNotes, readDomainRecords, } from '../storage/cognition-store.js';
3
+ import { readSessionState } from '../session/session-store.js';
3
4
  import { rankByFields } from './ranking.js';
4
5
  export async function queryContext(workspace, config, maps, query) {
5
6
  const cognition = await readCognitionNotes(workspace);
6
7
  const domains = await readDomainRecords(workspace);
8
+ const session = await readSessionState(workspace);
9
+ const sessionTouchedPaths = new Set(session.events
10
+ .map((event) => event.path)
11
+ .filter((path) => Boolean(path)));
7
12
  const max = config.maxContextItems;
8
13
  let relevantFiles = rankByFields(query, maps.fileMap.files, [
9
14
  { name: 'path', value: (file) => file.path },
@@ -11,7 +16,12 @@ export async function queryContext(workspace, config, maps, query) {
11
16
  ])
12
17
  .map((ranked) => ({
13
18
  ...ranked,
14
- score: ranked.score - Math.floor((ranked.item.tokenEstimate ?? 0) / 2000),
19
+ score: ranked.score -
20
+ Math.floor((ranked.item.tokenEstimate ?? 0) / 2000) +
21
+ (sessionTouchedPaths.has(ranked.item.path) ? 3 : 0),
22
+ reasons: sessionTouchedPaths.has(ranked.item.path)
23
+ ? [...ranked.reasons, 'touched in current session']
24
+ : ranked.reasons,
15
25
  }))
16
26
  .sort((a, b) => b.score - a.score)
17
27
  .slice(0, max);
@@ -23,12 +33,17 @@ export async function queryContext(workspace, config, maps, query) {
23
33
  ]).slice(0, max);
24
34
  const relevantCognition = rankByFields(query, cognition, [
25
35
  { name: 'title', value: (note) => note.title },
36
+ { name: 'type', value: (note) => note.kind },
37
+ { name: 'confidence', value: (note) => note.confidence },
26
38
  { name: 'domain', value: (note) => note.domain },
27
39
  { name: 'tags', value: (note) => note.tags },
28
40
  { name: 'files', value: (note) => note.relatedFiles },
29
41
  { name: 'symbols', value: (note) => note.relatedSymbols },
30
42
  { name: 'summary', value: (note) => note.summary },
31
- ]).slice(0, max);
43
+ ])
44
+ .map((ranked) => applyCognitionRankAdjustments(ranked))
45
+ .sort((a, b) => b.score - a.score)
46
+ .slice(0, max);
32
47
  const matchedDomains = rankByFields(query, domains, [
33
48
  { name: 'name', value: (domain) => domain.name },
34
49
  { name: 'tags', value: (domain) => domain.tags },
@@ -277,6 +292,36 @@ function explainRelationships(relationships, context) {
277
292
  return { relationship, reasons: [...reasons] };
278
293
  });
279
294
  }
295
+ function applyCognitionRankAdjustments(ranked) {
296
+ const reasons = [...ranked.reasons];
297
+ let score = ranked.score;
298
+ if (ranked.item.confidence === 'high') {
299
+ score += 3;
300
+ reasons.push('high confidence cognition');
301
+ }
302
+ else if (ranked.item.confidence === 'low') {
303
+ score -= 2;
304
+ reasons.push('low confidence penalty');
305
+ }
306
+ if (ranked.item.referencesStatus === 'current') {
307
+ score += 2;
308
+ reasons.push('current references');
309
+ }
310
+ else if (ranked.item.referencesStatus === 'mixed') {
311
+ score -= 1;
312
+ reasons.push('mixed reference penalty');
313
+ }
314
+ else if (ranked.item.referencesStatus === 'stale' ||
315
+ ranked.item.referencesStatus === 'unresolved') {
316
+ score -= 4;
317
+ reasons.push('stale reference penalty');
318
+ }
319
+ if (ranked.item.kind === 'decision' || ranked.item.kind === 'gotcha') {
320
+ score += 1;
321
+ reasons.push(`${ranked.item.kind} cognition`);
322
+ }
323
+ return { ...ranked, score, reasons };
324
+ }
280
325
  function dependenciesForImportedSymbol(symbol, dependencies) {
281
326
  return dependencies
282
327
  .filter((dependency) => dependency.kind === 'local' &&
@@ -5,7 +5,7 @@ export const claudeCodeAdapter = {
5
5
  targetPath: 'CLAUDE.md',
6
6
  instructions: `## KGraph Workflow
7
7
 
8
- {{KGRAPH_CONTEXT_POLICY}} Use /kgraph for the full automated workflow. Run \`kgraph doctor\` when setup or generated maps look wrong. Run \`kgraph scan\`, \`kgraph update\`, and \`kgraph context\` manually only when you need one specific step.
8
+ {{KGRAPH_CONTEXT_POLICY}} Use /kgraph for the full automated workflow. Run \`kgraph conclude\` for durable typed engineering memory and \`kgraph compact --dry-run\` when cognition looks duplicated or stale. Run \`kgraph doctor\` when setup or generated maps look wrong. Run \`kgraph scan\`, \`kgraph update\`, and \`kgraph context\` manually only when you need one specific step.
9
9
  `,
10
10
  commandFiles: [
11
11
  {
@@ -23,6 +23,11 @@ ${numberedWorkflow('claude-code', { sessionQualifier: 'when native hooks are una
23
23
  {
24
24
  path: '.claude/commands/kgraph-repair.md',
25
25
  content: `Run \`kgraph repair --dry-run\` first and summarize the proposed cognition cleanup. Run \`kgraph repair\` only when the user asks to apply the cleanup.
26
+ `,
27
+ },
28
+ {
29
+ path: '.claude/commands/kgraph-compact.md',
30
+ content: `Run \`kgraph compact --dry-run\` first and summarize duplicate cognition groups and stale low-confidence notes. Run \`kgraph compact\` only when the user asks to apply compaction.
26
31
  `,
27
32
  },
28
33
  {
@@ -47,7 +52,12 @@ ${numberedWorkflow('claude-code', { sessionQualifier: 'when native hooks are una
47
52
  },
48
53
  {
49
54
  path: '.claude/commands/kgraph-session.md',
50
- content: `Use \`kgraph session\` to inspect session read/write/token estimates. Record meaningful events with \`kgraph session start --agent claude-code\`, \`kgraph session read <path> --agent claude-code\`, \`kgraph session write <path> --agent claude-code\`, and \`kgraph session end --agent claude-code\`.
55
+ content: `Use \`kgraph session\` to inspect session read/write/token estimates. Record meaningful events with \`kgraph session start --agent claude-code\`, \`kgraph session read <path> --agent claude-code\`, \`kgraph session write <path> --agent claude-code\`, and \`kgraph session end --agent claude-code --conclude --topic "<topic>"\` when durable session memory is useful.
56
+ `,
57
+ },
58
+ {
59
+ path: '.claude/commands/kgraph-conclude.md',
60
+ content: `Use \`kgraph conclude "$ARGUMENTS"\` when the session produced reusable engineering knowledge. Choose one type from finding, decision, gotcha, summary, relationship, and one confidence from high, medium, low. Store only durable conclusions, not raw chain-of-thought, temporary reasoning, speculative exploration, or low-value observations.
51
61
  `,
52
62
  },
53
63
  {
@@ -5,7 +5,7 @@ export const codexAdapter = {
5
5
  targetPath: 'AGENTS.md',
6
6
  instructions: `## KGraph Workflow
7
7
 
8
- {{KGRAPH_CONTEXT_POLICY}} The /kgraph skill handles the full automated workflow. Run \`kgraph doctor\` when setup or generated maps look wrong. Run \`kgraph scan\`, \`kgraph update\`, and \`kgraph context\` manually only when you need one specific step.
8
+ {{KGRAPH_CONTEXT_POLICY}} The /kgraph skill handles the full automated workflow. Run \`kgraph conclude\` for durable typed engineering memory and \`kgraph compact --dry-run\` when cognition looks duplicated or stale. Run \`kgraph doctor\` when setup or generated maps look wrong. Run \`kgraph scan\`, \`kgraph update\`, and \`kgraph context\` manually only when you need one specific step.
9
9
  `,
10
10
  commandFiles: [
11
11
  {
@@ -12,7 +12,7 @@ ${numberedWorkflow('copilot')}
12
12
  path: '.github/agents/kgraph.agent.md',
13
13
  content: `---
14
14
  name: kgraph
15
- description: Use KGraph persistent repo intelligence to answer questions about this codebase. Runs kgraph context, scan, update, impact, history, and session commands to ground responses in durable local knowledge.
15
+ description: Use KGraph persistent repo intelligence to answer questions about this codebase. Runs kgraph context, scan, update, conclude, compact, impact, history, and session commands to ground responses in durable local knowledge.
16
16
  tools:
17
17
  - run_in_terminal
18
18
  - read_file
@@ -47,6 +47,17 @@ argument-hint: "--dry-run or apply"
47
47
  ---
48
48
 
49
49
  Run \`kgraph repair --dry-run\` first and summarize the proposed cognition cleanup. Run \`kgraph repair\` only when the user asks to apply the cleanup.
50
+ `,
51
+ },
52
+ {
53
+ path: '.github/prompts/kgraph-compact.prompt.md',
54
+ content: `---
55
+ description: Merge duplicate KGraph cognition and archive stale low-value entries
56
+ agent: agent
57
+ argument-hint: "--dry-run or apply"
58
+ ---
59
+
60
+ Run \`kgraph compact --dry-run\` first and summarize duplicate cognition groups and stale low-confidence notes. Run \`kgraph compact\` only when the user asks to apply compaction.
50
61
  `,
51
62
  },
52
63
  {
@@ -90,6 +101,17 @@ argument-hint: "Brief description of what was done"
90
101
  Capture this session into KGraph cognition.
91
102
 
92
103
  {{KGRAPH_CAPTURE_POLICY}}
104
+ `,
105
+ },
106
+ {
107
+ path: '.github/prompts/kgraph-conclude.prompt.md',
108
+ content: `---
109
+ description: Store a typed durable KGraph engineering conclusion
110
+ agent: agent
111
+ argument-hint: "Topic plus optional type, confidence, files, and symbols"
112
+ ---
113
+
114
+ Use \`kgraph conclude "$ARGUMENTS"\` when the session produced reusable engineering knowledge. Choose one type from finding, decision, gotcha, summary, relationship, and one confidence from high, medium, low. Store only durable conclusions, not raw chain-of-thought, temporary reasoning, speculative exploration, or low-value observations.
93
115
  `,
94
116
  },
95
117
  {
@@ -111,7 +133,7 @@ agent: agent
111
133
  argument-hint: "start, read <path>, write <path>, end, or status"
112
134
  ---
113
135
 
114
- Use \`kgraph session\` to inspect current session activity. Record meaningful events with \`kgraph session start --agent copilot\`, \`kgraph session read <path> --agent copilot\`, \`kgraph session write <path> --agent copilot\`, and \`kgraph session end --agent copilot\`.
136
+ Use \`kgraph session\` to inspect current session activity. Record meaningful events with \`kgraph session start --agent copilot\`, \`kgraph session read <path> --agent copilot\`, \`kgraph session write <path> --agent copilot\`, and \`kgraph session end --agent copilot --conclude --topic "<topic>"\` when durable session memory is useful.
115
137
  `,
116
138
  },
117
139
  {
@@ -50,14 +50,20 @@ export function renderContextPolicy(mode) {
50
50
  }
51
51
  export function renderCapturePolicy() {
52
52
  return `Capture policy:
53
- - At the end of any session that changed repository files, write one concise Markdown note to \`.kgraph/inbox/<slug>.md\` before finishing.
54
- - A \`.kgraph/inbox/*.md\` note is KGraph runtime capture, not project documentation. It is allowed and required by this workflow unless the user explicitly says not to capture to KGraph.
55
- - Do not skip capture for UI text, button, link, route, styling, or small file edits. Skip capture only when no repository files changed.
53
+ - At the end of any session that changed repository files, store durable engineering memory with \`kgraph conclude "<topic>" --type <finding|decision|gotcha|summary|relationship> --confidence <high|medium|low>\` or \`kgraph session end --agent <agent> --conclude --topic "<topic>"\`.
54
+ - Preserve only expensive-to-rediscover findings, decisions, gotchas, summaries, and relationships. Do not store raw chain-of-thought, temporary reasoning, speculative exploration, or low-value observations.
55
+ - Use \`.kgraph/inbox/<slug>.md\` only when a longer structured note is clearer than a single \`kgraph conclude\` command.
56
+ - A \`.kgraph/inbox/*.md\` note is KGraph runtime capture, not project documentation. It is allowed by this workflow unless the user explicitly says not to capture to KGraph.
57
+ - Do not skip capture for meaningful UI text, button, link, route, styling, or small file edits. Skip capture only when no reusable repository knowledge was created.
56
58
  - Do not run KGraph repeatedly. Run it once at the start with \`kgraph "<topic>"\`. If repo files changed, write the inbox note first, then run \`kgraph\` once at the end.
57
- - After the final \`kgraph\` run, mention whether the inbox note was processed.
59
+ - After the final \`kgraph\` run, mention whether durable cognition was stored or processed.
58
60
 
59
- The inbox note must use this structure:
61
+ When using an inbox note, use this structure:
60
62
  \`\`\`markdown
63
+ ---
64
+ type: finding
65
+ confidence: medium
66
+ ---
61
67
  # <Short Title>
62
68
 
63
69
  ## Summary
@@ -5,9 +5,10 @@
5
5
  const DOCTOR_STEP = `Run \`kgraph doctor\` when setup, maps, inbox processing, or integrations look wrong. Run \`kgraph doctor --quality\` when context shows stale/noisy cognition references.`;
6
6
  const IMPACT_STEP = `Run \`kgraph impact "<file-or-symbol>"\` when the user asks what a change may affect. Run \`kgraph history "<topic>"\` when prior work or decisions matter.`;
7
7
  const REPAIR_STEP = `Run \`kgraph repair --dry-run\` before cleanup when stale/noisy cognition needs fixing. Run \`kgraph repair\` only when the user asks to apply that cleanup.`;
8
+ const COMPACT_STEP = `Run \`kgraph compact --dry-run\` when cognition looks duplicated, noisy, or stale. Run \`kgraph compact\` only when the user asks to merge/archive cognition.`;
8
9
  const HISTORY_STEP = `Run \`kgraph history\` or \`kgraph history "<topic>"\` to review past cognition sessions with git author attribution.`;
9
10
  function sessionStep(agentName, qualifier) {
10
- const base = `Track meaningful session activity with \`kgraph session start --agent ${agentName}\`, \`kgraph session read <path> --agent ${agentName}\`, \`kgraph session write <path> --agent ${agentName}\`, and \`kgraph session end --agent ${agentName}\``;
11
+ const base = `Track meaningful session activity with \`kgraph session start --agent ${agentName}\`, \`kgraph session read <path> --agent ${agentName}\`, \`kgraph session write <path> --agent ${agentName}\`, and \`kgraph session end --agent ${agentName} --conclude --topic "<topic>"\` when durable session memory is useful`;
11
12
  return qualifier ? `${base} ${qualifier}.` : `${base}.`;
12
13
  }
13
14
  /**
@@ -25,8 +26,9 @@ export function numberedWorkflow(agentName, options = {}) {
25
26
  {{KGRAPH_CAPTURE_POLICY}}
26
27
 
27
28
  7. ${REPAIR_STEP}
28
- 8. Run \`kgraph visualize\` when the user wants to inspect the dependency graph — opens an interactive graph at http://localhost:4242 with PNG export.
29
- 9. ${HISTORY_STEP}`;
29
+ 8. ${COMPACT_STEP}
30
+ 9. Run \`kgraph visualize\` when the user wants to inspect the dependency graph — opens an interactive graph at http://localhost:4242 with PNG export.
31
+ 10. ${HISTORY_STEP}`;
30
32
  }
31
33
  /**
32
34
  * Returns the bullet-list workflow for rules files.
@@ -39,6 +41,7 @@ export function bulletWorkflow(agentName, options = {}) {
39
41
  - ${IMPACT_STEP}
40
42
  {{KGRAPH_CAPTURE_POLICY}}
41
43
  - ${REPAIR_STEP}
44
+ - ${COMPACT_STEP}
42
45
  - Run \`kgraph visualize\` to open the interactive dependency graph at http://localhost:4242 with PNG export.
43
46
  - ${HISTORY_STEP}`;
44
47
  }
@@ -32,6 +32,9 @@ export async function readSessionLedger(workspace) {
32
32
  export async function recordSessionEvent(workspace, input) {
33
33
  const now = new Date().toISOString();
34
34
  const state = await readSessionState(workspace);
35
+ if (input.type === 'end' && !state.active[input.agent]) {
36
+ throw new KGraphError(`No active session for agent "${input.agent}".`);
37
+ }
35
38
  // Auto-close any open session for this agent before starting a new one so
36
39
  // the ledger entry is never silently lost on repeated start calls.
37
40
  if (input.type === 'start' && state.active[input.agent]) {
@@ -53,7 +53,7 @@ export async function readCognitionNotes(workspace) {
53
53
  const raw = await readFile(filePath, "utf8");
54
54
  const encoded = parseEmbeddedJson(raw);
55
55
  if (encoded) {
56
- notes.push(encoded);
56
+ notes.push(normalizeCognitionNote(encoded));
57
57
  }
58
58
  }
59
59
  return notes;
@@ -80,6 +80,30 @@ function parseEmbeddedJson(raw) {
80
80
  const encoded = raw.match(/```json\n([\s\S]*?)\n```/);
81
81
  return encoded ? JSON.parse(encoded[1]) : undefined;
82
82
  }
83
+ function normalizeCognitionNote(note) {
84
+ const title = note.title ?? 'Untitled Cognition Note';
85
+ return {
86
+ title,
87
+ kind: note.kind ?? 'summary',
88
+ confidence: note.confidence ?? 'medium',
89
+ domain: note.domain,
90
+ tags: note.tags ?? [],
91
+ summary: note.summary,
92
+ sections: note.sections ?? {},
93
+ relatedFiles: note.relatedFiles ?? [],
94
+ relatedSymbols: note.relatedSymbols ?? [],
95
+ warnings: note.warnings ?? [],
96
+ id: (note.id ?? slugify(title)) || 'cognition-note',
97
+ sourceInboxPath: note.sourceInboxPath ?? '',
98
+ processedPath: note.processedPath ?? '',
99
+ createdAt: note.createdAt ?? '',
100
+ updatedAt: note.updatedAt,
101
+ source: note.source ?? 'inbox',
102
+ supersedes: note.supersedes,
103
+ supersededBy: note.supersededBy,
104
+ referencesStatus: note.referencesStatus ?? 'unresolved',
105
+ };
106
+ }
83
107
  function mergeDomainRecords(existing, next) {
84
108
  return {
85
109
  ...existing,
@@ -109,7 +133,7 @@ function renderCognitionNote(note) {
109
133
  const sectionText = Object.entries(note.sections)
110
134
  .map(([heading, content]) => `## ${heading}\n\n${content.trim()}`)
111
135
  .join("\n\n");
112
- return `# ${note.title}\n\nStatus: ${note.referencesStatus}\n\n${sectionText}\n\n## KGraph Metadata\n\n\`\`\`json\n${JSON.stringify(note, null, 2)}\n\`\`\`\n`;
136
+ return `# ${note.title}\n\nType: ${note.kind ?? 'summary'}\nConfidence: ${note.confidence ?? 'medium'}\nStatus: ${note.referencesStatus}\nSource: ${note.source ?? 'inbox'}\n\n${sectionText}\n\n## KGraph Metadata\n\n\`\`\`json\n${JSON.stringify(note, null, 2)}\n\`\`\`\n`;
113
137
  }
114
138
  function renderDomainRecord(domain) {
115
139
  return `# ${domain.name}\n\n${domain.description ?? ""}\n\n## Files\n\n${domain.files
@@ -1,7 +1,12 @@
1
1
  import type { CodeSymbol, Relationship, RepositoryFile } from './maps.js';
2
2
  export type ReferenceStatus = 'current' | 'stale' | 'unresolved' | 'mixed';
3
+ export type CognitionKind = 'finding' | 'decision' | 'gotcha' | 'summary' | 'relationship';
4
+ export type CognitionConfidence = 'high' | 'medium' | 'low';
5
+ export type CognitionSource = 'inbox' | 'conclude' | 'session-conclude' | 'compact';
3
6
  export interface ParsedCognitionNote {
4
7
  title: string;
8
+ kind: CognitionKind;
9
+ confidence: CognitionConfidence;
5
10
  domain?: string;
6
11
  tags: string[];
7
12
  summary?: string;
@@ -15,6 +20,10 @@ export interface CognitionNote extends ParsedCognitionNote {
15
20
  sourceInboxPath: string;
16
21
  processedPath: string;
17
22
  createdAt: string;
23
+ updatedAt?: string;
24
+ source: CognitionSource;
25
+ supersedes?: string[];
26
+ supersededBy?: string;
18
27
  referencesStatus: ReferenceStatus;
19
28
  }
20
29
  export interface DomainRecord {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kentwynn/kgraph",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "description": "Persistent repo intelligence for AI coding assistants.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -19,7 +19,10 @@
19
19
  "kgraph": "tsx src/cli/index.ts",
20
20
  "check:artifacts": "node scripts/check-clean-artifacts.mjs",
21
21
  "pack:dry": "npm pack --dry-run",
22
- "release:pack": "npm run build && npm test && npm run check:artifacts && npm pack"
22
+ "release:pack": "npm run build && npm test && npm run check:artifacts && npm pack",
23
+ "release": "release-it",
24
+ "release:minor": "release-it minor",
25
+ "release:major": "release-it major"
23
26
  },
24
27
  "keywords": [
25
28
  "ai",
@@ -57,7 +60,9 @@
57
60
  "yaml": "^2.5.1"
58
61
  },
59
62
  "devDependencies": {
63
+ "@release-it/conventional-changelog": "^11.0.0",
60
64
  "@types/node": "^20.17.10",
65
+ "release-it": "^20.0.1",
61
66
  "tsx": "^4.19.2",
62
67
  "vitest": "^3.2.4"
63
68
  },