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