@kentwynn/kgraph 0.2.28 → 0.2.30
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 +11 -5
- package/dist/cli/commands/knowledge.js +36 -21
- package/dist/cli/commands/pack.js +16 -0
- package/dist/cli/commands/workflow.js +22 -1
- package/dist/cognition/cognition-quality.js +49 -48
- package/dist/integrations/adapters/claude-code.js +2 -9
- package/dist/integrations/adapters/copilot.js +14 -1
- package/dist/integrations/instruction-blocks.js +14 -4
- package/dist/integrations/workflow-steps.js +24 -16
- package/dist/knowledge/atom-store.d.ts +4 -0
- package/dist/knowledge/atom-store.js +81 -22
- package/dist/types/knowledge.d.ts +4 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -163,10 +163,12 @@ kgraph doctor
|
|
|
163
163
|
|
|
164
164
|
After useful AI work, assistants save durable runtime-capture notes into `.kgraph/inbox/`. These notes are not project documentation; they are KGraph input files that the next `kgraph` run processes automatically. You can also process them directly with `kgraph update`.
|
|
165
165
|
|
|
166
|
-
Normal agent flow is intentionally small
|
|
166
|
+
Normal agent flow is intentionally small. For coding context, agents use the
|
|
167
|
+
machine-readable pack. When memory refresh or inbox processing matters, they use
|
|
168
|
+
the root workflow or `kgraph update`.
|
|
167
169
|
|
|
168
170
|
```bash
|
|
169
|
-
kgraph "topic"
|
|
171
|
+
kgraph pack "topic" --budget 8000 --json --agent codex
|
|
170
172
|
# work normally
|
|
171
173
|
kgraph "topic" --final --agent codex
|
|
172
174
|
# if final check requires capture:
|
|
@@ -175,6 +177,10 @@ kgraph "topic" --capture "durable conclusion" --capture-file path/to/file.ts --c
|
|
|
175
177
|
|
|
176
178
|
`kgraph "<topic>"` is the smart root workflow: it refreshes maps, processes capture notes, reports memory health, and returns focused context. Agents can pass `--agent <name>` to record a lightweight session context event. Agents can still use `kgraph pack "<topic>" --budget 8000 --json --agent <name>` when they need the stable machine-readable `ContextPack` contract with atoms, source ranges, git changes, omitted items, token estimates, and inclusion reasons.
|
|
177
179
|
|
|
180
|
+
`pack` does not process `.kgraph/inbox/`. If a pack reports pending inbox notes,
|
|
181
|
+
run `kgraph "<topic>" --agent <name>` or `kgraph update` before relying on
|
|
182
|
+
`kgraph history` or newly captured atoms.
|
|
183
|
+
|
|
178
184
|
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.
|
|
179
185
|
|
|
180
186
|
Agents can also report session activity so KGraph can estimate token waste:
|
|
@@ -381,12 +387,12 @@ kgraph integrate list
|
|
|
381
387
|
kgraph integrate remove cursor
|
|
382
388
|
```
|
|
383
389
|
|
|
384
|
-
New integrations default to `always` mode, so every chat in the repository starts with
|
|
390
|
+
New integrations default to `always` mode, so every chat in the repository starts with the matching KGraph command. Use `--mode smart` to run KGraph only for repo-specific work, or `--mode manual` to run only when explicitly asked.
|
|
385
391
|
|
|
386
392
|
| Mode | Behavior |
|
|
387
393
|
| -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
388
|
-
| `always` | Every chat in the repository starts with `kgraph
|
|
389
|
-
| `smart` | Runs KGraph automatically for repo-specific coding, debugging, architecture, refactor, review, or file-exploration requests. Skips simple conversational requests that do not depend on repo knowledge. |
|
|
394
|
+
| `always` | Every chat in the repository starts with the matching KGraph command. Normal coding context uses `kgraph pack`; history, inbox/update, knowledge, and doctor requests route to their specific commands. |
|
|
395
|
+
| `smart` | Runs the matching KGraph command automatically for repo-specific coding, debugging, architecture, refactor, review, history, inbox/update, knowledge, health, or file-exploration requests. Skips simple conversational requests that do not depend on repo knowledge. |
|
|
390
396
|
| `manual` | Exposes KGraph commands and instructions, but the agent runs KGraph only when the user explicitly asks. |
|
|
391
397
|
| `off` | Disables that integration and removes generated KGraph instruction blocks/command files. |
|
|
392
398
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { atomToCognitionNote, readKnowledgeAtoms, updateKnowledgeAtom, } from '../../knowledge/atom-store.js';
|
|
1
|
+
import { atomToCognitionNote, readKnowledgeAtoms, updateKnowledgeAtom, updateKnowledgeAtoms, } from '../../knowledge/atom-store.js';
|
|
2
2
|
import { rebuildDomainRecords } from '../../cognition/domain-records.js';
|
|
3
3
|
import { assertWorkspace } from '../../storage/kgraph-paths.js';
|
|
4
4
|
import { readMaps } from '../../storage/map-store.js';
|
|
@@ -82,28 +82,43 @@ export function registerKnowledgeCommand(program) {
|
|
|
82
82
|
.option('--json', 'Print JSON output')
|
|
83
83
|
.action((oldId, newId, options) => runCommand(async () => {
|
|
84
84
|
const workspace = await assertWorkspace(process.cwd());
|
|
85
|
-
await requireAtom(workspace, newId);
|
|
86
85
|
const now = new Date().toISOString();
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
86
|
+
const result = await updateKnowledgeAtoms(workspace, (atoms) => {
|
|
87
|
+
const oldIndex = atoms.findIndex((atom) => atom.id === oldId);
|
|
88
|
+
const newIndex = atoms.findIndex((atom) => atom.id === newId);
|
|
89
|
+
if (oldIndex === -1) {
|
|
90
|
+
throw new KGraphError(`Knowledge atom not found: ${oldId}`);
|
|
91
|
+
}
|
|
92
|
+
if (newIndex === -1) {
|
|
93
|
+
throw new KGraphError(`Knowledge atom not found: ${newId}`);
|
|
94
|
+
}
|
|
95
|
+
const nextAtoms = [...atoms];
|
|
96
|
+
nextAtoms[oldIndex] = {
|
|
97
|
+
...nextAtoms[oldIndex],
|
|
98
|
+
status: 'archived',
|
|
99
|
+
provenance: { ...nextAtoms[oldIndex].provenance, updatedAt: now },
|
|
100
|
+
lifecycle: {
|
|
101
|
+
...nextAtoms[oldIndex].lifecycle,
|
|
102
|
+
supersededBy: newId,
|
|
103
|
+
archivedAt: now,
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
nextAtoms[newIndex] = {
|
|
107
|
+
...nextAtoms[newIndex],
|
|
108
|
+
provenance: { ...nextAtoms[newIndex].provenance, updatedAt: now },
|
|
109
|
+
lifecycle: {
|
|
110
|
+
...nextAtoms[newIndex].lifecycle,
|
|
111
|
+
supersedes: [
|
|
112
|
+
...new Set([...nextAtoms[newIndex].lifecycle.supersedes, oldId]),
|
|
113
|
+
],
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
return {
|
|
117
|
+
atoms: nextAtoms,
|
|
118
|
+
result: { old: nextAtoms[oldIndex], new: nextAtoms[newIndex] },
|
|
119
|
+
};
|
|
120
|
+
});
|
|
105
121
|
await rebuildActiveDomainRecords(workspace);
|
|
106
|
-
const result = { old: oldAtom, new: newAtom };
|
|
107
122
|
console.log(options.json
|
|
108
123
|
? JSON.stringify(result, null, 2)
|
|
109
124
|
: `Superseded ${oldId} with ${newId}`);
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
1
2
|
import { buildContextPack } from '../../context/context-pack.js';
|
|
2
3
|
import { queryContext } from '../../context/context-query.js';
|
|
3
4
|
import { loadConfig } from '../../config/config.js';
|
|
4
5
|
import { assertSessionAgent, recordSessionEvent, } from '../../session/session-store.js';
|
|
5
6
|
import { assertWorkspace } from '../../storage/kgraph-paths.js';
|
|
7
|
+
import { listInboxNotes } from '../../storage/cognition-store.js';
|
|
6
8
|
import { mapsExist, readMaps } from '../../storage/map-store.js';
|
|
7
9
|
import { KGraphError, runCommand } from '../errors.js';
|
|
8
10
|
export function registerPackCommand(program) {
|
|
@@ -39,6 +41,17 @@ export function registerPackCommand(program) {
|
|
|
39
41
|
]);
|
|
40
42
|
const response = await queryContext(workspace, config, maps, task);
|
|
41
43
|
const pack = buildContextPack(response, budget, workspace.rootPath);
|
|
44
|
+
const pendingInboxFiles = (await listInboxNotes(workspace)).map((file) => path.relative(workspace.rootPath, file));
|
|
45
|
+
if (pendingInboxFiles.length > 0) {
|
|
46
|
+
pack.pendingInbox = {
|
|
47
|
+
count: pendingInboxFiles.length,
|
|
48
|
+
files: pendingInboxFiles,
|
|
49
|
+
};
|
|
50
|
+
pack.warnings = [
|
|
51
|
+
...pack.warnings,
|
|
52
|
+
`Pending inbox notes are not processed by pack. Run \`kgraph "${task}"${agent ? ` --agent ${agent}` : ''}\` or \`kgraph update\` before relying on history or newly captured atoms.`,
|
|
53
|
+
];
|
|
54
|
+
}
|
|
42
55
|
if (options.json) {
|
|
43
56
|
console.log(JSON.stringify(pack, null, 2));
|
|
44
57
|
return;
|
|
@@ -69,6 +82,9 @@ export function renderPackText(pack) {
|
|
|
69
82
|
` omitted ${pack.omitted.length}`,
|
|
70
83
|
``,
|
|
71
84
|
];
|
|
85
|
+
if (pack.pendingInbox && pack.pendingInbox.count > 0) {
|
|
86
|
+
lines.push(`● Pending Inbox`, ` ${pack.pendingInbox.count} note${pack.pendingInbox.count === 1 ? '' : 's'} waiting; pack does not process inbox notes`, ` run kgraph "${pack.task}" or kgraph update before relying on history/new atoms`, ``);
|
|
87
|
+
}
|
|
72
88
|
appendGroup(lines, 'Atoms', pack.items.filter((item) => item.kind === 'atom'));
|
|
73
89
|
appendGroup(lines, 'Git Changes', pack.items.filter((item) => item.kind === 'git-change'));
|
|
74
90
|
appendGroup(lines, 'Source Ranges', pack.items.filter((item) => item.kind === 'file-range'));
|
|
@@ -227,7 +227,24 @@ function atomsHaveReplacementSignal(a, b) {
|
|
|
227
227
|
bSymbols.add(ref.name);
|
|
228
228
|
}
|
|
229
229
|
const symbolOverlap = [...aSymbols].some((symbol) => bSymbols.has(symbol));
|
|
230
|
-
|
|
230
|
+
if (symbolOverlap)
|
|
231
|
+
return true;
|
|
232
|
+
if (!atomsShareFile(a, b))
|
|
233
|
+
return false;
|
|
234
|
+
return meaningfulTopicOverlap(a.topic, b.topic);
|
|
235
|
+
}
|
|
236
|
+
function atomsShareFile(a, b) {
|
|
237
|
+
const aFiles = new Set(a.scopeRefs.files);
|
|
238
|
+
for (const ref of a.evidenceRefs) {
|
|
239
|
+
if (ref.type === 'file')
|
|
240
|
+
aFiles.add(ref.path);
|
|
241
|
+
}
|
|
242
|
+
const bFiles = new Set(b.scopeRefs.files);
|
|
243
|
+
for (const ref of b.evidenceRefs) {
|
|
244
|
+
if (ref.type === 'file')
|
|
245
|
+
bFiles.add(ref.path);
|
|
246
|
+
}
|
|
247
|
+
return [...aFiles].some((file) => bFiles.has(file));
|
|
231
248
|
}
|
|
232
249
|
function meaningfulTopicOverlap(a, b) {
|
|
233
250
|
const weakTokens = new Set([
|
|
@@ -239,6 +256,10 @@ function meaningfulTopicOverlap(a, b) {
|
|
|
239
256
|
'new',
|
|
240
257
|
'old',
|
|
241
258
|
'review',
|
|
259
|
+
'check',
|
|
260
|
+
'current',
|
|
261
|
+
'session',
|
|
262
|
+
'smoke',
|
|
242
263
|
'update',
|
|
243
264
|
'with',
|
|
244
265
|
]);
|
|
@@ -13,7 +13,7 @@ export async function analyzeCognitionQuality(workspace, maps) {
|
|
|
13
13
|
.map((note) => analyzeNote(note, maps))
|
|
14
14
|
.filter((change) => change.removedFileRefs.length > 0 ||
|
|
15
15
|
change.removedSymbolRefs.length > 0);
|
|
16
|
-
const orphanedNoteCount = notes.filter((note) => note.
|
|
16
|
+
const orphanedNoteCount = notes.filter((note) => analyzeNote(note, maps).nextStatus === 'stale').length;
|
|
17
17
|
return {
|
|
18
18
|
atomCount: activeAtoms.length,
|
|
19
19
|
staleAtomCount: activeAtoms.filter((atom) => atom.status === 'stale').length,
|
|
@@ -64,54 +64,55 @@ export async function repairCognition(workspace, maps, dryRun = false) {
|
|
|
64
64
|
}
|
|
65
65
|
// Archive fully-orphaned notes (all refs dead) so they no longer appear in context
|
|
66
66
|
const orphanedNotes = nextNotes.filter((note) => note.referencesStatus === 'stale');
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
return {
|
|
78
|
-
...atom,
|
|
79
|
-
status: 'archived',
|
|
80
|
-
confidence: 'low',
|
|
81
|
-
lifecycle: { ...atom.lifecycle, archivedAt: now },
|
|
82
|
-
provenance: { ...atom.provenance, updatedAt: now },
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
const removedFiles = new Set(change?.removedFileRefs ?? []);
|
|
86
|
-
const removedSymbols = new Set(change?.removedSymbolRefs ?? []);
|
|
87
|
-
const nextStatus = atomStatusFromReferenceStatus(change?.nextStatus ?? 'current');
|
|
67
|
+
const now = new Date().toISOString();
|
|
68
|
+
const orphanedNoteIds = new Set(orphanedNotes.map((note) => note.id));
|
|
69
|
+
const nextAtoms = atoms.map((atom) => {
|
|
70
|
+
if (atom.status === 'archived')
|
|
71
|
+
return atom;
|
|
72
|
+
const change = changesById.get(atom.id);
|
|
73
|
+
if (!change && !orphanedNoteIds.has(atom.id)) {
|
|
74
|
+
return atom;
|
|
75
|
+
}
|
|
76
|
+
if (orphanedNoteIds.has(atom.id)) {
|
|
88
77
|
return {
|
|
89
78
|
...atom,
|
|
90
|
-
status:
|
|
91
|
-
confidence:
|
|
92
|
-
|
|
93
|
-
: atom.confidence,
|
|
94
|
-
scopeRefs: {
|
|
95
|
-
...atom.scopeRefs,
|
|
96
|
-
files: atom.scopeRefs.files.filter((file) => !removedFiles.has(file)),
|
|
97
|
-
symbols: atom.scopeRefs.symbols.filter((symbol) => !removedSymbols.has(symbol)),
|
|
98
|
-
},
|
|
99
|
-
evidenceRefs: atom.evidenceRefs.filter((ref) => {
|
|
100
|
-
if (ref.type === 'file')
|
|
101
|
-
return !removedFiles.has(ref.path);
|
|
102
|
-
if (ref.type === 'symbol')
|
|
103
|
-
return !removedSymbols.has(ref.name);
|
|
104
|
-
return true;
|
|
105
|
-
}),
|
|
106
|
-
lifecycle: {
|
|
107
|
-
...atom.lifecycle,
|
|
108
|
-
invalidatedBy: change?.nextStatus === 'current'
|
|
109
|
-
? undefined
|
|
110
|
-
: atom.lifecycle.invalidatedBy,
|
|
111
|
-
},
|
|
79
|
+
status: 'archived',
|
|
80
|
+
confidence: 'low',
|
|
81
|
+
lifecycle: { ...atom.lifecycle, archivedAt: now },
|
|
112
82
|
provenance: { ...atom.provenance, updatedAt: now },
|
|
113
83
|
};
|
|
114
|
-
}
|
|
84
|
+
}
|
|
85
|
+
const removedFiles = new Set(change?.removedFileRefs ?? []);
|
|
86
|
+
const removedSymbols = new Set(change?.removedSymbolRefs ?? []);
|
|
87
|
+
const nextStatus = atomStatusFromReferenceStatus(change?.nextStatus ?? 'current');
|
|
88
|
+
return {
|
|
89
|
+
...atom,
|
|
90
|
+
status: nextStatus,
|
|
91
|
+
confidence: atom.confidence === 'low' && atom.status === 'stale' && nextStatus !== 'stale'
|
|
92
|
+
? 'medium'
|
|
93
|
+
: atom.confidence,
|
|
94
|
+
scopeRefs: {
|
|
95
|
+
...atom.scopeRefs,
|
|
96
|
+
files: atom.scopeRefs.files.filter((file) => !removedFiles.has(file)),
|
|
97
|
+
symbols: atom.scopeRefs.symbols.filter((symbol) => !removedSymbols.has(symbol)),
|
|
98
|
+
},
|
|
99
|
+
evidenceRefs: atom.evidenceRefs.filter((ref) => {
|
|
100
|
+
if (ref.type === 'file')
|
|
101
|
+
return !removedFiles.has(ref.path);
|
|
102
|
+
if (ref.type === 'symbol')
|
|
103
|
+
return !removedSymbols.has(ref.name);
|
|
104
|
+
return true;
|
|
105
|
+
}),
|
|
106
|
+
lifecycle: {
|
|
107
|
+
...atom.lifecycle,
|
|
108
|
+
invalidatedBy: change?.nextStatus === 'current'
|
|
109
|
+
? undefined
|
|
110
|
+
: atom.lifecycle.invalidatedBy,
|
|
111
|
+
},
|
|
112
|
+
provenance: { ...atom.provenance, updatedAt: now },
|
|
113
|
+
};
|
|
114
|
+
});
|
|
115
|
+
if (!dryRun && (changes.length > 0 || orphanedNotes.length > 0)) {
|
|
115
116
|
await writeKnowledgeAtoms(workspace, nextAtoms);
|
|
116
117
|
// Exclude fully-orphaned notes from domain records — they are being archived
|
|
117
118
|
await repairDomainRecords(workspace, nextAtoms
|
|
@@ -126,9 +127,9 @@ export async function repairCognition(workspace, maps, dryRun = false) {
|
|
|
126
127
|
const orphanedNoteCount = orphanedNotes.length;
|
|
127
128
|
return {
|
|
128
129
|
atomCount: activeAtoms.length,
|
|
129
|
-
staleAtomCount:
|
|
130
|
-
needsReviewAtomCount:
|
|
131
|
-
archivedAtomCount:
|
|
130
|
+
staleAtomCount: nextAtoms.filter((atom) => atom.status === 'stale').length,
|
|
131
|
+
needsReviewAtomCount: nextAtoms.filter((atom) => atom.status === 'needs-review').length,
|
|
132
|
+
archivedAtomCount: nextAtoms.filter((atom) => atom.status === 'archived').length,
|
|
132
133
|
duplicateAtomTopicCount: countDuplicateTitles(nextNotes),
|
|
133
134
|
noteCount: notes.length,
|
|
134
135
|
mixedOrStaleCount: nextNotes.filter((note) => ['mixed', 'stale', 'unresolved'].includes(note.referencesStatus)).length,
|
|
@@ -12,16 +12,9 @@ ${numberedWorkflow('claude-code', {
|
|
|
12
12
|
commandFiles: [
|
|
13
13
|
{
|
|
14
14
|
path: '.claude/commands/kgraph.md',
|
|
15
|
-
content:
|
|
15
|
+
content: `## KGraph Workflow
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
2. Run exactly one command from the repository root: \`kgraph "<topic>" --agent claude-code\`.
|
|
19
|
-
3. Treat the returned files, symbols, relationships, atoms, and warnings as the first-pass source of truth.
|
|
20
|
-
4. If the user asked for an edit, inspect only the returned candidate file or the smallest necessary range, then make the edit.
|
|
21
|
-
5. Verify the change actually landed before claiming completion. Prefer a narrow read of the changed range or \`git diff -- <path>\`; if there is no diff or the expected text is missing, say the edit did not apply and fix it before summarizing.
|
|
22
|
-
6. Do not run \`kgraph\` again, \`kgraph context\`, \`kgraph pack\`, \`kgraph knowledge\`, \`kgraph stale\`, \`kgraph blame\`, \`kgraph scan\`, \`kgraph update\`, \`kgraph compact\`, or \`kgraph repair\` unless the user explicitly asks for that lower-level command.
|
|
23
|
-
7. Do not continue broad repository search after the target file is identified. If a path must be located, prefer \`rg --files\` and quote paths containing spaces or parentheses.
|
|
24
|
-
8. At the end of repository-file changes, run \`kgraph "<topic>" --final --agent claude-code\`. If KGraph reports capture-required, run \`kgraph "<topic>" --capture "<durable conclusion>" --capture-file <path> --capture-symbol <name> --agent claude-code\`, or explicitly say "No durable knowledge created" only when there is genuinely no reusable knowledge.
|
|
17
|
+
${numberedWorkflow('claude-code')}
|
|
25
18
|
`,
|
|
26
19
|
},
|
|
27
20
|
{
|
|
@@ -8,6 +8,19 @@ export const copilotAdapter = {
|
|
|
8
8
|
|
|
9
9
|
${numberedWorkflow('copilot')}
|
|
10
10
|
`,
|
|
11
|
-
commandFiles:
|
|
11
|
+
commandFiles: [
|
|
12
|
+
...agentSkillFiles('copilot'),
|
|
13
|
+
{
|
|
14
|
+
path: '.github/agents/kgraph.agent.md',
|
|
15
|
+
content: `---
|
|
16
|
+
description: Use KGraph persistent repo intelligence — runs kgraph before answering to provide file maps, symbols, relationships, and durable knowledge atoms.
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## KGraph Workflow
|
|
20
|
+
|
|
21
|
+
${numberedWorkflow('copilot')}
|
|
22
|
+
`,
|
|
23
|
+
},
|
|
24
|
+
],
|
|
12
25
|
obsoleteCommandFiles: [],
|
|
13
26
|
};
|
|
@@ -40,16 +40,26 @@ export function renderContextPolicy(mode, agentName) {
|
|
|
40
40
|
const packCommand = agentName
|
|
41
41
|
? `kgraph pack "<topic>" --budget 8000 --json --agent ${agentName}`
|
|
42
42
|
: 'kgraph pack "<topic>" --budget 8000 --json';
|
|
43
|
+
const rootCommand = agentName
|
|
44
|
+
? `kgraph "<topic>" --agent ${agentName}`
|
|
45
|
+
: 'kgraph "<topic>"';
|
|
46
|
+
const routing = [
|
|
47
|
+
'Command routing comes first.',
|
|
48
|
+
'If the user asks about prior work, previous decisions, timeline, or history, run `kgraph history "<topic>"` instead of pack.',
|
|
49
|
+
'If the user asks what KGraph remembers, atom provenance, or durable knowledge, run `kgraph knowledge list --topic "<topic>"` or `kgraph knowledge get <atom-id>` instead of pack.',
|
|
50
|
+
`If the user asks to process inbox notes, pending capture, refresh memory, or update cognition, run \`${rootCommand}\` or low-level \`kgraph update\` instead of pack.`,
|
|
51
|
+
'If the user asks for setup, health, broken integrations, or quality, run `kgraph doctor` or `kgraph doctor --quality` instead of pack.',
|
|
52
|
+
].join(' ');
|
|
43
53
|
switch (mode) {
|
|
44
54
|
case 'always':
|
|
45
|
-
return `Every chat in this repository must
|
|
55
|
+
return `Every chat in this repository must use the correct KGraph command before answering or exploring files. ${routing} For normal repo context, code navigation, debugging, review, or edits, run \`${packCommand}\`. If that pack reports pending inbox notes, run \`${rootCommand}\` or \`kgraph update\` before relying on history or newly captured atoms. Infer the topic from the user's message. This records a lightweight KGraph session context event for the agent. ${useResultBoundary}`;
|
|
46
56
|
case 'manual':
|
|
47
|
-
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.';
|
|
57
|
+
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. If the user explicitly asks for KGraph history, inbox/update, knowledge, or doctor, run that specific KGraph command instead of pack.';
|
|
48
58
|
case 'off':
|
|
49
59
|
return 'KGraph is disabled for this integration.';
|
|
50
60
|
case 'smart':
|
|
51
61
|
default:
|
|
52
|
-
return `For repo-specific coding, debugging, architecture, refactor, review,
|
|
62
|
+
return `For repo-specific coding, debugging, architecture, refactor, review, file-exploration, history, inbox/update, knowledge, or health requests, run the matching KGraph command before broad repository exploration. ${routing} For normal repo context, code navigation, debugging, review, or edits, run \`${packCommand}\`. If that pack reports pending inbox notes, run \`${rootCommand}\` or \`kgraph update\` before relying on history or newly captured atoms. Infer the topic from the user's message. This records a lightweight KGraph session context event only when KGraph is actually used. Skip KGraph for simple conversational requests that do not depend on repo knowledge. ${useResultBoundary}`;
|
|
53
63
|
}
|
|
54
64
|
}
|
|
55
65
|
export function renderCapturePolicy(agentName) {
|
|
@@ -68,7 +78,7 @@ export function renderCapturePolicy(agentName) {
|
|
|
68
78
|
- Use \`.kgraph/inbox/<slug>.md\` only when a longer structured note is clearer than a single \`kgraph conclude\` command.
|
|
69
79
|
- 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.
|
|
70
80
|
- 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.
|
|
71
|
-
- Do not run KGraph repeatedly.
|
|
81
|
+
- Do not run KGraph repeatedly. At the start, run the one KGraph command that matches the request: \`kgraph history "<topic>"\` for history, \`kgraph update\` or \`kgraph "<topic>"${agentName ? ` --agent ${agentName}` : ''}\` for inbox/update, and \`kgraph pack "<topic>" --budget 8000 --json${agentName ? ` --agent ${agentName}` : ''}\` for normal coding context. If repo files changed, run \`${finalCommand}\` once before the final answer.
|
|
72
82
|
- After the final \`kgraph\` run, mention whether durable cognition was stored or processed.
|
|
73
83
|
|
|
74
84
|
When using an inbox note, use this structure:
|
|
@@ -8,15 +8,19 @@ 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
|
+
function inboxStep(agentName) {
|
|
12
|
+
return `If the previous command output contains "pending inbox" or a \`pendingInbox\` field with count > 0, you MUST run \`kgraph "<topic>" --agent ${agentName}\` or \`kgraph update\` immediately before proceeding. Do not skip this step.`;
|
|
13
|
+
}
|
|
11
14
|
function packStep(agentName) {
|
|
12
|
-
return `
|
|
15
|
+
return `For normal coding context, treat \`kgraph pack "<task>" --budget 8000 --json --agent ${agentName}\` as the machine-readable context contract: use atoms, source ranges, git changes, omitted items, and inclusion reasons from the ContextPack before reading files.`;
|
|
13
16
|
}
|
|
14
17
|
function smartRootStep(agentName) {
|
|
15
|
-
return `
|
|
18
|
+
return `Use the root workflow when refresh or memory processing matters: run \`kgraph "<topic>" --agent ${agentName}\` to refresh maps, process inbox notes, and return a briefing; run \`kgraph "<topic>" --final --agent ${agentName}\` before the final answer when repository files changed; run \`kgraph "<topic>" --capture "<durable conclusion>" --capture-file <path> --capture-symbol <name> --agent ${agentName}\` when the final check requires durable knowledge.`;
|
|
16
19
|
}
|
|
17
20
|
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.`;
|
|
18
21
|
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.`;
|
|
19
22
|
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.`;
|
|
23
|
+
const ROUTING_STEP = `Route explicit KGraph requests to the matching command before any default context lookup: history/prior work -> \`kgraph history "<topic>"\`; inbox/pending capture/process notes/update cognition -> \`kgraph "<topic>"\` or \`kgraph update\`; remembered knowledge/atoms/provenance -> \`kgraph knowledge list --topic "<topic>"\` or \`kgraph knowledge get <atom-id>\`; setup/health -> \`kgraph doctor\`.`;
|
|
20
24
|
function sessionStep(agentName, qualifier) {
|
|
21
25
|
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`;
|
|
22
26
|
return qualifier ? `${base} ${qualifier}.` : `${base}.`;
|
|
@@ -28,23 +32,25 @@ function sessionStep(agentName, qualifier) {
|
|
|
28
32
|
export function numberedWorkflow(agentName, options = {}) {
|
|
29
33
|
return `1. Infer the topic from the user's request.
|
|
30
34
|
2. {{KGRAPH_CONTEXT_POLICY}}
|
|
31
|
-
3.
|
|
32
|
-
4. ${
|
|
33
|
-
5.
|
|
34
|
-
6. ${
|
|
35
|
-
7. ${
|
|
36
|
-
8. ${
|
|
37
|
-
9. ${
|
|
38
|
-
10. ${
|
|
39
|
-
11. ${
|
|
40
|
-
12. ${
|
|
35
|
+
3. ${inboxStep(agentName)}
|
|
36
|
+
4. ${ROUTING_STEP}
|
|
37
|
+
5. Use the returned files, symbols, relationships, and cognition before broad exploration.
|
|
38
|
+
6. ${EXPLORATION_BOUNDARY_STEP}
|
|
39
|
+
7. ${VERIFY_EDIT_STEP}
|
|
40
|
+
8. ${smartRootStep(agentName)}
|
|
41
|
+
9. ${packStep(agentName)}
|
|
42
|
+
10. ${KNOWLEDGE_STEP}
|
|
43
|
+
11. ${DOCTOR_STEP}
|
|
44
|
+
12. ${STALE_STEP}
|
|
45
|
+
13. ${sessionStep(agentName, options.sessionQualifier)}
|
|
46
|
+
14. ${IMPACT_STEP}
|
|
41
47
|
|
|
42
48
|
{{KGRAPH_CAPTURE_POLICY}}
|
|
43
49
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
50
|
+
15. ${REPAIR_STEP}
|
|
51
|
+
16. ${COMPACT_STEP}
|
|
52
|
+
17. Run \`kgraph visualize\` when the user wants to inspect the dependency graph — opens an interactive graph at http://localhost:4242 with PNG export.
|
|
53
|
+
18. ${HISTORY_STEP}`;
|
|
48
54
|
}
|
|
49
55
|
/**
|
|
50
56
|
* Returns the bullet-list workflow for rules files.
|
|
@@ -52,6 +58,8 @@ export function numberedWorkflow(agentName, options = {}) {
|
|
|
52
58
|
*/
|
|
53
59
|
export function bulletWorkflow(agentName, options = {}) {
|
|
54
60
|
return `- {{KGRAPH_CONTEXT_POLICY}}
|
|
61
|
+
- ${inboxStep(agentName)}
|
|
62
|
+
- ${ROUTING_STEP}
|
|
55
63
|
- ${EXPLORATION_BOUNDARY_STEP}
|
|
56
64
|
- ${VERIFY_EDIT_STEP}
|
|
57
65
|
- ${smartRootStep(agentName)}
|
|
@@ -41,6 +41,10 @@ export declare function createKnowledgeAtom(workspace: KGraphWorkspace, input: A
|
|
|
41
41
|
}): Promise<KnowledgeAtom>;
|
|
42
42
|
export declare function migrateLegacyCognitionToAtoms(workspace: KGraphWorkspace): Promise<void>;
|
|
43
43
|
export declare function updateKnowledgeAtom(workspace: KGraphWorkspace, atomId: string, updater: (atom: KnowledgeAtom) => KnowledgeAtom): Promise<KnowledgeAtom>;
|
|
44
|
+
export declare function updateKnowledgeAtoms<T>(workspace: KGraphWorkspace, updater: (atoms: KnowledgeAtom[]) => {
|
|
45
|
+
atoms: KnowledgeAtom[];
|
|
46
|
+
result: T;
|
|
47
|
+
}): Promise<T>;
|
|
44
48
|
export declare function refreshKnowledgeAtomStatuses(workspace: KGraphWorkspace, maps: {
|
|
45
49
|
fileMap: FileMap;
|
|
46
50
|
symbolMap: SymbolMap;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
2
3
|
import path from 'node:path';
|
|
4
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
3
5
|
import { KGraphError } from '../cli/errors.js';
|
|
4
6
|
import { getCurrentCommit } from '../scanner/git-utils.js';
|
|
5
7
|
import { readCognitionNotes } from '../storage/cognition-store.js';
|
|
@@ -17,7 +19,7 @@ export async function ensureKnowledgeStore(workspace) {
|
|
|
17
19
|
});
|
|
18
20
|
}
|
|
19
21
|
if (!(await pathExists(atomsPath(workspace)))) {
|
|
20
|
-
await
|
|
22
|
+
await atomicWriteFile(atomsPath(workspace), '');
|
|
21
23
|
}
|
|
22
24
|
}
|
|
23
25
|
export async function readKnowledgeAtoms(workspace) {
|
|
@@ -45,16 +47,21 @@ export function parseAtomsJsonl(raw) {
|
|
|
45
47
|
return atoms;
|
|
46
48
|
}
|
|
47
49
|
export async function writeKnowledgeAtoms(workspace, atoms) {
|
|
50
|
+
await withKnowledgeWriteLock(workspace, () => writeKnowledgeAtomsUnlocked(workspace, atoms));
|
|
51
|
+
}
|
|
52
|
+
async function writeKnowledgeAtomsUnlocked(workspace, atoms) {
|
|
48
53
|
await ensureKnowledgeStore(workspace);
|
|
49
|
-
await
|
|
50
|
-
(atoms.length > 0 ? '\n' : '')
|
|
54
|
+
await atomicWriteFile(atomsPath(workspace), atoms.map((atom) => JSON.stringify(atom)).join('\n') +
|
|
55
|
+
(atoms.length > 0 ? '\n' : ''));
|
|
51
56
|
await writeKnowledgeIndexes(workspace, atoms);
|
|
52
57
|
await touchSchema(workspace);
|
|
53
58
|
}
|
|
54
59
|
export async function appendKnowledgeAtom(workspace, atom) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
60
|
+
await withKnowledgeWriteLock(workspace, async () => {
|
|
61
|
+
const atoms = await readAtomsFile(workspace);
|
|
62
|
+
atoms.push(atom);
|
|
63
|
+
await writeKnowledgeAtomsUnlocked(workspace, atoms);
|
|
64
|
+
});
|
|
58
65
|
return atom;
|
|
59
66
|
}
|
|
60
67
|
export async function createKnowledgeAtom(workspace, input, maps) {
|
|
@@ -103,28 +110,51 @@ export async function migrateLegacyCognitionToAtoms(workspace) {
|
|
|
103
110
|
if (existingIds.has(id))
|
|
104
111
|
continue;
|
|
105
112
|
if (existing.some((atom) => atom.topic === note.title &&
|
|
106
|
-
atom.claim === (note.summary ?? note.title)
|
|
107
|
-
atom.provenance.createdAt === note.createdAt)) {
|
|
113
|
+
atom.claim === (note.summary ?? note.title))) {
|
|
108
114
|
continue;
|
|
109
115
|
}
|
|
110
116
|
migrated.push(legacyNoteToAtom(note, id));
|
|
111
117
|
}
|
|
112
118
|
if (migrated.length > 0) {
|
|
113
|
-
await
|
|
119
|
+
await withKnowledgeWriteLock(workspace, async () => {
|
|
120
|
+
const current = await readAtomsFile(workspace);
|
|
121
|
+
const currentIds = new Set(current.map((atom) => atom.id));
|
|
122
|
+
const nextMigrated = migrated.filter((atom) => !currentIds.has(atom.id) &&
|
|
123
|
+
!current.some((existing) => existing.topic === atom.topic &&
|
|
124
|
+
existing.claim === atom.claim));
|
|
125
|
+
if (nextMigrated.length > 0) {
|
|
126
|
+
await writeKnowledgeAtomsUnlocked(workspace, [
|
|
127
|
+
...current,
|
|
128
|
+
...nextMigrated,
|
|
129
|
+
]);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
114
132
|
}
|
|
115
133
|
else {
|
|
116
134
|
await writeKnowledgeIndexes(workspace, existing);
|
|
117
135
|
}
|
|
118
136
|
}
|
|
119
137
|
export async function updateKnowledgeAtom(workspace, atomId, updater) {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
138
|
+
await migrateLegacyCognitionToAtoms(workspace);
|
|
139
|
+
return withKnowledgeWriteLock(workspace, async () => {
|
|
140
|
+
const atoms = await readAtomsFile(workspace);
|
|
141
|
+
const index = atoms.findIndex((atom) => atom.id === atomId);
|
|
142
|
+
if (index === -1) {
|
|
143
|
+
throw new KGraphError(`Knowledge atom not found: ${atomId}`);
|
|
144
|
+
}
|
|
145
|
+
atoms[index] = updater(atoms[index]);
|
|
146
|
+
await writeKnowledgeAtomsUnlocked(workspace, atoms);
|
|
147
|
+
return atoms[index];
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
export async function updateKnowledgeAtoms(workspace, updater) {
|
|
151
|
+
await migrateLegacyCognitionToAtoms(workspace);
|
|
152
|
+
return withKnowledgeWriteLock(workspace, async () => {
|
|
153
|
+
const current = await readAtomsFile(workspace);
|
|
154
|
+
const { atoms, result } = updater(current);
|
|
155
|
+
await writeKnowledgeAtomsUnlocked(workspace, atoms);
|
|
156
|
+
return result;
|
|
157
|
+
});
|
|
128
158
|
}
|
|
129
159
|
export async function refreshKnowledgeAtomStatuses(workspace, maps, dryRun = false) {
|
|
130
160
|
const atoms = await readKnowledgeAtoms(workspace);
|
|
@@ -414,9 +444,9 @@ async function writeKnowledgeIndexes(workspace, atoms) {
|
|
|
414
444
|
await mkdir(indexesPath(workspace), { recursive: true });
|
|
415
445
|
const indexes = buildIndexes(atoms);
|
|
416
446
|
await Promise.all([
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
447
|
+
atomicWriteFile(path.join(indexesPath(workspace), 'terms.json'), JSON.stringify(indexes.terms, null, 2) + '\n'),
|
|
448
|
+
atomicWriteFile(path.join(indexesPath(workspace), 'refs.json'), JSON.stringify(indexes.refs, null, 2) + '\n'),
|
|
449
|
+
atomicWriteFile(path.join(indexesPath(workspace), 'topics.json'), JSON.stringify(indexes.topics, null, 2) + '\n'),
|
|
420
450
|
]);
|
|
421
451
|
}
|
|
422
452
|
function buildIndexes(atoms) {
|
|
@@ -454,7 +484,7 @@ async function readSchema(workspace) {
|
|
|
454
484
|
}
|
|
455
485
|
async function writeSchema(workspace, schema) {
|
|
456
486
|
await mkdir(workspace.knowledgePath, { recursive: true });
|
|
457
|
-
await
|
|
487
|
+
await atomicWriteFile(schemaPath(workspace), JSON.stringify(schema, null, 2) + '\n');
|
|
458
488
|
}
|
|
459
489
|
function buildAtomId(createdAt, seed) {
|
|
460
490
|
return `${createdAt.replace(/[:.]/g, '-')}-${slugify(seed) || 'atom'}`;
|
|
@@ -484,3 +514,32 @@ function schemaPath(workspace) {
|
|
|
484
514
|
function indexesPath(workspace) {
|
|
485
515
|
return path.join(workspace.knowledgePath, 'indexes');
|
|
486
516
|
}
|
|
517
|
+
async function atomicWriteFile(targetPath, content) {
|
|
518
|
+
await mkdir(path.dirname(targetPath), { recursive: true });
|
|
519
|
+
const tempPath = path.join(path.dirname(targetPath), `.${path.basename(targetPath)}.${process.pid}.${randomUUID()}.tmp`);
|
|
520
|
+
await writeFile(tempPath, content, 'utf8');
|
|
521
|
+
await rename(tempPath, targetPath);
|
|
522
|
+
}
|
|
523
|
+
async function withKnowledgeWriteLock(workspace, operation) {
|
|
524
|
+
await mkdir(workspace.knowledgePath, { recursive: true });
|
|
525
|
+
const lockPath = path.join(workspace.knowledgePath, '.write.lock');
|
|
526
|
+
const startedAt = Date.now();
|
|
527
|
+
while (true) {
|
|
528
|
+
try {
|
|
529
|
+
await mkdir(lockPath);
|
|
530
|
+
break;
|
|
531
|
+
}
|
|
532
|
+
catch (error) {
|
|
533
|
+
if (Date.now() - startedAt > 10_000) {
|
|
534
|
+
throw new KGraphError('Timed out waiting for KGraph knowledge write lock.');
|
|
535
|
+
}
|
|
536
|
+
await delay(50);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
try {
|
|
540
|
+
return await operation();
|
|
541
|
+
}
|
|
542
|
+
finally {
|
|
543
|
+
await rm(lockPath, { recursive: true, force: true });
|
|
544
|
+
}
|
|
545
|
+
}
|