@kentwynn/kgraph 0.2.3 → 0.2.5

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
@@ -267,7 +267,7 @@ kgraph integrate list
267
267
  kgraph integrate remove cursor
268
268
  ```
269
269
 
270
- New integrations default to `always` mode because coding agents often under-classify small UI, route, button, and link changes as not needing repo context.
270
+ New integrations default to `smart` mode. Use `--mode always` to force KGraph on every chat, or `--mode manual` to run only when explicitly asked.
271
271
 
272
272
  | Mode | Behavior |
273
273
  | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -3,6 +3,7 @@ import path from 'node:path';
3
3
  import { analyzeCognitionQuality, } from '../../cognition/cognition-quality.js';
4
4
  import { loadConfig } from '../../config/config.js';
5
5
  import { listIntegrations } from '../../integrations/integration-store.js';
6
+ import { getCurrentCommit, isGitRepo } from '../../scanner/git-utils.js';
6
7
  import { assertWorkspace, pathExists, resolveWorkspace, } from '../../storage/kgraph-paths.js';
7
8
  import { mapPaths, mapsExist, readMaps } from '../../storage/map-store.js';
8
9
  import { runCommand } from '../errors.js';
@@ -37,7 +38,9 @@ export function registerDoctorCommand(program) {
37
38
  checks.push({
38
39
  label: 'maps',
39
40
  ok: mapStatus,
40
- detail: mapStatus ? 'structural maps are present' : 'run `kgraph scan` or just `kgraph`',
41
+ detail: mapStatus
42
+ ? 'structural maps are present'
43
+ : 'run `kgraph scan` or just `kgraph`',
41
44
  });
42
45
  const maps = mapStatus ? await readMaps(workspace) : undefined;
43
46
  if (maps) {
@@ -46,6 +49,19 @@ export function registerDoctorCommand(program) {
46
49
  ok: true,
47
50
  detail: `${maps.fileMap.files.length} files, ${maps.symbolMap.symbols.length} symbols, ${maps.dependencyMap.dependencies.length} dependencies`,
48
51
  });
52
+ // Detect whether the repo has advanced past the commit that was scanned
53
+ if (maps.fileMap.scannedAtCommit && (await isGitRepo(rootPath))) {
54
+ const headCommit = await getCurrentCommit(rootPath);
55
+ const stale = headCommit !== null &&
56
+ headCommit !== maps.fileMap.scannedAtCommit;
57
+ checks.push({
58
+ label: 'scan freshness',
59
+ ok: !stale,
60
+ detail: stale
61
+ ? `scanned at ${maps.fileMap.scannedAtCommit.slice(0, 7)}, HEAD is ${headCommit.slice(0, 7)} — run \`kgraph scan\``
62
+ : `maps current at ${maps.fileMap.scannedAtCommit.slice(0, 7)}`,
63
+ });
64
+ }
49
65
  }
50
66
  else {
51
67
  const paths = mapPaths(workspace);
@@ -125,6 +141,7 @@ function printChecks(checks) {
125
141
  export function printQualityReport(report) {
126
142
  console.log(`Notes: ${report.noteCount}`);
127
143
  console.log(`Mixed/stale/unresolved notes: ${report.mixedOrStaleCount}`);
144
+ console.log(`Orphaned notes (all refs dead): ${report.orphanedNoteCount}`);
128
145
  console.log(`Noisy file refs: ${report.noisyFileRefCount}`);
129
146
  console.log(`Noisy symbol refs: ${report.noisySymbolRefCount}`);
130
147
  console.log(`Unresolved local imports: ${report.unresolvedLocalImportCount}`);
@@ -152,6 +169,9 @@ export function printQualityReport(report) {
152
169
  }
153
170
  function summarizeQualityFindings(report) {
154
171
  const findings = [];
172
+ if (report.orphanedNoteCount > 0) {
173
+ findings.push(`${report.orphanedNoteCount} orphaned cognition note(s) (all refs dead); run \`kgraph repair\` to archive`);
174
+ }
155
175
  if (report.mixedOrStaleCount > 0) {
156
176
  findings.push(`${report.mixedOrStaleCount} stale/mixed/unresolved note(s)`);
157
177
  }
@@ -14,7 +14,7 @@ export function registerInitCommand(program) {
14
14
  .description('Initialize a .kgraph workspace')
15
15
  .option('--integration <name>', 'Configure an AI tool integration', collectOption, [])
16
16
  .option('--integrations <names>', 'Configure comma-separated AI tool integrations')
17
- .option('--mode <mode>', 'Integration mode: always, smart, manual, or off', 'always')
17
+ .option('--mode <mode>', 'Integration mode: always, smart, manual, or off', 'smart')
18
18
  .action((options) => runCommand(async () => {
19
19
  const workspace = await ensureWorkspace(process.cwd());
20
20
  const wroteConfig = await writeDefaultConfig(workspace);
@@ -53,7 +53,7 @@ export function registerInitCommand(program) {
53
53
  })) {
54
54
  const selected = await promptForInitIntegrations(recommendedIntegrations);
55
55
  if (selected.length > 0) {
56
- const changed = await addIntegrations(workspace, selected, 'always');
56
+ const changed = await addIntegrations(workspace, selected, 'smart');
57
57
  console.log(`Configured integrations: ${changed.map((item) => `${item.name}:${item.mode}`).join(', ')}`);
58
58
  config = await loadConfig(workspace);
59
59
  recommendedIntegrations = recommendedIntegrationsForInit({
@@ -25,7 +25,7 @@ export function registerIntegrateCommand(program) {
25
25
  .command('add')
26
26
  .description('Add AI tool integrations')
27
27
  .argument('<names...>')
28
- .option('--mode <mode>', 'always, smart, manual, or off', 'always')
28
+ .option('--mode <mode>', 'always, smart, manual, or off', 'smart')
29
29
  .action((names, options) => runCommand(async () => {
30
30
  const workspace = await assertWorkspace(process.cwd());
31
31
  const normalized = normalizeIntegrationNames(names);
@@ -1,5 +1,5 @@
1
- import type { KGraphWorkspace } from '../types/config.js';
2
1
  import type { ReferenceStatus } from '../types/cognition.js';
2
+ import type { KGraphWorkspace } from '../types/config.js';
3
3
  import type { DependencyMap, FileMap, RelationshipMap, SymbolMap } from '../types/maps.js';
4
4
  export interface CognitionRepairChange {
5
5
  noteId: string;
@@ -21,6 +21,7 @@ export interface CognitionQualityReport {
21
21
  sessionRepeatedReadCount: number;
22
22
  sessionEstimatedReadTokens: number;
23
23
  sessionEstimatedRepeatedReadTokens: number;
24
+ orphanedNoteCount: number;
24
25
  changes: CognitionRepairChange[];
25
26
  }
26
27
  export declare function analyzeCognitionQuality(workspace: KGraphWorkspace, maps: {
@@ -1,5 +1,7 @@
1
- import { overwriteDomainRecord, readCognitionNotes, readDomainRecords, writeCognitionNote, } from '../storage/cognition-store.js';
1
+ import { mkdir, rename } from 'node:fs/promises';
2
+ import path from 'node:path';
2
3
  import { buildSessionReport } from '../session/session-store.js';
4
+ import { overwriteDomainRecord, readCognitionNotes, readDomainRecords, writeCognitionNote, } from '../storage/cognition-store.js';
3
5
  export async function analyzeCognitionQuality(workspace, maps) {
4
6
  const notes = await readCognitionNotes(workspace);
5
7
  const session = await buildSessionReport(workspace);
@@ -7,6 +9,7 @@ export async function analyzeCognitionQuality(workspace, maps) {
7
9
  .map((note) => analyzeNote(note, maps))
8
10
  .filter((change) => change.removedFileRefs.length > 0 ||
9
11
  change.removedSymbolRefs.length > 0);
12
+ const orphanedNoteCount = notes.filter((note) => note.referencesStatus === 'stale').length;
10
13
  return {
11
14
  noteCount: notes.length,
12
15
  mixedOrStaleCount: notes.filter((note) => ['mixed', 'stale', 'unresolved'].includes(note.referencesStatus)).length,
@@ -20,6 +23,7 @@ export async function analyzeCognitionQuality(workspace, maps) {
20
23
  sessionRepeatedReadCount: session.repeatedReadCount,
21
24
  sessionEstimatedReadTokens: session.estimatedReadTokens,
22
25
  sessionEstimatedRepeatedReadTokens: session.estimatedRepeatedReadTokens,
26
+ orphanedNoteCount,
23
27
  changes,
24
28
  };
25
29
  }
@@ -40,9 +44,18 @@ export async function repairCognition(workspace, maps, dryRun = false) {
40
44
  }
41
45
  }
42
46
  }
43
- if (!dryRun && changes.length > 0) {
44
- await repairDomainRecords(workspace, nextNotes, maps);
47
+ // Archive fully-orphaned notes (all refs dead) so they no longer appear in context
48
+ const orphanedNotes = nextNotes.filter((note) => note.referencesStatus === 'stale');
49
+ if (!dryRun && (changes.length > 0 || orphanedNotes.length > 0)) {
50
+ // Exclude fully-orphaned notes from domain records — they are being archived
51
+ await repairDomainRecords(workspace, nextNotes.filter((n) => n.referencesStatus !== 'stale'), maps);
52
+ }
53
+ if (!dryRun) {
54
+ for (const note of orphanedNotes) {
55
+ await archiveOrphanedNote(workspace, note);
56
+ }
45
57
  }
58
+ const orphanedNoteCount = orphanedNotes.length;
46
59
  return {
47
60
  noteCount: notes.length,
48
61
  mixedOrStaleCount: nextNotes.filter((note) => ['mixed', 'stale', 'unresolved'].includes(note.referencesStatus)).length,
@@ -56,6 +69,7 @@ export async function repairCognition(workspace, maps, dryRun = false) {
56
69
  sessionRepeatedReadCount: session.repeatedReadCount,
57
70
  sessionEstimatedReadTokens: session.estimatedReadTokens,
58
71
  sessionEstimatedRepeatedReadTokens: session.estimatedRepeatedReadTokens,
72
+ orphanedNoteCount,
59
73
  changes,
60
74
  };
61
75
  }
@@ -98,7 +112,8 @@ function countGeneratedScannedFiles(fileMap) {
98
112
  ].some((prefix) => file.path === prefix || file.path.startsWith(prefix))).length;
99
113
  }
100
114
  function countExpensiveFiles(fileMap) {
101
- return fileMap.files.filter((file) => (file.tokenEstimate ?? 0) >= 1000).length;
115
+ return fileMap.files.filter((file) => (file.tokenEstimate ?? 0) >= 1000)
116
+ .length;
102
117
  }
103
118
  function analyzeNote(note, maps) {
104
119
  const filePaths = new Set(maps.fileMap.files.map((file) => file.path));
@@ -161,11 +176,27 @@ function evaluateReferenceStatus(relatedFiles, relatedSymbols, maps) {
161
176
  return 'mixed';
162
177
  }
163
178
  function isNoisyFileRef(ref) {
164
- return !ref.includes('/') && /^[A-Z][A-Za-z0-9_-]*\.[A-Za-z0-9_-]+$/.test(ref);
179
+ return (!ref.includes('/') && /^[A-Z][A-Za-z0-9_-]*\.[A-Za-z0-9_-]+$/.test(ref));
165
180
  }
166
181
  function isNoisySymbolRef(ref) {
167
- return /^[a-z][A-Za-z0-9_$]*$/.test(ref);
182
+ // Only treat as noise if the ref is a short all-lowercase word (no camelCase, no _ or $).
183
+ // Preserve camelCase refs even when unresolved — the symbol may have been renamed.
184
+ if (/[A-Z_$]/.test(ref))
185
+ return false;
186
+ return ref.length <= 5;
168
187
  }
169
188
  function unique(items) {
170
189
  return [...new Set(items)];
171
190
  }
191
+ async function archiveOrphanedNote(workspace, note) {
192
+ const archivedDir = path.join(workspace.cognitionPath, 'archived');
193
+ await mkdir(archivedDir, { recursive: true });
194
+ const source = path.join(workspace.cognitionPath, `${note.id}.md`);
195
+ const target = path.join(archivedDir, `${note.id}.md`);
196
+ try {
197
+ await rename(source, target);
198
+ }
199
+ catch {
200
+ // source file may already be missing — ignore
201
+ }
202
+ }
@@ -11,7 +11,11 @@ export async function updateCognition(workspace, currentMaps, dryRun = false) {
11
11
  const raw = await readFile(inboxPath, 'utf8');
12
12
  const parsed = parseMarkdownNote(raw);
13
13
  const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
14
- const id = `${timestamp}-${slugify(parsed.title) || path.basename(inboxPath, '.md')}`;
14
+ // Always include the inbox basename as a per-note unique suffix so two notes with
15
+ // the same title processed in the same millisecond never receive the same ID.
16
+ const base = path.basename(inboxPath, '.md');
17
+ const slug = slugify(parsed.title);
18
+ const id = slug ? `${timestamp}-${slug}-${base}` : `${timestamp}-${base}`;
15
19
  const archivedPath = path.join(workspace.processedInteractionsPath, `${timestamp}-${path.basename(inboxPath)}`);
16
20
  const note = {
17
21
  ...parsed,
@@ -39,6 +43,11 @@ export async function updateCognition(workspace, currentMaps, dryRun = false) {
39
43
  warnings.push(`${path.basename(inboxPath)}: ${error instanceof Error ? error.message : String(error)}`);
40
44
  }
41
45
  }
46
+ // Refresh reference statuses on all existing notes so that notes which
47
+ // became stale since the last scan reflect the current map state.
48
+ if (!dryRun) {
49
+ await refreshCognitionReferenceStatuses(workspace, currentMaps);
50
+ }
42
51
  return { processed, warnings };
43
52
  }
44
53
  export async function refreshCognitionReferenceStatuses(workspace, currentMaps) {
@@ -14,7 +14,7 @@ export function parseMarkdownNote(markdown) {
14
14
  tags: Array.isArray(frontmatter.tags) ? frontmatter.tags.map(String) : [],
15
15
  summary: sections.Summary,
16
16
  sections,
17
- relatedFiles: unique(extractMatches(combined, PATH_REF)),
17
+ relatedFiles: unique(extractMatches(stripCodeFences(combined), PATH_REF)),
18
18
  relatedSymbols: unique(extractSymbolRefs(sections)),
19
19
  warnings,
20
20
  };
@@ -70,3 +70,8 @@ function extractSymbolRefs(sections) {
70
70
  function unique(items) {
71
71
  return [...new Set(items)];
72
72
  }
73
+ function stripCodeFences(text) {
74
+ // Remove triple-backtick code blocks so paths inside code examples are not
75
+ // mistaken for real file references, which would create phantom stale refs.
76
+ return text.replace(/```[\s\S]*?```/g, '');
77
+ }
@@ -5,10 +5,16 @@ export async function queryContext(workspace, config, maps, query) {
5
5
  const cognition = await readCognitionNotes(workspace);
6
6
  const domains = await readDomainRecords(workspace);
7
7
  const max = config.maxContextItems;
8
- const relevantFiles = rankByFields(query, maps.fileMap.files, [
8
+ let relevantFiles = rankByFields(query, maps.fileMap.files, [
9
9
  { name: 'path', value: (file) => file.path },
10
10
  { name: 'language', value: (file) => file.language },
11
- ]).slice(0, max);
11
+ ])
12
+ .map((ranked) => ({
13
+ ...ranked,
14
+ score: ranked.score - Math.floor((ranked.item.tokenEstimate ?? 0) / 2000),
15
+ }))
16
+ .sort((a, b) => b.score - a.score)
17
+ .slice(0, max);
12
18
  const relevantSymbols = rankByFields(query, maps.symbolMap.symbols, [
13
19
  { name: 'name', value: (symbol) => symbol.name },
14
20
  { name: 'path', value: (symbol) => symbol.filePath },
@@ -28,6 +34,57 @@ export async function queryContext(workspace, config, maps, query) {
28
34
  { name: 'tags', value: (domain) => domain.tags },
29
35
  { name: 'path', value: (domain) => domain.pathHints },
30
36
  ]).slice(0, max);
37
+ // Inject files linked by matched cognition notes/domains that didn't score on name alone
38
+ const rankedFilePaths = new Set(relevantFiles.map((f) => f.item.path));
39
+ const cognitionLinkedMap = new Map();
40
+ for (const ranked of relevantCognition) {
41
+ for (const fp of ranked.item.relatedFiles) {
42
+ if (!rankedFilePaths.has(fp)) {
43
+ const reasons = cognitionLinkedMap.get(fp) ?? [];
44
+ reasons.push(`linked by cognition note "${ranked.item.title}"`);
45
+ cognitionLinkedMap.set(fp, reasons);
46
+ }
47
+ }
48
+ }
49
+ for (const ranked of matchedDomains) {
50
+ for (const fp of ranked.item.files) {
51
+ if (!rankedFilePaths.has(fp)) {
52
+ const reasons = cognitionLinkedMap.get(fp) ?? [];
53
+ reasons.push(`in domain "${ranked.item.name}"`);
54
+ cognitionLinkedMap.set(fp, reasons);
55
+ }
56
+ }
57
+ }
58
+ // Apply domainHints from config: inject paths for hints whose name matches the query
59
+ const queryTokens = new Set(query
60
+ .toLowerCase()
61
+ .split(/[^a-z0-9]+/)
62
+ .filter(Boolean));
63
+ for (const [hintName, hint] of Object.entries(config.domainHints)) {
64
+ const hintWords = hintName
65
+ .toLowerCase()
66
+ .split(/[^a-z0-9]+/)
67
+ .filter(Boolean);
68
+ if (!hintWords.some((w) => queryTokens.has(w)))
69
+ continue;
70
+ for (const fp of hint.paths ?? []) {
71
+ if (!rankedFilePaths.has(fp)) {
72
+ const reasons = cognitionLinkedMap.get(fp) ?? [];
73
+ reasons.push(`in configured domain hint "${hintName}"`);
74
+ cognitionLinkedMap.set(fp, reasons);
75
+ }
76
+ }
77
+ }
78
+ relevantFiles = [
79
+ ...relevantFiles,
80
+ ...maps.fileMap.files
81
+ .filter((f) => cognitionLinkedMap.has(f.path))
82
+ .map((f) => ({
83
+ item: f,
84
+ score: 1,
85
+ reasons: cognitionLinkedMap.get(f.path),
86
+ })),
87
+ ];
31
88
  const relatedIds = new Set([
32
89
  ...relevantFiles.map((file) => file.item.path),
33
90
  ...relevantSymbols.map((symbol) => symbol.item.id),
@@ -69,10 +126,12 @@ export async function queryContext(workspace, config, maps, query) {
69
126
  });
70
127
  const filePaths = new Set(maps.fileMap.files.map((f) => f.path));
71
128
  const symbolNames = new Set(maps.symbolMap.symbols.map((s) => s.name));
129
+ const matchedCognitionIds = new Set(relevantCognition.map((r) => r.item.id));
72
130
  const staleReferences = cognition
73
- .filter((note) => note.referencesStatus === 'stale' ||
74
- note.referencesStatus === 'unresolved' ||
75
- note.referencesStatus === 'mixed')
131
+ .filter((note) => matchedCognitionIds.has(note.id) &&
132
+ (note.referencesStatus === 'stale' ||
133
+ note.referencesStatus === 'unresolved' ||
134
+ note.referencesStatus === 'mixed'))
76
135
  .flatMap((note) => [
77
136
  ...note.relatedFiles
78
137
  .filter((f) => !filePaths.has(f))
@@ -98,9 +157,18 @@ export async function queryContext(workspace, config, maps, query) {
98
157
  // Remove files already in the matched set
99
158
  for (const p of matchedFilePaths)
100
159
  importedFilePaths.delete(p);
160
+ // Skip generic utility/barrel files with many exports — surface only focused modules
161
+ const exportCountByFile = new Map();
162
+ for (const s of maps.symbolMap.symbols) {
163
+ if (s.exported) {
164
+ exportCountByFile.set(s.filePath, (exportCountByFile.get(s.filePath) ?? 0) + 1);
165
+ }
166
+ }
167
+ const MAX_NEARBY_FILE_EXPORTS = 15;
168
+ const relevantImportedFilePaths = new Set([...importedFilePaths].filter((fp) => (exportCountByFile.get(fp) ?? 0) <= MAX_NEARBY_FILE_EXPORTS));
101
169
  const nearbySymbols = maps.symbolMap.symbols
102
170
  .filter((s) => s.exported &&
103
- importedFilePaths.has(s.filePath) &&
171
+ relevantImportedFilePaths.has(s.filePath) &&
104
172
  !matchedSymbolIds.has(s.id))
105
173
  .slice(0, max);
106
174
  const nearbySymbolExplanations = nearbySymbols.map((symbol) => ({
@@ -1,3 +1,4 @@
1
+ import { numberedWorkflow } from '../workflow-steps.js';
1
2
  export const claudeCodeAdapter = {
2
3
  name: 'claude-code',
3
4
  label: 'Claude Code',
@@ -11,18 +12,7 @@ export const claudeCodeAdapter = {
11
12
  path: '.claude/commands/kgraph.md',
12
13
  content: `Use KGraph persistent repo intelligence for the current request.
13
14
 
14
- 1. Infer the topic from the user's request.
15
- 2. {{KGRAPH_CONTEXT_POLICY}}
16
- 3. Use the returned files, symbols, relationships, and cognition before broad exploration.
17
- 4. Run \`kgraph doctor\` when setup, maps, inbox processing, or integrations look wrong. Run \`kgraph doctor --quality\` when context shows stale/noisy cognition references.
18
- 5. Track meaningful session activity 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\` when native hooks are unavailable.
19
- 6. 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.
20
-
21
- {{KGRAPH_CAPTURE_POLICY}}
22
-
23
- 7. 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.
24
- 8. Run \`kgraph visualize\` when the user wants to inspect the dependency graph — opens an interactive graph at http://localhost:4242 with PNG export.
25
- 9. Run \`kgraph history\` or \`kgraph history "<topic>"\` to review past cognition sessions with git author attribution.
15
+ ${numberedWorkflow('claude-code', { sessionQualifier: 'when native hooks are unavailable' })}
26
16
  `,
27
17
  },
28
18
  {
@@ -1,16 +1,10 @@
1
+ import { bulletWorkflow } from '../workflow-steps.js';
1
2
  export const clineAdapter = {
2
3
  name: 'cline',
3
4
  label: 'Cline',
4
5
  targetPath: '.clinerules/kgraph.md',
5
6
  instructions: `# KGraph Workflow
6
7
 
7
- - {{KGRAPH_CONTEXT_POLICY}}
8
- - Run \`kgraph doctor\` when setup, maps, inbox processing, or integrations look wrong. Run \`kgraph doctor --quality\` when context shows stale/noisy cognition references.
9
- - Track meaningful session activity with \`kgraph session start --agent cline\`, \`kgraph session read <path> --agent cline\`, \`kgraph session write <path> --agent cline\`, and \`kgraph session end --agent cline\`.
10
- - 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.
11
- {{KGRAPH_CAPTURE_POLICY}}
12
- - 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.
13
- - Run \`kgraph visualize\` to open the interactive dependency graph at http://localhost:4242 with PNG export.
14
- - Run \`kgraph history\` or \`kgraph history "<topic>"\` to review past cognition sessions with git author attribution.
8
+ ${bulletWorkflow('cline')}
15
9
  `,
16
10
  };
@@ -1,3 +1,4 @@
1
+ import { numberedWorkflow } from '../workflow-steps.js';
1
2
  export const codexAdapter = {
2
3
  name: 'codex',
3
4
  label: 'Codex',
@@ -18,23 +19,12 @@ description: Use KGraph persistent repo intelligence according to the configured
18
19
 
19
20
  Workflow:
20
21
 
21
- 1. Infer the current topic from the user request.
22
- 2. {{KGRAPH_CONTEXT_POLICY}}
23
- 3. Use KGraph's returned files, symbols, relationships, and cognition as navigation hints.
24
- 4. Run \`kgraph doctor\` when setup, maps, inbox processing, or integrations look wrong. Run \`kgraph doctor --quality\` when context shows stale/noisy cognition references.
25
- 5. Track meaningful session activity with \`kgraph session start --agent codex\`, \`kgraph session read <path> --agent codex\`, \`kgraph session write <path> --agent codex\`, and \`kgraph session end --agent codex\`.
26
- 6. 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.
27
-
28
- {{KGRAPH_CAPTURE_POLICY}}
29
-
30
- 7. 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.
31
- 8. Run \`kgraph visualize\` when the user wants to inspect the dependency graph — opens an interactive graph at http://localhost:4242 with PNG export.
32
- 9. Run \`kgraph history\` or \`kgraph history "<topic>"\` to review past cognition sessions with git author attribution.
22
+ ${numberedWorkflow('codex')}
33
23
  `,
34
24
  },
35
25
  ],
36
26
  obsoleteCommandFiles: [
37
- '.agents/skills/kgraph-update',
38
- '.agents/skills/kgraph-scan',
27
+ '.agents/skills/kgraph-scan/SKILL.md',
28
+ '.agents/skills/kgraph-update/SKILL.md',
39
29
  ],
40
30
  };
@@ -1,19 +1,33 @@
1
+ import { numberedWorkflow } from '../workflow-steps.js';
1
2
  export const copilotAdapter = {
2
3
  name: 'copilot',
3
4
  label: 'GitHub Copilot',
4
5
  targetPath: '.github/copilot-instructions.md',
5
6
  instructions: `## KGraph Workflow
6
7
 
7
- 1. {{KGRAPH_CONTEXT_POLICY}}
8
- 2. Run \`kgraph doctor\` when setup, maps, inbox processing, or integrations look wrong. Run \`kgraph doctor --quality\` when context shows stale/noisy cognition references.
9
- 3. Track meaningful session activity with \`kgraph session start --agent copilot\`, \`kgraph session read <path> --agent copilot\`, \`kgraph session write <path> --agent copilot\`, and \`kgraph session end --agent copilot\`.
10
- 4. 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.
8
+ ${numberedWorkflow('copilot')}
9
+ `,
10
+ commandFiles: [
11
+ {
12
+ path: '.github/agents/kgraph.agent.md',
13
+ content: `---
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.
16
+ tools:
17
+ - run_in_terminal
18
+ - read_file
19
+ - file_search
20
+ - grep_search
21
+ - semantic_search
22
+ ---
11
23
 
12
- {{KGRAPH_CAPTURE_POLICY}}
24
+ ## KGraph Agent
25
+
26
+ You are a KGraph-powered agent. Before exploring the repository freely, always:
13
27
 
14
- 5. 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.
28
+ ${numberedWorkflow('copilot')}
15
29
  `,
16
- commandFiles: [
30
+ },
17
31
  {
18
32
  path: '.github/prompts/kgraph-doctor.prompt.md',
19
33
  content: `---
@@ -112,5 +126,8 @@ Run \`kgraph history\` or \`kgraph history "$ARGUMENTS"\` to display processed c
112
126
  `,
113
127
  },
114
128
  ],
115
- obsoleteCommandFiles: ['.github/prompts/kgraph.prompt.md'],
129
+ obsoleteCommandFiles: [
130
+ '.github/prompts/kgraph.prompt.md',
131
+ '.github/kgraph.agent.md',
132
+ ],
116
133
  };
@@ -1,3 +1,4 @@
1
+ import { bulletWorkflow } from '../workflow-steps.js';
1
2
  export const cursorAdapter = {
2
3
  name: 'cursor',
3
4
  label: 'Cursor',
@@ -9,14 +10,7 @@ alwaysApply: true
9
10
 
10
11
  ## KGraph Workflow
11
12
 
12
- - {{KGRAPH_CONTEXT_POLICY}}
13
- - Run \`kgraph doctor\` when setup, maps, inbox processing, or integrations look wrong. Run \`kgraph doctor --quality\` when context shows stale/noisy cognition references.
14
- - Track meaningful session activity with \`kgraph session start --agent cursor\`, \`kgraph session read <path> --agent cursor\`, \`kgraph session write <path> --agent cursor\`, and \`kgraph session end --agent cursor\`.
15
- - 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.
16
- {{KGRAPH_CAPTURE_POLICY}}
17
- - 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.
18
- - Run \`kgraph visualize\` to open the interactive dependency graph at http://localhost:4242 with PNG export.
19
- - Run \`kgraph history\` or \`kgraph history "<topic>"\` to review past cognition sessions with git author attribution.
13
+ ${bulletWorkflow('cursor')}
20
14
  `,
21
15
  obsoleteCommandFiles: ['.cursor/rules/kgraph-commands.mdc'],
22
16
  };
@@ -1,16 +1,10 @@
1
+ import { bulletWorkflow } from '../workflow-steps.js';
1
2
  export const geminiAdapter = {
2
3
  name: 'gemini',
3
4
  label: 'Gemini CLI',
4
5
  targetPath: 'GEMINI.md',
5
6
  instructions: `## KGraph Workflow
6
7
 
7
- - {{KGRAPH_CONTEXT_POLICY}}
8
- - Run \`kgraph doctor\` when setup, maps, inbox processing, or integrations look wrong. Run \`kgraph doctor --quality\` when context shows stale/noisy cognition references.
9
- - Track meaningful session activity with \`kgraph session start --agent gemini\`, \`kgraph session read <path> --agent gemini\`, \`kgraph session write <path> --agent gemini\`, and \`kgraph session end --agent gemini\`.
10
- - 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.
11
- {{KGRAPH_CAPTURE_POLICY}}
12
- - 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.
13
- - Run \`kgraph visualize\` to open the interactive dependency graph at http://localhost:4242 with PNG export.
14
- - Run \`kgraph history\` or \`kgraph history "<topic>"\` to review past cognition sessions with git author attribution.
8
+ ${bulletWorkflow('gemini')}
15
9
  `,
16
10
  };
@@ -1,16 +1,10 @@
1
+ import { bulletWorkflow } from '../workflow-steps.js';
1
2
  export const windsurfAdapter = {
2
3
  name: 'windsurf',
3
4
  label: 'Windsurf',
4
5
  targetPath: '.windsurf/rules/kgraph.md',
5
6
  instructions: `# KGraph Workflow
6
7
 
7
- - {{KGRAPH_CONTEXT_POLICY}}
8
- - Run \`kgraph doctor\` when setup, maps, inbox processing, or integrations look wrong. Run \`kgraph doctor --quality\` when context shows stale/noisy cognition references.
9
- - Track meaningful session activity with \`kgraph session start --agent windsurf\`, \`kgraph session read <path> --agent windsurf\`, \`kgraph session write <path> --agent windsurf\`, and \`kgraph session end --agent windsurf\`.
10
- - 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.
11
- {{KGRAPH_CAPTURE_POLICY}}
12
- - 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.
13
- - Run \`kgraph visualize\` to open the interactive dependency graph at http://localhost:4242 with PNG export.
14
- - Run \`kgraph history\` or \`kgraph history "<topic>"\` to review past cognition sessions with git author attribution.
8
+ ${bulletWorkflow('windsurf')}
15
9
  `,
16
10
  };
@@ -15,7 +15,7 @@ export async function listIntegrations(workspace) {
15
15
  })));
16
16
  return statuses.sort((left, right) => left.name.localeCompare(right.name));
17
17
  }
18
- export async function addIntegrations(workspace, names, mode = 'always') {
18
+ export async function addIntegrations(workspace, names, mode = 'smart') {
19
19
  const config = await loadConfig(workspace);
20
20
  const byName = new Map(config.integrations.map((integration) => [integration.name, integration]));
21
21
  const changed = [];
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Shared KGraph workflow step strings used across all AI tool adapters.
3
+ * Update here once instead of in each adapter file.
4
+ */
5
+ export interface WorkflowOptions {
6
+ /** Qualifier appended to the session step, e.g. "when native hooks are unavailable" */
7
+ sessionQualifier?: string;
8
+ }
9
+ /**
10
+ * Returns the 9-step numbered workflow for skill/agent/command files.
11
+ * Used by: copilot (agent file), codex (skill file), claude-code (command file).
12
+ */
13
+ export declare function numberedWorkflow(agentName: string, options?: WorkflowOptions): string;
14
+ /**
15
+ * Returns the bullet-list workflow for rules files.
16
+ * Used by: cursor, cline, windsurf, gemini.
17
+ */
18
+ export declare function bulletWorkflow(agentName: string, options?: WorkflowOptions): string;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Shared KGraph workflow step strings used across all AI tool adapters.
3
+ * Update here once instead of in each adapter file.
4
+ */
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
+ 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
+ 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 HISTORY_STEP = `Run \`kgraph history\` or \`kgraph history "<topic>"\` to review past cognition sessions with git author attribution.`;
9
+ 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
+ return qualifier ? `${base} ${qualifier}.` : `${base}.`;
12
+ }
13
+ /**
14
+ * Returns the 9-step numbered workflow for skill/agent/command files.
15
+ * Used by: copilot (agent file), codex (skill file), claude-code (command file).
16
+ */
17
+ export function numberedWorkflow(agentName, options = {}) {
18
+ return `1. Infer the topic from the user's request.
19
+ 2. {{KGRAPH_CONTEXT_POLICY}}
20
+ 3. Use the returned files, symbols, relationships, and cognition before broad exploration.
21
+ 4. ${DOCTOR_STEP}
22
+ 5. ${sessionStep(agentName, options.sessionQualifier)}
23
+ 6. ${IMPACT_STEP}
24
+
25
+ {{KGRAPH_CAPTURE_POLICY}}
26
+
27
+ 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}`;
30
+ }
31
+ /**
32
+ * Returns the bullet-list workflow for rules files.
33
+ * Used by: cursor, cline, windsurf, gemini.
34
+ */
35
+ export function bulletWorkflow(agentName, options = {}) {
36
+ return `- {{KGRAPH_CONTEXT_POLICY}}
37
+ - ${DOCTOR_STEP}
38
+ - ${sessionStep(agentName, options.sessionQualifier)}
39
+ - ${IMPACT_STEP}
40
+ {{KGRAPH_CAPTURE_POLICY}}
41
+ - ${REPAIR_STEP}
42
+ - Run \`kgraph visualize\` to open the interactive dependency graph at http://localhost:4242 with PNG export.
43
+ - ${HISTORY_STEP}`;
44
+ }
@@ -1,8 +1,8 @@
1
1
  import { mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
+ import { KGraphError } from '../cli/errors.js';
3
4
  import { getIntegrationAdapter } from '../integrations/integration-registry.js';
4
5
  import { pathExists } from '../storage/kgraph-paths.js';
5
- import { KGraphError } from '../cli/errors.js';
6
6
  import { estimateTokens } from './token-estimator.js';
7
7
  const EMPTY_STATE = {
8
8
  active: {},
@@ -32,6 +32,12 @@ 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
+ // Auto-close any open session for this agent before starting a new one so
36
+ // the ledger entry is never silently lost on repeated start calls.
37
+ if (input.type === 'start' && state.active[input.agent]) {
38
+ await appendLedgerEntry(workspace, summarizeAgentSession(input.agent, state, now));
39
+ delete state.active[input.agent];
40
+ }
35
41
  const active = state.active[input.agent] ?? {
36
42
  agent: input.agent,
37
43
  sessionId: `${input.agent}-${now.replace(/[:.]/g, '-')}`,
@@ -146,7 +152,11 @@ function topRepeatedReads(events) {
146
152
  for (const event of events) {
147
153
  if (!event.path)
148
154
  continue;
149
- const current = byPath.get(event.path) ?? { path: event.path, count: 0, estimatedTokens: 0 };
155
+ const current = byPath.get(event.path) ?? {
156
+ path: event.path,
157
+ count: 0,
158
+ estimatedTokens: 0,
159
+ };
150
160
  current.count += 1;
151
161
  current.estimatedTokens += event.tokenEstimate ?? 0;
152
162
  byPath.set(event.path, current);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kentwynn/kgraph",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "Persistent repo intelligence for AI coding assistants.",
5
5
  "type": "module",
6
6
  "bin": {