Keep multi-line agent notes intact through emit and parse - #40
Merged
Merged
Conversation
`lash add --agent-note "$(printf 'first\nsecond')"` wrote ` @agent-note: first` followed by a bare, unindented `second`. The parser treats an unindented line as the end of the annotation block, so `lash show` afterwards returned only "first" and the rest of the note was gone, with exit code 0. `format_task_annotations` built the note with a single `format!` and pushed the result as one "line", so any newline in the value landed in the file with no indentation. It now writes the first line as the `@agent-note:` line and each continuation with the annotation indent, which is the shape `parser::annotations::parse_annotation_block` folds back into one value. Indentation alone is not enough for every value, because the folding is lossy in two ways. A blank line is skipped, so a note containing one comes back with the gap closed up. A line starting with `@` is read as the start of a new annotation, truncating the note and possibly inventing an annotation that was never intended. `MarkdownEmitter::check_agent_note` rejects both at validation time with `E_CREATE_INVALID_AGENT_NOTE`, naming the offending line, and the file is left untouched. That is the "reject rather than write something the parser will silently truncate" half of the fix. Leading whitespace on a continuation line is normalized instead of rejected. The parser trims it, so indentation is lost but no text is, and rejecting it would rule out ordinary things like an indented list inside a note. Tests: a round-trip property test over notes of one to four lines at four indent levels, emitting through `format_task_annotations` and reading back through `parse_annotation_block`; unit tests for the accept and reject sets and for continuation indentation; and three integration tests covering a two-line note surviving to `lash show`, and both rejection cases leaving the file byte-identical.
This was referenced Aug 9, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Replaces #37, which GitHub closed when its base branch was deleted on merge of
#36. Same change, rebased onto main.
What was wrong
lash add --agent-note "$(printf 'first\nsecond')"wrote@agent-note: firstfollowed by a bare, unindentedsecond. The parsertreats an unindented line as the end of the annotation block, so
lash showafterwards returned only "first" and the rest of the note was gone, exit code 0.
I had previously assessed this as unreachable from the CLI on the grounds that
notes are single-line. That was wrong; the shell one-liner above is all it
takes.
format_task_annotationsbuilt the note with a singleformat!and pushed theresult as one "line", so any newline in the value landed in the file with no
indentation.
The fix
The emitter writes the first line as the
@agent-note:line and eachcontinuation with the annotation indent, which is the shape
parser::annotations::parse_annotation_blockfolds back into one value.Indentation alone is not enough for every value, because the folding is lossy
in two ways. A blank line is skipped, so a note containing one comes back with
the gap closed up. A line starting with
@is read as the start of a newannotation, truncating the note and possibly inventing an annotation nobody
asked for.
MarkdownEmitter::check_agent_noterejects both at validation timewith
E_CREATE_INVALID_AGENT_NOTE, naming the offending line, and leaves thefile untouched.
Leading whitespace on a continuation line is normalized rather than rejected.
The parser trims it, so indentation is lost but no text is, and rejecting it
would rule out ordinary things like an indented list inside a note.
Tests
A round-trip property test over notes of one to four lines at four indent
levels, emitting through
format_task_annotationsand reading back throughparse_annotation_block. Unit tests for the accept and reject sets and forcontinuation indentation. Three integration tests covering a two-line note
surviving through to
lash show, and both rejection cases leaving the filebyte-identical.