@kentwynn/kgraph 0.2.12 → 0.2.14

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
@@ -308,13 +308,13 @@ KGraph integrations are local files. They do not start background agents, call A
308
308
 
309
309
  ```bash
310
310
  kgraph integrate add codex copilot cursor claude-code gemini windsurf cline
311
- kgraph integrate add copilot --mode always
311
+ kgraph integrate add copilot --mode smart
312
312
  kgraph integrate set copilot --mode manual
313
313
  kgraph integrate list
314
314
  kgraph integrate remove cursor
315
315
  ```
316
316
 
317
- New integrations default to `smart` mode. Use `--mode always` to force KGraph on every chat, or `--mode manual` to run only when explicitly asked.
317
+ New integrations default to `always` mode, so every chat in the repository starts with `kgraph "<topic>"`. Use `--mode smart` to run KGraph only for repo-specific work, or `--mode manual` to run only when explicitly asked.
318
318
 
319
319
  | Mode | Behavior |
320
320
  | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -179,6 +179,7 @@ export function printQualityReport(report) {
179
179
  console.log(`Duplicate compatibility note titles: ${report.duplicateTitleCount}`);
180
180
  console.log(`Generated files scanned: ${report.generatedFileScanCount}`);
181
181
  console.log(`Expensive files: ${report.expensiveFileCount}`);
182
+ console.log(`High-confidence atoms without evidence: ${report.highConfidenceMissingEvidenceCount}`);
182
183
  console.log(`Session repeated reads: ${report.sessionRepeatedReadCount}`);
183
184
  console.log(`Session estimated read tokens: ${report.sessionEstimatedReadTokens}`);
184
185
  console.log(`Session repeated-read tokens: ${report.sessionEstimatedRepeatedReadTokens}`);
@@ -214,6 +215,9 @@ function summarizeQualityFindings(report) {
214
215
  if (report.generatedFileScanCount > 0) {
215
216
  findings.push(`${report.generatedFileScanCount} generated/integration file(s) scanned; update excludes`);
216
217
  }
218
+ if (report.highConfidenceMissingEvidenceCount > 0) {
219
+ findings.push(`${report.highConfidenceMissingEvidenceCount} high-confidence atom(s) without evidence; add file/symbol refs or supersede`);
220
+ }
217
221
  return findings;
218
222
  }
219
223
  function summarizeCoverageNotes(report) {
@@ -15,7 +15,7 @@ export function registerInitCommand(program) {
15
15
  .description('Initialize a .kgraph workspace')
16
16
  .option('--integration <name>', 'Configure an AI tool integration', collectOption, [])
17
17
  .option('--integrations <names>', 'Configure comma-separated AI tool integrations')
18
- .option('--mode <mode>', 'Integration mode: always, smart, manual, or off', 'smart')
18
+ .option('--mode <mode>', 'Integration mode: always, smart, manual, or off', 'always')
19
19
  .action((options) => runCommand(async () => {
20
20
  const workspace = await ensureWorkspace(process.cwd());
21
21
  await ensureKnowledgeStore(workspace);
@@ -55,7 +55,7 @@ export function registerInitCommand(program) {
55
55
  })) {
56
56
  const selected = await promptForInitIntegrations(recommendedIntegrations);
57
57
  if (selected.length > 0) {
58
- const changed = await addIntegrations(workspace, selected, 'smart');
58
+ const changed = await addIntegrations(workspace, selected, 'always');
59
59
  console.log(`Configured integrations: ${changed.map((item) => `${item.name}:${item.mode}`).join(', ')}`);
60
60
  config = await loadConfig(workspace);
61
61
  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', 'smart')
28
+ .option('--mode <mode>', 'always, smart, manual, or off', 'always')
29
29
  .action((names, options) => runCommand(async () => {
30
30
  const workspace = await assertWorkspace(process.cwd());
31
31
  const normalized = normalizeIntegrationNames(names);
@@ -1,8 +1,13 @@
1
1
  import { rm } from 'node:fs/promises';
2
+ import path from 'node:path';
2
3
  import { loadConfig } from '../../config/config.js';
3
4
  import { removeIntegrations } from '../../integrations/integration-store.js';
4
5
  import { pathExists, resolveWorkspace } from '../../storage/kgraph-paths.js';
5
6
  import { runCommand } from '../errors.js';
7
+ const LEGACY_GENERATED_FILES = [
8
+ '.github/agents/kgraph.agent.md',
9
+ '.github/kgraph.agent.md',
10
+ ];
6
11
  export function registerUninstallCommand(program) {
7
12
  program
8
13
  .command('uninstall')
@@ -28,6 +33,7 @@ export function registerUninstallCommand(program) {
28
33
  !options.keepIntegrations &&
29
34
  configuredIntegrations.length > 0) {
30
35
  await removeIntegrations(workspace, configuredIntegrations);
36
+ await removeLegacyGeneratedFiles(workspace.rootPath);
31
37
  }
32
38
  if (initialized) {
33
39
  await rm(workspace.kgraphPath, { recursive: true, force: true });
@@ -37,6 +43,9 @@ export function registerUninstallCommand(program) {
37
43
  console.log('Run `kgraph init` to set up this repository again.');
38
44
  }));
39
45
  }
46
+ async function removeLegacyGeneratedFiles(rootPath) {
47
+ await Promise.all(LEGACY_GENERATED_FILES.map((filePath) => rm(path.join(rootPath, filePath), { force: true })));
48
+ }
40
49
  function printUninstallPreview(input) {
41
50
  console.log('KGraph Uninstall Preview');
42
51
  console.log('');
package/dist/cli/help.js CHANGED
@@ -50,8 +50,8 @@ export function renderRootHelp(useColor = supportsColor()) {
50
50
  '',
51
51
  theme.bold('Integrations'),
52
52
  command('integrate list', 'Show configured AI tool integrations'),
53
- command('integrate add gemini windsurf cline', 'Write KGraph instructions using smart mode by default'),
54
- command('integrate add copilot --mode always', 'Every Copilot chat starts with kgraph "<topic>"'),
53
+ command('integrate add gemini windsurf cline', 'Write KGraph instructions using always mode by default'),
54
+ command('integrate add copilot --mode smart', 'Run KGraph for repo-specific Copilot work only'),
55
55
  command('integrate set copilot --mode manual', 'Only run KGraph when explicitly requested'),
56
56
  command('integrate remove cursor', 'Remove KGraph-managed instruction blocks'),
57
57
  command('--mode smart|always|manual|off', 'Control automatic KGraph involvement per integration'),
@@ -23,6 +23,7 @@ export interface CognitionQualityReport {
23
23
  duplicateTitleCount: number;
24
24
  generatedFileScanCount: number;
25
25
  expensiveFileCount: number;
26
+ highConfidenceMissingEvidenceCount: number;
26
27
  sessionRepeatedReadCount: number;
27
28
  sessionEstimatedReadTokens: number;
28
29
  sessionEstimatedRepeatedReadTokens: number;
@@ -29,6 +29,7 @@ export async function analyzeCognitionQuality(workspace, maps) {
29
29
  duplicateTitleCount: countDuplicateAtomTopics(activeAtoms),
30
30
  generatedFileScanCount: countGeneratedScannedFiles(maps.fileMap),
31
31
  expensiveFileCount: countExpensiveFiles(maps.fileMap),
32
+ highConfidenceMissingEvidenceCount: countHighConfidenceMissingEvidence(activeAtoms),
32
33
  sessionRepeatedReadCount: session.repeatedReadCount,
33
34
  sessionEstimatedReadTokens: session.estimatedReadTokens,
34
35
  sessionEstimatedRepeatedReadTokens: session.estimatedRepeatedReadTokens,
@@ -36,6 +37,9 @@ export async function analyzeCognitionQuality(workspace, maps) {
36
37
  changes,
37
38
  };
38
39
  }
40
+ function countHighConfidenceMissingEvidence(atoms) {
41
+ return atoms.filter((atom) => atom.confidence === 'high' && atom.evidenceRefs.length === 0).length;
42
+ }
39
43
  export async function repairCognition(workspace, maps, dryRun = false) {
40
44
  const refreshed = await refreshKnowledgeAtomStatuses(workspace, { fileMap: maps.fileMap, symbolMap: maps.symbolMap }, dryRun);
41
45
  const atoms = refreshed.atoms;
@@ -135,6 +139,7 @@ export async function repairCognition(workspace, maps, dryRun = false) {
135
139
  duplicateTitleCount: countDuplicateTitles(nextNotes),
136
140
  generatedFileScanCount: countGeneratedScannedFiles(maps.fileMap),
137
141
  expensiveFileCount: countExpensiveFiles(maps.fileMap),
142
+ highConfidenceMissingEvidenceCount: countHighConfidenceMissingEvidence(atoms),
138
143
  sessionRepeatedReadCount: session.repeatedReadCount,
139
144
  sessionEstimatedReadTokens: session.estimatedReadTokens,
140
145
  sessionEstimatedRepeatedReadTokens: session.estimatedRepeatedReadTokens,
@@ -2,6 +2,7 @@ import { readMaps } from '../storage/map-store.js';
2
2
  import { slugify, writeCognitionNote, writeDomainRecord } from '../storage/cognition-store.js';
3
3
  import { KGraphError } from '../cli/errors.js';
4
4
  import { createKnowledgeAtom } from '../knowledge/atom-store.js';
5
+ import { getWorkingTreeChanges } from '../scanner/git-utils.js';
5
6
  import { evaluateReferenceStatus } from './cognition-updater.js';
6
7
  import { readSessionState } from '../session/session-store.js';
7
8
  export async function concludeTopic(workspace, input) {
@@ -10,6 +11,10 @@ export async function concludeTopic(workspace, input) {
10
11
  const timestamp = now.replace(/[:.]/g, '-');
11
12
  const title = input.topic.trim();
12
13
  const summary = normalizeBody(input.body) ?? title;
14
+ const relatedFiles = input.relatedFiles && input.relatedFiles.length > 0
15
+ ? input.relatedFiles
16
+ : await inferChangedFiles(workspace, maps);
17
+ const relatedSymbols = input.relatedSymbols ?? [];
13
18
  const note = {
14
19
  title,
15
20
  kind: input.kind ?? 'summary',
@@ -19,18 +24,18 @@ export async function concludeTopic(workspace, input) {
19
24
  summary,
20
25
  sections: {
21
26
  Summary: summary,
22
- ...(input.relatedFiles?.length ? { 'Related Files': input.relatedFiles.map((file) => `- ${file}`).join('\n') } : {}),
23
- ...(input.relatedSymbols?.length ? { 'Key Symbols': input.relatedSymbols.map((symbol) => `- \`${symbol}\``).join('\n') } : {}),
27
+ ...(relatedFiles.length ? { 'Related Files': relatedFiles.map((file) => `- ${file}`).join('\n') } : {}),
28
+ ...(relatedSymbols.length ? { 'Key Symbols': relatedSymbols.map((symbol) => `- \`${symbol}\``).join('\n') } : {}),
24
29
  },
25
- relatedFiles: input.relatedFiles ?? [],
26
- relatedSymbols: input.relatedSymbols ?? [],
30
+ relatedFiles,
31
+ relatedSymbols,
27
32
  warnings: [],
28
33
  id: `${timestamp}-${slugify(title) || 'conclusion'}`,
29
34
  sourceInboxPath: '',
30
35
  processedPath: `.kgraph/cognition/${timestamp}-${slugify(title) || 'conclusion'}.md`,
31
36
  createdAt: now,
32
37
  source: input.source,
33
- referencesStatus: evaluateReferenceStatus(input.relatedFiles ?? [], input.relatedSymbols ?? [], { files: maps.fileMap.files, symbols: maps.symbolMap.symbols }),
38
+ referencesStatus: evaluateReferenceStatus(relatedFiles, relatedSymbols, { files: maps.fileMap.files, symbols: maps.symbolMap.symbols }),
34
39
  };
35
40
  await writeCognitionNote(workspace, note);
36
41
  await writeDomainRecord(workspace, toDomainRecord(note, {
@@ -43,8 +48,8 @@ export async function concludeTopic(workspace, input) {
43
48
  claim: note.summary ?? note.title,
44
49
  summary: note.summary,
45
50
  confidence: note.confidence,
46
- files: note.relatedFiles,
47
- symbols: note.relatedSymbols,
51
+ files: relatedFiles,
52
+ symbols: relatedSymbols,
48
53
  domains: note.domain ? [note.domain] : [],
49
54
  sourceCommand: input.source === 'session-conclude'
50
55
  ? 'session-conclude'
@@ -58,6 +63,13 @@ export async function concludeTopic(workspace, input) {
58
63
  }, maps);
59
64
  return note;
60
65
  }
66
+ async function inferChangedFiles(workspace, maps) {
67
+ const changed = await getWorkingTreeChanges(workspace.rootPath);
68
+ if (changed.length === 0)
69
+ return [];
70
+ const currentFiles = new Set(maps.fileMap.files.map((file) => file.path));
71
+ return changed.filter((file) => currentFiles.has(file));
72
+ }
61
73
  export async function concludeActiveSession(workspace, agent, input) {
62
74
  return concludeTopic(workspace, await buildActiveSessionConclusion(workspace, agent, input));
63
75
  }
@@ -157,7 +157,7 @@ function normalizeIntegrations(value) {
157
157
  return integrations;
158
158
  }
159
159
  function normalizeIntegrationMode(value) {
160
- return value === 'always' || value === 'manual' || value === 'off'
160
+ return value === 'smart' || value === 'manual' || value === 'off'
161
161
  ? value
162
- : 'smart';
162
+ : 'always';
163
163
  }
@@ -48,10 +48,11 @@ export function buildContextPack(response, budget) {
48
48
  data: change,
49
49
  })),
50
50
  ];
51
+ const orderedCandidates = candidates.sort(comparePackCandidates);
51
52
  const items = [];
52
53
  const omitted = [];
53
54
  let usedTokens = 0;
54
- for (const candidate of candidates) {
55
+ for (const candidate of orderedCandidates) {
55
56
  if (usedTokens + candidate.tokenEstimate <= budget) {
56
57
  items.push(candidate);
57
58
  usedTokens += candidate.tokenEstimate;
@@ -69,3 +70,29 @@ export function buildContextPack(response, budget) {
69
70
  warnings: response.warnings,
70
71
  };
71
72
  }
73
+ function comparePackCandidates(left, right) {
74
+ return packPriority(right) - packPriority(left);
75
+ }
76
+ function packPriority(item) {
77
+ let score = 0;
78
+ if (item.kind === 'atom')
79
+ score += 40;
80
+ if (item.kind === 'git-change')
81
+ score += 35;
82
+ if (item.kind === 'symbol')
83
+ score += 25;
84
+ if (item.kind === 'file')
85
+ score += 15;
86
+ if (item.kind === 'relationship')
87
+ score += 5;
88
+ if (item.reasons.some((reason) => reason.includes('matched atom')))
89
+ score += 30;
90
+ if (item.reasons.some((reason) => reason.includes('current git change')))
91
+ score += 25;
92
+ if (item.reasons.some((reason) => reason.includes('specific query token')))
93
+ score += 10;
94
+ if (item.reasons.some((reason) => reason.includes('generic path-only match penalty')))
95
+ score -= 20;
96
+ score -= Math.floor(item.tokenEstimate / 2000);
97
+ return score;
98
+ }
@@ -3,6 +3,7 @@ import { readDomainRecords } from '../storage/cognition-store.js';
3
3
  import { readSessionState } from '../session/session-store.js';
4
4
  import { atomToCognitionNote, refreshKnowledgeAtomStatuses, } from '../knowledge/atom-store.js';
5
5
  import { rankByFields } from './ranking.js';
6
+ import { tokenize } from './ranking.js';
6
7
  export async function queryContext(workspace, config, maps, query) {
7
8
  const refreshedAtoms = await refreshKnowledgeAtomStatuses(workspace, {
8
9
  fileMap: maps.fileMap,
@@ -18,27 +19,43 @@ export async function queryContext(workspace, config, maps, query) {
18
19
  .map((event) => event.path)
19
20
  .filter((path) => Boolean(path)));
20
21
  const max = config.maxContextItems;
21
- let relevantFiles = rankByFields(query, maps.fileMap.files, [
22
- { name: 'path', value: (file) => file.path },
23
- { name: 'language', value: (file) => file.language },
24
- ])
25
- .map((ranked) => ({
26
- ...ranked,
27
- score: ranked.score -
28
- Math.floor((ranked.item.tokenEstimate ?? 0) / 2000) +
29
- (sessionTouchedPaths.has(ranked.item.path) ? 3 : 0),
30
- reasons: sessionTouchedPaths.has(ranked.item.path)
31
- ? [...ranked.reasons, 'touched in current session']
32
- : ranked.reasons,
33
- }))
34
- .sort((a, b) => b.score - a.score)
35
- .slice(0, max);
36
- const relevantSymbols = rankByFields(query, maps.symbolMap.symbols, [
37
- { name: 'name', value: (symbol) => symbol.name },
38
- { name: 'path', value: (symbol) => symbol.filePath },
39
- { name: 'kind', value: (symbol) => symbol.kind },
40
- { name: 'parent', value: (symbol) => symbol.parentName },
41
- ]).slice(0, max);
22
+ // Collect git changes before file ranking so dirty files can influence ranking,
23
+ // not just appear later as a low-token pack item.
24
+ const knownFilePaths = new Set(maps.fileMap.files.map((f) => f.path));
25
+ const gitChanges = [];
26
+ if (await isGitRepo(workspace.rootPath)) {
27
+ const workingTreeChanges = await getWorkingTreeChangesDetailed(workspace.rootPath);
28
+ for (const change of workingTreeChanges) {
29
+ if (!knownFilePaths.has(change.path))
30
+ continue;
31
+ const status = change.staged && !change.unstaged
32
+ ? 'staged'
33
+ : change.unstaged && !change.staged
34
+ ? 'unstaged'
35
+ : 'staged'; // both staged and unstaged -> report as staged
36
+ gitChanges.push({
37
+ path: change.path,
38
+ status,
39
+ reason: change.staged && change.unstaged
40
+ ? 'partially staged'
41
+ : status === 'staged'
42
+ ? 'staged change'
43
+ : 'unstaged change',
44
+ });
45
+ }
46
+ const committedPaths = new Set(gitChanges.map((c) => c.path));
47
+ const recentCommitted = await getRecentlyCommittedFiles(workspace.rootPath);
48
+ for (const filePath of recentCommitted) {
49
+ if (!knownFilePaths.has(filePath) || committedPaths.has(filePath))
50
+ continue;
51
+ gitChanges.push({
52
+ path: filePath,
53
+ status: 'recent-commit',
54
+ reason: 'changed in recent commits',
55
+ });
56
+ }
57
+ }
58
+ const gitChangedPaths = new Set(gitChanges.map((change) => change.path));
42
59
  const relevantCognition = rankByFields(query, atoms.filter((atom) => atom.status !== 'archived'), [
43
60
  { name: 'topic', value: (atom) => atom.topic },
44
61
  { name: 'claim', value: (atom) => atom.claim },
@@ -58,11 +75,38 @@ export async function queryContext(workspace, config, maps, query) {
58
75
  }))
59
76
  .sort((a, b) => b.score - a.score)
60
77
  .slice(0, max);
78
+ const atomLinkedFiles = new Map();
79
+ for (const ranked of relevantCognition) {
80
+ for (const fp of ranked.item.relatedFiles) {
81
+ atomLinkedFiles.set(fp, [
82
+ ...(atomLinkedFiles.get(fp) ?? []),
83
+ `referenced by matched atom "${ranked.item.title}"`,
84
+ ]);
85
+ }
86
+ }
61
87
  const matchedDomains = rankByFields(query, domains, [
62
88
  { name: 'name', value: (domain) => domain.name },
63
89
  { name: 'tags', value: (domain) => domain.tags },
64
90
  { name: 'path', value: (domain) => domain.pathHints },
65
91
  ]).slice(0, max);
92
+ let relevantFiles = rankByFields(query, maps.fileMap.files, [
93
+ { name: 'path', value: (file) => file.path },
94
+ { name: 'language', value: (file) => file.language },
95
+ ])
96
+ .map((ranked) => applyFileRankAdjustments(ranked, {
97
+ query,
98
+ atomLinkedFiles,
99
+ gitChangedPaths,
100
+ sessionTouchedPaths,
101
+ }))
102
+ .sort((a, b) => b.score - a.score)
103
+ .slice(0, max);
104
+ const relevantSymbols = rankByFields(query, maps.symbolMap.symbols, [
105
+ { name: 'name', value: (symbol) => symbol.name },
106
+ { name: 'path', value: (symbol) => symbol.filePath },
107
+ { name: 'kind', value: (symbol) => symbol.kind },
108
+ { name: 'parent', value: (symbol) => symbol.parentName },
109
+ ]).slice(0, max);
66
110
  // Inject files linked by matched cognition notes/domains that didn't score on name alone
67
111
  const rankedFilePaths = new Set(relevantFiles.map((f) => f.item.path));
68
112
  const cognitionLinkedMap = new Map();
@@ -110,10 +154,10 @@ export async function queryContext(workspace, config, maps, query) {
110
154
  .filter((f) => cognitionLinkedMap.has(f.path))
111
155
  .map((f) => ({
112
156
  item: f,
113
- score: 1,
157
+ score: 12,
114
158
  reasons: cognitionLinkedMap.get(f.path),
115
159
  })),
116
- ];
160
+ ].sort((a, b) => b.score - a.score);
117
161
  const relatedIds = new Set([
118
162
  ...relevantFiles.map((file) => file.item.path),
119
163
  ...relevantSymbols.map((symbol) => symbol.item.id),
@@ -207,41 +251,6 @@ export async function queryContext(workspace, config, maps, query) {
207
251
  ...dependenciesForImportedSymbol(symbol, maps.dependencyMap.dependencies),
208
252
  ],
209
253
  }));
210
- // Collect git changes: working-tree and recently committed files known to KGraph
211
- const knownFilePaths = new Set(maps.fileMap.files.map((f) => f.path));
212
- const gitChanges = [];
213
- if (await isGitRepo(workspace.rootPath)) {
214
- const workingTreeChanges = await getWorkingTreeChangesDetailed(workspace.rootPath);
215
- for (const change of workingTreeChanges) {
216
- if (!knownFilePaths.has(change.path))
217
- continue;
218
- const status = change.staged && !change.unstaged
219
- ? 'staged'
220
- : change.unstaged && !change.staged
221
- ? 'unstaged'
222
- : 'staged'; // both staged and unstaged → report as staged
223
- gitChanges.push({
224
- path: change.path,
225
- status,
226
- reason: change.staged && change.unstaged
227
- ? 'partially staged'
228
- : status === 'staged'
229
- ? 'staged change'
230
- : 'unstaged change',
231
- });
232
- }
233
- const committedPaths = new Set(gitChanges.map((c) => c.path));
234
- const recentCommitted = await getRecentlyCommittedFiles(workspace.rootPath);
235
- for (const filePath of recentCommitted) {
236
- if (!knownFilePaths.has(filePath) || committedPaths.has(filePath))
237
- continue;
238
- gitChanges.push({
239
- path: filePath,
240
- status: 'recent-commit',
241
- reason: 'changed in recent commits',
242
- });
243
- }
244
- }
245
254
  return {
246
255
  query,
247
256
  matchedDomains,
@@ -257,6 +266,36 @@ export async function queryContext(workspace, config, maps, query) {
257
266
  warnings: [],
258
267
  };
259
268
  }
269
+ function applyFileRankAdjustments(ranked, context) {
270
+ const reasons = [...ranked.reasons];
271
+ let score = ranked.score - Math.floor((ranked.item.tokenEstimate ?? 0) / 2000);
272
+ if (context.sessionTouchedPaths.has(ranked.item.path)) {
273
+ score += 3;
274
+ reasons.push('touched in current session');
275
+ }
276
+ if (context.gitChangedPaths.has(ranked.item.path)) {
277
+ score += 10;
278
+ reasons.push('current git change');
279
+ }
280
+ const atomReasons = context.atomLinkedFiles.get(ranked.item.path) ?? [];
281
+ if (atomReasons.length > 0) {
282
+ score += 12;
283
+ reasons.push(...atomReasons);
284
+ }
285
+ const strongTokens = tokenize(context.query).filter((token) => token.length >= 4 &&
286
+ !['page', 'work', 'file', 'component', 'app'].includes(token));
287
+ const pathTokens = new Set(tokenize(ranked.item.path));
288
+ const strongMatches = strongTokens.filter((token) => pathTokens.has(token));
289
+ if (strongMatches.length > 0) {
290
+ score += strongMatches.length * 3;
291
+ reasons.push(`path matched specific query token(s): ${strongMatches.join(', ')}`);
292
+ }
293
+ else if (strongTokens.length > 0) {
294
+ score -= 6;
295
+ reasons.push('generic path-only match penalty');
296
+ }
297
+ return { ...ranked, score, reasons };
298
+ }
260
299
  function explainRelationships(relationships, context) {
261
300
  const rankedReasons = new Map(context.rankedRelationships.map((ranked) => [
262
301
  relationshipKey(ranked.item),
@@ -1,4 +1,3 @@
1
- import { numberedWorkflow } from '../workflow-steps.js';
2
1
  export const claudeCodeAdapter = {
3
2
  name: 'claude-code',
4
3
  label: 'Claude Code',
@@ -6,13 +5,22 @@ export const claudeCodeAdapter = {
6
5
  instructions: `## KGraph Workflow
7
6
 
8
7
  {{KGRAPH_CONTEXT_POLICY}} Use /kgraph for the full automated workflow. Run \`kgraph pack "<task>" --budget 8000 --json\` for a machine-readable token-budgeted context pack, \`kgraph knowledge list\` or \`kgraph knowledge get <atom-id>\` to inspect durable atoms, \`kgraph stale\` and \`kgraph blame <atom-id>\` when lifecycle/provenance matters, \`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.
8
+
9
+ {{KGRAPH_CAPTURE_POLICY}}
9
10
  `,
10
11
  commandFiles: [
11
12
  {
12
13
  path: '.claude/commands/kgraph.md',
13
- content: `Use KGraph persistent repo intelligence for the current request.
14
+ content: `Use KGraph persistent repo intelligence through the single normal \`kgraph "<topic>"\` entry point.
14
15
 
15
- ${numberedWorkflow('claude-code', { sessionQualifier: 'when native hooks are unavailable' })}
16
+ 1. Infer a concise topic from the user's request.
17
+ 2. Run exactly one command from the repository root: \`kgraph "<topic>"\`.
18
+ 3. Treat the returned files, symbols, relationships, atoms, and warnings as the first-pass source of truth.
19
+ 4. If the user asked for an edit, inspect only the returned candidate file or the smallest necessary range, then make the edit.
20
+ 5. Verify the change actually landed before claiming completion. Prefer a narrow read of the changed range or \`git diff -- <path>\`; if there is no diff or the expected text is missing, say the edit did not apply and fix it before summarizing.
21
+ 6. Do not run \`kgraph\` again, \`kgraph context\`, \`kgraph pack\`, \`kgraph knowledge\`, \`kgraph stale\`, \`kgraph blame\`, \`kgraph scan\`, \`kgraph update\`, \`kgraph compact\`, or \`kgraph repair\` unless the user explicitly asks for that lower-level command.
22
+ 7. Do not continue broad repository search after the target file is identified. If a path must be located, prefer \`rg --files\` and quote paths containing spaces or parentheses.
23
+ 8. At the end of repository-file changes, store durable engineering memory with \`kgraph conclude "<topic>" --type <finding|decision|gotcha|summary|relationship> --confidence <high|medium|low>\` only when the work created reusable engineering knowledge.
16
24
  `,
17
25
  },
18
26
  {
@@ -8,26 +8,6 @@ export const copilotAdapter = {
8
8
  ${numberedWorkflow('copilot')}
9
9
  `,
10
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, pack, knowledge, stale, blame, scan, update, conclude, compact, 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
- ---
23
-
24
- ## KGraph Agent
25
-
26
- You are a KGraph-powered agent. Before exploring the repository freely, always:
27
-
28
- ${numberedWorkflow('copilot')}
29
- `,
30
- },
31
11
  {
32
12
  path: '.github/prompts/kgraph-doctor.prompt.md',
33
13
  content: `---
@@ -36,16 +36,17 @@ export function applyContextPolicy(content, mode) {
36
36
  .replaceAll(KGRAPH_CAPTURE_POLICY_PLACEHOLDER, renderCapturePolicy());
37
37
  }
38
38
  export function renderContextPolicy(mode) {
39
+ const useResultBoundary = 'Use the returned KGraph candidate files as the first-pass source of truth. Do not rerun the same KGraph query just to tail or reformat output, do not continue broad repository search after the target file is identified, do not retry malformed shell commands with broader variants, and do not run broad `find`, recursive `grep`, or repeated full-file dumps after KGraph has narrowed the target.';
39
40
  switch (mode) {
40
41
  case 'always':
41
- return 'Every chat in this repository must start by running `kgraph "<topic>"` before answering or exploring files. Infer the topic from the user\'s message; no exceptions, including simple or conversational requests.';
42
+ return `Every chat in this repository must start by running \`kgraph "<topic>"\` before answering or exploring files. Infer the topic from the user's message; no exceptions, including simple or conversational requests. ${useResultBoundary}`;
42
43
  case 'manual':
43
44
  return 'Do not run KGraph automatically. Run `kgraph "<topic>"` only when the user explicitly asks for KGraph context or invokes the KGraph command.';
44
45
  case 'off':
45
46
  return 'KGraph is disabled for this integration.';
46
47
  case 'smart':
47
48
  default:
48
- return 'For repo-specific coding, debugging, architecture, refactor, review, or file-exploration requests, run `kgraph "<topic>"` before broad repository exploration. Infer the topic from the user\'s message. Skip KGraph for simple conversational requests that do not depend on repo knowledge.';
49
+ return `For repo-specific coding, debugging, architecture, refactor, review, or file-exploration requests, run \`kgraph "<topic>"\` before broad repository exploration. Infer the topic from the user's message. Skip KGraph for simple conversational requests that do not depend on repo knowledge. ${useResultBoundary}`;
49
50
  }
50
51
  }
51
52
  export function renderCapturePolicy() {
@@ -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 = 'smart') {
18
+ export async function addIntegrations(workspace, names, mode = 'always') {
19
19
  const config = await loadConfig(workspace);
20
20
  const byName = new Map(config.integrations.map((integration) => [integration.name, integration]));
21
21
  const changed = [];
@@ -10,6 +10,8 @@ const HISTORY_STEP = `Run \`kgraph history\` or \`kgraph history "<topic>"\` to
10
10
  const KNOWLEDGE_STEP = `Run \`kgraph knowledge list --topic "<topic>"\` or \`kgraph knowledge get <atom-id>\` when the user asks what KGraph remembers or atom provenance/lifecycle matters.`;
11
11
  const PACK_STEP = `Run \`kgraph pack "<task>" --budget 8000 --json\` when an agent needs a machine-readable, token-budgeted context pack instead of human Markdown context.`;
12
12
  const STALE_STEP = `Run \`kgraph stale\` when changed or deleted code may have invalidated durable knowledge. Run \`kgraph blame <atom-id>\` when provenance or evidence for a memory matters.`;
13
+ const EXPLORATION_BOUNDARY_STEP = `Keep exploration bounded by the task. For simple edits, use KGraph to identify the likely file, then read only that file or a narrow range and make the edit. Do not keep searching after the target file is found, do not retry malformed shell commands with broader variants, and do not run broad \`find\`, recursive \`grep\`, or repeated full-file dumps after KGraph already returned candidate files. Use \`rg --files\` and quoted paths when a path must be located.`;
14
+ const VERIFY_EDIT_STEP = `After editing, verify the change actually landed before claiming completion. Prefer a narrow read of the changed range or \`git diff -- <path>\`; if there is no diff or the expected text is missing, say the edit did not apply and fix it before summarizing.`;
13
15
  function sessionStep(agentName, qualifier) {
14
16
  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`;
15
17
  return qualifier ? `${base} ${qualifier}.` : `${base}.`;
@@ -22,19 +24,21 @@ export function numberedWorkflow(agentName, options = {}) {
22
24
  return `1. Infer the topic from the user's request.
23
25
  2. {{KGRAPH_CONTEXT_POLICY}}
24
26
  3. Use the returned files, symbols, relationships, and cognition before broad exploration.
25
- 4. ${PACK_STEP}
26
- 5. ${KNOWLEDGE_STEP}
27
- 6. ${DOCTOR_STEP}
28
- 7. ${STALE_STEP}
29
- 8. ${sessionStep(agentName, options.sessionQualifier)}
30
- 9. ${IMPACT_STEP}
27
+ 4. ${EXPLORATION_BOUNDARY_STEP}
28
+ 5. ${VERIFY_EDIT_STEP}
29
+ 6. ${PACK_STEP}
30
+ 7. ${KNOWLEDGE_STEP}
31
+ 8. ${DOCTOR_STEP}
32
+ 9. ${STALE_STEP}
33
+ 10. ${sessionStep(agentName, options.sessionQualifier)}
34
+ 11. ${IMPACT_STEP}
31
35
 
32
36
  {{KGRAPH_CAPTURE_POLICY}}
33
37
 
34
- 10. ${REPAIR_STEP}
35
- 11. ${COMPACT_STEP}
36
- 12. Run \`kgraph visualize\` when the user wants to inspect the dependency graph — opens an interactive graph at http://localhost:4242 with PNG export.
37
- 13. ${HISTORY_STEP}`;
38
+ 12. ${REPAIR_STEP}
39
+ 13. ${COMPACT_STEP}
40
+ 14. Run \`kgraph visualize\` when the user wants to inspect the dependency graph — opens an interactive graph at http://localhost:4242 with PNG export.
41
+ 15. ${HISTORY_STEP}`;
38
42
  }
39
43
  /**
40
44
  * Returns the bullet-list workflow for rules files.
@@ -42,6 +46,8 @@ export function numberedWorkflow(agentName, options = {}) {
42
46
  */
43
47
  export function bulletWorkflow(agentName, options = {}) {
44
48
  return `- {{KGRAPH_CONTEXT_POLICY}}
49
+ - ${EXPLORATION_BOUNDARY_STEP}
50
+ - ${VERIFY_EDIT_STEP}
45
51
  - ${PACK_STEP}
46
52
  - ${KNOWLEDGE_STEP}
47
53
  - ${DOCTOR_STEP}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kentwynn/kgraph",
3
- "version": "0.2.12",
3
+ "version": "0.2.14",
4
4
  "description": "Persistent repo intelligence for AI coding assistants.",
5
5
  "type": "module",
6
6
  "bin": {