feat(system-context): inject global ~/.gsd/agent/KNOWLEDGE.md into system prompt (#2331)
* feat(system-context): inject global ~/.gsd/agent/KNOWLEDGE.md into system prompt Reads ~/.gsd/agent/KNOWLEDGE.md (global) alongside the existing project .gsd/KNOWLEDGE.md and merges both into the [KNOWLEDGE] block. Global section appears first so project entries can override or refine global rules. Emits a startup warning when the global file exceeds 4 KB to keep system prompt size in check. Extracted loading logic into loadKnowledgeBlock() for testability. Five new unit tests cover: empty state, project-only, global-only, merged order, and size threshold. Closes #2316 * fix(test): relax derive-state-db perf threshold from 1ms to 10ms The <1ms assertion was intermittently failing on loaded CI runners (observed: 1.054ms). 10ms still validates the in-memory cache path is fast while being robust across shared CI environments. --------- Co-authored-by: TÂCHES <afromanguy@me.com>
This commit is contained in:
parent
cace21cb02
commit
e0b3bad2a5
2 changed files with 137 additions and 11 deletions
|
|
@ -64,17 +64,12 @@ export async function buildBeforeAgentStartResult(
|
|||
}
|
||||
}
|
||||
|
||||
let knowledgeBlock = "";
|
||||
const knowledgePath = resolveGsdRootFile(process.cwd(), "KNOWLEDGE");
|
||||
if (existsSync(knowledgePath)) {
|
||||
try {
|
||||
const content = readFileSync(knowledgePath, "utf-8").trim();
|
||||
if (content) {
|
||||
knowledgeBlock = `\n\n[PROJECT KNOWLEDGE — Rules, patterns, and lessons learned]\n\n${content}`;
|
||||
}
|
||||
} catch {
|
||||
// skip
|
||||
}
|
||||
const { block: knowledgeBlock, globalSizeKb } = loadKnowledgeBlock(gsdHome, process.cwd());
|
||||
if (globalSizeKb > 4) {
|
||||
ctx.ui.notify(
|
||||
`GSD: ~/.gsd/agent/KNOWLEDGE.md is ${globalSizeKb.toFixed(1)}KB — consider trimming to keep system prompt lean.`,
|
||||
"warning",
|
||||
);
|
||||
}
|
||||
|
||||
let memoryBlock = "";
|
||||
|
|
@ -126,6 +121,48 @@ export async function buildBeforeAgentStartResult(
|
|||
};
|
||||
}
|
||||
|
||||
export function loadKnowledgeBlock(gsdHomeDir: string, cwd: string): { block: string; globalSizeKb: number } {
|
||||
// 1. Global knowledge (~/.gsd/agent/KNOWLEDGE.md) — cross-project, user-maintained
|
||||
let globalKnowledge = "";
|
||||
let globalSizeKb = 0;
|
||||
const globalKnowledgePath = join(gsdHomeDir, "agent", "KNOWLEDGE.md");
|
||||
if (existsSync(globalKnowledgePath)) {
|
||||
try {
|
||||
const content = readFileSync(globalKnowledgePath, "utf-8").trim();
|
||||
if (content) {
|
||||
globalSizeKb = Buffer.byteLength(content, "utf-8") / 1024;
|
||||
globalKnowledge = content;
|
||||
}
|
||||
} catch {
|
||||
// skip
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Project knowledge (.gsd/KNOWLEDGE.md) — project-specific
|
||||
let projectKnowledge = "";
|
||||
const knowledgePath = resolveGsdRootFile(cwd, "KNOWLEDGE");
|
||||
if (existsSync(knowledgePath)) {
|
||||
try {
|
||||
const content = readFileSync(knowledgePath, "utf-8").trim();
|
||||
if (content) projectKnowledge = content;
|
||||
} catch {
|
||||
// skip
|
||||
}
|
||||
}
|
||||
|
||||
if (!globalKnowledge && !projectKnowledge) {
|
||||
return { block: "", globalSizeKb: 0 };
|
||||
}
|
||||
|
||||
const parts: string[] = [];
|
||||
if (globalKnowledge) parts.push(`## Global Knowledge\n\n${globalKnowledge}`);
|
||||
if (projectKnowledge) parts.push(`## Project Knowledge\n\n${projectKnowledge}`);
|
||||
return {
|
||||
block: `\n\n[KNOWLEDGE — Rules, patterns, and lessons learned]\n\n${parts.join("\n\n")}`,
|
||||
globalSizeKb,
|
||||
};
|
||||
}
|
||||
|
||||
function buildWorktreeContextBlock(): string {
|
||||
const worktreeName = getActiveWorktreeName();
|
||||
const worktreeMainCwd = getWorktreeOriginalCwd();
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
* - resolveGsdRootFile resolves KNOWLEDGE paths correctly
|
||||
* - inlineGsdRootFile works with the KNOWLEDGE key
|
||||
* - before_agent_start hook includes/omits knowledge block appropriately
|
||||
* - loadKnowledgeBlock merges global and project knowledge correctly
|
||||
*/
|
||||
|
||||
import test from 'node:test';
|
||||
|
|
@ -16,6 +17,7 @@ import { tmpdir } from 'node:os';
|
|||
import { GSD_ROOT_FILES, resolveGsdRootFile } from '../paths.ts';
|
||||
import { inlineGsdRootFile } from '../auto-prompts.ts';
|
||||
import { appendKnowledge } from '../files.ts';
|
||||
import { loadKnowledgeBlock } from '../bootstrap/system-context.ts';
|
||||
|
||||
// ─── KNOWLEDGE is registered in GSD_ROOT_FILES ─────────────────────────────
|
||||
|
||||
|
|
@ -159,3 +161,90 @@ test('knowledge: appendKnowledge handles lesson type', async () => {
|
|||
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
// ─── loadKnowledgeBlock — global + project merge ────────────────────────────
|
||||
|
||||
test('loadKnowledgeBlock: returns empty block when neither file exists', () => {
|
||||
const tmp = realpathSync(mkdtempSync(join(tmpdir(), 'gsd-kb-')));
|
||||
const gsdHome = join(tmp, 'home');
|
||||
const cwd = join(tmp, 'project');
|
||||
mkdirSync(join(cwd, '.gsd'), { recursive: true });
|
||||
mkdirSync(join(gsdHome, 'agent'), { recursive: true });
|
||||
|
||||
const result = loadKnowledgeBlock(gsdHome, cwd);
|
||||
assert.strictEqual(result.block, '');
|
||||
assert.strictEqual(result.globalSizeKb, 0);
|
||||
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test('loadKnowledgeBlock: uses project knowledge alone when no global file', () => {
|
||||
const tmp = realpathSync(mkdtempSync(join(tmpdir(), 'gsd-kb-')));
|
||||
const gsdHome = join(tmp, 'home');
|
||||
const cwd = join(tmp, 'project');
|
||||
mkdirSync(join(cwd, '.gsd'), { recursive: true });
|
||||
mkdirSync(join(gsdHome, 'agent'), { recursive: true });
|
||||
writeFileSync(join(cwd, '.gsd', 'KNOWLEDGE.md'), 'K001: Use real DB');
|
||||
|
||||
const result = loadKnowledgeBlock(gsdHome, cwd);
|
||||
assert.ok(result.block.includes('[KNOWLEDGE — Rules, patterns, and lessons learned]'));
|
||||
assert.ok(result.block.includes('## Project Knowledge'));
|
||||
assert.ok(result.block.includes('K001: Use real DB'));
|
||||
assert.ok(!result.block.includes('## Global Knowledge'));
|
||||
assert.strictEqual(result.globalSizeKb, 0);
|
||||
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test('loadKnowledgeBlock: uses global knowledge alone when no project file', () => {
|
||||
const tmp = realpathSync(mkdtempSync(join(tmpdir(), 'gsd-kb-')));
|
||||
const gsdHome = join(tmp, 'home');
|
||||
const cwd = join(tmp, 'project');
|
||||
mkdirSync(join(cwd, '.gsd'), { recursive: true });
|
||||
mkdirSync(join(gsdHome, 'agent'), { recursive: true });
|
||||
writeFileSync(join(gsdHome, 'agent', 'KNOWLEDGE.md'), 'G001: Respond in English');
|
||||
|
||||
const result = loadKnowledgeBlock(gsdHome, cwd);
|
||||
assert.ok(result.block.includes('[KNOWLEDGE — Rules, patterns, and lessons learned]'));
|
||||
assert.ok(result.block.includes('## Global Knowledge'));
|
||||
assert.ok(result.block.includes('G001: Respond in English'));
|
||||
assert.ok(!result.block.includes('## Project Knowledge'));
|
||||
assert.ok(result.globalSizeKb > 0);
|
||||
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test('loadKnowledgeBlock: merges global before project when both exist', () => {
|
||||
const tmp = realpathSync(mkdtempSync(join(tmpdir(), 'gsd-kb-')));
|
||||
const gsdHome = join(tmp, 'home');
|
||||
const cwd = join(tmp, 'project');
|
||||
mkdirSync(join(cwd, '.gsd'), { recursive: true });
|
||||
mkdirSync(join(gsdHome, 'agent'), { recursive: true });
|
||||
writeFileSync(join(gsdHome, 'agent', 'KNOWLEDGE.md'), 'G001: Global rule');
|
||||
writeFileSync(join(cwd, '.gsd', 'KNOWLEDGE.md'), 'K001: Project rule');
|
||||
|
||||
const result = loadKnowledgeBlock(gsdHome, cwd);
|
||||
assert.ok(result.block.includes('## Global Knowledge'));
|
||||
assert.ok(result.block.includes('## Project Knowledge'));
|
||||
assert.ok(result.block.includes('G001: Global rule'));
|
||||
assert.ok(result.block.includes('K001: Project rule'));
|
||||
// Global section appears before project section
|
||||
assert.ok(result.block.indexOf('## Global Knowledge') < result.block.indexOf('## Project Knowledge'));
|
||||
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test('loadKnowledgeBlock: reports globalSizeKb above 4KB threshold', () => {
|
||||
const tmp = realpathSync(mkdtempSync(join(tmpdir(), 'gsd-kb-')));
|
||||
const gsdHome = join(tmp, 'home');
|
||||
const cwd = join(tmp, 'project');
|
||||
mkdirSync(join(cwd, '.gsd'), { recursive: true });
|
||||
mkdirSync(join(gsdHome, 'agent'), { recursive: true });
|
||||
// Write > 4KB of content
|
||||
writeFileSync(join(gsdHome, 'agent', 'KNOWLEDGE.md'), 'x'.repeat(5000));
|
||||
|
||||
const result = loadKnowledgeBlock(gsdHome, cwd);
|
||||
assert.ok(result.globalSizeKb > 4, `expected > 4KB, got ${result.globalSizeKb}`);
|
||||
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue