Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 78 additions & 66 deletions src/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { SESSION_DIR } from "./paths.js";
import { indexSession } from "./qmd.js";
import { redactSecrets } from "./redact.js";
import {
addFrontmatterField,
addTags,
appendDecision,
appendMistake,
Expand Down Expand Up @@ -94,79 +95,90 @@ try {
// 2. Extract tags, decisions, mistakes
const sections = extractSections(summary);

// Check if AI flagged this session as not relevant for knowledge
if (sections.skipKnowledge) {
log("Session flagged as skip_knowledge by AI — skipping knowledge ingestion");
// Mark in frontmatter so downstream consumers can filter
const currentContent = readFileSync(sessionPath, "utf8");
const updated = addFrontmatterField(currentContent, "skip_knowledge", true);
writeFileSync(sessionPath, updated);
}

if (sections.tags.length > 0) {
addTags(repoRoot, sessionId, sections.tags);
log(`Tagged: ${sections.tags.join(", ")}`);
}

// Read session data for context
const sessionContent = readFileSync(sessionPath, "utf8");
const { frontmatter } = parseFrontmatter(sessionContent);
const modifiedFiles = extractModifiedFiles(sessionContent);
const commitSha = (frontmatter.base_commit as string) || "unknown";
const sessionDate = sessionId.slice(0, 10);

for (const decision of sections.decisions) {
const { title, description } = parseTitleDescription(decision.text);
if (isJunkEntry(title)) continue;
const files = decision.files.length > 0 ? decision.files : modifiedFiles.slice(0, 5);
appendDecision(repoRoot, {
title,
description,
sessionId,
commitSha,
files,
area: deriveArea(files),
date: sessionDate,
tried: decision.tried,
rule: decision.rule,
});
}
if (sections.decisions.length > 0) {
log(`Logged ${sections.decisions.length} decision(s)`);
}

for (const mistake of sections.mistakes) {
const { title, description } = parseTitleDescription(mistake.text);
if (isJunkEntry(title)) continue;
const files = mistake.files.length > 0 ? mistake.files : modifiedFiles.slice(0, 5);
appendMistake(repoRoot, {
title,
description,
sessionId,
commitSha,
files,
area: deriveArea(files),
date: sessionDate,
tried: mistake.tried,
rule: mistake.rule,
});
}
if (sections.mistakes.length > 0) {
log(`Logged ${sections.mistakes.length} mistake(s)`);
}
if (!sections.skipKnowledge) {
// Read session data for context
const sessionContent = readFileSync(sessionPath, "utf8");
const { frontmatter } = parseFrontmatter(sessionContent);
const modifiedFiles = extractModifiedFiles(sessionContent);
const commitSha = (frontmatter.base_commit as string) || "unknown";
const sessionDate = sessionId.slice(0, 10);

for (const decision of sections.decisions) {
const { title, description } = parseTitleDescription(decision.text);
if (isJunkEntry(title)) continue;
const files = decision.files.length > 0 ? decision.files : modifiedFiles.slice(0, 5);
appendDecision(repoRoot, {
title,
description,
sessionId,
commitSha,
files,
area: deriveArea(files),
date: sessionDate,
tried: decision.tried,
rule: decision.rule,
});
}
if (sections.decisions.length > 0) {
log(`Logged ${sections.decisions.length} decision(s)`);
}

// Auto-detect corrections: files modified in consecutive turns
const corrections = detectCorrections(sessionContent);
if (corrections.length > 0) {
const fileCounts: Record<string, number> = {};
for (const c of corrections) {
fileCounts[c.file] = (fileCounts[c.file] || 0) + 1;
for (const mistake of sections.mistakes) {
const { title, description } = parseTitleDescription(mistake.text);
if (isJunkEntry(title)) continue;
const files = mistake.files.length > 0 ? mistake.files : modifiedFiles.slice(0, 5);
appendMistake(repoRoot, {
title,
description,
sessionId,
commitSha,
files,
area: deriveArea(files),
date: sessionDate,
tried: mistake.tried,
rule: mistake.rule,
});
}
for (const [file, count] of Object.entries(fileCounts)) {
if (count >= 2) {
appendMistake(repoRoot, {
title: `Repeated modifications to ${file}`,
description: `File was modified across multiple consecutive turns — may indicate the AI struggled with this file. Review session ${sessionId} for the correct approach.`,
sessionId,
commitSha,
files: [file],
area: deriveArea([file]),
date: sessionDate,
tried: [],
rule: "",
});
log(`Auto-detected correction pattern for ${file}`);
if (sections.mistakes.length > 0) {
log(`Logged ${sections.mistakes.length} mistake(s)`);
}

// Auto-detect corrections: files modified in consecutive turns
const corrections = detectCorrections(sessionContent);
if (corrections.length > 0) {
const fileCounts: Record<string, number> = {};
for (const c of corrections) {
fileCounts[c.file] = (fileCounts[c.file] || 0) + 1;
}
for (const [file, count] of Object.entries(fileCounts)) {
if (count >= 2) {
appendMistake(repoRoot, {
title: `Repeated modifications to ${file}`,
description: `File was modified across multiple consecutive turns — may indicate the AI struggled with this file. Review session ${sessionId} for the correct approach.`,
sessionId,
commitSha,
files: [file],
area: deriveArea([file]),
date: sessionDate,
tried: [],
rule: "",
});
log(`Auto-detected correction pattern for ${file}`);
}
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,8 @@ if (import.meta.main) {
const { frontmatter } = parseFrontmatter(content);
const tags = (frontmatter.tags as string[]) || [];
const tagStr = tags.length > 0 ? ` ${c.dim}[${tags.join(", ")}]${c.reset}` : "";
console.log(`${c.cyan}${id}${c.reset} ${c.dim}${frontmatter.branch || ""}${c.reset}${tagStr}`);
const skipStr = frontmatter.skip_knowledge ? ` ${c.dim}(skipped)${c.reset}` : "";
console.log(`${c.cyan}${id}${c.reset} ${c.dim}${frontmatter.branch || ""}${c.reset}${tagStr}${skipStr}`);
}
if (sessions.length > count) {
console.log(`${c.dim}... and ${sessions.length - count} more${c.reset}`);
Expand Down
20 changes: 18 additions & 2 deletions src/knowledge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,14 @@ import { checkClaude } from "./deps.js";
import { completedDir, decisionsPath, knowledgePath, mistakesPath, SESSION_DIR } from "./paths.js";
import { searchSessions } from "./qmd.js";
import type { KnowledgeEntry } from "./session.js";
import { appendDecision, appendMistake, deriveArea, listCompletedSessions, parseKnowledgeEntries } from "./session.js";
import {
appendDecision,
appendMistake,
deriveArea,
listCompletedSessions,
parseFrontmatter,
parseKnowledgeEntries,
} from "./session.js";

// =============================================================================
// Claude CLI Check
Expand Down Expand Up @@ -66,17 +73,26 @@ export async function buildKnowledge(repoRoot: string): Promise<void> {
return;
}

// Gather all session summaries
// Gather all session summaries (skip sessions flagged as not relevant)
const summaries: string[] = [];
let skippedCount = 0;
for (const id of sessions) {
const path = join(completedDir(repoRoot), `${id}.md`);
if (!existsSync(path)) continue;
const content = readFileSync(path, "utf8");
const { frontmatter } = parseFrontmatter(content);
if (frontmatter.skip_knowledge) {
skippedCount++;
continue;
}
const summaryMatch = content.match(/## Summary\n([\s\S]*?)$/);
if (summaryMatch) {
summaries.push(`### Session ${id}\n${summaryMatch[1]!.trim()}`);
}
}
if (skippedCount > 0) {
console.log(`Skipped ${skippedCount} session(s) flagged as not relevant.`);
}

if (summaries.length === 0) {
console.log("No session summaries found. Run sessions first or wait for AI summarization.");
Expand Down
2 changes: 1 addition & 1 deletion src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1027,7 +1027,7 @@ export function parseFrontmatter(content: string): { frontmatter: Record<string,
}

/** Add a field to existing frontmatter */
function addFrontmatterField(content: string, key: string, value: unknown): string {
export function addFrontmatterField(content: string, key: string, value: unknown): string {
const parsed = parseFrontmatter(content);
parsed.frontmatter[key] = value;
return `---\n${YAML.stringify(parsed.frontmatter).trim()}\n---\n${parsed.body}`;
Expand Down
79 changes: 79 additions & 0 deletions src/summarize.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,85 @@ Rule: WHEN adding cache layers ALWAYS use Redis`;
});
});

describe("extractSections — skipKnowledge", () => {
test("returns skipKnowledge=true when Relevance is 'skip'", () => {
const summary = `## Intent
Testing the AI tool.

## Changes
- No real changes.

## Decisions
None

## Mistakes
None

## Open Items
None

## Relevance
skip

## Tags
test`;

const sections = extractSections(summary);
expect(sections.skipKnowledge).toBe(true);
});

test("returns skipKnowledge=false when Relevance is 'keep'", () => {
const summary = `## Intent
Migrate cart system.

## Changes
- Updated fees.

## Decisions
None

## Mistakes
None

## Open Items
None

## Relevance
keep

## Tags
area:cart`;

const sections = extractSections(summary);
expect(sections.skipKnowledge).toBe(false);
});

test("returns skipKnowledge=false when Relevance section is missing", () => {
const summary = `## Intent
Quick fix.

## Tags
fix`;

const sections = extractSections(summary);
expect(sections.skipKnowledge).toBe(false);
});

test("handles case-insensitive 'Skip'", () => {
const summary = `## Intent
Test chat.

## Relevance
Skip

## Tags
test`;

const sections = extractSections(summary);
expect(sections.skipKnowledge).toBe(true);
});
});

describe("extractMistakeEntries — junk filtering", () => {
test("filters 'None' variations", () => {
expect(extractMistakeEntries("## Mistakes\nNone")).toEqual([]);
Expand Down
17 changes: 17 additions & 0 deletions src/summarize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,14 @@ The Tried:, Files:, and Rule: lines are optional — omit if not applicable.
## Open Items
Anything left unfinished or flagged for follow-up

## Relevance
Assess whether this session contains knowledge worth preserving for future project context.
Write "skip" on its own line if the session is NOT relevant — e.g. test/demo chats, conversations about
the AI itself, troubleshooting the tooling rather than the project, trivial or throwaway interactions,
or anything that would add noise rather than signal to the project knowledge base.
Write "keep" on its own line if the session contains useful project context (decisions, patterns, mistakes, etc.).
Default to "keep" if uncertain.

## Tags
Comma-separated topic tags inferred from the session content.
Use namespace:value format where appropriate (e.g. area:cart, type:bug-fix).`;
Expand Down Expand Up @@ -87,6 +95,7 @@ export interface ExtractedSections {
intent: string;
changes: string;
openItems: string;
skipKnowledge: boolean;
}

/** Extract structured sections from an AI summary */
Expand All @@ -98,9 +107,17 @@ export function extractSections(summary: string): ExtractedSections {
intent: extractNamedSection(summary, "Intent"),
changes: extractNamedSection(summary, "Changes"),
openItems: extractNamedSection(summary, "Open Items"),
skipKnowledge: extractSkipKnowledge(summary),
};
}

/** Check if the AI flagged this session as not worth preserving */
function extractSkipKnowledge(summary: string): boolean {
const section = extractNamedSection(summary, "Relevance");
if (!section) return false;
return /^\s*skip\s*$/im.test(section);
}

/** Extract tags from the Tags section */
function extractTags(summary: string): string[] {
const section = extractNamedSection(summary, "Tags");
Expand Down