Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions packages/cli/src/commands/handoff.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,15 +196,21 @@ describe("basou handoff generate", () => {
expect(joinCalls(out)).toContain("sessions: 1");
});

it("case 3: aggregates multiple sessions and unions related_files", async () => {
it("case 3: 直近の変更ファイル shows only the most recent session's related_files", async () => {
const repo = await setupInitedRepo();
// Older session — its files must NOT appear once a newer session supersedes it.
await placeSession(repo, { id: SES("X02"), relatedFiles: ["src/a.ts"] });
await placeSession(repo, { id: SES("X03"), relatedFiles: ["src/b.ts"] });
// More recent session (later started_at) — this one wins.
await placeSession(repo, {
id: SES("X03"),
relatedFiles: ["src/b.ts"],
startedAt: "2026-05-08T12:00:00+09:00",
});
const out = captureStdout();
await doRunHandoffGenerate({}, { cwd: repo, nowProvider: () => FIXED_DATE });
expect(joinCalls(out)).toContain("sessions: 2");
const body = await readFile(basouPaths(repo).files.handoff, "utf8");
expect(body).toContain("- src/a.ts");
expect(body).not.toContain("- src/a.ts");
expect(body).toContain("- src/b.ts");
});

Expand Down
15 changes: 8 additions & 7 deletions packages/core/src/handoff/handoff-renderer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,28 +250,29 @@ describe("handoff-renderer", () => {
expect(result.body).toContain(`from ${SHORT(id)}..${SHORT(id)}`);
});

it("case 3: aggregates related_files across sessions and dedups", async () => {
it("case 3: 直近の変更ファイル shows only the most recent session's related_files", async () => {
const paths = await setupPaths();
// Older session — its unique files must NOT appear once a newer session
// supersedes it.
await placeSession(paths, {
id: SES("X02"),
relatedFiles: ["src/a.ts", "src/b.ts"],
});
// More recent session (later started_at) — this one wins.
await placeSession(paths, {
id: SES("X03"),
relatedFiles: ["src/b.ts", "src/c.ts"],
startedAt: "2026-05-08T12:00:00+09:00",
});
const result = await renderHandoff({ paths, nowIso: FIXED_NOW_ISO });
expect(result.sessionCount).toBe(2);
// dedup + sort asc, scoped to the 直近の変更ファイル section. The next-to-
// read section deliberately reuses `displayedFiles.slice(0, 3)`, so a
// global count would over-report by design.
const recentSection = sliceSection(result.body, "## 直近の変更ファイル", "##");
const idxA = recentSection.indexOf("- src/a.ts");
// src/a.ts is only in the older session, so it is excluded; the newer
// session's files appear, sorted asc.
expect(recentSection).not.toContain("- src/a.ts");
const idxB = recentSection.indexOf("- src/b.ts");
const idxC = recentSection.indexOf("- src/c.ts");
expect(idxA).toBeGreaterThanOrEqual(0);
expect(idxB).toBeGreaterThan(idxA);
expect(idxB).toBeGreaterThanOrEqual(0);
expect(idxC).toBeGreaterThan(idxB);
expect(recentSection.split("- src/b.ts\n").length - 1).toBe(1);
});
Expand Down
20 changes: 9 additions & 11 deletions packages/core/src/handoff/handoff-renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ type TaskStatusChangedRecord = {
* the spec's `handoff.md` sections in order:
*
* 1. `現在の状態`: latest live session (status not archived, source not import).
* 2. `直近の変更ファイル`: union of `related_files` across sessions, dedup +
* 2. `直近の変更ファイル`: the most recent session's `related_files`, dedup +
* sorted asc + truncated to `relatedFilesLimit` (default 20).
* 3. `直近の判断`: latest `decision_recorded` event (chronological).
* 4. `未決事項`: pending-approval count + suspect-session count.
Expand Down Expand Up @@ -202,16 +202,14 @@ export async function renderHandoff(input: HandoffRendererInput): Promise<Handof
(a, b) => Date.parse(b.session.session.started_at) - Date.parse(a.session.session.started_at),
)[0];

// 「直近の変更ファイル」 collects related_files from live sessions only.
// Imported sessions are historical / cross-workspace and would otherwise
// pollute the "what changed recently" view with paths that don't belong
// to the current workspace's live work.
const allFiles = new Set<string>();
for (const e of entries) {
if (e.session.session.source.kind === "import") continue;
for (const f of e.session.session.related_files) allFiles.add(f);
}
const sortedFiles = [...allFiles].sort();
// 「直近の変更ファイル」 shows the files touched by the single most recent
// session — the same session surfaced as 最終 session above — so the section
// reflects the latest activity rather than the whole history. Unioning every
// session's related_files turned this into a whole-history dump once
// transcript imports became the primary source, since each import carries a
// full day of file changes.
const latestFiles = latestSession?.session.session.related_files ?? [];
const sortedFiles = [...new Set(latestFiles)].sort();
const displayedFiles = sortedFiles.slice(0, limit);
const overflow = Math.max(0, sortedFiles.length - limit);

Expand Down