chore(changelog): land entries as per-change fragments in changelog.d/ - #72
Conversation
Concurrent PRs editing the shared '## Unreleased' section of CHANGELOG.md conflict whenever one PR outlives another merge (e.g. #54, which edits CHANGELOG.md directly and is exposed to every entry that lands while it is in review). Entries now land as uniquely-named fragment files (changelog.d/<slug>.<category>.md), which git merges without conflict; the npm-version hook folds them into the release section and deletes them. Direct '## Unreleased' edits remain supported and are merged at the same point, so in-flight PRs need no rework. Tag-time publish guard and github-release job are unchanged. Part of the ecosystem-wide rollout of this scheme; reference implementation and full rationale in anima-research/agent-framework#128. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nts in CI Two findings from Greptile's review of the membrane companion PR (antra-tess/membrane#49), applied wave-wide: a fragment whose first line was a bullet could smuggle top-level prose or a rogue '## ' heading (a fake section boundary) into the released changelog, and the CI check's path-only grep let a deleted or renamed fragment satisfy the entry requirement. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Greptile's review of the membrane companion (antra-tess/membrane#49) found two issues in the shared code, fixed here in b5087e9: fragment validation now checks every line (top-level prose or a rogue |
…egression tests Codex QA pass on the membrane companion (antra-tess/membrane#49) found that a slug taken verbatim from a 'fix/foo' branch name — which the docs invited — created changelog.d/fix/foo.fixed.md, and the scanner skipped the 'fix' directory without a word: a silent release-note data-loss path that PR CI catches but direct pushes bypass. The scanner now aborts on any entry that is not README.md or a well-formed fragment file, and the docs prescribe filesystem-safe slugs ('/' -> '-'). The continuation-line check also treated any whitespace-prefixed line as valid, so an indented '## ' heading still passed. Continuations must now be indented two or more spaces, and headings/rules are refused at any indentation. The new regression test runs the real script in a throwaway directory: fragment-only and mixed assembly, canonical ordering, audience-qualified headings, deletion + README preservation, and fourteen refusal cases asserting nothing is modified on failure. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
A Codex QA pass on the membrane companion (antra-tess/membrane#49, blocking) found two more shared-code issues, fixed here in 6fe1685: a fragment slug taken verbatim from a |
… and spaced rules Second Codex QA round on the membrane companion (antra-tess/membrane#49, head f72e00a): - Refusals used console.error + process.exit(1). Where stderr is a pipe (asynchronous on macOS), the exit can pre-empt the write, so callers — including the new tests — saw exit 1 with empty stderr. Refusals now throw a ReleaseError caught at the entry point, which prints and sets process.exitCode = 1 so the process drains stdio on its own. - The block-construct check missed thematic breaks with interior whitespace ('- - -') and headings used as item content ('- ## x'). Both are now refused; content after the bullet marker is inspected. Six regression cases added, plus one guarding against over-refusal of bullets that merely resemble rules or headings. - CI runs the suite on both OSes and on Node 20 and 24 (was: ubuntu only, Node 20), so platform-specific stdio behaviour is actually exercised. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Round 2 of the Codex QA pass on membrane (antra-tess/membrane#49) — fixed here in c3253b1: refusals reported via |
main adopted per-change changelog fragments (anima-research#72) precisely because concurrent PRs editing the shared Unreleased section conflict — as this one just did. CHANGELOG.md resolves to main's version; the anima-research#41 entry now lands as changelog.d/71-export-overbudget-error.added.md, verbatim. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ts (#76) * feat(strategy): thread the host's live system prompt into mint requests The mint prompt is deliberately reconstructed as-of the original context: the same head, the same recall ladder, the same tool definitions the live instance runs with (setToolDefinitions, 2026-07-09). One layer of that reconstruction was structurally missing — the system prompt the host serves the live agent on every activation never reached the summarizer. On hosts whose identity and conduct live in system voice, every memory was therefore authored by a system-promptless variant of the agent, and merges re-summarize summaries, so the drift compounded upward through the pyramid. ContextManager.setSystemPrompt(text) mirrors setToolDefinitions exactly — adjacent member, same storage shape, same live getter on StrategyContext (a drain captures ctx once and must see the latest activation's value). Both mint request builders serve it as the request's `system` field, which NormalizedRequest already carries, placed ahead of the head exactly as a live activation lays it out. KnowledgeStrategy inherits both builders unchanged; recall-curve variants and every retry rung derive from the canonical request by spread, so they carry it identically. Both builders previously carried a worked "NO system prompt" rationale: a synthetic summarizer header would (a) be something the original instance never saw, disturbing KV consistency, and (b) compete with the structural identity the conversation's head already carries. Both objections are premised on the header being SYNTHETIC and invert when the host serves a real one every activation — then replaying it IS the KV-honest reconstruction, and omitting it is what installs the competing identity. The comments now say that, and keep the original argument as the rationale for the undeclared path. Opt-in, and byte-exact when unset: the field is conditionally spread, not assigned, so a request with no host prompt keeps its previous shape — no `system` key, unchanged canonicalRequestHash and quarantine identity. Token accounting needed no change: estimateCompressionRequestTokens and compressionRequestInputBoundTokens both serialize the COMPLETE normalized request, so the prompt is counted the moment it is present. The llm-calls telemetry, which hard-coded `system: null` under the old design, now logs the same {present, textChars} summary shape used for messages when a prompt was actually sent, and still logs null when none was. Tests (test/mint-system-prompt.test.ts): L1 mints carry it; merge mints carry it; it leads the request rather than being spliced into the replayed messages; unset leaves every mint request without a `system` key at all; an empty/undefined later push never downgrades a recorded prompt; KnowledgeStrategy inherits the threading. Five of the six fail on the unfixed strategy (the unset byte-shape test passes both sides, by design). npm test: 503 pass / 0 fail / 89 suites, against a 497/0 baseline on main @ 56ca912 — no existing test perturbed. Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(strategy): split the mint system-prompt docs by declared/undeclared Addressed review finding 1: doc contradictions left standing. Two docs still stated the pre-feature absolute after the feature made the system prompt conditional on the host. compressChunkHierarchical's method doc said "No system prompt — framing via message structure only", which is now true only of the undeclared path. It states the split, and points at the request builder for the full rationale rather than duplicating it. AutobiographicalConfig.identityReminder's public JSDoc opened its argument with "Compression requests carry no system prompt" — load-bearing for a reader deciding whether they need the reminder at all. It now names the host's call as the discriminator, and says why the reminder is appended on BOTH paths: it addresses in-chunk dominant-speaker capture, which neither the head window nor a system prompt reaches. The 2026-08-03 observation is kept and dated to the path it was actually made on (the no-system-prompt one, then the only one) rather than being implied to generalize to a path that did not yet exist. Comment-only: no emitted behaviour changes. tsc --noEmit clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(strategy): log the dispatched system prompt, not the live getter Addressed review finding 2: the telemetry race — the receipt could lie. Both mint builders serve ctx.systemPrompt into the request's `system` field at ASSEMBLY, but both logged the call from a `finally` that re-read that getter after an awaited membrane round trip. ctx.systemPrompt is a live getter onto the last value the host pushed (deliberately so — a drain captures ctx once and must see the latest activation), so a host activation landing between dispatch and resolution made the receipt attribute the memory to a prompt that call never sent. The compression log exists precisely so prompts can be audited post-hoc with "no reconstruction, no assumption about whether the strategy code matches what produced historical summaries" — a field that can silently misname its own input defeats that. Both sites now log the captured request's own `system`: compress_l1 reads `request`, and merge_l* reads `dispatchRequest`, which is what actually went on the wire (`request`, or its tools-less escalation). Verified that every rung reachable from these sites carries `system` through untouched by spread — buildRecallCurveVariants, withAppendedInstruction, withoutToolsParam and stripReasoningFromRequest each rebuild only messages or drop the tools param — so the canonical request's field is the sent value on every path, including retries and curve fallbacks. No mechanism change: the request bytes, the canonical hash and the quarantine identity are all untouched, and the null-when-absent shape that preserves the pre-threading log format is kept. test/mint-system-prompt-telemetry.test.ts pins it. The harness membrane refreshes the host prompt from inside complete() — strictly between dispatch and resolution — on every call, each refresh a distinct length, and the test asserts each receipt's textChars equals the length its own call dispatched. It guards against passing vacuously: it requires L1 AND merge receipts, requires the race to have actually fired, and requires one receipt per call. Red at 1969d75 ("call 0 (compress_l1) must report the 126-char prompt it SENT, not the 139-char one the host pushed while the call was in flight"), green here. Shared-surface note (review stop-arm): the `system` telemetry field on operation compress_l1 is also written by the quiet-stretch stub path, which logs null. That path makes no membrane call and awaits nothing, so it has no race and no misattribution to fix; the test filters it out explicitly by metadata.stub. bun test --timeout 60000 dist/test/: 504 pass / 0 fail (503 baseline + 1). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(strategy): state the prompt-currency truth instead of assuming stability Addressed review finding 3: an unstated stability assumption in the rewritten rationale comments. Both builders argued the declared path with "the original instance saw it, so replaying it is the KV-honest reconstruction". That silently assumes the host's system prompt is stable across the compressed span. The mechanism does not supply that: ContextManager holds the prompt in a single slot that setSystemPrompt overwrites on every activation, with no per-message history, so what a mint is served is the host's CURRENT identity policy — not what was in force while the chunk was being lived. When the host has changed the prompt, the older text is not recoverable from here at all. The comments now say that plainly, and keep the KV-honesty claim scoped to the case where it holds: stable prompt, the two coincide; changed prompt, the memory is authored under the identity the host serves now. The conclusion the feature rests on is unaffected either way — omitting the prompt is still what installs a competing, system-promptless identity. The merge site carries the same caveat by reference, noting that its sources may have been authored under a different policy than the merge. Truth-in-comments only: no mechanism change, no request-shape change. Scope judgment, stated for review: the same assumption was carried verbatim by two doc sites the finding did not enumerate — ContextManager.setSystemPrompt's own JSDoc and the public StrategyContext.systemPrompt JSDoc, both asserting the prompt is "part of the instance being reconstructed". Fixing only the two cited comments would have left the identical false framing on the public interface, and on the very symbol the finding cites as evidence that only the latest value is retained. Both are corrected here in the same terms. No other site in the tree carries the claim (grepped for "original instance saw it" / "instance being reconstructed"); the one remaining hit at autobiographical.ts:4841 is about in-band marker placement, unrelated. tsc clean. bun test --timeout 60000 dist/test/: 504 pass / 0 fail. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(changelog): carry the prompt-currency correction to the two doc sites it missed Addressed the second sol review's single blocking finding: the prompt-currency correction landed in the source comments (765b0cf) but two tree sites still carried semantic variants of the claim it corrected. A phrase-only grep missed them because neither repeats the corrected wording. CHANGELOG.md, Unreleased/Added, the setSystemPrompt entry: said the mint "reconstructs the agent's own past view" and that the system prompt is "part of that view". test/mint-system-prompt.test.ts's file header: said the summarizer is "reconstructed as-of the original context" and named the system prompt as a missing layer of that reconstruction. Both are false whenever the host changed the prompt during the compressed span -- the mechanism holds one slot with no per-message history, so the request carries the CURRENT-at-mint policy. Both rewritten in the corrected source comments' own vocabulary (single slot, no per-message history, in force AT MINT TIME, stable across the compressed span, older text not recoverable from here), so a future phrase-grep on any of those hits every site at once. The as-of claim is SCOPED rather than deleted: the rest of a mint request genuinely is built as-of the span (same head, same recall ladder, no tail after the chunk) and only the prompt is current-at-mint. The test header also now says which of its cases pin the threading and which one pins the slot's last-value-wins rule, and states that no case asserts a per-span historical prompt. Deviation from the carried-forward wording, stated for review: the CHANGELOG's as-of list also named "same tool definitions", and tool definitions are NOT as-of either. ctx.tools is the identical mechanism -- setToolDefinitions writes one last-write-wins slot (context-manager.ts:909), StrategyContext.tools is a live getter onto it (:952), and both mint builders send `tools: ctx.tools` (autobiographical.ts:5184, :6545). Restating that clause as an as-of replay would have authored a fresh false claim of exactly the class under repair, so tool definitions are dropped from the as-of list rather than asserted. No tools-currency caveat is added here: that claim belongs to the tool-threading feature, not to this entry, and no review finding raised it. Flagged in the wrap as adjacent work. Semantic census (the receipt for "no third site remains"): every comment or doc block in the tree that speaks about the threaded prompt, or about the mint's as-of relationship to the compressed span, was read -- 44 blocks across 16 files: 2 fixed here, 8 already stating the current-at-mint truth (the six sites the fix commits corrected, plus the private-field doc and the telemetry test header), 34 silent on the question. No third semantic-variant site exists. Enumerated block by block with verdicts in the job wrap. Census run with `grep -a`: autobiographical.ts carries a raw NUL byte at ~line 7636, and skip-binary greps report zero matches on the whole file. Prose-only: no mechanism change, no request-shape change, no test assertion touched. tsc clean. bun test --timeout 60000 dist/test/: 504 pass / 0 fail. node v22.14.0 `npm test`: 504 pass / 0 fail. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(changelog): land the entry as a changelog.d fragment Same content, fragment file per the post-#72 convention, so concurrent PRs never conflict in CHANGELOG.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
Problem
Same structural conflict generator as everywhere else in the ecosystem: every PR inserts prose at the same textual location (top of
## UnreleasedinCHANGELOG.md), so any PR that outlives another merge conflicts there — full rationale in anima-research/agent-framework#128. In this repo the live case is #54, which editsCHANGELOG.mddirectly and is exposed to every entry that lands while it is in review.Changes
Entries now land as one uniquely-named file per change —
changelog.d/<slug>.<breaking|added|changed|fixed>.md, containing the entry's markdown bullets verbatim (slug = PR number or branch name). Distinct files never conflict in git. Same scheme as agent-framework#128, adapted:scripts/release-changelog.mjs— taken verbatim from the AF implementation (it is repo-agnostic; readspackage.jsonfor the version). context-manager's previous script was byte-identical to AF's previous script, so there was no repo-specific behavior to preserve. Thenpm versionhook now folds pending fragments plus anything filed directly under## Unreleasedinto the new## X.Y.Z — datesection (canonical Breaking/Added/Changed/Fixed order; fragments join existing matching subsections, including audience-qualified### Breaking (…)headings), deletes consumed fragments, and keeps all existing refusals plus two new ones (unrecognized category suffix, non-bullet fragment).changelog.ymlsoft check passes on a well-formed new fragment or aCHANGELOG.mdedit;changelog.d/README.mdand badly-named fragments do not satisfy it;no-changeloglabel escape unchanged. Same^src/trigger path as before.publish.ymlis untouched — the tag-time section guard and thegithub-releaseawk both read the assembled## X.Y.Zsection, which exists at tag time exactly as before because assembly runs pre-tag (verified below, leg 7).## Unreleasededits remain a supported input, merged at release time — in-flight CHANGELOG-editing PRs like feat: strategy-view composition — viewFilter, auxiliary merged slots, windowed passthrough (tune-out groundwork) #54 keep passing CI and merge normally; the entries currently underUnreleasedon main stay where they are and release normally.## Changelogsection restructured as in AF (repo-specific prose — what needs an entry, the config-becomes-required breaking guidance — kept verbatim); PR-template checkbox updated;changelog.d/README.mddocuments the format in-place;package.jsonversion hook stageschangelog.d; this PR dogfoods the scheme with its own fragment.- **Host operators:** …); the heading-qualifier form (### Breaking (host operators only)) still works via a direct Unreleased edit.Companion to anima-research/agent-framework#128, with siblings in membrane, chronicle, and connectome-host — safe to merge in any order; each repo's scheme is self-contained.
Tests
Release script exercised on copies (bumped to 0.7.0) through the same 8 legs as AF#128; outputs pasted from the runs:
CHANGELOG.md: released '## 0.7.0 — 2026-08-24' from 4 fragment(s) plus the Unreleased section.Fragments merged into the existing### Added/### Fixed, new### Breakingemitted first,### Changedin canonical position, continuation lines preserved, consumed fragments deleted,README.mdkept, fresh empty## Unreleasedon top. Exit 0.release-changelog: a '## 0.6.3' section already exists.Exit 1.release-changelog: nothing to release as 0.7.0 — no fragments in changelog.d/ and no entries under '## Unreleased'.Exit 1.changelog.d/oops.md) —unrecognized category — name fragments '<slug>.<breaking|added|changed|fixed>.md' so the entry is not silently stranded.Exit 1.a fragment is one or more markdown bullets ('- …').Exit 1.github-releaseawk simulated on the leg-1 assembled output forv0.7.0: guard passes (grep -Eq "^## 0\.7\.0([^0-9]|$)"matches), awk extracts a non-empty 47-linenotes.mdstarting at### Breaking.changelog.ymlgrep logic simulated on six changed-file sets: src+fragment PASS, src+CHANGELOG.md PASS (legacy), src-alone FAIL, src+changelog.d/README.md-only FAIL, src+badly-named-fragment FAIL, docs-only PASS.Not verified
A real
npm versionend-to-end run — the next actual release is the live exercise, same status as the existing tag-time guards. Thechangelogcheck runs live on this PR itself (satisfied by its own fragment).changelog.d/changelog-fragments.changed.md.🤖 Generated with Claude Code