Skip to content

Commit e67bc67

Browse files
Merge pull request #314 from corbitsdev/stack/cl-5333-changelog-notes
Post-upgrade release notes on interactive start
2 parents 1bb1cbb + 75ad9fb commit e67bc67

12 files changed

Lines changed: 598 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Patch-ready tip on `main` after the PerfTrace / Codex measurement stack and rela
1717
- **Latency eval harness** — assert phase presence and relative magnitudes in tests (`assert-spans`, multi-tool fixture). (CL-5174, #310)
1818
- **Reasoning effort by agent role** — orchestrator vs task-leaf defaults so high-effort leaves stop multiplying wall time. (CL-5162, #302)
1919
- **Session state under `~/.corbits/projects`** — project key from git toplevel (worktrees share); dual-read migrate from in-repo `.agent-state`; path-restriction exception for the global state root. (CL-5257, #313)
20+
- **Post-upgrade release notes** — on a fresh interactive start after upgrade, show bounded Keep-a-Changelog sections in the session banner; stamp `lastChangelogVersion` in global settings; first install is quiet; `/changelog` and `/changelog full` for on-demand history. Ships `CHANGELOG.md` next to release binaries. (CL-5333, CL-5332, CL-5334)
2021
- **Streaming stall / loop detection** — trailing-window repetition detection; preserve partial streamed output in exec and TUI; partial-capture lifecycle owned by the cycle recorder. (#280, #281)
2122
- **Nested UI polish** — quieter chrome, context meter, task/shell rows, observe-leave behavior. (#312)
2223
- **Approval queue re-eval** — when a grant widens, re-check the pending queue; stored approvals evaluated through `@intx/authz`. (#288, #295)
@@ -45,7 +46,6 @@ Patch-ready tip on `main` after the PerfTrace / Codex measurement stack and rela
4546

4647
### Planned
4748

48-
- What's-new banner on interactive start after upgrade (CL-4604 — **canceled** as a ticket; still not implemented; see note below)
4949
- Local context estimate for compaction when providers omit usage (CL-4345)
5050
- Image age → rehydratable attachment URI (CL-4349)
5151
- Always-return subagent salvage without a default wall-clock death clock (CL-4401)

scripts/release.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ TAP_REPO="corbitsdev/homebrew-tap" # tap repo (formula)
3535
TAP_SLUG="corbitsdev/tap" # `brew tap' name of TAP_REPO
3636
FORMULA="corbits" # formula / binary name
3737
DESC="Single-process coding agent CLI built on the Interchange runtime"
38-
DOC_FILES=(LICENSE.md README.md GPLv2-AI-Exception.md GPL-2.0.txt) # shipped with the binary
38+
DOC_FILES=(LICENSE.md README.md CHANGELOG.md GPLv2-AI-Exception.md GPL-2.0.txt) # shipped with the binary
3939

4040
# Build matrix: "label|bun-target|kind|deb-arch". kind is macos or linux;
4141
# deb-arch is the Debian architecture for linux targets, "-" for macOS.

src/changelog/index.test.ts

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import { describe, expect, test } from "bun:test";
2+
import { mkdtempSync, writeFileSync } from "node:fs";
3+
import { tmpdir } from "node:os";
4+
import { join } from "node:path";
5+
6+
import {
7+
compareVersions,
8+
decideStartupChangelog,
9+
formatStartupChangelog,
10+
getNewEntries,
11+
parseChangelog,
12+
parseChangelogText,
13+
parseVersionString,
14+
resolveChangelogPath,
15+
} from "./index.js";
16+
17+
const SAMPLE = `# Changelog
18+
19+
## [Unreleased]
20+
21+
### New Features
22+
- not a release
23+
24+
## [0.2.86] - 2026-07-30
25+
26+
### New Features
27+
- Feature A
28+
29+
## [0.2.85] - 2026-07-20
30+
31+
### Fixed
32+
- Bug B
33+
34+
## [0.1.0] - 2026-01-01
35+
36+
### New Features
37+
- Initial
38+
`;
39+
40+
describe("parseChangelogText", () => {
41+
test("parses versioned sections and skips Unreleased", () => {
42+
const entries = parseChangelogText(SAMPLE);
43+
expect(entries.map((e) => `${e.major}.${e.minor}.${e.patch}`)).toEqual([
44+
"0.2.86",
45+
"0.2.85",
46+
"0.1.0",
47+
]);
48+
expect(entries[0]!.content).toContain("## [0.2.86]");
49+
expect(entries[0]!.content).toContain("Feature A");
50+
expect(entries.every((e) => !e.content.includes("Unreleased"))).toBe(true);
51+
});
52+
53+
test("accepts unbracketed version headers", () => {
54+
const entries = parseChangelogText("## 1.2.3\n\n- note\n");
55+
expect(entries).toHaveLength(1);
56+
expect(entries[0]!.major).toBe(1);
57+
expect(entries[0]!.minor).toBe(2);
58+
expect(entries[0]!.patch).toBe(3);
59+
});
60+
});
61+
62+
describe("compareVersions / getNewEntries", () => {
63+
test("orders major.minor.patch", () => {
64+
const a = parseVersionString("0.2.86")!;
65+
const b = parseVersionString("0.2.85")!;
66+
expect(compareVersions(a, b)).toBeGreaterThan(0);
67+
expect(compareVersions(b, a)).toBeLessThan(0);
68+
expect(compareVersions(a, a)).toBe(0);
69+
});
70+
71+
test("returns only newer entries", () => {
72+
const entries = parseChangelogText(SAMPLE);
73+
const newer = getNewEntries(entries, "0.2.85");
74+
expect(newer.map((e) => `${e.major}.${e.minor}.${e.patch}`)).toEqual(["0.2.86"]);
75+
});
76+
});
77+
78+
describe("decideStartupChangelog", () => {
79+
const entries = parseChangelogText(SAMPLE);
80+
81+
test("missing watermark is first install — stamp, no history", () => {
82+
const d = decideStartupChangelog({
83+
entries,
84+
lastChangelogVersion: undefined,
85+
packageVersion: "0.2.86",
86+
});
87+
expect(d).toEqual({ kind: "first_install", stampVersion: "0.2.86" });
88+
});
89+
90+
test("malformed watermark is first install", () => {
91+
const d = decideStartupChangelog({
92+
entries,
93+
lastChangelogVersion: "not-a-version",
94+
packageVersion: "0.2.86",
95+
});
96+
expect(d.kind).toBe("first_install");
97+
});
98+
99+
test("upgrade shows notes and stamps package version", () => {
100+
const d = decideStartupChangelog({
101+
entries,
102+
lastChangelogVersion: "0.2.85",
103+
packageVersion: "0.2.86",
104+
});
105+
expect(d.kind).toBe("upgrade");
106+
if (d.kind === "upgrade") {
107+
expect(d.markdown).toContain("0.2.86");
108+
expect(d.markdown).toContain("Feature A");
109+
expect(d.markdown).not.toContain("0.1.0");
110+
expect(d.stampVersion).toBe("0.2.86");
111+
expect(d.versions).toContain("0.2.86");
112+
}
113+
});
114+
115+
test("current version is quiet", () => {
116+
const d = decideStartupChangelog({
117+
entries,
118+
lastChangelogVersion: "0.2.86",
119+
packageVersion: "0.2.86",
120+
});
121+
expect(d).toEqual({ kind: "current" });
122+
});
123+
});
124+
125+
describe("formatStartupChangelog", () => {
126+
test("caps entry count and marks truncated", () => {
127+
const entries = parseChangelogText(SAMPLE);
128+
const formatted = formatStartupChangelog(entries, { maxEntries: 1 });
129+
expect(formatted.versions).toEqual(["0.2.86"]);
130+
expect(formatted.truncated).toBe(true);
131+
expect(formatted.markdown).toContain("/changelog");
132+
});
133+
134+
test("caps byte size", () => {
135+
const big = parseChangelogText(
136+
`## [9.0.0]\n\n${"x".repeat(200)}\n\n## [8.0.0]\n\n${"y".repeat(200)}\n`,
137+
);
138+
const formatted = formatStartupChangelog(big, { maxEntries: 5, maxBytes: 120 });
139+
expect(Buffer.byteLength(formatted.markdown, "utf8")).toBeLessThanOrEqual(120);
140+
expect(formatted.truncated).toBe(true);
141+
});
142+
});
143+
144+
describe("parseChangelog file + resolveChangelogPath", () => {
145+
test("missing file yields empty", () => {
146+
expect(parseChangelog("/no/such/CHANGELOG.md")).toEqual([]);
147+
});
148+
149+
test("reads a real file; resolve prefers package then cwd", () => {
150+
const dir = mkdtempSync(join(tmpdir(), "corbits-changelog-"));
151+
const path = join(dir, "CHANGELOG.md");
152+
writeFileSync(path, SAMPLE, "utf8");
153+
expect(parseChangelog(path)).toHaveLength(3);
154+
// Package-root candidates win when the worktree has CHANGELOG.md; when they
155+
// do not exist, cwd is used.
156+
const resolved = resolveChangelogPath({
157+
cwd: dir,
158+
execPath: "/nonexistent/bin/corbits",
159+
moduleUrl: `file://${join(dir, "src", "changelog", "index.ts")}`,
160+
});
161+
expect(resolved).toBe(path);
162+
});
163+
});

0 commit comments

Comments
 (0)