@kentwynn/kgraph 0.2.37 → 0.2.38

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.
@@ -2,6 +2,7 @@ import path from 'node:path';
2
2
  import { loadConfig } from '../../config/config.js';
3
3
  import { buildContextPack } from '../../context/context-pack.js';
4
4
  import { queryContext } from '../../context/context-query.js';
5
+ import { getCurrentCommit, isGitRepo } from '../../scanner/git-utils.js';
5
6
  import { assertSessionAgent, recordSessionEvent, } from '../../session/session-store.js';
6
7
  import { listInboxNotes } from '../../storage/cognition-store.js';
7
8
  import { assertWorkspace } from '../../storage/kgraph-paths.js';
@@ -34,6 +35,35 @@ export function registerPackCommand(program) {
34
35
  ]);
35
36
  const response = await queryContext(workspace, config, maps, task);
36
37
  const pack = buildContextPack(response, budget, workspace.rootPath);
38
+ // P4: warn when maps are behind HEAD (user edited files without re-scanning)
39
+ if (await isGitRepo(workspace.rootPath)) {
40
+ const head = await getCurrentCommit(workspace.rootPath);
41
+ if (head &&
42
+ maps.fileMap.scannedAtCommit &&
43
+ maps.fileMap.scannedAtCommit !== head) {
44
+ pack.warnings = [
45
+ `Maps are behind HEAD — run \`kgraph scan\` or \`kgraph "${task}"${agent ? ` --agent ${agent}` : ''}\` for accurate atom status. Knowledge freshness may be reduced.`,
46
+ ...pack.warnings,
47
+ ];
48
+ }
49
+ }
50
+ // P1: warn when needs-review or stale atoms are served in context
51
+ const degradedAtoms = pack.items.filter((item) => item.kind === 'atom' &&
52
+ (item.data.status === 'needs-review' ||
53
+ item.data.status === 'stale'));
54
+ if (degradedAtoms.length > 0) {
55
+ const needsReview = degradedAtoms.filter((item) => item.data.status === 'needs-review').length;
56
+ const stale = degradedAtoms.filter((item) => item.data.status === 'stale').length;
57
+ const parts = [];
58
+ if (needsReview > 0)
59
+ parts.push(`${needsReview} needs-review`);
60
+ if (stale > 0)
61
+ parts.push(`${stale} stale`);
62
+ pack.warnings = [
63
+ ...pack.warnings,
64
+ `${parts.join(', ')} atom(s) in context may reflect outdated knowledge — run \`kgraph stale\` to inspect, or \`kgraph "${task}" --final${agent ? ` --agent ${agent}` : ''}\` to resolve.`,
65
+ ];
66
+ }
37
67
  if (agent) {
38
68
  const omittedTokens = pack.omitted.reduce((sum, item) => sum + item.tokenEstimate, 0);
39
69
  await recordSessionEvent(workspace, {
@@ -14,7 +14,10 @@ export function rankByFields(query, items, fields) {
14
14
  for (const field of fields) {
15
15
  const value = field.value(item);
16
16
  const values = Array.isArray(value) ? value : value ? [value] : [];
17
- const haystack = values.flatMap((value) => [value, splitIdentifier(value).join(' ')]).join(' ').toLowerCase();
17
+ const haystack = values
18
+ .flatMap((value) => [value, splitIdentifier(value).join(' ')])
19
+ .join(' ')
20
+ .toLowerCase();
18
21
  for (const token of tokens) {
19
22
  if (haystack.includes(token)) {
20
23
  const baseScore = field.name === 'path' || field.name === 'name'
@@ -36,7 +39,25 @@ export function rankByFields(query, items, fields) {
36
39
  .sort((a, b) => b.score - a.score);
37
40
  }
38
41
  function expandTokens(tokens) {
39
- return [...new Set(tokens.flatMap((token) => [token, ...splitIdentifier(token)]))];
42
+ return [
43
+ ...new Set(tokens.flatMap((token) => {
44
+ const parts = [token, ...splitIdentifier(token)];
45
+ // Add prefix stems so "authentication" also matches "auth",
46
+ // "configuration" matches "config", etc. Only for longer tokens
47
+ // to avoid over-matching short words.
48
+ // Use a short 4-char stem (catches root prefixes like "auth", "conf", "init")
49
+ // and a half-length stem (catches "config" from "configuration", "authent" from
50
+ // "authentication"). Both are narrower than the full token so they find real matches
51
+ // without over-fetching.
52
+ if (token.length >= 8) {
53
+ parts.push(token.slice(0, 4));
54
+ const half = Math.floor(token.length * 0.5);
55
+ if (half > 4)
56
+ parts.push(token.slice(0, half));
57
+ }
58
+ return parts;
59
+ })),
60
+ ];
40
61
  }
41
62
  function splitIdentifier(value) {
42
63
  return value
@@ -202,6 +202,22 @@ const filePath = payload?.tool_input?.file_path || payload?.toolInput?.file_path
202
202
  if (!filePath) process.exit(0);
203
203
  args.push(filePath);`
204
204
  : '';
205
+ // On session end: run stale check first (refreshes atom statuses against current
206
+ // maps without needing a topic), then end the session with --conclude so any
207
+ // pending knowledge is captured. Both run regardless of each other's exit code.
208
+ if (event === 'end') {
209
+ return `#!/usr/bin/env node
210
+ const { spawnSync } = require('node:child_process');
211
+
212
+ // P3: refresh atom statuses against current maps so needs-review/stale atoms
213
+ // are up to date before the session ends. Output ignored — hook is silent.
214
+ spawnSync('kgraph', ['stale', '--json'], { stdio: 'ignore' });
215
+
216
+ // P2: end the session and trigger conclude so durable knowledge is captured.
217
+ const result = spawnSync('kgraph', ['session', 'end', '--agent', 'claude-code', '--conclude', '--source', 'automatic'], { stdio: 'ignore' });
218
+ process.exit(result.status || 0);
219
+ `;
220
+ }
205
221
  return `#!/usr/bin/env node
206
222
  const { spawnSync } = require('node:child_process');
207
223
 
@@ -1,3 +1,4 @@
1
+ import { agentSkillFiles } from '../agent-skills.js';
1
2
  import { bulletWorkflow } from '../workflow-steps.js';
2
3
  export const clineAdapter = {
3
4
  name: 'cline',
@@ -7,4 +8,5 @@ export const clineAdapter = {
7
8
 
8
9
  ${bulletWorkflow('cline')}
9
10
  `,
11
+ commandFiles: agentSkillFiles('cline'),
10
12
  };
@@ -1,3 +1,4 @@
1
+ import { agentSkillFiles } from '../agent-skills.js';
1
2
  import { bulletWorkflow } from '../workflow-steps.js';
2
3
  export const cursorAdapter = {
3
4
  name: 'cursor',
@@ -12,5 +13,6 @@ alwaysApply: true
12
13
 
13
14
  ${bulletWorkflow('cursor')}
14
15
  `,
16
+ commandFiles: agentSkillFiles('cursor'),
15
17
  obsoleteCommandFiles: ['.cursor/rules/kgraph-commands.mdc'],
16
18
  };
@@ -1,10 +1,13 @@
1
- import { bulletWorkflow } from '../workflow-steps.js';
1
+ import { agentSkillFiles } from '../agent-skills.js';
2
+ import { numberedWorkflow } from '../workflow-steps.js';
2
3
  export const geminiAdapter = {
3
4
  name: 'gemini',
4
5
  label: 'Gemini CLI',
5
6
  targetPath: 'GEMINI.md',
6
7
  instructions: `## KGraph Workflow
7
8
 
8
- ${bulletWorkflow('gemini')}
9
+ ${numberedWorkflow('gemini')}
9
10
  `,
11
+ commandFiles: agentSkillFiles('gemini'),
12
+ obsoleteCommandFiles: [],
10
13
  };
@@ -1,3 +1,4 @@
1
+ import { agentSkillFiles } from '../agent-skills.js';
1
2
  import { bulletWorkflow } from '../workflow-steps.js';
2
3
  export const windsurfAdapter = {
3
4
  name: 'windsurf',
@@ -7,4 +8,6 @@ export const windsurfAdapter = {
7
8
 
8
9
  ${bulletWorkflow('windsurf')}
9
10
  `,
11
+ commandFiles: agentSkillFiles('windsurf'),
12
+ obsoleteCommandFiles: [],
10
13
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kentwynn/kgraph",
3
- "version": "0.2.37",
3
+ "version": "0.2.38",
4
4
  "description": "Persistent repo intelligence for AI coding assistants.",
5
5
  "type": "module",
6
6
  "bin": {