Repository navigation
fix(memory): stop treating thematic breaks as instruction units - #213
Merged
kunchenguid merged 5 commits intoOct 15, 2026
Merged
Conversation
A break-only line (`---`, `***`, `___` and CommonMark variants) outside a fence was parsed as its own instruction unit, inflating the status count, taking an AG id, and matching any skill body break as a 1.00 overlap. parseMemoryUnits now drops such lines and flushes the current unit. A dash run directly under a plain paragraph is treated as a setext H2 heading, as in CommonMark; under a list item, quote, or fence it stays a break. Positional AG ids shift for unchanged memory text, so cached evidence would point at the wrong units: bump ANALYSIS_INDEX_VERSION to 4. Fixes kunchenguid#192 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
[Medium impact] The PR appears safe to merge; the previous command-loss issue is fixed. Reviews (2) · Last reviewed commit: "no-mistakes(ci): Fixed Greptile finding ..." · Reviewed by Greptile |
…MemoryUnits` (src/memory.js), indented code was treated as a setext heading title. Because of that, ` npm test\n---\nNext.\n` dropped the command and used it as the section name for `Next.`. Rule that must hold: a line can only be a setext title if it has at most 3 leading spaces (CommonMark). A line indented 4+ spaces or with a tab is indented code; it stays a unit, and the `---` after it is a plain thematic break. Fix: one extra condition on the setext check, `/^ {0,3}\S/.test(buffer[0])`. Checking only `buffer[0]` is enough: indented code cannot interrupt a paragraph, so if the first line is indented, the whole buffer is code. The only other place that decides what a setext title is, `isSetextTitle` in `splitRemovalHunk` (src/workspace.js), already skips any line that belongs to a unit. The indented line is now a unit, so that function picks up the fix without a separate change. I added both the space-indented example from the finding and a tab-indented one to the existing "dash run under a list item, quote, indented code, or fence is a break" test in `test/memory-units.test.js`. That test fails on the original code (7 pass, 1 fail) and passes with the fix. `pnpm run check` (lint, format, typecheck, all tests) passes: 940/940
Owner
|
Speaking as Kun's firstmate: this is merged. Thank you @Pantoria — really appreciate you taking the time on this. |
This was referenced Oct 15, 2026
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.
Intent
Fix issue #192: a thematic-break-only line (---, **, ___) was parsed as an instruction unit by parseMemoryUnits (src/memory.js), inflating the status instruction count, getting an AG id in the analysis index, and showing as a similarity-1.00 cross-surface overlap against skills that also contain ---. The user (issue author, Pantoria) asked to implement their own ready-for-pr backpass issues, each in its own worktree and PR. Follow AGENTS.md invariants and VISION.md. No live model calls were made while implementing.
Change: CommonMark thematic breaks outside fences (3+ of the same -//_ char, optional internal spaces/tabs, up to 3 leading spaces) are not units and flush the current unit. Maintainer cautions honored: '- item' list items stay units, and --- inside fenced blocks stays content. Deliberate decision: a dash-only line directly under a plain paragraph is a setext H2 underline as in CommonMark, so the paragraph becomes the section name like a ## heading (neither is a unit); under a list item, quote, or fence it is an ordinary break; *** and ___ never underline. ANALYSIS_INDEX_VERSION bumped 3->4 deliberately, per the AGENTS.md rule, because analysis evidence stores positional AG ids validated by sanitizeEvidence and the cache key (memory content hash) would not change when only the parser does - so cached evidence on files containing --- would otherwise cite shifted ids. Rejection instructionIds are content hashes and stay valid. Out of scope: === setext and 1-2 dash underlines.
What Changed
parseMemoryUnits(src/memory.js) uses a new exportedTHEMATIC_BREAKpattern (CommonMark: 3 or more of the same-,*or_, optional internal spaces or tabs, up to 3 leading spaces). A break line outside a fence is no longer an instruction unit and ends the current unit. A dash-only line directly under a plain paragraph is now read as a setext H2 underline, so that paragraph becomes the section heading. Under a list item, a quote, or a fenced line it stays an ordinary break, and***/___never act as underlines.- itemlist items still count as units, and---inside fences stays content.splitRemovalHunk(src/workspace.js) keeps a setext title and its dash underline with the content they introduce, the same way it handles ATX headings. It skips plain thematic breaks when looking for a heading's content.ANALYSIS_INDEX_VERSIONgoes from 3 to 4 (src/state.js). Cached analysis evidence uses positional AG ids, and those ids shift for files that contain---, so old cache entries are invalidated. Covered by the newtest/memory-units.test.jsplus new cases intest/status.test.jsandtest/workspace.test.js.🤖 Generated with Claude Code
Risk Assessment
Testing
I ran the real
backpass statusCLI, in both text and JSON form, on a throwaway git repo with an isolated home directory, once at the base commit and once at the target commit. The target no longer counts---,***or_ _ _lines as instructions (10 → 6) and drops the false similarity-1.00 skill overlaps. List items, content inside fences and setext sections behave as the intent describes. Adversarial edge-case files were parsed at both commits. The removal-split guard and the analysis-index version bump can only be reached through stages that call a model, so they are marked untested live. Offline checks of both passed: direct function calls, plus the three focused test files (43/43). The temporary files were removed.backpass statuson an AGENTS.md containing ---, *** and _ _ _ breaks; the instruction count no longer counts the breaksbackpass statuswith a skill whose frontmatter/body contains ---; no similarity-1.00 CROSS-SURFACE line is reported for a break\n---\n' kept; list items AG-002/003 kept)backpass analyzemodel call through acpx, which was out of scope (no live model calls). It was only chec…Evidence: status before/after (base vs target CLI transcript)
Evidence: edge-case unit parsing (lists, quotes, fences, *** / ___)
Evidence: splitRemovalHunk review cases base vs target
Evidence: analysis index version invalidation
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
src/memory.js:112- Setext headings now leavesplitRemovalHunk(src/workspace.js:404-421) unguarded, so an extraction can separate a heading from its content. Before this change,Release steps\n---was one unit, and the unit-kind check returned null whenever its lines had mixed kinds. With the setext branch (src/memory.js:112-125), the title line and its underline belong to no unit. The heading/content guard at src/workspace.js:416-421 only recognizes ATX#headings, so nothing else catches this. Verified: with old text# Guide\n\nKeep it small.\n\nRelease steps\n---\n\nTag the commit.\n\n## Other..., a created skill---\nname: release\n...\n---\n\n# Release\n\nTag the commit.and a removal hunk over lines 5-9, the merged hunk is now split into two decisions:Release steps\n(deleted) and---\n\nTag the commit.\n\n(recovered). The old parser kept them as one hunk. Every created SKILL.md's frontmatter contributes---to the recovered counts, which makes the mismatch easy to hit. If the user accepts only the extraction,Release stepsis left as an orphan paragraph. Fix: extend the heading/content guard insplitRemovalHunkto treat a setext title plus its---underline as a heading, e.g. require the title line, the underline and the next content line to share one kind. Alternatively, expose heading spans fromparseMemoryUnitsso this check doesn't re-derive them.src/memory.js:113- The setext check only looks atbuffer[0]. A paragraph whose later line starts a list item (the parser keeps it in the same buffer because only a list-itembuffer[0]flushes) is turned into a heading as a whole, e.g.Intro\n- item\n---becomes the sectionIntro - item. In CommonMark, the list item interrupts the paragraph, so the---would be a thematic break. This is a rare edge case, and the old parser already merged these lines into one unit, so it is noted only.🔧 Fix applied.
2 issues (1 warning, 1 info) still open:
src/memory.js:113- The setext check only looks atbuffer[0]. A paragraph whose later line starts a list item (the parser keeps it in the same buffer because only a list-itembuffer[0]flushes) is turned into a heading as a whole, e.g.Intro\n- item\n---becomes the sectionIntro - item. In CommonMark, the list item interrupts the paragraph, so the---would be a thematic break. This is a rare edge case, and the old parser already merged these lines into one unit, so it is noted only.src/workspace.js:418- Round 1's fix fixed the setext case but made the guard too broad. It now treats every non-blank line outside a unit as a heading that must share a kind with the next unit. That includes plain thematic breaks, which introduce nothing, so splitting now fails in the common case this change creates. Every created SKILL.md's frontmatter adds two---lines torecovered, so a---separator in the memory file usually counts as "recovered". When that break directly precedes a deleted paragraph or list item, the guard returns null and the extraction and the unrelated deletion merge back into one decision. Verified on the current code: old text# Guide\n\nKeep it small.\n\nTag the commit before release.\n\n---\n\nDrop this stale note.\n\n## Other\n\nStay.\n, a skill containingTag the commit before release., and hunk 5-10 givesnull. At the base commit,---was a single-line unit, and at feaf96a it was ignored by the ATX-only guard, so both split this into[recovered, deleted]. The same thing happens for- a\n---\n- b, where a break follows a list item. A related case: an ATX heading directly followed by---(## X\n---) is now one block that must share a kind, so it also refuses to split. Minimal fix: apply the guard only to heading blocks, meaning ATX#lines or a block of two or more lines ending in a dash-only setext underline. Skip a block that is a lone thematic break, which preserves the base splitting behavior. This defect is in code added by the round-1 fix for review-1.🔧 Fix applied.
3 issues (2 warnings, 1 info) still open:
src/memory.js:113- The setext check only looks atbuffer[0]. A paragraph whose later line starts a list item (the parser keeps it in the same buffer because only a list-itembuffer[0]flushes) is turned into a heading as a whole, e.g.Intro\n- item\n---becomes the sectionIntro - item. In CommonMark, the list item interrupts the paragraph, so the---would be a thematic break. This is a rare edge case, and the old parser already merged these lines into one unit, so it is noted only.src/workspace.js:418- Round 1's fix fixed the setext case but made the guard too broad. It now treats every non-blank line outside a unit as a heading that must share a kind with the next unit. That includes plain thematic breaks, which introduce nothing, so splitting now fails in the common case this change creates. Every created SKILL.md's frontmatter adds two---lines torecovered, so a---separator in the memory file usually counts as "recovered". When that break directly precedes a deleted paragraph or list item, the guard returns null and the extraction and the unrelated deletion merge back into one decision. Verified on the current code: old text# Guide\n\nKeep it small.\n\nTag the commit before release.\n\n---\n\nDrop this stale note.\n\n## Other\n\nStay.\n, a skill containingTag the commit before release., and hunk 5-10 givesnull. At the base commit,---was a single-line unit, and at feaf96a it was ignored by the ATX-only guard, so both split this into[recovered, deleted]. The same thing happens for- a\n---\n- b, where a break follows a list item. A related case: an ATX heading directly followed by---(## X\n---) is now one block that must share a kind, so it also refuses to split. Minimal fix: apply the guard only to heading blocks, meaning ATX#lines or a block of two or more lines ending in a dash-only setext underline. Skip a block that is a lone thematic break, which preserves the base splitting behavior. This defect is in code added by the round-1 fix for review-1.src/workspace.js:436- The round-2 narrowing (which fixed review-r2-1) introduced a regression: an ATX heading can now be split from its content when a thematic break sits between them. For an ATX heading,contentLineis the next non-blank line in the hunk. When that line is a---/***break, the break is no longer a unit (this change's purpose), so!unitLines.has(contentLine)hitscontinueand the heading is never compared with the content that follows the break. The break line itself also failsisSetextTitle(lineNo - 1)because the line before it is ATX, so nothing guards the pair.Verified on HEAD: old text
# Guide\n\nKeep it small.\n\n## Notes\n---\n\nTag the commit before release.\n\n## Other\n\nStay.\n, a created skill---\nname: release\n...\n---\n\n# Release\n\nTag the commit before release.\n, and hunk 5-9 return["## Notes\n", "---\n\nTag the commit before release.\n\n"]. That splits the deleted heading from its extracted section. If the user accepts only the extraction,## Notesis left as an orphan heading over## Other. At the base commit,---was a unit whose kind (recovered) differed from the heading (deleted), so the result was null; at feaf96a the ATX guard compared against the next non-blank line, also giving null. The## X\n\n---\n\ncontentlayout follows the same path.The round-2 test
afterHeading(test/workspace.test.js) only covers a heading the skill also contains, so it passes either way. Fix: when looking for an ATX heading'scontentLine(and a setext underline's, same loop), skip lines that matchTHEMATIC_BREAK, so the heading is compared with the first real unit line after it. Add the case above to the test, expectingnull.🔧 Fix applied.
1 info still open:
src/memory.js:113- The setext check only looks atbuffer[0]. A paragraph whose later line starts a list item (the parser keeps it in the same buffer because only a list-itembuffer[0]flushes) is turned into a heading as a whole, e.g.Intro\n- item\n---becomes the sectionIntro - item. In CommonMark, the list item interrupts the paragraph, so the---would be a thematic break. This is a rare edge case, and the old parser already merged these lines into one unit, so it is noted only.✅ **Test** - passed
✅ No issues found.
backpass statuson an AGENTS.md containing ---, *** and _ _ _ breaks; the instruction count no longer counts the breaksbackpass statuswith a skill whose frontmatter/body contains ---; no similarity-1.00 CROSS-SURFACE line is reported for a break\n---\n' kept; list items AG-002/003 kept)backpass analyzemodel call through acpx, which was out of scope (no live model calls). It was only chec…backpass statusandbackpass status --json(real CLI, isolated HOME/XDG_CONFIG_HOME, disposable git repo) at base e6a030a and target b96b4fbparseMemoryUnits dump of the disposable AGENTS.md and two adversarial edge-case files at base and targetsplitRemovalHunk run on the setext, plain-break and ATX+break review cases at base and target (offline)evidenceKey/isEvidenceFresh check that v3-keyed evidence is stale under v4 (offline)node --import ./test/helpers/hermetic-config.js --test test/workspace.test.js test/memory-units.test.js test/status.test.js✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.