diff --git a/MeetingAssistant.Tests/MeetingSummaryToolTests.cs b/MeetingAssistant.Tests/MeetingSummaryToolTests.cs index eb044c8..e589e1c 100644 --- a/MeetingAssistant.Tests/MeetingSummaryToolTests.cs +++ b/MeetingAssistant.Tests/MeetingSummaryToolTests.cs @@ -56,6 +56,8 @@ public sealed class MeetingSummaryToolTests Assert.Contains("end_time: \"2026-05-20T10:30:00.0000000+02:00\"", summary); Assert.Contains("attendees:", summary); Assert.Contains("- Ada", summary); + Assert.Contains("projects:", summary); + Assert.Contains("- Meeting Assistant", summary); Assert.Contains("meeting: \"[[../Notes/meeting|Meeting Note]]\"", summary); Assert.Contains("transcript: \"[[../Transcripts/transcript|Transcript]]\"", summary); Assert.Contains("assistant_context: \"[[../Assistant Context/context|Assistant Context]]\"", summary); @@ -106,6 +108,49 @@ public sealed class MeetingSummaryToolTests Assert.DoesNotContain("- Grace", summary); } + [Fact] + public async Task WriteSummaryCopiesCurrentMeetingProjectsOverExistingSummaryProjects() + { + var root = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N")); + var artifacts = new MeetingSessionArtifacts( + MeetingNotePath: Path.Combine(root, "Meetings", "Notes", "meeting.md"), + TranscriptPath: Path.Combine(root, "Meetings", "Transcripts", "transcript.md"), + AssistantContextPath: Path.Combine(root, "Meetings", "Assistant Context", "context.md"), + SummaryPath: Path.Combine(root, "Meetings", "Summaries", "summary.md")); + Directory.CreateDirectory(Path.GetDirectoryName(artifacts.MeetingNotePath)!); + Directory.CreateDirectory(Path.GetDirectoryName(artifacts.SummaryPath)!); + await File.WriteAllTextAsync( + artifacts.MeetingNotePath, + """ + --- + title: Meeting + projects: + - Current Project + - Second Project + --- + """); + await File.WriteAllTextAsync( + artifacts.SummaryPath, + """ + --- + title: Existing + projects: + - Stale Project + --- + + Old summary. + """); + var tools = new MeetingSummaryTools(artifacts); + + await tools.WriteSummary("# Summary"); + + var summary = await File.ReadAllTextAsync(artifacts.SummaryPath); + Assert.Contains("projects:", summary); + Assert.Contains("- Current Project", summary); + Assert.Contains("- Second Project", summary); + Assert.DoesNotContain("- Stale Project", summary); + } + [Fact] public async Task ReadToolsSupportClampedLineRanges() { diff --git a/MeetingAssistant/MeetingNotes/MeetingArtifactFrontmatter.cs b/MeetingAssistant/MeetingNotes/MeetingArtifactFrontmatter.cs index a65ce33..7b2afdc 100644 --- a/MeetingAssistant/MeetingNotes/MeetingArtifactFrontmatter.cs +++ b/MeetingAssistant/MeetingNotes/MeetingArtifactFrontmatter.cs @@ -12,6 +12,8 @@ public sealed class MeetingArtifactFrontmatter public List? Attendees { get; set; } + public List? Projects { get; set; } + public string Meeting { get; set; } = ""; public string Transcript { get; set; } = ""; @@ -39,6 +41,7 @@ public static class MeetingArtifactFrontmatterRenderer AppendDateTime(builder, "start_time", frontmatter.StartTime); AppendDateTime(builder, "end_time", frontmatter.EndTime); AppendListIfNotNull(builder, "attendees", frontmatter.Attendees); + AppendListIfNotNull(builder, "projects", frontmatter.Projects); AppendQuotedIfNotEmpty(builder, "meeting", frontmatter.Meeting); AppendQuotedIfNotEmpty(builder, "transcript", frontmatter.Transcript); AppendQuotedIfNotEmpty(builder, "assistant_context", frontmatter.AssistantContext); diff --git a/MeetingAssistant/Summary/MeetingSummaryTools.cs b/MeetingAssistant/Summary/MeetingSummaryTools.cs index d2b2f52..2a181bc 100644 --- a/MeetingAssistant/Summary/MeetingSummaryTools.cs +++ b/MeetingAssistant/Summary/MeetingSummaryTools.cs @@ -107,6 +107,7 @@ public sealed class MeetingSummaryTools string.IsNullOrWhiteSpace(summaryTitle) ? "Meeting Summary" : summaryTitle, artifacts.SummaryPath); frontmatter.Attendees = await ResolveSummaryAttendeesAsync(meetingNote); + frontmatter.Projects = ResolveSummaryProjects(meetingNote); await File.WriteAllTextAsync( artifacts.SummaryPath, MeetingArtifactFrontmatterRenderer.Render(frontmatter, markdown)); @@ -591,6 +592,13 @@ public sealed class MeetingSummaryTools : meetingNote.Frontmatter.Attendees.ToList(); } + private static List? ResolveSummaryProjects(MeetingNote meetingNote) + { + return meetingNote.Frontmatter.Projects.Count == 0 + ? null + : meetingNote.Frontmatter.Projects.ToList(); + } + private async Task?> ReadExistingSummaryAttendeesAsync() { if (!File.Exists(artifacts.SummaryPath)) diff --git a/openspec/specs/meeting-summary/spec.md b/openspec/specs/meeting-summary/spec.md index e9b2e3c..64c71bd 100644 --- a/openspec/specs/meeting-summary/spec.md +++ b/openspec/specs/meeting-summary/spec.md @@ -61,7 +61,7 @@ After writing the meeting summary, the summary agent SHALL update existing proje The assistant context note SHALL keep `title`, `start_time`, `end_time`, `meeting`, `transcript`, `summary`, `state`, and `agenda` in frontmatter. Meeting Assistant SHALL write `title` and `start_time` when the assistant context note is created, update `title` when later meeting metadata is discovered, and write `end_time` when transcript processing stops. When a Teams appointment is detected at recording start, the assistant context note SHALL also keep `scheduled_end` in frontmatter. The `state` value SHALL be one of `collecting metadata`, `transcribing`, `speaker recognition`, `summarizing`, `finished`, or `error`. -The summary note SHALL keep frontmatter links to meeting artifacts and SHALL copy `title`, `start_time`, `end_time`, and `attendees` from the meeting note when available. If the meeting note has no title, the summary agent SHALL be able to provide a title when writing the summary. +The summary note SHALL keep frontmatter links to meeting artifacts and SHALL copy `title`, `start_time`, `end_time`, `attendees`, and `projects` from the meeting note when available. If the meeting note has no title, the summary agent SHALL be able to provide a title when writing the summary. When the summary note already has an `attendees` frontmatter property, Meeting Assistant SHALL preserve the existing summary attendees instead of overwriting them from the meeting note. @@ -88,6 +88,10 @@ The summary agent SHALL be able to read and write the assistant context body as - **WHEN** the summary agent rewrites the summary - **THEN** Meeting Assistant preserves the existing summary attendees +#### Scenario: Summary copies final meeting projects +- **WHEN** the summary agent writes a summary note after meeting projects have changed +- **THEN** the summary note frontmatter includes the final meeting note projects + #### Scenario: Summary waits for pending screenshot OCR - **GIVEN** screenshot OCR is still running for a meeting - **WHEN** transcript processing is ready to run the automatic summary