@kentwynn/kgraph 0.2.30 → 0.2.32
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/dist/cli/commands/knowledge.js +15 -7
- package/dist/cli/commands/pack.js +12 -4
- package/dist/cli/commands/workflow.js +26 -9
- package/dist/cli/index.js +3 -1
- package/dist/context/context-pack.js +40 -10
- package/dist/integrations/adapters/copilot.js +5 -1
- package/dist/integrations/agent-skills.js +5 -5
- package/dist/integrations/instruction-blocks.js +2 -2
- package/dist/integrations/workflow-steps.d.ts +1 -1
- package/dist/integrations/workflow-steps.js +22 -31
- package/dist/knowledge/atom-store.js +60 -15
- package/dist/visualization/html-template.js +1 -1
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { atomToCognitionNote, readKnowledgeAtoms, updateKnowledgeAtom, updateKnowledgeAtoms, } from '../../knowledge/atom-store.js';
|
|
2
1
|
import { rebuildDomainRecords } from '../../cognition/domain-records.js';
|
|
2
|
+
import { atomToCognitionNote, readKnowledgeAtoms, updateKnowledgeAtom, updateKnowledgeAtoms, } from '../../knowledge/atom-store.js';
|
|
3
3
|
import { assertWorkspace } from '../../storage/kgraph-paths.js';
|
|
4
4
|
import { readMaps } from '../../storage/map-store.js';
|
|
5
5
|
import { KGraphError, runCommand } from '../errors.js';
|
|
@@ -109,7 +109,10 @@ export function registerKnowledgeCommand(program) {
|
|
|
109
109
|
lifecycle: {
|
|
110
110
|
...nextAtoms[newIndex].lifecycle,
|
|
111
111
|
supersedes: [
|
|
112
|
-
...new Set([
|
|
112
|
+
...new Set([
|
|
113
|
+
...nextAtoms[newIndex].lifecycle.supersedes,
|
|
114
|
+
oldId,
|
|
115
|
+
]),
|
|
113
116
|
],
|
|
114
117
|
},
|
|
115
118
|
};
|
|
@@ -144,13 +147,18 @@ function filterAtoms(atoms, options) {
|
|
|
144
147
|
return atoms.filter((atom) => {
|
|
145
148
|
if (options.type && atom.type !== options.type)
|
|
146
149
|
return false;
|
|
147
|
-
if (options.status &&
|
|
148
|
-
atom.status !== normalizeStatus(options.status)) {
|
|
150
|
+
if (options.status && atom.status !== normalizeStatus(options.status)) {
|
|
149
151
|
return false;
|
|
150
152
|
}
|
|
151
|
-
if (options.topic
|
|
152
|
-
|
|
153
|
-
|
|
153
|
+
if (options.topic) {
|
|
154
|
+
const q = options.topic.toLowerCase();
|
|
155
|
+
const matches = atom.topic.toLowerCase().includes(q) ||
|
|
156
|
+
atom.claim.toLowerCase().includes(q) ||
|
|
157
|
+
(atom.summary?.toLowerCase().includes(q) ?? false) ||
|
|
158
|
+
atom.scopeRefs.files.some((f) => f.toLowerCase().includes(q)) ||
|
|
159
|
+
atom.scopeRefs.symbols.some((s) => s.toLowerCase().includes(q));
|
|
160
|
+
if (!matches)
|
|
161
|
+
return false;
|
|
154
162
|
}
|
|
155
163
|
return true;
|
|
156
164
|
});
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
+
import { loadConfig } from '../../config/config.js';
|
|
2
3
|
import { buildContextPack } from '../../context/context-pack.js';
|
|
3
4
|
import { queryContext } from '../../context/context-query.js';
|
|
4
|
-
import { loadConfig } from '../../config/config.js';
|
|
5
5
|
import { assertSessionAgent, recordSessionEvent, } from '../../session/session-store.js';
|
|
6
|
-
import { assertWorkspace } from '../../storage/kgraph-paths.js';
|
|
7
6
|
import { listInboxNotes } from '../../storage/cognition-store.js';
|
|
7
|
+
import { assertWorkspace } from '../../storage/kgraph-paths.js';
|
|
8
8
|
import { mapsExist, readMaps } from '../../storage/map-store.js';
|
|
9
9
|
import { KGraphError, runCommand } from '../errors.js';
|
|
10
10
|
export function registerPackCommand(program) {
|
|
@@ -41,7 +41,7 @@ export function registerPackCommand(program) {
|
|
|
41
41
|
]);
|
|
42
42
|
const response = await queryContext(workspace, config, maps, task);
|
|
43
43
|
const pack = buildContextPack(response, budget, workspace.rootPath);
|
|
44
|
-
const pendingInboxFiles = (await listInboxNotes(workspace)).map((file) => path.relative(workspace.rootPath, file));
|
|
44
|
+
const pendingInboxFiles = (await listInboxNotes(workspace)).map((file) => path.relative(workspace.rootPath, file).split(path.sep).join('/'));
|
|
45
45
|
if (pendingInboxFiles.length > 0) {
|
|
46
46
|
pack.pendingInbox = {
|
|
47
47
|
count: pendingInboxFiles.length,
|
|
@@ -122,6 +122,12 @@ function appendGroup(lines, title, items) {
|
|
|
122
122
|
lines.push(` range ${data.path}:${data.startLine}-${data.endLine}`);
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
|
+
if (item.kind === 'symbol') {
|
|
126
|
+
const data = item.data;
|
|
127
|
+
if (data.filePath && data.startLine != null && data.endLine != null) {
|
|
128
|
+
lines.push(` ${data.filePath}:${data.startLine}-${data.endLine}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
125
131
|
}
|
|
126
132
|
if (items.length > 6)
|
|
127
133
|
lines.push(` ◌ ${items.length - 6} more ${title.toLowerCase()} omitted from display`);
|
|
@@ -132,5 +138,7 @@ function formatReasons(reasons) {
|
|
|
132
138
|
return 'included by pack ranking';
|
|
133
139
|
const shown = reasons.slice(0, 3);
|
|
134
140
|
const remaining = reasons.length - shown.length;
|
|
135
|
-
return remaining > 0
|
|
141
|
+
return remaining > 0
|
|
142
|
+
? `${shown.join('; ')}; and ${remaining} more`
|
|
143
|
+
: shown.join('; ');
|
|
136
144
|
}
|
|
@@ -3,8 +3,8 @@ import { concludeTopic } from '../../cognition/conclusion.js';
|
|
|
3
3
|
import { loadConfig } from '../../config/config.js';
|
|
4
4
|
import { queryContext } from '../../context/context-query.js';
|
|
5
5
|
import { refreshKnowledgeAtomStatuses } from '../../knowledge/atom-store.js';
|
|
6
|
-
import { getWorkingTreeChanges } from '../../scanner/git-utils.js';
|
|
7
6
|
import { shouldExclude } from '../../scanner/file-classifier.js';
|
|
7
|
+
import { getWorkingTreeChanges } from '../../scanner/git-utils.js';
|
|
8
8
|
import { scanRepository } from '../../scanner/repo-scanner.js';
|
|
9
9
|
import { assertSessionAgent, recordSessionEvent, } from '../../session/session-store.js';
|
|
10
10
|
import { listInboxNotes } from '../../storage/cognition-store.js';
|
|
@@ -104,8 +104,7 @@ export async function runDefaultWorkflow(query, options = {}) {
|
|
|
104
104
|
atomsProcessed: update.processed.length,
|
|
105
105
|
pendingInbox: pendingInbox.length,
|
|
106
106
|
activeAtoms: activeAtoms.length,
|
|
107
|
-
needsReviewAtoms: atoms.filter((atom) => atom.status === 'needs-review')
|
|
108
|
-
.length,
|
|
107
|
+
needsReviewAtoms: atoms.filter((atom) => atom.status === 'needs-review').length,
|
|
109
108
|
staleAtoms: atoms.filter((atom) => atom.status === 'stale').length,
|
|
110
109
|
highConfidenceMissingEvidence: activeAtoms.filter((atom) => atom.confidence === 'high' && atom.evidenceRefs.length === 0).length,
|
|
111
110
|
captureRequired: captureCheck.required,
|
|
@@ -250,17 +249,33 @@ function meaningfulTopicOverlap(a, b) {
|
|
|
250
249
|
const weakTokens = new Set([
|
|
251
250
|
'add',
|
|
252
251
|
'after',
|
|
252
|
+
'and',
|
|
253
253
|
'behavior',
|
|
254
254
|
'change',
|
|
255
255
|
'changed',
|
|
256
|
+
'check',
|
|
257
|
+
'code',
|
|
258
|
+
'current',
|
|
259
|
+
'file',
|
|
260
|
+
'fix',
|
|
261
|
+
'for',
|
|
262
|
+
'from',
|
|
263
|
+
'get',
|
|
264
|
+
'into',
|
|
256
265
|
'new',
|
|
266
|
+
'not',
|
|
257
267
|
'old',
|
|
258
268
|
'review',
|
|
259
|
-
'
|
|
260
|
-
'current',
|
|
269
|
+
'run',
|
|
261
270
|
'session',
|
|
271
|
+
'set',
|
|
262
272
|
'smoke',
|
|
273
|
+
'test',
|
|
274
|
+
'that',
|
|
275
|
+
'the',
|
|
276
|
+
'this',
|
|
263
277
|
'update',
|
|
278
|
+
'use',
|
|
264
279
|
'with',
|
|
265
280
|
]);
|
|
266
281
|
const aTokens = new Set(a
|
|
@@ -277,12 +292,14 @@ function tokenOverlap(a, b) {
|
|
|
277
292
|
const aTokens = new Set(a
|
|
278
293
|
.toLowerCase()
|
|
279
294
|
.split(/[^a-z0-9]+/)
|
|
280
|
-
.filter(
|
|
281
|
-
|
|
295
|
+
.filter((token) => token.length >= 4));
|
|
296
|
+
if (aTokens.size === 0)
|
|
297
|
+
return false;
|
|
298
|
+
const matches = b
|
|
282
299
|
.toLowerCase()
|
|
283
300
|
.split(/[^a-z0-9]+/)
|
|
284
|
-
.filter(
|
|
285
|
-
|
|
301
|
+
.filter((token) => token.length >= 4 && aTokens.has(token));
|
|
302
|
+
return matches.length >= 2;
|
|
286
303
|
}
|
|
287
304
|
function matchingInvalidatedAtoms(atoms, topic) {
|
|
288
305
|
const tokens = new Set((topic ?? '')
|
package/dist/cli/index.js
CHANGED
|
@@ -130,6 +130,8 @@ function isCliEntrypoint() {
|
|
|
130
130
|
realpathSync(process.argv[1]));
|
|
131
131
|
}
|
|
132
132
|
catch {
|
|
133
|
-
|
|
133
|
+
const argv1 = process.argv[1].replace(/\\/g, '/');
|
|
134
|
+
return (import.meta.url === `file://${argv1}` ||
|
|
135
|
+
import.meta.url === `file:///${argv1}`);
|
|
134
136
|
}
|
|
135
137
|
}
|
|
@@ -13,14 +13,7 @@ export function buildContextPack(response, budget, rootPath) {
|
|
|
13
13
|
data: ranked.item,
|
|
14
14
|
})),
|
|
15
15
|
...buildFileRangeCandidates(response, budget, rootPath),
|
|
16
|
-
...response.relevantSymbols.map((ranked) => (
|
|
17
|
-
kind: 'symbol',
|
|
18
|
-
id: ranked.item.id,
|
|
19
|
-
title: ranked.item.name,
|
|
20
|
-
tokenEstimate: 20,
|
|
21
|
-
reasons: ranked.reasons,
|
|
22
|
-
data: ranked.item,
|
|
23
|
-
})),
|
|
16
|
+
...response.relevantSymbols.map((ranked) => buildSymbolCandidate(ranked, rootPath)),
|
|
24
17
|
...response.relevantCognition.map((ranked) => ({
|
|
25
18
|
kind: 'atom',
|
|
26
19
|
id: ranked.item.id,
|
|
@@ -131,14 +124,18 @@ function strongPackPaths(candidates) {
|
|
|
131
124
|
function isLowSignalCandidate(candidate, strongPaths) {
|
|
132
125
|
if (strongPaths.size === 0)
|
|
133
126
|
return false;
|
|
134
|
-
if (candidate.kind === 'atom' ||
|
|
127
|
+
if (candidate.kind === 'atom' ||
|
|
128
|
+
candidate.kind === 'git-change' ||
|
|
129
|
+
candidate.kind === 'file-range') {
|
|
135
130
|
return false;
|
|
136
131
|
}
|
|
137
132
|
if (hasStrongReason(candidate))
|
|
138
133
|
return false;
|
|
139
134
|
if (candidateTouchesStrongPath(candidate, strongPaths))
|
|
140
135
|
return false;
|
|
141
|
-
return candidate.kind === 'file' ||
|
|
136
|
+
return (candidate.kind === 'file' ||
|
|
137
|
+
candidate.kind === 'symbol' ||
|
|
138
|
+
candidate.kind === 'relationship');
|
|
142
139
|
}
|
|
143
140
|
function hasStrongReason(candidate) {
|
|
144
141
|
return candidate.reasons.some((reason) => reason.includes('matched atom') ||
|
|
@@ -170,6 +167,39 @@ const GENERIC_RANGE_TOKENS = new Set([
|
|
|
170
167
|
'repo',
|
|
171
168
|
'work',
|
|
172
169
|
]);
|
|
170
|
+
const MAX_SYMBOL_EXCERPT_LINES = 40;
|
|
171
|
+
function buildSymbolCandidate(ranked, rootPath) {
|
|
172
|
+
const symbol = ranked.item;
|
|
173
|
+
let excerpt;
|
|
174
|
+
let tokenEstimate = 20;
|
|
175
|
+
if (rootPath &&
|
|
176
|
+
symbol.startLine != null &&
|
|
177
|
+
symbol.endLine != null &&
|
|
178
|
+
symbol.endLine - symbol.startLine + 1 <= MAX_SYMBOL_EXCERPT_LINES) {
|
|
179
|
+
const fullPath = path.join(rootPath, symbol.filePath);
|
|
180
|
+
if (existsSync(fullPath)) {
|
|
181
|
+
try {
|
|
182
|
+
const content = readFileSync(fullPath, 'utf8');
|
|
183
|
+
const allLines = content.split(/\r?\n/);
|
|
184
|
+
const from = symbol.startLine - 1; // 0-based
|
|
185
|
+
const to = symbol.endLine; // exclusive
|
|
186
|
+
excerpt = allLines.slice(from, to).join('\n');
|
|
187
|
+
tokenEstimate = estimateTokens(excerpt, symbol.filePath);
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
// best-effort; fall back to default estimate
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
kind: 'symbol',
|
|
196
|
+
id: symbol.id,
|
|
197
|
+
title: symbol.name,
|
|
198
|
+
tokenEstimate,
|
|
199
|
+
reasons: ranked.reasons,
|
|
200
|
+
data: excerpt != null ? { ...symbol, excerpt } : symbol,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
173
203
|
function buildFileRangeCandidates(response, budget, rootPath) {
|
|
174
204
|
if (!rootPath)
|
|
175
205
|
return [];
|
|
@@ -16,7 +16,11 @@ ${numberedWorkflow('copilot')}
|
|
|
16
16
|
description: Use KGraph persistent repo intelligence — runs kgraph before answering to provide file maps, symbols, relationships, and durable knowledge atoms.
|
|
17
17
|
---
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
# KGraph Agent
|
|
20
|
+
|
|
21
|
+
You are the KGraph agent. You use the \`kgraph\` CLI via the terminal to provide persistent repo intelligence before answering questions or making edits.
|
|
22
|
+
|
|
23
|
+
## Workflow
|
|
20
24
|
|
|
21
25
|
${numberedWorkflow('copilot')}
|
|
22
26
|
`,
|
|
@@ -45,7 +45,7 @@ name: kgraph-pack
|
|
|
45
45
|
description: Build a budget-aware KGraph context pack
|
|
46
46
|
---
|
|
47
47
|
|
|
48
|
-
Run \`kgraph pack "
|
|
48
|
+
Run \`kgraph pack "<topic>" --budget 8000 --json --agent $AGENT\` to build a machine-readable context pack and record lightweight session context. Summarize token use, included files, symbols, relationships, git changes, session history, atoms, and omitted items with the inclusion reasons. When a symbol item includes an \`excerpt\` field, you already have the source — do not read that file for the symbol.
|
|
49
49
|
`,
|
|
50
50
|
},
|
|
51
51
|
{
|
|
@@ -75,7 +75,7 @@ name: kgraph-blame
|
|
|
75
75
|
description: Show KGraph atom provenance and evidence
|
|
76
76
|
---
|
|
77
77
|
|
|
78
|
-
Run \`kgraph blame
|
|
78
|
+
Run \`kgraph blame <atom-id>\` to show who or what created a knowledge atom, the source command/session/commit, evidence refs, and lifecycle links.
|
|
79
79
|
`,
|
|
80
80
|
},
|
|
81
81
|
{
|
|
@@ -127,7 +127,7 @@ name: kgraph-conclude
|
|
|
127
127
|
description: Store a typed durable KGraph engineering conclusion
|
|
128
128
|
---
|
|
129
129
|
|
|
130
|
-
Prefer \`kgraph "<topic>" --capture "
|
|
130
|
+
Prefer \`kgraph "<topic>" --capture "<durable conclusion>" --capture-file <path> --capture-symbol <name>\` when the session produced reusable engineering knowledge. Use low-level \`kgraph conclude "<topic>" --note "<conclusion>"\` only when the user explicitly asks for the conclude command. Choose one type from finding, decision, gotcha, summary, relationship, and one confidence from high, medium, low. Add file or symbol evidence whenever possible; high-confidence conclusions require evidence. Store only durable conclusions, not raw chain-of-thought, temporary reasoning, speculative exploration, or low-value observations.
|
|
131
131
|
`,
|
|
132
132
|
},
|
|
133
133
|
{
|
|
@@ -137,7 +137,7 @@ name: kgraph-impact
|
|
|
137
137
|
description: Show KGraph change impact for a file, symbol, or topic
|
|
138
138
|
---
|
|
139
139
|
|
|
140
|
-
Run \`kgraph impact "
|
|
140
|
+
Run \`kgraph impact "<file-or-symbol>"\` to show matched files/symbols, import users, callers, callees, related knowledge atoms, and risk hints.
|
|
141
141
|
`,
|
|
142
142
|
},
|
|
143
143
|
{
|
|
@@ -157,7 +157,7 @@ name: kgraph-history
|
|
|
157
157
|
description: Show timeline of KGraph cognition sessions with git attribution
|
|
158
158
|
---
|
|
159
159
|
|
|
160
|
-
Run \`kgraph history\` or \`kgraph history "
|
|
160
|
+
Run \`kgraph history\` or \`kgraph history "<topic>"\` to display processed cognition sessions. Summarize who contributed what and when. Use \`--last <n>\` to limit entries.
|
|
161
161
|
`,
|
|
162
162
|
},
|
|
163
163
|
];
|
|
@@ -9,7 +9,7 @@ export function upsertManagedBlock(content, integrationName, instructions) {
|
|
|
9
9
|
if (pattern.test(content)) {
|
|
10
10
|
return content.replace(pattern, block);
|
|
11
11
|
}
|
|
12
|
-
return normalized ? `${
|
|
12
|
+
return normalized ? `${normalized}\n\n${block}\n` : `${block}\n`;
|
|
13
13
|
}
|
|
14
14
|
export function removeManagedBlock(content, integrationName) {
|
|
15
15
|
return (content
|
|
@@ -25,7 +25,7 @@ function renderManagedBlock(integrationName, instructions) {
|
|
|
25
25
|
].join('\n');
|
|
26
26
|
}
|
|
27
27
|
function managedBlockPattern(integrationName) {
|
|
28
|
-
return new RegExp(`${escapeRegExp(MARKER_PREFIX)} BEGIN KGRAPH ${escapeRegExp(integrationName)} ${escapeRegExp(MARKER_SUFFIX)}[\\s\\S]*?${escapeRegExp(MARKER_PREFIX)} END KGRAPH ${escapeRegExp(integrationName)} ${escapeRegExp(MARKER_SUFFIX)}\\n?`, 'm');
|
|
28
|
+
return new RegExp(`${escapeRegExp(MARKER_PREFIX)} BEGIN KGRAPH ${escapeRegExp(integrationName)} ${escapeRegExp(MARKER_SUFFIX)}[\\s\\S]*?${escapeRegExp(MARKER_PREFIX)} END KGRAPH ${escapeRegExp(integrationName)} ${escapeRegExp(MARKER_SUFFIX)}\\r?\\n?`, 'm');
|
|
29
29
|
}
|
|
30
30
|
function escapeRegExp(value) {
|
|
31
31
|
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
@@ -7,7 +7,7 @@ export interface WorkflowOptions {
|
|
|
7
7
|
sessionQualifier?: string;
|
|
8
8
|
}
|
|
9
9
|
/**
|
|
10
|
-
* Returns the
|
|
10
|
+
* Returns the numbered workflow for skill/agent/command files.
|
|
11
11
|
* Used by: copilot (agent file), codex (skill file), claude-code (command file).
|
|
12
12
|
*/
|
|
13
13
|
export declare function numberedWorkflow(agentName: string, options?: WorkflowOptions): string;
|
|
@@ -8,49 +8,41 @@ 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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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.`;
|
|
16
|
-
}
|
|
11
|
+
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.`;
|
|
12
|
+
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.`;
|
|
13
|
+
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.`;
|
|
14
|
+
const SCAN_STEP = `After bulk file creation, deletion, or rename (3+ files), run \`kgraph scan\` before the next \`kgraph pack\` so maps stay accurate.`;
|
|
17
15
|
function smartRootStep(agentName) {
|
|
18
16
|
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.`;
|
|
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.`;
|
|
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.`;
|
|
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\`.`;
|
|
24
18
|
function sessionStep(agentName, qualifier) {
|
|
25
19
|
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`;
|
|
26
20
|
return qualifier ? `${base} ${qualifier}.` : `${base}.`;
|
|
27
21
|
}
|
|
28
22
|
/**
|
|
29
|
-
* Returns the
|
|
23
|
+
* Returns the numbered workflow for skill/agent/command files.
|
|
30
24
|
* Used by: copilot (agent file), codex (skill file), claude-code (command file).
|
|
31
25
|
*/
|
|
32
26
|
export function numberedWorkflow(agentName, options = {}) {
|
|
33
27
|
return `1. Infer the topic from the user's request.
|
|
34
28
|
2. {{KGRAPH_CONTEXT_POLICY}}
|
|
35
|
-
3.
|
|
36
|
-
4. ${
|
|
37
|
-
5.
|
|
38
|
-
6. ${
|
|
39
|
-
7. ${
|
|
40
|
-
8. ${
|
|
41
|
-
9. ${
|
|
42
|
-
10. ${
|
|
43
|
-
11. ${
|
|
44
|
-
12. ${
|
|
45
|
-
13. ${sessionStep(agentName, options.sessionQualifier)}
|
|
46
|
-
14. ${IMPACT_STEP}
|
|
29
|
+
3. When the pack includes a symbol with an \`excerpt\` field, you already have the source code — do not read that file again for that symbol. Items in the \`omitted\` array were evaluated and excluded — do not manually search for them unless the user explicitly asks.
|
|
30
|
+
4. ${EXPLORATION_BOUNDARY_STEP}
|
|
31
|
+
5. ${VERIFY_EDIT_STEP}
|
|
32
|
+
6. ${smartRootStep(agentName)}
|
|
33
|
+
7. ${KNOWLEDGE_STEP}
|
|
34
|
+
8. ${DOCTOR_STEP}
|
|
35
|
+
9. ${STALE_STEP}
|
|
36
|
+
10. ${SCAN_STEP}
|
|
37
|
+
11. ${sessionStep(agentName, options.sessionQualifier)}
|
|
38
|
+
12. ${IMPACT_STEP}
|
|
47
39
|
|
|
48
40
|
{{KGRAPH_CAPTURE_POLICY}}
|
|
49
41
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
42
|
+
13. ${REPAIR_STEP}
|
|
43
|
+
14. ${COMPACT_STEP}
|
|
44
|
+
15. Run \`kgraph visualize\` when the user wants to inspect the dependency graph — opens an interactive graph locally with PNG export.
|
|
45
|
+
16. ${HISTORY_STEP}`;
|
|
54
46
|
}
|
|
55
47
|
/**
|
|
56
48
|
* Returns the bullet-list workflow for rules files.
|
|
@@ -58,20 +50,19 @@ export function numberedWorkflow(agentName, options = {}) {
|
|
|
58
50
|
*/
|
|
59
51
|
export function bulletWorkflow(agentName, options = {}) {
|
|
60
52
|
return `- {{KGRAPH_CONTEXT_POLICY}}
|
|
61
|
-
-
|
|
62
|
-
- ${ROUTING_STEP}
|
|
53
|
+
- When the pack includes a symbol with an \`excerpt\` field, you already have the source code — do not read that file again for that symbol. Items in the \`omitted\` array were evaluated and excluded — do not manually search for them unless the user explicitly asks.
|
|
63
54
|
- ${EXPLORATION_BOUNDARY_STEP}
|
|
64
55
|
- ${VERIFY_EDIT_STEP}
|
|
65
56
|
- ${smartRootStep(agentName)}
|
|
66
|
-
- ${packStep(agentName)}
|
|
67
57
|
- ${KNOWLEDGE_STEP}
|
|
68
58
|
- ${DOCTOR_STEP}
|
|
69
59
|
- ${STALE_STEP}
|
|
60
|
+
- ${SCAN_STEP}
|
|
70
61
|
- ${sessionStep(agentName, options.sessionQualifier)}
|
|
71
62
|
- ${IMPACT_STEP}
|
|
72
63
|
{{KGRAPH_CAPTURE_POLICY}}
|
|
73
64
|
- ${REPAIR_STEP}
|
|
74
65
|
- ${COMPACT_STEP}
|
|
75
|
-
- Run \`kgraph visualize\` to open the interactive dependency graph
|
|
66
|
+
- Run \`kgraph visualize\` to open the interactive dependency graph locally with PNG export.
|
|
76
67
|
- ${HISTORY_STEP}`;
|
|
77
68
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { mkdir, readFile, rename, rm, unlink, writeFile, } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
5
5
|
import { KGraphError } from '../cli/errors.js';
|
|
@@ -33,7 +33,7 @@ export async function readAtomsFile(workspace) {
|
|
|
33
33
|
}
|
|
34
34
|
export function parseAtomsJsonl(raw) {
|
|
35
35
|
const atoms = [];
|
|
36
|
-
for (const [index, line] of raw.split(
|
|
36
|
+
for (const [index, line] of raw.split(/\r?\n/).entries()) {
|
|
37
37
|
if (!line.trim())
|
|
38
38
|
continue;
|
|
39
39
|
try {
|
|
@@ -120,8 +120,7 @@ export async function migrateLegacyCognitionToAtoms(workspace) {
|
|
|
120
120
|
const current = await readAtomsFile(workspace);
|
|
121
121
|
const currentIds = new Set(current.map((atom) => atom.id));
|
|
122
122
|
const nextMigrated = migrated.filter((atom) => !currentIds.has(atom.id) &&
|
|
123
|
-
!current.some((existing) => existing.topic === atom.topic &&
|
|
124
|
-
existing.claim === atom.claim));
|
|
123
|
+
!current.some((existing) => existing.topic === atom.topic && existing.claim === atom.claim));
|
|
125
124
|
if (nextMigrated.length > 0) {
|
|
126
125
|
await writeKnowledgeAtomsUnlocked(workspace, [
|
|
127
126
|
...current,
|
|
@@ -206,7 +205,10 @@ export async function refreshKnowledgeAtomStatuses(workspace, maps, dryRun = fal
|
|
|
206
205
|
export async function validateKnowledgeStore(workspace, maps) {
|
|
207
206
|
const issues = [];
|
|
208
207
|
if (!(await pathExists(schemaPath(workspace)))) {
|
|
209
|
-
issues.push({
|
|
208
|
+
issues.push({
|
|
209
|
+
code: 'missing-schema',
|
|
210
|
+
message: 'missing knowledge/schema.json',
|
|
211
|
+
});
|
|
210
212
|
}
|
|
211
213
|
else {
|
|
212
214
|
try {
|
|
@@ -220,7 +222,10 @@ export async function validateKnowledgeStore(workspace, maps) {
|
|
|
220
222
|
}
|
|
221
223
|
catch (error) {
|
|
222
224
|
const message = error instanceof Error ? error.message : String(error);
|
|
223
|
-
issues.push({
|
|
225
|
+
issues.push({
|
|
226
|
+
code: 'missing-schema',
|
|
227
|
+
message: `invalid knowledge schema: ${message}`,
|
|
228
|
+
});
|
|
224
229
|
}
|
|
225
230
|
}
|
|
226
231
|
let atoms = [];
|
|
@@ -252,11 +257,15 @@ export async function validateKnowledgeStore(workspace, maps) {
|
|
|
252
257
|
});
|
|
253
258
|
}
|
|
254
259
|
else if (ref.contentHash && ref.contentHash !== file.contentHash) {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
+
// Only report if the atom hasn't already been moved to needs-review;
|
|
261
|
+
// needs-review atoms are already caught by the quality gate.
|
|
262
|
+
if (atom.status !== 'needs-review') {
|
|
263
|
+
issues.push({
|
|
264
|
+
code: 'stale-file-hash',
|
|
265
|
+
atomId: atom.id,
|
|
266
|
+
message: `${atom.id} references changed file ${ref.path}; run \`kgraph stale\` to update atom status`,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
260
269
|
}
|
|
261
270
|
}
|
|
262
271
|
if (ref.type === 'symbol') {
|
|
@@ -344,7 +353,11 @@ function buildEvidenceRefs(input, maps) {
|
|
|
344
353
|
refs.push({ type: 'git', commit: input.commit });
|
|
345
354
|
}
|
|
346
355
|
if (input.sessionId || input.agent) {
|
|
347
|
-
refs.push({
|
|
356
|
+
refs.push({
|
|
357
|
+
type: 'session',
|
|
358
|
+
sessionId: input.sessionId,
|
|
359
|
+
agent: input.agent,
|
|
360
|
+
});
|
|
348
361
|
}
|
|
349
362
|
return refs;
|
|
350
363
|
}
|
|
@@ -373,7 +386,8 @@ function evaluateAtomHealth(refs, maps) {
|
|
|
373
386
|
}
|
|
374
387
|
}
|
|
375
388
|
if (ref.type === 'symbol') {
|
|
376
|
-
const exists = (ref.symbolId && symbolIds.has(ref.symbolId)) ||
|
|
389
|
+
const exists = (ref.symbolId && symbolIds.has(ref.symbolId)) ||
|
|
390
|
+
symbolNames.has(ref.name);
|
|
377
391
|
if (!exists) {
|
|
378
392
|
stale = true;
|
|
379
393
|
reasons.push(`missing symbol:${ref.name}`);
|
|
@@ -393,7 +407,8 @@ function computeConfidence(initial, status, atom) {
|
|
|
393
407
|
return 'low';
|
|
394
408
|
if (status === 'needs-review' && initial === 'high')
|
|
395
409
|
return 'medium';
|
|
396
|
-
if (atom?.provenance.sourceCommand === 'legacy-migration' &&
|
|
410
|
+
if (atom?.provenance.sourceCommand === 'legacy-migration' &&
|
|
411
|
+
initial === 'high') {
|
|
397
412
|
return 'medium';
|
|
398
413
|
}
|
|
399
414
|
return initial;
|
|
@@ -518,7 +533,37 @@ async function atomicWriteFile(targetPath, content) {
|
|
|
518
533
|
await mkdir(path.dirname(targetPath), { recursive: true });
|
|
519
534
|
const tempPath = path.join(path.dirname(targetPath), `.${path.basename(targetPath)}.${process.pid}.${randomUUID()}.tmp`);
|
|
520
535
|
await writeFile(tempPath, content, 'utf8');
|
|
521
|
-
|
|
536
|
+
// On Windows, rename over an existing file can transiently fail with EPERM
|
|
537
|
+
// when another handle was recently released. Retry with short delays.
|
|
538
|
+
let lastError;
|
|
539
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
540
|
+
try {
|
|
541
|
+
await rename(tempPath, targetPath);
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
catch (error) {
|
|
545
|
+
const code = error.code;
|
|
546
|
+
if ((code === 'EPERM' || code === 'EACCES') && attempt < 2) {
|
|
547
|
+
lastError = error;
|
|
548
|
+
await delay(20 * (attempt + 1));
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
try {
|
|
552
|
+
await unlink(tempPath);
|
|
553
|
+
}
|
|
554
|
+
catch {
|
|
555
|
+
/* best-effort cleanup */
|
|
556
|
+
}
|
|
557
|
+
throw error;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
try {
|
|
561
|
+
await unlink(tempPath);
|
|
562
|
+
}
|
|
563
|
+
catch {
|
|
564
|
+
/* best-effort cleanup */
|
|
565
|
+
}
|
|
566
|
+
throw lastError;
|
|
522
567
|
}
|
|
523
568
|
async function withKnowledgeWriteLock(workspace, operation) {
|
|
524
569
|
await mkdir(workspace.knowledgePath, { recursive: true });
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export function renderHtml(graphData, rootPath) {
|
|
2
|
-
const repoName = escAttr(rootPath.split(
|
|
2
|
+
const repoName = escAttr(rootPath.split(/[\\/]/).pop() ?? 'Repository');
|
|
3
3
|
const { meta } = graphData;
|
|
4
4
|
// Prevent </script> tag injection from embedded JSON
|
|
5
5
|
const safeData = JSON.stringify(graphData).replace(/<\/script>/gi, '<\\/script>');
|