Make agent file writes append by default
PR and Push Build/Test / build-and-test (push) Failing after 6m36s

This commit is contained in:
2026-05-28 12:36:23 +02:00
parent 78549645bc
commit 7ff93b73b3
19 changed files with 401 additions and 27 deletions
@@ -503,12 +503,42 @@ public sealed class MeetingSummaryToolTests
""");
var tools = new MeetingSummaryTools(artifacts);
await tools.WriteContext("appended");
await tools.WriteContext("inserted", insert: 2);
await tools.WriteContext("replacement", from: 1, to: 1);
var context = await File.ReadAllTextAsync(artifacts.AssistantContextPath);
Assert.Contains("state: summarizing", context);
Assert.Contains("replacement\ninserted\nline two", context);
Assert.Contains("replacement\ninserted\nline two\nappended", context);
}
[Fact]
public async Task ToolsReplaceAssistantContextBodyOnlyWhenRequested()
{
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.AssistantContextPath)!);
await File.WriteAllTextAsync(
artifacts.AssistantContextPath,
"""
---
state: summarizing
---
line one
""");
var tools = new MeetingSummaryTools(artifacts);
await tools.WriteContext("replacement", replace_file: true);
var context = await File.ReadAllTextAsync(artifacts.AssistantContextPath);
Assert.Contains("state: summarizing", context);
Assert.Contains("replacement", context);
Assert.DoesNotContain("line one", context);
}
[Fact]
@@ -561,7 +591,8 @@ public sealed class MeetingSummaryToolTests
# Assistant Context
## Live Context
""");
""",
replace_file: true);
var context = await File.ReadAllTextAsync(artifacts.AssistantContextPath);
Assert.Contains("state: summarizing", context);
@@ -57,14 +57,14 @@ public sealed class ProjectKnowledgeToolTests
Assert.Equal("MeetingAssistant/notes/summary.md", writeResult);
Assert.Equal("# Project Update", await File.ReadAllTextAsync(Path.Combine(meetingAssistantRoot, "notes", "summary.md")));
Assert.Equal("IgnoredProject/ignored.md", await tools.WriteProjectFile("IgnoredProject", "ignored.md", "changed"));
Assert.Equal("changed", await File.ReadAllTextAsync(Path.Combine(ignoredRoot, "ignored.md")));
Assert.Equal("alpha should not be searched\nchanged", await File.ReadAllTextAsync(Path.Combine(ignoredRoot, "ignored.md")));
Assert.Equal(
"README.md:2 Second alpha line",
await tools.Search("alpha"));
}
[Fact]
public async Task WriteProjectFileSupportsOverwriteReplaceInsertAndCreate()
public async Task WriteProjectFileSupportsAppendReplaceInsertOverwriteAndCreate()
{
var root = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N"));
var projectsRoot = Path.Combine(root, "Projects");
@@ -88,6 +88,12 @@ public sealed class ProjectKnowledgeToolTests
await tools.WriteProjectFile("MeetingAssistant", "notes.md", "inserted", insert: 2);
Assert.Equal("one\ninserted\nTWO\nTHREE\nfour", await File.ReadAllTextAsync(projectFile));
await tools.WriteProjectFile("MeetingAssistant", "notes.md", "appended");
Assert.Equal("one\ninserted\nTWO\nTHREE\nfour\nappended", await File.ReadAllTextAsync(projectFile));
await tools.WriteProjectFile("MeetingAssistant", "notes.md", "replacement", replace_file: true);
Assert.Equal("replacement", await File.ReadAllTextAsync(projectFile));
await tools.WriteProjectFile("MeetingAssistant", "created/new.md", "created content");
Assert.Equal("created content", await File.ReadAllTextAsync(Path.Combine(projectRoot, "created", "new.md")));
}
@@ -112,6 +118,7 @@ public sealed class ProjectKnowledgeToolTests
Assert.StartsWith("Refused:", await tools.WriteProjectFile("MeetingAssistant", "../outside.md", "content"));
Assert.StartsWith("Refused:", await tools.WriteProjectFile("MeetingAssistant", "notes.md", "content", from: 1));
Assert.StartsWith("Refused:", await tools.WriteProjectFile("MeetingAssistant", "notes.md", "content", from: 1, to: 1, insert: 1));
Assert.StartsWith("Refused:", await tools.WriteProjectFile("MeetingAssistant", "notes.md", "content", from: 1, to: 1, replace_file: true));
}
private static MeetingSessionArtifacts CreateArtifacts(string root)
@@ -82,6 +82,46 @@ public sealed class WorkflowRulesEditorTests
Assert.DoesNotContain("other.yaml", searchResult);
}
[Fact]
public async Task RulesEditorToolsAppendByDefaultAndReplaceOnlyWhenRequested()
{
var root = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(root);
var rulesPath = Path.Combine(root, "rules.yaml");
await File.WriteAllTextAsync(
rulesPath,
"""
rules:
- name: existing
on:
- created: {}
steps:
- uses: add_attendee
value: Ada
""");
var tools = new WorkflowRulesEditorTools(new MeetingAssistantOptions
{
Automation = new AutomationOptions { RulesPath = rulesPath }
});
await tools.WriteRules("""
- name: appended
on:
- created: {}
steps:
- uses: add_attendee
value: Grace
""");
var appended = await File.ReadAllTextAsync(rulesPath);
Assert.Contains("name: existing", appended);
Assert.Contains("name: appended", appended);
await tools.WriteRules("rules: []", replace_file: true);
Assert.Equal("rules: []", await File.ReadAllTextAsync(rulesPath));
}
[Fact]
public async Task RulesEditorToolsRefuseInvalidYamlWithoutOverwritingFile()
{
@@ -94,7 +134,7 @@ public sealed class WorkflowRulesEditorTests
Automation = new AutomationOptions { RulesPath = rulesPath }
});
var result = await tools.WriteRules("rules:\n- name: [");
var result = await tools.WriteRules("rules:\n- name: [", replace_file: true);
Assert.StartsWith("Refused: workflow rules YAML is invalid.", result);
Assert.Equal("rules: []", await File.ReadAllTextAsync(rulesPath));
@@ -13,7 +13,7 @@ public sealed class MeetingSummaryInstructionBuilder : IMeetingSummaryInstructio
Then write the finished meeting summary as markdown by calling write_summary. The write_summary call requires an oneliner parameter: keep it very short, blurb-like, and concise. No line breaks. Complete sentences are not necessary; conciseness is more important.
If the meeting note has no title, provide a concise title parameter to write_summary.
Use read_meetingnote to inspect frontmatter such as title, attendees, projects, start_time, and end_time.
Use read_context and write_context as your own meeting notebook. Its frontmatter may include agenda from the calendar appointment. Record useful internal notes, missing context, requests for future tools, suggested improvements, and relevant context discovered from other sources. Keep user-facing summary content in the summary note.
Use read_context and write_context as your own meeting notebook. Its frontmatter may include agenda from the calendar appointment. Record useful internal notes, missing context, requests for future tools, suggested improvements, and relevant context discovered from other sources. write_context appends by default; use replace_file=true only when you are intentionally replacing the whole assistant context body. Keep user-facing summary content in the summary note.
Use add_dictation_word when project context, user notes, or transcript evidence show that a domain term, acronym, name, or unusual word is likely to be repeatedly mistranscribed. Add only the canonical spelling, one term at a time.
Use add_attendee and remove_attendee to sharpen the meeting attendees list from clear transcript evidence and screenshot OCR participant evidence. Treat OCR that says visible people are a partial screenshot result as incomplete evidence; do not remove attendees solely because they are absent from a partial screenshot.
Use override_speaker only when you are very certain that a transcript speaker label belongs to a named person, for example from user notes, OCR evidence with a matching timestamp, or very clear context cues. Provide the exact speaker label from the transcript and the replacement speaker name. If the replacement speaker already exists in the transcript, use merge=true only when you are certain both speaker labels are the same identity; otherwise do not merge them.
@@ -249,12 +249,13 @@ public sealed class MeetingSummaryTools
string content,
int? @from = null,
int? to = null,
int? insert = null)
int? insert = null,
bool replace_file = false)
{
var editMode = FileLineEditMode.Create(@from, to, insert);
var editMode = FileLineEditMode.Create(@from, to, insert, replace_file);
if (editMode is null)
{
return "Refused: supply either both from and to for replacement, insert for insertion, or no line arguments for overwrite.";
return "Refused: supply either both from and to for replacement, insert for insertion, or no line arguments for append; set replace_file=true only for whole-file replacement.";
}
if (!File.Exists(artifacts.AssistantContextPath))
@@ -316,12 +317,13 @@ public sealed class MeetingSummaryTools
string content,
int? @from = null,
int? to = null,
int? insert = null)
int? insert = null,
bool replace_file = false)
{
var editMode = FileLineEditMode.Create(@from, to, insert);
var editMode = FileLineEditMode.Create(@from, to, insert, replace_file);
if (editMode is null)
{
return "Refused: supply either both from and to for replacement, insert for insertion, or no line arguments for overwrite.";
return "Refused: supply either both from and to for replacement, insert for insertion, or no line arguments for append; set replace_file=true only for whole-file replacement.";
}
var target = ResolveExistingProjectFilePath(project, path);
@@ -544,6 +546,15 @@ public sealed class MeetingSummaryTools
return;
}
if (editMode.Kind == FileLineEditKind.Append)
{
var existing = File.Exists(filePath)
? await File.ReadAllTextAsync(filePath)
: "";
await File.WriteAllTextAsync(filePath, AppendContent(existing, content));
return;
}
var lines = File.Exists(filePath)
? (await File.ReadAllLinesAsync(filePath)).ToList()
: [];
@@ -616,6 +627,11 @@ public sealed class MeetingSummaryTools
return replacementContent;
}
if (editMode.Kind == FileLineEditKind.Append)
{
return AppendContent(existingContent, replacementContent);
}
var lines = SplitLines(existingContent);
var replacementLines = SplitLines(replacementContent);
if (editMode.Kind == FileLineEditKind.Insert)
@@ -637,6 +653,24 @@ public sealed class MeetingSummaryTools
return string.Join('\n', lines);
}
private static string AppendContent(string existingContent, string content)
{
if (string.IsNullOrEmpty(existingContent))
{
return content;
}
if (string.IsNullOrEmpty(content))
{
return existingContent;
}
var separator = existingContent.EndsWith('\n') || existingContent.EndsWith('\r')
? ""
: "\n";
return existingContent + separator + content;
}
private Task<List<BoundMeetingProject>> GetBoundProjectsAsync()
{
return projectResolver.GetBoundProjectsAsync(artifacts);
@@ -778,8 +812,13 @@ public sealed class MeetingSummaryTools
int? To = null,
int? Insert = null)
{
public static FileLineEditMode? Create(int? from, int? to, int? insert)
public static FileLineEditMode? Create(int? from, int? to, int? insert, bool replaceFile)
{
if (replaceFile && (from.HasValue || to.HasValue || insert.HasValue))
{
return null;
}
if (insert.HasValue)
{
return from.HasValue || to.HasValue
@@ -794,12 +833,13 @@ public sealed class MeetingSummaryTools
: null;
}
return new FileLineEditMode(FileLineEditKind.Overwrite);
return new FileLineEditMode(replaceFile ? FileLineEditKind.Overwrite : FileLineEditKind.Append);
}
}
private enum FileLineEditKind
{
Append,
Overwrite,
Replace,
Insert
@@ -143,7 +143,7 @@ public sealed class OpenAiMeetingSummaryAgentPipeline : IMeetingSummaryPipeline
AIFunctionFactory.Create(
tools.WriteContext,
"write_context",
"Write internal assistant notes to the assistant context body. With no line arguments, overwrite the body. With from and to, replace that inclusive 1-based line range. With insert, insert content at that 1-based line position."),
"Write internal assistant notes to the assistant context body. With no line arguments, append to the body. Set replace_file=true only when intentionally replacing the whole body. With from and to, replace that inclusive 1-based line range. With insert, insert content at that 1-based line position."),
AIFunctionFactory.Create(
tools.ReadGlossary,
"read_glossary",
@@ -187,7 +187,7 @@ public sealed class OpenAiMeetingSummaryAgentPipeline : IMeetingSummaryPipeline
AIFunctionFactory.Create(
tools.WriteProjectFile,
"write_projectfile",
"Write a file inside an existing project folder. With no line arguments, overwrite or create the file. With from and to, replace that inclusive 1-based line range. With insert, insert content at that 1-based line position."),
"Write a file inside an existing project folder. With no line arguments, append or create the file. Set replace_file=true only when intentionally replacing the whole file. With from and to, replace that inclusive 1-based line range. With insert, insert content at that 1-based line position."),
AIFunctionFactory.Create(
tools.Search,
"search",
@@ -124,7 +124,7 @@ public sealed class WorkflowRulesEditorChatPipeline : IWorkflowRulesEditorChatPi
AIFunctionFactory.Create(
tools.WriteRules,
"write_rules",
"Overwrite the configured workflow rules YAML file after validating that it parses as a workflow rules document."),
"Append to the configured workflow rules YAML file by default after validating that the complete file parses as a workflow rules document. Set replace_file=true only when intentionally replacing the whole rules file."),
AIFunctionFactory.Create(
tools.Search,
"search",
@@ -15,6 +15,7 @@ public sealed class WorkflowRulesEditorInstructionBuilder : IWorkflowRulesEditor
Your purpose is to help edit the configured local workflow rules YAML file and manage the local speaker identity database for Meeting Assistant.
Use the read_rules, search, and write_rules tools for workflow rules. Do not ask for or modify unrelated files.
Read the existing rules before making changes unless the user explicitly asks to replace the whole file.
write_rules appends by default. Pass replace_file=true only when you intentionally replace the complete rules file.
Preserve valid YAML, keep personal/local rules in the configured ignored rules file, and keep changes minimal.
When writing rules, prefer existing rule style and names.
Use search_identities and read_identity before changing identities unless the user gives an exact identity id.
@@ -51,7 +51,7 @@ public sealed class WorkflowRulesEditorTools
return ReadLines(await File.ReadAllTextAsync(rulesPath), from, to);
}
public async Task<string> WriteRules(string yaml)
public async Task<string> WriteRules(string yaml, bool replace_file = false)
{
if (rulesPath is null)
{
@@ -63,14 +63,20 @@ public sealed class WorkflowRulesEditorTools
return "Refused: yaml must not be null.";
}
var validation = ValidateYaml(yaml);
var existing = File.Exists(rulesPath)
? await File.ReadAllTextAsync(rulesPath)
: "";
var updated = replace_file
? yaml
: AppendContent(existing, yaml);
var validation = ValidateYaml(updated);
if (validation is not null)
{
return validation;
}
Directory.CreateDirectory(Path.GetDirectoryName(rulesPath)!);
await File.WriteAllTextAsync(rulesPath, yaml);
await File.WriteAllTextAsync(rulesPath, updated);
return rulesPath;
}
@@ -359,6 +365,24 @@ public sealed class WorkflowRulesEditorTools
}
}
private static string AppendContent(string existingContent, string content)
{
if (string.IsNullOrEmpty(existingContent))
{
return content;
}
if (string.IsNullOrEmpty(content))
{
return existingContent;
}
var separator = existingContent.EndsWith('\n') || existingContent.EndsWith('\r')
? ""
: "\n";
return existingContent + separator + content;
}
private static async Task<string?> RunRipgrepAsync(string rulesPath, string keywords)
{
try
+2 -2
View File
@@ -290,7 +290,7 @@ GET /meetings/summary/retry?summaryPath=...
Failure summary notes include a clickable retry link using `Api:PublicBaseUrl`, for example `http://localhost:5090/meetings/summary/retry?summaryPath=20260519-124110-summary.md`. The GET endpoint exists for Obsidian/browser clicks and performs the same overwrite behavior as the POST endpoint. Meeting note bodies stay reserved for user-authored notes.
The summary agent currently has meeting-scoped tools to read the transcript, full meeting note including frontmatter, assistant context, user notes, glossary, and project files. Every read tool that returns file-like content can read the whole content or a clamped inclusive 1-based line range with `from` and `to`, so the agent can inspect large transcripts incrementally. The assistant context note is the agent's notebook and user-visible context window; the agent can write internal notes, discovered context, missing tool requests, suggested improvements, or follow-up task ideas there using line-based overwrite, replace, and insert modes. Project lookup and search tools are constrained to the projects listed in the meeting note frontmatter:
The summary agent currently has meeting-scoped tools to read the transcript, full meeting note including frontmatter, assistant context, user notes, glossary, and project files. Every read tool that returns file-like content can read the whole content or a clamped inclusive 1-based line range with `from` and `to`, so the agent can inspect large transcripts incrementally. The assistant context note is the agent's notebook and user-visible context window; the agent can write internal notes, discovered context, missing tool requests, suggested improvements, or follow-up task ideas there using append-by-default, explicit whole-file replacement, replace, and insert modes. Project lookup and search tools are constrained to the projects listed in the meeting note frontmatter:
When assistant context contains cropped screenshot links produced by OCR, the summary instructions tell the agent to include only the most relevant cropped screenshots in the summary and markdown-link them near the related summary text.
@@ -308,7 +308,7 @@ When assistant context contains cropped screenshot links produced by OCR, the su
`search` accepts ripgrep syntax and returns matches as `filename:line text`. If one project is searched, paths are relative to that project; if multiple projects are searched, paths include the project folder name.
`write_projectfile` writes inside an existing project folder. With no line arguments it overwrites or creates the file. With `from` and `to` it replaces that inclusive 1-based line range. With `insert` it inserts content at that 1-based line position.
`write_projectfile` writes inside an existing project folder. With no line arguments it appends or creates the file. With `replace_file: true` it replaces the whole file. With `from` and `to` it replaces that inclusive 1-based line range. With `insert` it inserts content at that 1-based line position.
## Spec Workflow
+2 -2
View File
@@ -22,7 +22,7 @@ The rules file path is configured through `MeetingAssistant:Automation:RulesPath
If the configured path is empty, missing, or points to a blank file, the workflow engine does nothing.
The tray menu includes `Edit rules`, which opens a small MewUI chat editor for this configured rules file. The editor uses the summarizer agent configuration by default and can be overridden through `MeetingAssistant:WorkflowRulesEditor`.
The tray menu includes `Edit rules and identities`, which opens a small MewUI chat editor for this configured rules file and the local speaker identity database. The editor uses the summarizer agent configuration by default and can be overridden through `MeetingAssistant:WorkflowRulesEditor`.
```json
{
@@ -48,7 +48,7 @@ Blank editor values inherit from `MeetingAssistant:Agent`. This keeps the editor
The editor agent prompt includes this document and is restricted to these tools:
- `read_rules`: read the configured rules file, optionally by inclusive 1-based line range.
- `write_rules`: overwrite the configured rules file after validating that the YAML parses as a workflow rules document.
- `write_rules`: append to the configured rules file by default after validating that the complete YAML document parses as a workflow rules document. Pass `replace_file: true` only when intentionally replacing the whole rules file.
- `search`: search only the configured rules file with ripgrep-style syntax.
`POST /diagnostics/workflow/reload` reloads application configuration without restarting the process. Use it after changing workflow automation configuration such as `MeetingAssistant:Automation:RulesPath`. The endpoint returns the currently bound rules path:
@@ -0,0 +1,14 @@
# Change: Append file-write tools by default
## Why
Agent-facing file write tools currently replace whole files when no line arguments are supplied. This is risky because agents often intend to add context and can accidentally overwrite project knowledge, assistant context, or local workflow rules.
## What Changes
- Make summary-agent assistant context and project file writes append by default.
- Add an optional `replace_file` boolean to explicitly request whole-file replacement.
- Make workflow rules editor writes append by default and require `replace_file` for whole-file replacement.
- Keep line-range replacement and insertion behavior unchanged.
## Impact
- Reduces accidental destructive writes from summarizer and rules editor agents.
- Existing callers that intentionally replace whole files must pass `replace_file: true`.
@@ -0,0 +1,40 @@
## MODIFIED Requirements
### Requirement: Agents can write files in existing projects
Meeting Assistant SHALL expose a `write_projectfile` tool that can create or update files inside an existing project folder.
The target project SHALL be an existing direct subfolder of the configured projects folder. The target file path SHALL stay inside that project folder.
When no line edit arguments are supplied and `replace_file` is not true, `write_projectfile` SHALL append the supplied content to the file and SHALL create the file when it does not exist.
When `replace_file` is true, `write_projectfile` SHALL replace the whole file with the supplied content.
When both `from` and `to` are supplied, `write_projectfile` SHALL replace the inclusive 1-based line range with the supplied content.
When `insert` is supplied, `write_projectfile` SHALL insert the supplied content at that 1-based line position.
Meeting Assistant SHALL refuse ambiguous writes that combine `replace_file` with line edit arguments.
#### Scenario: Project file is appended or created
- **WHEN** the agent writes a project file without line edit arguments
- **THEN** Meeting Assistant appends the supplied content to an existing file
- **AND** creates the file with the supplied content when it does not exist
#### Scenario: Project file is explicitly replaced
- **WHEN** the agent writes a project file with `replace_file` set to true
- **THEN** Meeting Assistant writes the supplied content as the complete file content
#### Scenario: Project file line range is replaced
- **WHEN** the agent writes a project file with `from` and `to` line numbers
- **THEN** Meeting Assistant replaces the inclusive line range with the supplied content
#### Scenario: Project file content is inserted
- **WHEN** the agent writes a project file with an `insert` line number
- **THEN** Meeting Assistant inserts the supplied content at that line position
#### Scenario: Project file write target is invalid
- **WHEN** the target project is missing or the target path escapes the project folder
- **THEN** Meeting Assistant refuses the project file write
#### Scenario: Project file write mode is ambiguous
- **WHEN** the agent combines `replace_file` with `from`, `to`, or `insert`
- **THEN** Meeting Assistant refuses the project file write
@@ -0,0 +1,36 @@
## MODIFIED Requirements
### Requirement: Workflow rules and speaker identities can be edited through a tray-launched assistant
Meeting Assistant SHALL expose an `Edit rules and identities` item from the tray icon menu.
When the user selects `Edit rules and identities`, Meeting Assistant SHALL open a basic MewUI chat window for editing the configured workflow rules file and speaker identities.
The rules and identities editor agent SHALL include the workflow engine documentation in its system prompt and SHALL receive read, write, and search tools scoped to the configured workflow rules file.
The rules editor `write_rules` tool SHALL append to the configured workflow rules file by default and SHALL validate the complete resulting YAML document before writing it.
The rules editor `write_rules` tool SHALL replace the whole configured workflow rules file only when `replace_file` is true.
The rules editor `write_rules` tool SHALL refuse invalid YAML without changing the configured workflow rules file.
#### Scenario: Rules editor is scoped to the configured rules file
- **GIVEN** a configured workflow rules file
- **WHEN** the rules editor agent runs
- **THEN** its read, write, and search tools can access only that configured workflow rules file
- **AND** its system prompt includes the workflow engine documentation
#### Scenario: Rules editor appends by default
- **GIVEN** a configured workflow rules file with existing valid rules
- **WHEN** the rules editor writes an additional valid rule without `replace_file`
- **THEN** Meeting Assistant appends the new rule
- **AND** preserves the existing rules
#### Scenario: Rules editor replaces only when requested
- **GIVEN** a configured workflow rules file with existing rules
- **WHEN** the rules editor writes valid YAML with `replace_file` set to true
- **THEN** Meeting Assistant replaces the complete rules file with the supplied YAML
#### Scenario: Rules editor refuses invalid YAML
- **GIVEN** a configured workflow rules file with valid existing rules
- **WHEN** the rules editor writes YAML that would make the file invalid
- **THEN** Meeting Assistant refuses the write
- **AND** keeps the existing rules file unchanged
@@ -0,0 +1,77 @@
## MODIFIED Requirements
### Requirement: Meeting Assistant generates meeting outputs
Meeting Assistant SHALL generate a summary, decisions, and next steps from the meeting transcript, metadata, user notes, and assistant-discovered context.
Meeting Assistant SHALL use a Microsoft Agent Framework pipeline for the first summary implementation. The pipeline SHALL use a configurable OpenAI-compatible endpoint, model, and API key source, including support for a direct key or an environment variable name.
The summary pipeline SHALL retry transient model endpoint failures according to configurable reconnection attempts and delay settings.
The summary pipeline SHALL expose tools scoped to the current meeting:
- `read_meetingnote`
- `read_transcript`
- `read_context`
- `read_usernotes`
- `read_glossary`
- `add_dictation_word`
- `add_attendee`
- `remove_attendee`
- `override_speaker`
- `delete_identity`
- `write_summary`
- `write_context`
- `list_projects`
- `list_projectfiles`
- `read_projectfile`
- `write_projectfile`
- `search`
All summary-agent read tools that return file-like content SHALL support either reading the whole file or reading a clamped inclusive 1-based line range when `from` and `to` are supplied.
The summary pipeline SHALL write the summary note as a markdown artifact linked to the meeting note, transcript, and assistant context.
The summary agent SHALL be able to read the meeting note, transcript, assistant context, glossary, and bound project files through tools.
The summary agent SHALL be able to write the summary and assistant context files as its owned artifacts.
The summary agent SHALL be able to read and write the assistant context body as its own notebook using append-by-default, explicit whole-body replacement, replace, and insert line modes.
When the summary agent calls `write_context` without line edit arguments and without `replace_file`, Meeting Assistant SHALL append the supplied content to the assistant context body while preserving assistant context frontmatter.
When the summary agent calls `write_context` with `replace_file` true, Meeting Assistant SHALL replace the assistant context body while preserving assistant context frontmatter.
Meeting Assistant SHALL refuse ambiguous `write_context` calls that combine `replace_file` with line edit arguments.
For summary-agent writes to any file other than the summary file and assistant context file, Meeting Assistant SHALL record an in-memory diff containing removed and added lines.
Meeting Assistant SHALL use a diff implementation that can reduce whole-file rewrites to changed lines instead of treating every rewrite as a full replacement.
When the summary agent finishes, Meeting Assistant SHALL append the collected external write diffs to the assistant context file.
#### Scenario: Assistant context note is initialized
- **WHEN** Meeting Assistant starts a meeting session
- **THEN** it creates the assistant context note with frontmatter links to the meeting note, transcript note, and summary note
#### Scenario: Assistant context body appends by default
- **WHEN** the summary agent writes assistant context content without line arguments
- **THEN** Meeting Assistant appends the content to the existing assistant context body
- **AND** preserves the existing assistant context frontmatter
#### Scenario: Assistant context body can be explicitly replaced
- **WHEN** the summary agent writes assistant context content with `replace_file` set to true
- **THEN** Meeting Assistant replaces the assistant context body
- **AND** preserves the existing assistant context frontmatter
#### Scenario: Assistant context body writes do not duplicate frontmatter
- **WHEN** the summary agent writes assistant context content that accidentally includes a markdown frontmatter block
- **THEN** Meeting Assistant preserves the existing assistant context frontmatter
- **AND** writes only the replacement body content after that frontmatter
#### Scenario: Assistant context write mode is ambiguous
- **WHEN** the summary agent combines `replace_file` with `from`, `to`, or `insert`
- **THEN** Meeting Assistant refuses the assistant context write
#### Scenario: External project write is audited
- **WHEN** the summary agent writes a bound project file
- **THEN** Meeting Assistant records the changed removed and added lines for that project file
- **AND** appends that diff to the assistant context after the agent finishes
@@ -0,0 +1,8 @@
## Implementation
- [x] Repair the accidentally overwritten project note to the state it would have had after an append.
- [x] Add behavior tests for append-by-default and explicit replacement.
- [x] Implement `replace_file` on summary file-write tools and workflow rules writes.
- [x] Update agent tool descriptions and instructions.
- [x] Run focused tests.
- [x] Run `dotnet test MeetingAssistant.slnx`.
- [x] Run `openspec validate append-file-write-tools-by-default --strict`.
+15 -2
View File
@@ -37,14 +37,23 @@ Meeting Assistant SHALL expose a `write_projectfile` tool that can create or upd
The target project SHALL be an existing direct subfolder of the configured projects folder. The target file path SHALL stay inside that project folder.
When no line edit arguments are supplied, `write_projectfile` SHALL overwrite the whole file and SHALL create the file when it does not exist.
When no line edit arguments are supplied and `replace_file` is not true, `write_projectfile` SHALL append the supplied content to the file and SHALL create the file when it does not exist.
When `replace_file` is true, `write_projectfile` SHALL replace the whole file with the supplied content.
When both `from` and `to` are supplied, `write_projectfile` SHALL replace the inclusive 1-based line range with the supplied content.
When `insert` is supplied, `write_projectfile` SHALL insert the supplied content at that 1-based line position.
#### Scenario: Project file is overwritten or created
Meeting Assistant SHALL refuse ambiguous writes that combine `replace_file` with line edit arguments.
#### Scenario: Project file is appended or created
- **WHEN** the agent writes a project file without line edit arguments
- **THEN** Meeting Assistant appends the supplied content to an existing file
- **AND** creates the file with the supplied content when it does not exist
#### Scenario: Project file is explicitly replaced
- **WHEN** the agent writes a project file with `replace_file` set to true
- **THEN** Meeting Assistant writes the supplied content as the complete file content
#### Scenario: Project file line range is replaced
@@ -59,3 +68,7 @@ When `insert` is supplied, `write_projectfile` SHALL insert the supplied content
- **WHEN** the target project is missing or the target path escapes the project folder
- **THEN** Meeting Assistant refuses the project file write
#### Scenario: Project file write mode is ambiguous
- **WHEN** the agent combines `replace_file` with `from`, `to`, or `insert`
- **THEN** Meeting Assistant refuses the project file write
+23
View File
@@ -202,6 +202,12 @@ The rules editor agent SHALL be configured from the summarizer agent settings by
The rules and identities editor agent SHALL include the workflow engine documentation in its system prompt and SHALL receive read, write, and search tools scoped to the configured workflow rules file.
The rules editor `write_rules` tool SHALL append to the configured workflow rules file by default and SHALL validate the complete resulting YAML document before writing it.
The rules editor `write_rules` tool SHALL replace the whole configured workflow rules file only when `replace_file` is true.
The rules editor `write_rules` tool SHALL refuse invalid YAML without changing the configured workflow rules file.
The rules and identities editor agent SHALL receive speaker identity tools to search/list, read, create, update, delete, and merge identities in the local speaker identity database.
The rules and identities editor agent SHALL receive speaker sample tools to list, read, delete, and queue playback of samples linked to identities.
@@ -233,6 +239,23 @@ Meeting Assistant SHALL provide a diagnostic endpoint that opens the workflow ru
- **THEN** its read, write, and search tools can access only that configured workflow rules file
- **AND** its system prompt includes the workflow engine documentation
#### Scenario: Rules editor appends by default
- **GIVEN** a configured workflow rules file with existing valid rules
- **WHEN** the rules editor writes an additional valid rule without `replace_file`
- **THEN** Meeting Assistant appends the new rule
- **AND** preserves the existing rules
#### Scenario: Rules editor replaces only when requested
- **GIVEN** a configured workflow rules file with existing rules
- **WHEN** the rules editor writes valid YAML with `replace_file` set to true
- **THEN** Meeting Assistant replaces the complete rules file with the supplied YAML
#### Scenario: Rules editor refuses invalid YAML
- **GIVEN** a configured workflow rules file with valid existing rules
- **WHEN** the rules editor writes YAML that would make the file invalid
- **THEN** Meeting Assistant refuses the write
- **AND** keeps the existing rules file unchanged
#### Scenario: Rules editor can manage speaker identities
- **GIVEN** the speaker identity database contains speaker identities and samples
- **WHEN** the rules editor agent runs
+21 -1
View File
@@ -68,7 +68,13 @@ The summary note SHALL keep frontmatter links to meeting artifacts and SHALL cop
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.
The summary agent SHALL be able to read and write the assistant context body as its own notebook using overwrite, replace, and insert line modes.
The summary agent SHALL be able to read and write the assistant context body as its own notebook using append-by-default, explicit whole-body replacement, replace, and insert line modes.
When the summary agent calls `write_context` without line edit arguments and without `replace_file`, Meeting Assistant SHALL append the supplied content to the assistant context body while preserving assistant context frontmatter.
When the summary agent calls `write_context` with `replace_file` true, Meeting Assistant SHALL replace the assistant context body while preserving assistant context frontmatter.
Meeting Assistant SHALL refuse ambiguous `write_context` calls that combine `replace_file` with line edit arguments.
The summary instructions SHALL tell the summary agent to use transcript evidence and screenshot OCR participant evidence to refine the meeting note attendee list conservatively, adding or removing attendees only when the evidence is clear.
@@ -133,11 +139,25 @@ When `delete_identity` is called with an existing transcript speaker identity, M
- **WHEN** the summary agent writes the summary or assistant context file
- **THEN** Meeting Assistant does not include those writes in the external write diff audit
#### Scenario: Assistant context body appends by default
- **WHEN** the summary agent writes assistant context content without line arguments
- **THEN** Meeting Assistant appends the content to the existing assistant context body
- **AND** preserves the existing assistant context frontmatter
#### Scenario: Assistant context body can be explicitly replaced
- **WHEN** the summary agent writes assistant context content with `replace_file` set to true
- **THEN** Meeting Assistant replaces the assistant context body
- **AND** preserves the existing assistant context frontmatter
#### Scenario: Assistant context body writes do not duplicate frontmatter
- **WHEN** the summary agent writes assistant context content that accidentally includes a markdown frontmatter block
- **THEN** Meeting Assistant preserves the existing assistant context frontmatter
- **AND** writes only the replacement body content after that frontmatter
#### Scenario: Assistant context write mode is ambiguous
- **WHEN** the summary agent combines `replace_file` with `from`, `to`, or `insert`
- **THEN** Meeting Assistant refuses the assistant context write
#### Scenario: Summary agent refines attendees from clear participant evidence
- **GIVEN** assistant context OCR states that visible meeting participants are complete
- **AND** the OCR result names a participant missing from the meeting note