feat: Enhance observation and summary structures in hooks

- Updated observation schema to include hierarchical fields: title, subtitle, facts, narrative, concepts, files_read, and files_modified.
- Modified the save-hook and summary-hook scripts to accommodate the new observation structure.
- Added migration logic to the HooksDatabase for adding new fields to the observations table.
- Refactored the parser to extract new fields from XML formatted observations.
- Adjusted prompt generation to reflect the new observation format and requirements.
- Updated worker service to handle new observation and summary structures.
This commit is contained in:
Alex Newman
2025-10-18 17:34:24 -04:00
parent 938eb9dc0e
commit 81101ef1a6
11 changed files with 447 additions and 1041 deletions
+67 -21
View File
@@ -5,7 +5,13 @@
export interface ParsedObservation {
type: string;
text: string;
title: string;
subtitle: string;
facts: string[];
narrative: string;
concepts: string[];
files_read: string[];
files_modified: string[];
}
export interface ParsedSummary {
@@ -14,9 +20,7 @@ export interface ParsedSummary {
learned: string;
completed: string;
next_steps: string;
files_read: string[];
files_edited: string[];
notes: string;
notes: string | null;
}
/**
@@ -27,23 +31,44 @@ export function parseObservations(text: string): ParsedObservation[] {
const observations: ParsedObservation[] = [];
// Match <observation>...</observation> blocks (non-greedy)
const observationRegex = /<observation>\s*<type>([^<]+)<\/type>\s*<text>([^<]+)<\/text>\s*<\/observation>/g;
const observationRegex = /<observation>([\s\S]*?)<\/observation>/g;
let match;
while ((match = observationRegex.exec(text)) !== null) {
const type = match[1].trim();
const observationText = match[2].trim();
const obsContent = match[1];
// Extract all fields
const type = extractField(obsContent, 'type');
const title = extractField(obsContent, 'title');
const subtitle = extractField(obsContent, 'subtitle');
const narrative = extractField(obsContent, 'narrative');
const facts = extractArrayElements(obsContent, 'facts', 'fact');
const concepts = extractArrayElements(obsContent, 'concepts', 'concept');
const files_read = extractArrayElements(obsContent, 'files_read', 'file');
const files_modified = extractArrayElements(obsContent, 'files_modified', 'file');
// Validate required fields
if (!type || !title || !subtitle || !narrative) {
console.warn('[SDK Parser] Observation missing required fields, skipping');
continue;
}
// Validate type
const validTypes = ['decision', 'bugfix', 'feature', 'refactor', 'discovery'];
if (!validTypes.includes(type)) {
const validTypes = ['change', 'discovery', 'decision'];
if (!validTypes.includes(type.trim())) {
console.warn(`[SDK Parser] Invalid observation type: ${type}, skipping`);
continue;
}
observations.push({
type,
text: observationText
type: type.trim(),
title,
subtitle,
facts,
narrative,
concepts,
files_read,
files_modified
});
}
@@ -65,20 +90,16 @@ export function parseSummary(text: string): ParsedSummary | null {
const summaryContent = summaryMatch[1];
// Extract required fields
// Extract fields
const request = extractField(summaryContent, 'request');
const investigated = extractField(summaryContent, 'investigated');
const learned = extractField(summaryContent, 'learned');
const completed = extractField(summaryContent, 'completed');
const next_steps = extractField(summaryContent, 'next_steps');
const notes = extractField(summaryContent, 'notes');
const notes = extractField(summaryContent, 'notes'); // Optional
// Extract file arrays
const files_read = extractFileArray(summaryContent, 'files_read');
const files_edited = extractFileArray(summaryContent, 'files_edited');
// Validate all required fields are present
if (!request || !investigated || !learned || !completed || !next_steps || !notes) {
// Validate required fields are present (notes is optional)
if (!request || !investigated || !learned || !completed || !next_steps) {
console.warn('[SDK Parser] Summary missing required fields');
return null;
}
@@ -89,8 +110,6 @@ export function parseSummary(text: string): ParsedSummary | null {
learned,
completed,
next_steps,
files_read,
files_edited,
notes
};
}
@@ -130,3 +149,30 @@ function extractFileArray(content: string, arrayName: string): string[] {
return files;
}
/**
* Extract array of elements from XML content
* Generic version of extractFileArray that works with any element name
*/
function extractArrayElements(content: string, arrayName: string, elementName: string): string[] {
const elements: string[] = [];
// Match the array block
const arrayRegex = new RegExp(`<${arrayName}>(.*?)</${arrayName}>`, 's');
const arrayMatch = arrayRegex.exec(content);
if (!arrayMatch) {
return elements;
}
const arrayContent = arrayMatch[1];
// Extract individual elements
const elementRegex = new RegExp(`<${elementName}>([^<]+)</${elementName}>`, 'g');
let elementMatch;
while ((elementMatch = elementRegex.exec(arrayContent)) !== null) {
elements.push(elementMatch[1].trim());
}
return elements;
}