9 Commits
Author SHA1 Message Date
renovate-bot f0aac40dfb Update dependency Microsoft.NET.Test.Sdk to 18.8.1
PR and Push Build/Test / build-and-test (push) Successful in 9m38s
PR and Push Build/Test / build-and-test (pull_request) Successful in 9m28s
2026-07-16 02:36:52 +00:00
codex 0e0feedad2 Fix YAML escaping for meeting note frontmatter
PR and Push Build/Test / build-and-test (push) Successful in 10m21s
2026-07-15 14:55:37 +02:00
codex 89d81fa4c6 fix: encode spaces in summary image links
PR and Push Build/Test / build-and-test (push) Successful in 11m2s
2026-07-15 14:55:25 +02:00
Manuel dc49bc3330 Merge pull request 'Update dependency System.Drawing.Common to 10.0.10' (#32) from renovate/system.drawing.common-10.x into main
PR and Push Build/Test / build-and-test (push) Successful in 9m38s
2026-07-15 13:29:06 +02:00
Manuel aff3528406 Merge pull request 'Update dependency Microsoft.EntityFrameworkCore.Sqlite to 10.0.10' (#31) from renovate/microsoft.entityframeworkcore.sqlite-10.x into main
PR and Push Build/Test / build-and-test (push) Failing after 9m50s
2026-07-15 13:27:51 +02:00
Manuel d92da18d08 Merge pull request 'Update dependency Microsoft.AspNetCore.Mvc.Testing to 10.0.10' (#30) from renovate/microsoft.aspnetcore.mvc.testing-10.x into main
PR and Push Build/Test / build-and-test (push) Successful in 9m41s
2026-07-15 13:27:20 +02:00
renovate-bot ec59464340 Update dependency System.Drawing.Common to 10.0.10
PR and Push Build/Test / build-and-test (pull_request) Successful in 9m19s
PR and Push Build/Test / build-and-test (push) Successful in 9m33s
2026-07-15 09:58:37 +00:00
renovate-bot 5667102272 Update dependency Microsoft.EntityFrameworkCore.Sqlite to 10.0.10
PR and Push Build/Test / build-and-test (pull_request) Successful in 9m33s
PR and Push Build/Test / build-and-test (push) Successful in 9m9s
2026-07-15 02:35:11 +00:00
renovate-bot 2f62ed467a Update dependency Microsoft.AspNetCore.Mvc.Testing to 10.0.10
PR and Push Build/Test / build-and-test (push) Successful in 9m8s
PR and Push Build/Test / build-and-test (pull_request) Successful in 9m15s
2026-07-15 02:35:02 +00:00
7 changed files with 164 additions and 8 deletions
@@ -9,8 +9,8 @@
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="10.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.9" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.7.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.10" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.8.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5" />
</ItemGroup>
@@ -135,6 +135,72 @@ public sealed class MeetingNoteStoreTests
Assert.Equal("User notes.", loaded.UserNotes);
}
[Fact]
public async Task StoreEscapesApostrophePrefixedAttendeesBeforeWritingFrontmatter()
{
var (store, saved) = await SaveNoteAsync(
title: "Escaped Attendees",
attendees: ["'Ada Lovelace"],
projects: [],
userNotes: "Discuss attendee import.");
var content = await File.ReadAllTextAsync(saved.Path);
var loaded = await store.ReadAsync(saved.Path, CancellationToken.None);
Assert.Contains("- \"'Ada Lovelace\"", content);
Assert.Equal(["'Ada Lovelace"], loaded.Frontmatter.Attendees);
Assert.Equal("Discuss attendee import.", loaded.UserNotes);
}
[Theory]
[InlineData("Ada # platform lead")]
[InlineData("- Ada Lovelace")]
[InlineData("? Ada Lovelace")]
[InlineData("{Ada: Platform}")]
[InlineData("*Ada")]
[InlineData("&Ada")]
[InlineData("!Ada")]
[InlineData("| Ada")]
[InlineData("> Ada")]
[InlineData("@Ada")]
[InlineData("`Ada")]
[InlineData("true")]
[InlineData("null")]
[InlineData("2026-07-08")]
[InlineData("Ada \"The Architect\" Lovelace")]
[InlineData("C:\\People\\Ada")]
public async Task StoreEscapesYamlSensitiveAttendeesBeforeWritingFrontmatter(string attendee)
{
var (store, saved) = await SaveNoteAsync(
title: "Escaped Attendees",
attendees: [attendee],
projects: [],
userNotes: "Discuss attendee import.");
var loaded = await store.ReadAsync(saved.Path, CancellationToken.None);
Assert.Equal([attendee], loaded.Frontmatter.Attendees);
Assert.Equal("Discuss attendee import.", loaded.UserNotes);
}
[Theory]
[InlineData("Planning # Q3")]
[InlineData("- Planning")]
[InlineData("{Planning: Q3}")]
[InlineData("true")]
[InlineData("2026-07-08")]
public async Task StoreEscapesYamlSensitiveScalarAndProjectFrontmatterValues(string value)
{
var (store, saved) = await SaveNoteAsync(
title: value,
attendees: [],
projects: [value],
userNotes: "Discuss YAML escaping.");
var loaded = await store.ReadAsync(saved.Path, CancellationToken.None);
Assert.Equal(value, loaded.Frontmatter.Title);
Assert.Equal([value], loaded.Frontmatter.Projects);
Assert.Equal("Discuss YAML escaping.", loaded.UserNotes);
}
[Fact]
public void ActionLinkEscapesSummaryFileName()
{
@@ -147,6 +213,26 @@ public sealed class MeetingNoteStoreTests
link);
}
private static async Task<(MarkdownMeetingNoteStore Store, MeetingNote Saved)> SaveNoteAsync(
string title,
IReadOnlyList<string> attendees,
IReadOnlyList<string> projects,
string userNotes)
{
var (vaultRoot, store) = CreateStore();
var note = MeetingNoteTemplate.Create(
title: title,
attendees: attendees,
projects: projects,
transcriptPath: Path.Combine(vaultRoot, "Meetings", "Transcripts", "20260519-transcript.md"),
assistantContextPath: Path.Combine(vaultRoot, "Meetings", "Assistant Context", "20260519-context.md"),
summaryPath: Path.Combine(vaultRoot, "Meetings", "Summaries", "20260519-summary.md"),
userNotes: userNotes);
var saved = await store.SaveAsync(note, CancellationToken.None);
return (store, saved);
}
private static (string VaultRoot, MarkdownMeetingNoteStore Store) CreateStore()
{
var vaultRoot = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N"));
@@ -24,6 +24,7 @@ public sealed class MeetingSummaryInstructionBuilderTests
Assert.Contains("You are the Meeting Assistant summary agent.", instructions);
Assert.Contains("include only the most relevant cropped screenshots", instructions);
Assert.Contains("Do not include every cropped screenshot", instructions);
Assert.Contains("encode spaces in image-link targets as `%20`", instructions);
Assert.Contains("Use add_attendee and remove_attendee", instructions);
Assert.Contains("partial screenshot", instructions);
Assert.Contains("override_speaker", instructions);
+2 -2
View File
@@ -24,12 +24,12 @@
<PackageReference Include="DiffPlex" Version="1.9.0" />
<PackageReference Include="Microsoft.CognitiveServices.Speech" Version="$(MicrosoftSpeechVersion)" />
<PackageReference Include="Microsoft.CognitiveServices.Speech.Extension.MAS" Version="$(MicrosoftSpeechVersion)" ExcludeAssets="build" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.10" />
<PackageReference Include="NAudio" Version="2.3.0" />
<PackageReference Include="NCalcSync" Version="6.4.0" />
<PackageReference Include="RazorLight" Version="2.3.1" />
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="3.0.3" />
<PackageReference Include="System.Drawing.Common" Version="10.0.9" />
<PackageReference Include="System.Drawing.Common" Version="10.0.10" />
<PackageReference Include="Whisper.net" Version="1.9.1" />
<PackageReference Include="Whisper.net.Runtime" Version="1.9.1" />
<PackageReference Include="YamlDotNet" Version="18.1.0" />
@@ -1,4 +1,6 @@
using Microsoft.Extensions.Options;
using System.Globalization;
using System.Text;
using YamlDotNet.Core;
using YamlDotNet.Serialization;
@@ -113,7 +115,24 @@ public sealed class MarkdownMeetingNoteStore : IMeetingNoteStore
private static string EscapeQuoted(string value)
{
return $"\"{value.Replace("\"", "\\\"", StringComparison.Ordinal)}\"";
var escaped = new StringBuilder(value.Length + 2);
escaped.Append('"');
foreach (var character in value)
{
escaped.Append(character switch
{
'\\' => "\\\\",
'"' => "\\\"",
'\r' => "\\r",
'\n' => "\\n",
'\t' => "\\t",
< ' ' => "\\x" + ((int)character).ToString("X2", CultureInfo.InvariantCulture),
_ => character.ToString()
});
}
escaped.Append('"');
return escaped.ToString();
}
private static string EscapeNullableDateTime(DateTimeOffset? value)
@@ -135,7 +154,7 @@ public sealed class MarkdownMeetingNoteStore : IMeetingNoteStore
return "\"\"";
}
if (value.Contains(':', StringComparison.Ordinal) || value.StartsWith("[", StringComparison.Ordinal))
if (RequiresQuotedScalar(value))
{
return EscapeQuoted(value);
}
@@ -143,6 +162,49 @@ public sealed class MarkdownMeetingNoteStore : IMeetingNoteStore
return value;
}
private static bool RequiresQuotedScalar(string value)
{
if (char.IsWhiteSpace(value[0]) || char.IsWhiteSpace(value[^1]))
{
return true;
}
if (value.Contains(':', StringComparison.Ordinal) ||
value.Contains('\'', StringComparison.Ordinal) ||
value.Contains(" #", StringComparison.Ordinal) ||
value.Any(char.IsControl))
{
return true;
}
if (IsYamlIndicator(value[0]))
{
return true;
}
if (IsYamlCoreSchemaKeyword(value))
{
return true;
}
return DateTimeOffset.TryParse(value, out _) ||
double.TryParse(value, CultureInfo.InvariantCulture, out _);
}
private static bool IsYamlIndicator(char character)
{
return character is '-' or '?' or ':' or ',' or '[' or ']' or '{' or '}' or
'#' or '&' or '*' or '!' or '|' or '>' or '\'' or '"' or '%' or '@' or '`';
}
private static bool IsYamlCoreSchemaKeyword(string value)
{
return string.Equals(value, "true", StringComparison.OrdinalIgnoreCase) ||
string.Equals(value, "false", StringComparison.OrdinalIgnoreCase) ||
string.Equals(value, "null", StringComparison.OrdinalIgnoreCase) ||
value == "~";
}
private sealed class MeetingNoteYaml
{
[YamlMember(Alias = "title")]
@@ -22,7 +22,7 @@ public sealed class MeetingSummaryInstructionBuilder : IMeetingSummaryInstructio
After writing the summary, update existing project files when the meeting produced durable project knowledge, decisions, next steps, or context.
Use list_projects first to see which projects are bound to this meeting. Use search, list_past_project_meetings, read_past_project_meeting_summary, and read_projectfile before changing existing project files. search includes both project files and past meeting summaries for the requested current-meeting project scope.
The summary note should contain concise sections for summary, decisions, open questions, and next steps.
If the assistant context contains cropped screenshot markdown links, include only the most relevant cropped screenshots in the summary by markdown-linking them near the related summary text. Do not include every cropped screenshot by default, and do not link uncropped screenshots unless no cropped version exists and the image is important.
If the assistant context contains cropped screenshot markdown links, include only the most relevant cropped screenshots in the summary by markdown-linking them near the related summary text. When embedding them, encode spaces in image-link targets as `%20`; never leave literal spaces in the link target. Do not include every cropped screenshot by default, and do not link uncropped screenshots unless no cropped version exists and the image is important.
Keep the output grounded in the source material and explicitly say when a section has no known items.
""";
+8 -1
View File
@@ -44,6 +44,8 @@ The meeting note frontmatter SHALL link to the transcript, assistant context, an
Generated artifact notes SHALL link only to the other notes from the same run and SHALL omit the frontmatter property that would reference themselves.
Meeting Assistant SHALL escape generated meeting-note frontmatter string values after metadata enrichment and workflow rules have been applied, immediately before writing the final markdown file.
#### Scenario: Meeting note links to generated artifacts
- **WHEN** Meeting Assistant creates a meeting note
- **THEN** the note frontmatter links to the configured transcript, assistant context, and summary note locations
@@ -59,6 +61,12 @@ Generated artifact notes SHALL link only to the other notes from the same run an
- **THEN** the artifact frontmatter links to the other run notes
- **AND** the artifact frontmatter omits the property for the artifact's own note type
#### Scenario: Generated frontmatter remains parseable after attendee transforms
- **GIVEN** meeting metadata or workflow rules produce an attendee name that contains a single quote
- **WHEN** Meeting Assistant writes the final meeting note
- **THEN** the attendee value is escaped in frontmatter
- **AND** the meeting note frontmatter remains parseable when read back
### Requirement: Meeting notes preserve user-authored content
Meeting Assistant SHALL preserve user-authored meeting notes and include them as input when generating summaries, decisions, and next steps.
@@ -354,4 +362,3 @@ Meeting Assistant SHALL provide a diagnostic endpoint that opens the workflow ru
- **GIVEN** the rules editor chat window content fits without scrolling or is already near the bottom
- **WHEN** a new user or assistant message is appended
- **THEN** the conversation scrolls to the bottom of the newly rendered message content