@kentwynn/kgraph 0.2.15 → 0.2.17

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
@@ -4,6 +4,8 @@ Persistent repository intelligence for AI coding tools.
4
4
 
5
5
  KGraph gives Codex, GitHub Copilot, Cursor, Claude Code, Gemini CLI, Windsurf, and Cline a local knowledge layer for your repo: file maps, symbols, imports, relationships, and durable knowledge atoms from previous AI sessions. The goal is simple: your assistant should not spend every session re-learning the same codebase.
6
6
 
7
+ The CLI presents this as **Atom Core**: lightweight local atoms plus deterministic repo maps, context packs, and session history that remain inspectable under `.kgraph/`.
8
+
7
9
  ## The Workflow
8
10
 
9
11
  Use KGraph in two steps:
@@ -119,12 +121,14 @@ After useful AI work, assistants save durable runtime-capture notes into `.kgrap
119
121
  Normal agent flow is intentionally small:
120
122
 
121
123
  ```bash
122
- kgraph "topic"
124
+ kgraph pack "topic" --budget 8000 --json
123
125
  # work normally
124
126
  # if repo files changed, write an inbox note before the final refresh
125
127
  kgraph
126
128
  ```
127
129
 
130
+ `kgraph "<topic>"` remains the human-readable briefing. Agents should prefer `kgraph pack "<topic>" --budget 8000 --json` because it returns the stable `ContextPack` contract with atoms, source ranges, git changes, omitted items, token estimates, and inclusion reasons.
131
+
128
132
  Use `kgraph doctor` after setup and before trusting a repo's saved intelligence. It checks initialization, maps, pending inbox notes, integration targets, and actionable quality problems. Use `kgraph doctor --quality` and `kgraph repair --dry-run` when stale or noisy atom references start making context harder to trust.
129
133
 
130
134
  Agents can also report session activity so KGraph can estimate token waste:
@@ -247,7 +251,7 @@ kgraph pack "auth token refresh" --budget 8000
247
251
  kgraph pack "auth token refresh" --budget 8000 --json
248
252
  ```
249
253
 
250
- Build a budget-aware context pack from files, symbols, relationships, git changes, session history, and knowledge atoms. JSON output is the stable machine-readable contract for agents.
254
+ Build a budget-aware context pack from files, source ranges, symbols, relationships, git changes, session history, and knowledge atoms. JSON output is the stable machine-readable contract for agents; text output is an Atom Core briefing for humans.
251
255
 
252
256
  ```bash
253
257
  kgraph compact --dry-run
@@ -25,11 +25,20 @@ export function registerContextCommand(program) {
25
25
  }));
26
26
  }
27
27
  export function renderContextMarkdown(response) {
28
- const lines = [`# KGraph Context`, ``, `Query: ${response.query}`, ``];
29
- lines.push('## Matched Domains', '');
30
- lines.push(...formatList(response.matchedDomains.map((item) => `- ${item.item.name} because ${formatReasons(item.reasons)}`)));
31
- lines.push('', '## Relevant Files', '');
32
- lines.push(...formatList(response.relevantFiles.map((item) => {
28
+ const lines = [
29
+ `KGraph Context · ${response.query}`,
30
+ `local-first · deterministic · inspectable`,
31
+ ``,
32
+ `● Signal`,
33
+ ` confidence ${contextConfidence(response)}`,
34
+ ` source ${contextSources(response).join(' + ') || 'file map'}`,
35
+ ` warnings ${response.warnings.length > 0 ? response.warnings.length : 'none'}`,
36
+ ``,
37
+ ];
38
+ lines.push('● Matched Domains', '');
39
+ lines.push(...formatList(response.matchedDomains.map((item) => atomLine(item.item.name, formatReasons(item.reasons)))));
40
+ lines.push('', '● Source', '');
41
+ lines.push(...formatList(limited(response.relevantFiles, 5).map((item) => {
33
42
  const f = item.item;
34
43
  const meta = [
35
44
  f.language,
@@ -37,32 +46,34 @@ export function renderContextMarkdown(response) {
37
46
  ]
38
47
  .filter(Boolean)
39
48
  .join(', ');
40
- return `- ${f.path}${meta ? ` [${meta}]` : ''} because ${formatReasons(item.reasons)}`;
49
+ return atomLine(`${f.path}${meta ? ` [${meta}]` : ''}`, formatReasons(item.reasons));
41
50
  })));
42
- lines.push('', '## Relevant Symbols', '');
43
- lines.push(...formatList(response.relevantSymbols.map((item) => {
51
+ appendMore(lines, response.relevantFiles.length, 5, 'source item');
52
+ lines.push('', '● Symbols', '');
53
+ lines.push(...formatList(limited(response.relevantSymbols, 6).map((item) => {
44
54
  const s = item.item;
45
55
  const kindInfo = [s.kind, s.parentName].filter(Boolean).join(', ');
46
56
  const lineRange = s.startLine != null && s.endLine != null
47
57
  ? `:${s.startLine}-${s.endLine}`
48
58
  : '';
49
- return `- ${s.name} (${kindInfo}) in ${s.filePath}${lineRange} because ${formatReasons(item.reasons)}`;
59
+ return atomLine(`${s.name} (${kindInfo}) in ${s.filePath}${lineRange}`, formatReasons(item.reasons));
50
60
  })));
51
- lines.push('', '## Relevant Cognition', '');
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
- lines.push('', '## Relationships', '');
54
- lines.push(...formatGroupedRelationships(response.relationships, response.relationshipExplanations));
55
- lines.push('', '## Nearby Symbols (1-hop imports)', '');
61
+ appendMore(lines, response.relevantSymbols.length, 6, 'symbol');
62
+ lines.push('', '● Atoms', '');
63
+ lines.push(...formatList(response.relevantCognition.map((item) => atomLine(`${item.item.title} [${item.item.kind ?? 'summary'}, ${item.item.confidence ?? 'medium'}, ${item.item.referencesStatus}]`, formatReasons(item.reasons)))));
64
+ lines.push('', '● Graph', '');
65
+ lines.push(...formatGroupedRelationships(relevantGraphRelationships(response), response.relationshipExplanations));
66
+ lines.push('', '● Nearby Symbols (1-hop imports)', '');
56
67
  lines.push(...formatList(nearbySymbolItems(response).map(({ symbol: s, reasons }) => {
57
68
  const kindInfo = [s.kind, s.parentName].filter(Boolean).join(', ');
58
69
  const lineRange = s.startLine != null && s.endLine != null
59
70
  ? `:${s.startLine}-${s.endLine}`
60
71
  : '';
61
- return `- ${s.name} (${kindInfo}) in ${s.filePath}${lineRange} because ${formatReasons(reasons)}`;
72
+ return atomLine(`${s.name} (${kindInfo}) in ${s.filePath}${lineRange}`, formatReasons(reasons));
62
73
  })));
63
- lines.push('', '## Stale References', '');
64
- lines.push(...formatList(response.staleReferences.map((ref) => `- ${ref}`)));
65
- lines.push('', '## Recent Git Changes', '');
74
+ lines.push('', ' Stale References', '');
75
+ lines.push(...formatList(response.staleReferences.map((ref) => ` ◌ ${ref}`)));
76
+ lines.push('', ' Recent Git Changes', '');
66
77
  if (response.gitChanges && response.gitChanges.length > 0) {
67
78
  const staged = response.gitChanges.filter((c) => c.status === 'staged');
68
79
  const unstaged = response.gitChanges.filter((c) => c.status === 'unstaged');
@@ -70,22 +81,29 @@ export function renderContextMarkdown(response) {
70
81
  if (staged.length > 0) {
71
82
  lines.push('Staged:');
72
83
  for (const c of staged)
73
- lines.push(` ${c.path} (${c.reason})`);
84
+ lines.push(` ${c.path} (${c.reason})`);
74
85
  }
75
86
  if (unstaged.length > 0) {
76
87
  lines.push('Unstaged:');
77
88
  for (const c of unstaged)
78
- lines.push(` ${c.path} (${c.reason})`);
89
+ lines.push(` ${c.path} (${c.reason})`);
79
90
  }
80
91
  if (recent.length > 0) {
81
92
  lines.push('Recent commits:');
82
93
  for (const c of recent)
83
- lines.push(` ${c.path} (${c.reason})`);
94
+ lines.push(` ${c.path} (${c.reason})`);
84
95
  }
85
96
  }
86
97
  else {
87
98
  lines.push('- None');
88
99
  }
100
+ lines.push('', '● Next', '');
101
+ if (response.relevantFiles.some((item) => (item.item.tokenEstimate ?? 0) > 4000)) {
102
+ lines.push(` use budgeted source ranges: kgraph pack "${response.query}" --budget 4000`);
103
+ }
104
+ else {
105
+ lines.push(` read the ranked source, edit, verify, conclude only if durable knowledge changed`);
106
+ }
89
107
  return lines.join('\n');
90
108
  }
91
109
  function formatGroupedRelationships(relationships, explanations) {
@@ -105,33 +123,49 @@ function formatGroupedRelationships(relationships, explanations) {
105
123
  const lines = [];
106
124
  if (imports.length > 0) {
107
125
  lines.push('Imports:');
108
- for (const r of imports) {
126
+ for (const r of imports.slice(0, 6)) {
109
127
  lines.push(` ${r.sourceId} → ${r.targetId}${formatRelationshipReason(r, reasonsByRelationship)}`);
110
128
  }
129
+ appendMore(lines, imports.length, 6, 'import edge');
111
130
  }
112
131
  if (calls.length > 0) {
113
132
  lines.push('Calls:');
114
- for (const r of calls) {
133
+ for (const r of calls.slice(0, 6)) {
115
134
  lines.push(` ${r.sourceId} → ${r.targetId}${formatRelationshipReason(r, reasonsByRelationship)}`);
116
135
  }
136
+ appendMore(lines, calls.length, 6, 'call edge');
117
137
  }
118
138
  if (contains.length > 0) {
119
139
  lines.push('Contains:');
120
- for (const r of contains) {
140
+ for (const r of contains.slice(0, 6)) {
121
141
  lines.push(` ${r.sourceId} contains ${r.targetId}${formatRelationshipReason(r, reasonsByRelationship)}`);
122
142
  }
143
+ appendMore(lines, contains.length, 6, 'containment edge');
123
144
  }
124
145
  if (other.length > 0) {
125
146
  lines.push('Other:');
126
- for (const r of other) {
147
+ for (const r of other.slice(0, 6)) {
127
148
  lines.push(` ${r.sourceId} ${r.relationshipType} ${r.targetId}${formatRelationshipReason(r, reasonsByRelationship)}`);
128
149
  }
150
+ appendMore(lines, other.length, 6, 'graph edge');
129
151
  }
130
152
  return lines.length > 0 ? lines : ['- None'];
131
153
  }
132
154
  function formatList(items) {
133
155
  return items.length > 0 ? items : ['- None'];
134
156
  }
157
+ function limited(items, count) {
158
+ return items.slice(0, count);
159
+ }
160
+ function appendMore(lines, total, shown, label) {
161
+ const remaining = total - shown;
162
+ if (remaining > 0) {
163
+ lines.push(` ◌ ${remaining} more ${label}${remaining === 1 ? '' : 's'} omitted from display`);
164
+ }
165
+ }
166
+ function atomLine(title, detail) {
167
+ return ` ● ${title}\n because ${detail}`;
168
+ }
135
169
  function formatReasons(reasons) {
136
170
  if (reasons.length === 0) {
137
171
  return 'it is near the query';
@@ -151,6 +185,39 @@ function nearbySymbolItems(response) {
151
185
  reasons: ['exported symbol from 1-hop import'],
152
186
  }));
153
187
  }
188
+ function relevantGraphRelationships(response) {
189
+ const anchorPaths = response.relevantFiles
190
+ .filter((item) => !item.reasons.some((reason) => reason.includes('generic path-only match penalty')))
191
+ .slice(0, 3)
192
+ .map((item) => item.item.path);
193
+ const paths = anchorPaths.length > 0
194
+ ? anchorPaths
195
+ : response.relevantFiles.slice(0, 1).map((item) => item.item.path);
196
+ if (paths.length === 0)
197
+ return response.relationships;
198
+ return response.relationships.filter((relationship) => paths.some((path) => relationship.sourceId.includes(path) ||
199
+ relationship.targetId.includes(path)));
200
+ }
201
+ function contextConfidence(response) {
202
+ const confidence = response.relevantCognition
203
+ .map((item) => item.item.confidence)
204
+ .find(Boolean);
205
+ return confidence ?? (response.relevantFiles.length > 0 ? 'medium' : 'low');
206
+ }
207
+ function contextSources(response) {
208
+ const sources = [];
209
+ if (response.relevantCognition.length > 0)
210
+ sources.push('atom');
211
+ if ((response.gitChanges ?? []).length > 0)
212
+ sources.push('git change');
213
+ if (response.relevantSymbols.length > 0)
214
+ sources.push('symbol');
215
+ if (relevantGraphRelationships(response).length > 0)
216
+ sources.push('graph');
217
+ if (response.relevantFiles.length > 0)
218
+ sources.push('file');
219
+ return sources;
220
+ }
154
221
  function formatRelationshipReason(relationship, reasonsByRelationship) {
155
222
  const reasons = reasonsByRelationship.get(relationshipKey(relationship));
156
223
  return reasons && reasons.length > 0
@@ -1,2 +1,4 @@
1
1
  import type { Command } from 'commander';
2
+ import type { ContextPack } from '../../types/knowledge.js';
2
3
  export declare function registerPackCommand(program: Command): void;
4
+ export declare function renderPackText(pack: ContextPack): string;
@@ -31,19 +31,66 @@ export function registerPackCommand(program) {
31
31
  console.log(JSON.stringify(pack, null, 2));
32
32
  return;
33
33
  }
34
- console.log(`# KGraph Context Pack`);
35
- console.log('');
36
- console.log(`Task: ${pack.task}`);
37
- console.log(`Budget: ${pack.budget}`);
38
- console.log(`Used: ${pack.usedTokens}`);
39
- console.log('');
40
- for (const item of pack.items) {
41
- console.log(`- [${item.kind}] ${item.title} (~${item.tokenEstimate} tokens)`);
42
- console.log(` because ${item.reasons.slice(0, 3).join('; ')}`);
34
+ console.log(renderPackText(pack));
35
+ }));
36
+ }
37
+ export function renderPackText(pack) {
38
+ const lines = [
39
+ `KGraph Pack · ${pack.task}`,
40
+ `local-first · budget-aware · machine contract: --json`,
41
+ ``,
42
+ `● Budget`,
43
+ ` used ${pack.usedTokens} / ${pack.budget}`,
44
+ ` included ${pack.items.length}`,
45
+ ` omitted ${pack.omitted.length}`,
46
+ ``,
47
+ ];
48
+ appendGroup(lines, 'Atoms', pack.items.filter((item) => item.kind === 'atom'));
49
+ appendGroup(lines, 'Git Changes', pack.items.filter((item) => item.kind === 'git-change'));
50
+ appendGroup(lines, 'Source Ranges', pack.items.filter((item) => item.kind === 'file-range'));
51
+ appendGroup(lines, 'Symbols', pack.items.filter((item) => item.kind === 'symbol'));
52
+ appendGroup(lines, 'Files', pack.items.filter((item) => item.kind === 'file'));
53
+ appendGroup(lines, 'Graph', pack.items.filter((item) => item.kind === 'relationship'));
54
+ lines.push(`● Omitted`);
55
+ const omitted = pack.omitted.slice(0, 8);
56
+ if (omitted.length === 0) {
57
+ lines.push('- None');
58
+ }
59
+ else {
60
+ for (const item of omitted) {
61
+ lines.push(` ◌ ${item.kind} ${item.title} (~${item.tokenEstimate} tokens)`);
43
62
  }
44
- if (pack.omitted.length > 0) {
45
- console.log('');
46
- console.log(`Omitted: ${pack.omitted.length} item(s) over budget`);
63
+ if (pack.omitted.length > omitted.length) {
64
+ lines.push(` ◌ ${pack.omitted.length - omitted.length} more omitted items`);
47
65
  }
48
- }));
66
+ }
67
+ lines.push('', '● Next', ' agents should consume this command with --json for the full ContextPack contract');
68
+ return lines.join('\n');
69
+ }
70
+ function appendGroup(lines, title, items) {
71
+ lines.push(`● ${title}`);
72
+ if (items.length === 0) {
73
+ lines.push('- None', '');
74
+ return;
75
+ }
76
+ for (const item of items.slice(0, 6)) {
77
+ lines.push(` ● ${item.title} (~${item.tokenEstimate} tokens)`);
78
+ lines.push(` because ${formatReasons(item.reasons)}`);
79
+ if (item.kind === 'file-range') {
80
+ const data = item.data;
81
+ if (data.path && data.startLine != null && data.endLine != null) {
82
+ lines.push(` range ${data.path}:${data.startLine}-${data.endLine}`);
83
+ }
84
+ }
85
+ }
86
+ if (items.length > 6)
87
+ lines.push(` ◌ ${items.length - 6} more ${title.toLowerCase()} omitted from display`);
88
+ lines.push('');
89
+ }
90
+ function formatReasons(reasons) {
91
+ if (reasons.length === 0)
92
+ return 'included by pack ranking';
93
+ const shown = reasons.slice(0, 3);
94
+ const remaining = reasons.length - shown.length;
95
+ return remaining > 0 ? `${shown.join('; ')}; and ${remaining} more` : shown.join('; ');
49
96
  }
package/dist/cli/help.js CHANGED
@@ -1,31 +1,32 @@
1
1
  import { Chalk } from 'chalk';
2
- import figlet from 'figlet';
3
2
  export function renderRootHelp(useColor = supportsColor()) {
4
3
  const theme = new Chalk({ level: useColor ? 3 : 0 });
5
4
  const command = (name, description) => ` ${theme.green(name.padEnd(42))} ${description}`;
6
- const logo = renderLogo();
5
+ const accent = atomAccent(theme);
7
6
  return [
8
7
  '',
9
- theme.hex('#7dd3fc').bold(logo),
8
+ renderAtomLogo(theme),
10
9
  '',
11
- ` ${theme.bold('KGraph')} ${theme.dim('Persistent repo intelligence for AI coding tools')}`,
10
+ renderSignalPanel(theme, [
11
+ ['purpose', 'durable engineering memory for AI coding tools'],
12
+ ['storage', '.kgraph/ atoms, maps, indexes, and session history'],
13
+ ['stance', 'local-first · deterministic-first · inspectable'],
14
+ ['agents', 'Codex · Copilot · Cursor · Claude Code · Gemini · Windsurf · Cline'],
15
+ ]),
12
16
  '',
13
- ` ${theme.hex('#c084fc')('Build a local knowledge layer that helps Codex, Copilot, Cursor,')}`,
14
- ` ${theme.hex('#c084fc')('Claude Code, Gemini, Windsurf, and Cline reuse repo intelligence.')}`,
15
- '',
16
- theme.bold('Usage'),
17
+ sectionTitle(theme, `${accent} Usage`),
17
18
  ' kgraph [topic]',
18
19
  ' kgraph <command> [options]',
19
20
  '',
20
- theme.bold('Start'),
21
+ sectionTitle(theme, `${accent} Start`),
21
22
  command('init', 'Required once: create .kgraph/ workspace'),
22
23
  command('init --integrations codex,gemini', 'Initialize and connect AI tools'),
23
24
  '',
24
- theme.bold('Daily workflow'),
25
+ sectionTitle(theme, `${accent} Daily workflow`),
25
26
  command('kgraph', 'Refresh scan maps and process pending capture notes'),
26
27
  command('kgraph "auth token refresh"', 'Refresh everything and return compact context for a topic'),
27
28
  '',
28
- theme.bold('Workflows'),
29
+ sectionTitle(theme, `${accent} Workflows`),
29
30
  command('scan', 'Optional: refresh only file, symbol, import, and relationship maps'),
30
31
  command('session', 'Show agent read/write activity and token estimates'),
31
32
  command('session read src/auth.ts --agent codex', 'Record an agent file read'),
@@ -48,7 +49,7 @@ export function renderRootHelp(useColor = supportsColor()) {
48
49
  command('visualize', 'Interactive dependency graph at http://localhost:4242'),
49
50
  command('history "blog button"', 'Search processed cognition sessions'),
50
51
  '',
51
- theme.bold('Integrations'),
52
+ sectionTitle(theme, `${accent} Integrations`),
52
53
  command('integrate list', 'Show configured AI tool integrations'),
53
54
  command('integrate add gemini windsurf cline', 'Write KGraph instructions using always mode by default'),
54
55
  command('integrate add copilot --mode smart', 'Run KGraph for repo-specific Copilot work only'),
@@ -56,21 +57,24 @@ export function renderRootHelp(useColor = supportsColor()) {
56
57
  command('integrate remove cursor', 'Remove KGraph-managed instruction blocks'),
57
58
  command('--mode smart|always|manual|off', 'Control automatic KGraph involvement per integration'),
58
59
  '',
59
- theme.bold('Options'),
60
+ sectionTitle(theme, `${accent} Options`),
60
61
  command('-V, --version', 'Show version'),
61
62
  command('-h, --help', 'Show this help'),
62
63
  '',
63
- `${theme.yellow('Examples')}`,
64
+ sectionTitle(theme, `${accent} Examples`),
64
65
  ' kgraph init --integrations codex,copilot,cursor,claude-code,gemini,windsurf,cline',
65
66
  ' kgraph "blog admin token usage"',
67
+ ' kgraph pack "about page update" --budget 4000',
66
68
  ' kgraph doctor',
67
69
  '',
68
70
  theme.dim('Docs: https://github.com/kentwynn/KGraph#readme'),
71
+ theme.dim('Powered by Kent Wynn: https://kentwynn.com'),
69
72
  '',
70
73
  ].join('\n');
71
74
  }
72
75
  export function renderWorkflowBanner(stats, useColor = supportsColor()) {
73
76
  const theme = new Chalk({ level: useColor ? 3 : 0 });
77
+ const accent = atomAccent(theme);
74
78
  const command = (name, description) => ` ${theme.green(name.padEnd(42))} ${description}`;
75
79
  const integrationLine = stats.integrations && stats.integrations.length > 0
76
80
  ? stats.integrations
@@ -81,11 +85,11 @@ export function renderWorkflowBanner(stats, useColor = supportsColor()) {
81
85
  : 'none configured';
82
86
  return [
83
87
  '',
84
- theme.hex('#7dd3fc').bold(renderLogo()),
88
+ renderAtomLogo(theme),
85
89
  '',
86
- ` ${theme.bold('KGraph')} ${theme.dim('repo intelligence refreshed')}`,
90
+ ` ${theme.bold('KGraph')} ${theme.dim('· repo intelligence refreshed')}`,
87
91
  '',
88
- theme.bold('Refresh Complete'),
92
+ sectionTitle(theme, `${accent} Refresh Complete`),
89
93
  command('files', String(stats.files) +
90
94
  (stats.skippedFiles
91
95
  ? ` (${stats.skippedFiles} unchanged, skipped)`
@@ -94,7 +98,7 @@ export function renderWorkflowBanner(stats, useColor = supportsColor()) {
94
98
  command('capture notes processed', String(stats.cognitionNotes)),
95
99
  command('integration modes', integrationLine),
96
100
  '',
97
- theme.bold('Next'),
101
+ sectionTitle(theme, `${accent} Next`),
98
102
  command('kgraph "auth token refresh"', 'Return compact context for a topic'),
99
103
  command('kgraph doctor', 'Check workspace health'),
100
104
  command('kgraph doctor --quality', 'Check atom quality'),
@@ -104,17 +108,27 @@ export function renderWorkflowBanner(stats, useColor = supportsColor()) {
104
108
  command('kgraph --help', 'Show all commands'),
105
109
  ].join('\n');
106
110
  }
107
- function renderLogo() {
108
- try {
109
- return figlet.textSync('KGraph', {
110
- font: 'ANSI Shadow',
111
- horizontalLayout: 'default',
112
- verticalLayout: 'default',
113
- });
114
- }
115
- catch {
116
- return 'KGraph';
117
- }
111
+ function renderAtomLogo(theme) {
112
+ const title = `${theme.hex('#38bdf8').bold('KGraph')} ${theme.dim('·')} ${theme.hex('#c084fc').bold('Atom Core')}`;
113
+ const atom = theme.hex('#22d3ee').bold('⚛');
114
+ const memory = theme.hex('#a78bfa')('persistent repo intelligence for AI coding tools');
115
+ return [
116
+ ` ${atom} ${theme.dim('atoms · evidence · context packs')}`,
117
+ ` ${title}`,
118
+ ` ${memory}`,
119
+ ].join('\n');
120
+ }
121
+ function renderSignalPanel(theme, rows) {
122
+ const labelWidth = Math.max(...rows.map(([label]) => label.length));
123
+ return rows
124
+ .map(([label, value]) => ` ${theme.hex('#22d3ee')('●')} ${theme.bold(label.padEnd(labelWidth))} ${theme.dim(value)}`)
125
+ .join('\n');
126
+ }
127
+ function sectionTitle(theme, title) {
128
+ return theme.bold(title);
129
+ }
130
+ function atomAccent(theme) {
131
+ return theme.hex('#22d3ee')('●');
118
132
  }
119
133
  function supportsColor() {
120
134
  return Boolean(process.stdout.isTTY) && process.env.NO_COLOR === undefined;
@@ -53,10 +53,15 @@ export function buildContextPack(response, budget, rootPath) {
53
53
  })),
54
54
  ];
55
55
  const orderedCandidates = candidates.sort(comparePackCandidates);
56
+ const strongPaths = strongPackPaths(candidates);
56
57
  const items = [];
57
58
  const omitted = [];
58
59
  let usedTokens = 0;
59
60
  for (const candidate of orderedCandidates) {
61
+ if (isLowSignalCandidate(candidate, strongPaths)) {
62
+ omitted.push(candidate);
63
+ continue;
64
+ }
60
65
  if (usedTokens + candidate.tokenEstimate <= budget) {
61
66
  items.push(candidate);
62
67
  usedTokens += candidate.tokenEstimate;
@@ -102,6 +107,60 @@ function packPriority(item) {
102
107
  score -= Math.floor(item.tokenEstimate / 2000);
103
108
  return score;
104
109
  }
110
+ function strongPackPaths(candidates) {
111
+ const paths = new Set();
112
+ for (const candidate of candidates) {
113
+ if (candidate.kind === 'file-range' || candidate.kind === 'git-change') {
114
+ const pathValue = candidatePath(candidate);
115
+ if (pathValue)
116
+ paths.add(pathValue);
117
+ }
118
+ if (candidate.kind === 'file' && hasStrongReason(candidate)) {
119
+ const pathValue = candidatePath(candidate);
120
+ if (pathValue)
121
+ paths.add(pathValue);
122
+ }
123
+ if (candidate.kind === 'atom') {
124
+ const atom = candidate.data;
125
+ for (const file of atom?.relatedFiles ?? [])
126
+ paths.add(file);
127
+ }
128
+ }
129
+ return paths;
130
+ }
131
+ function isLowSignalCandidate(candidate, strongPaths) {
132
+ if (strongPaths.size === 0)
133
+ return false;
134
+ if (candidate.kind === 'atom' || candidate.kind === 'git-change' || candidate.kind === 'file-range') {
135
+ return false;
136
+ }
137
+ if (hasStrongReason(candidate))
138
+ return false;
139
+ if (candidateTouchesStrongPath(candidate, strongPaths))
140
+ return false;
141
+ return candidate.kind === 'file' || candidate.kind === 'symbol' || candidate.kind === 'relationship';
142
+ }
143
+ function hasStrongReason(candidate) {
144
+ return candidate.reasons.some((reason) => reason.includes('matched atom') ||
145
+ reason.includes('current git change') ||
146
+ reason.includes('changed in recent commits') ||
147
+ reason.includes('unstaged change') ||
148
+ reason.includes('staged change'));
149
+ }
150
+ function candidateTouchesStrongPath(candidate, strongPaths) {
151
+ const pathValue = candidatePath(candidate);
152
+ if (pathValue && strongPaths.has(pathValue))
153
+ return true;
154
+ if (candidate.kind !== 'relationship')
155
+ return false;
156
+ const relationship = candidate.data;
157
+ return [...strongPaths].some((strongPath) => relationship.sourceId?.includes(strongPath) ||
158
+ relationship.targetId?.includes(strongPath));
159
+ }
160
+ function candidatePath(candidate) {
161
+ const data = candidate.data;
162
+ return data?.path ?? data?.filePath;
163
+ }
105
164
  const GENERIC_RANGE_TOKENS = new Set([
106
165
  'app',
107
166
  'code',
@@ -36,17 +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
+ const useResultBoundary = 'Use the returned KGraph ContextPack items as the first-pass source of truth. Prefer source ranges, atoms, git changes, and inclusion reasons from the pack before broad repository search. Do not rerun the same KGraph query just to tail or reformat output, do not continue broad repository search after the target file or range 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.';
40
40
  switch (mode) {
41
41
  case 'always':
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
+ return `Every chat in this repository must start by running \`kgraph pack "<topic>" --budget 8000 --json\` before answering or exploring files. Infer the topic from the user's message; no exceptions, including simple or conversational requests. Use \`kgraph "<topic>"\` only when a human-readable briefing is explicitly needed. ${useResultBoundary}`;
43
43
  case 'manual':
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
+ return 'Do not run KGraph automatically. Run `kgraph pack "<topic>" --budget 8000 --json` only when the user explicitly asks for KGraph context, invokes KGraph, or needs a machine-readable repo-memory pack.';
45
45
  case 'off':
46
46
  return 'KGraph is disabled for this integration.';
47
47
  case 'smart':
48
48
  default:
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
+ return `For repo-specific coding, debugging, architecture, refactor, review, or file-exploration requests, run \`kgraph pack "<topic>" --budget 8000 --json\` 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. Use \`kgraph "<topic>"\` only when a human-readable briefing is explicitly needed. ${useResultBoundary}`;
50
50
  }
51
51
  }
52
52
  export function renderCapturePolicy() {
@@ -56,7 +56,7 @@ export function renderCapturePolicy() {
56
56
  - Use \`.kgraph/inbox/<slug>.md\` only when a longer structured note is clearer than a single \`kgraph conclude\` command.
57
57
  - 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.
58
58
  - 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.
59
- - 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.
59
+ - Do not run KGraph repeatedly. Run it once at the start with \`kgraph pack "<topic>" --budget 8000 --json\` for agent-readable context. If repo files changed, write the inbox note first, then run \`kgraph\` once at the end.
60
60
  - After the final \`kgraph\` run, mention whether durable cognition was stored or processed.
61
61
 
62
62
  When using an inbox note, use this structure:
@@ -8,7 +8,7 @@ const REPAIR_STEP = `Run \`kgraph repair --dry-run\` before cleanup when stale/n
8
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.`;
9
9
  const HISTORY_STEP = `Run \`kgraph history\` or \`kgraph history "<topic>"\` to review past cognition sessions with git author attribution.`;
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
- 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.`;
11
+ const PACK_STEP = `Treat \`kgraph pack "<task>" --budget 8000 --json\` as the primary agent contract: use atoms, source ranges, git changes, omitted items, and inclusion reasons from the ContextPack before reading files. Use human \`kgraph "<topic>"\` output only when the user explicitly wants a briefing.`;
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
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
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.`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kentwynn/kgraph",
3
- "version": "0.2.15",
3
+ "version": "0.2.17",
4
4
  "description": "Persistent repo intelligence for AI coding assistants.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -47,7 +47,6 @@
47
47
  "chalk": "^5.6.2",
48
48
  "commander": "^12.1.0",
49
49
  "fast-glob": "^3.3.2",
50
- "figlet": "^1.11.0",
51
50
  "tree-sitter-c": "^0.24.1",
52
51
  "tree-sitter-c-sharp": "^0.23.5",
53
52
  "tree-sitter-cpp": "^0.23.4",