ultimate-pi 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/.pi/PACKAGING.md +3 -2
  2. package/.pi/extensions/lib/harness-vcc-settings.ts +50 -0
  3. package/.pi/extensions/ultimate-pi-vcc.ts +17 -0
  4. package/.pi/harness/docs/adrs/0030-inhouse-vcc-compaction.md +40 -0
  5. package/.pi/harness/docs/adrs/README.md +1 -0
  6. package/.pi/prompts/harness-setup.md +2 -2
  7. package/.pi/scripts/vendor-pi-vcc-settings.stub.ts +8 -0
  8. package/.pi/scripts/vendor-sync-pi-vcc.sh +40 -0
  9. package/.pi/settings.example.json +1 -6
  10. package/CHANGELOG.md +9 -7
  11. package/THIRD_PARTY_NOTICES.md +8 -22
  12. package/package.json +7 -6
  13. package/vendor/pi-vcc/README.md +215 -0
  14. package/vendor/pi-vcc/UPSTREAM_PIN.md +12 -0
  15. package/vendor/pi-vcc/demo.gif +0 -0
  16. package/vendor/pi-vcc/index.ts +12 -0
  17. package/vendor/pi-vcc/package.json +26 -0
  18. package/vendor/pi-vcc/scripts/audit-sessions.ts +88 -0
  19. package/vendor/pi-vcc/scripts/benchmark-real-sessions.ts +25 -0
  20. package/vendor/pi-vcc/scripts/compare-before-after.ts +36 -0
  21. package/vendor/pi-vcc/scripts/dump-branch-output.ts +20 -0
  22. package/vendor/pi-vcc/src/commands/pi-vcc.ts +36 -0
  23. package/vendor/pi-vcc/src/commands/vcc-recall.ts +65 -0
  24. package/vendor/pi-vcc/src/core/brief.ts +381 -0
  25. package/vendor/pi-vcc/src/core/build-sections.ts +79 -0
  26. package/vendor/pi-vcc/src/core/content.ts +60 -0
  27. package/vendor/pi-vcc/src/core/filter-noise.ts +42 -0
  28. package/vendor/pi-vcc/src/core/format-recall.ts +27 -0
  29. package/vendor/pi-vcc/src/core/format.ts +49 -0
  30. package/vendor/pi-vcc/src/core/lineage.ts +26 -0
  31. package/vendor/pi-vcc/src/core/load-messages.ts +41 -0
  32. package/vendor/pi-vcc/src/core/normalize.ts +66 -0
  33. package/vendor/pi-vcc/src/core/recall-scope.ts +14 -0
  34. package/vendor/pi-vcc/src/core/render-entries.ts +55 -0
  35. package/vendor/pi-vcc/src/core/report.ts +237 -0
  36. package/vendor/pi-vcc/src/core/sanitize.ts +5 -0
  37. package/vendor/pi-vcc/src/core/search-entries.ts +221 -0
  38. package/vendor/pi-vcc/src/core/settings.ts +8 -0
  39. package/vendor/pi-vcc/src/core/skill-collapse.ts +35 -0
  40. package/vendor/pi-vcc/src/core/summarize.ts +157 -0
  41. package/vendor/pi-vcc/src/core/tool-args.ts +14 -0
  42. package/vendor/pi-vcc/src/details.ts +7 -0
  43. package/vendor/pi-vcc/src/extract/commits.ts +69 -0
  44. package/vendor/pi-vcc/src/extract/files.ts +80 -0
  45. package/vendor/pi-vcc/src/extract/goals.ts +79 -0
  46. package/vendor/pi-vcc/src/extract/preferences.ts +55 -0
  47. package/vendor/pi-vcc/src/hooks/before-compact.ts +314 -0
  48. package/vendor/pi-vcc/src/sections.ts +12 -0
  49. package/vendor/pi-vcc/src/tools/recall.ts +109 -0
  50. package/vendor/pi-vcc/src/types.ts +14 -0
  51. package/vendor/pi-vcc/tests/before-compact-hook.test.ts +204 -0
  52. package/vendor/pi-vcc/tests/before-compact.test.ts +145 -0
  53. package/vendor/pi-vcc/tests/brief.test.ts +206 -0
  54. package/vendor/pi-vcc/tests/build-sections.test.ts +59 -0
  55. package/vendor/pi-vcc/tests/compile.test.ts +80 -0
  56. package/vendor/pi-vcc/tests/content.test.ts +31 -0
  57. package/vendor/pi-vcc/tests/extract-goals.test.ts +86 -0
  58. package/vendor/pi-vcc/tests/extract-preferences.test.ts +30 -0
  59. package/vendor/pi-vcc/tests/filter-noise.test.ts +61 -0
  60. package/vendor/pi-vcc/tests/fixtures.ts +61 -0
  61. package/vendor/pi-vcc/tests/format-recall.test.ts +30 -0
  62. package/vendor/pi-vcc/tests/format.test.ts +62 -0
  63. package/vendor/pi-vcc/tests/lineage.test.ts +33 -0
  64. package/vendor/pi-vcc/tests/load-messages.test.ts +51 -0
  65. package/vendor/pi-vcc/tests/normalize.test.ts +97 -0
  66. package/vendor/pi-vcc/tests/real-sessions.test.ts +38 -0
  67. package/vendor/pi-vcc/tests/recall-expand.test.ts +15 -0
  68. package/vendor/pi-vcc/tests/recall-scope.test.ts +32 -0
  69. package/vendor/pi-vcc/tests/recall-tool-scope.test.ts +67 -0
  70. package/vendor/pi-vcc/tests/render-entries.test.ts +62 -0
  71. package/vendor/pi-vcc/tests/report.test.ts +44 -0
  72. package/vendor/pi-vcc/tests/sanitize.test.ts +24 -0
  73. package/vendor/pi-vcc/tests/search-entries.test.ts +144 -0
  74. package/vendor/pi-vcc/tests/support/load-session.ts +23 -0
  75. package/vendor/pi-vcc/tests/support/real-sessions.ts +51 -0
  76. package/.pi/pi-vcc-config.json +0 -4
@@ -0,0 +1,59 @@
1
+ import { describe, it, expect } from "bun:test";
2
+ import { buildSections } from "../src/core/build-sections";
3
+ import type { NormalizedBlock } from "../src/types";
4
+
5
+ describe("buildSections", () => {
6
+ it("returns all-empty for no blocks", () => {
7
+ const r = buildSections({ blocks: [] });
8
+ expect(r.sessionGoal).toEqual([]);
9
+ expect(r.outstandingContext).toEqual([]);
10
+ expect(r.briefTranscript).toBe("");
11
+ });
12
+
13
+ it("populates sections from realistic blocks", () => {
14
+ const blocks: NormalizedBlock[] = [
15
+ { kind: "user", text: "Fix the auth bug" },
16
+ { kind: "tool_call", name: "Read", args: { file_path: "auth.ts" } },
17
+ { kind: "tool_result", name: "Read", text: "const x = 1;", isError: false },
18
+ { kind: "tool_call", name: "Edit", args: { file_path: "auth.ts" } },
19
+ { kind: "tool_result", name: "Edit", text: "ok", isError: false },
20
+ { kind: "assistant", text: "- run tests next" },
21
+ ];
22
+ const r = buildSections({ blocks });
23
+ expect(r.sessionGoal).toContain("Fix the auth bug");
24
+ expect(r.briefTranscript).toContain('[user]');
25
+ expect(r.briefTranscript).toContain('* Read "auth.ts"');
26
+ expect(r.briefTranscript).toContain('* Edit "auth.ts"');
27
+ });
28
+
29
+ it("captures outstanding context from errors", () => {
30
+ const blocks: NormalizedBlock[] = [
31
+ { kind: "tool_result", name: "bash", text: "FAIL: test broken\ndetails here", isError: true },
32
+ ];
33
+ const r = buildSections({ blocks });
34
+ expect(r.outstandingContext.length).toBeGreaterThan(0);
35
+ expect(r.outstandingContext[0]).toContain("FAIL");
36
+ });
37
+
38
+ it("brief transcript hides tool results but shows errors", () => {
39
+ const blocks: NormalizedBlock[] = [
40
+ { kind: "tool_result", name: "Read", text: "lots of code here ...", isError: false },
41
+ { kind: "tool_result", name: "bash", text: "Command not found", isError: true },
42
+ ];
43
+ const r = buildSections({ blocks });
44
+ expect(r.briefTranscript).not.toContain("lots of code");
45
+ expect(r.briefTranscript).toContain("[tool_error] bash");
46
+ expect(r.briefTranscript).toContain("Command not found");
47
+ });
48
+
49
+ it("brief transcript merges adjacent assistant sections", () => {
50
+ const blocks: NormalizedBlock[] = [
51
+ { kind: "assistant", text: "Part one." },
52
+ { kind: "tool_call", name: "Read", args: { file_path: "a.ts" } },
53
+ { kind: "assistant", text: "Part two." },
54
+ ];
55
+ const r = buildSections({ blocks });
56
+ const matches = r.briefTranscript.match(/\[assistant\]/g);
57
+ expect(matches?.length).toBe(1);
58
+ });
59
+ });
@@ -0,0 +1,80 @@
1
+ import { describe, it, expect } from "bun:test";
2
+ import { compile } from "../src/core/summarize";
3
+ import {
4
+ userMsg,
5
+ assistantText,
6
+ assistantWithToolCall,
7
+ toolResult,
8
+ } from "./fixtures";
9
+
10
+ describe("compile", () => {
11
+ it("returns empty string for no messages", () => {
12
+ expect(compile({ messages: [] })).toBe("");
13
+ });
14
+
15
+ it("produces hybrid output with header + brief transcript", () => {
16
+ const r = compile({
17
+ messages: [
18
+ userMsg("Fix login bug"),
19
+ assistantWithToolCall("Read", { path: "auth.ts" }),
20
+ assistantText("Found the issue.\n1. Fix validation"),
21
+ ],
22
+ });
23
+ expect(r).toContain("[Session Goal]");
24
+ expect(r).toContain("Fix login bug");
25
+ expect(r).toContain("---");
26
+ expect(r).toContain("[user]\nFix login bug");
27
+ expect(r).toContain('* Read "auth.ts"');
28
+ expect(r).toContain("Found the issue.");
29
+ });
30
+
31
+ it("merges previous summary goals", () => {
32
+ const r = compile({
33
+ messages: [userMsg("New task")],
34
+ previousSummary: "[Session Goal]\n- Original goal\n\n---\n\n[user]\nOriginal goal",
35
+ });
36
+ expect(r).toContain("- Original goal");
37
+ expect(r).toContain("- New task");
38
+ });
39
+
40
+ it("appends brief transcript on merge", () => {
41
+ const previousSummary = [
42
+ "[Session Goal]\n- Original goal",
43
+ "---",
44
+ "[user]\nOriginal goal\n\n[assistant]\n* Read \"old.ts\"",
45
+ ].join("\n\n");
46
+ const r = compile({
47
+ previousSummary,
48
+ messages: [
49
+ userMsg("Next step"),
50
+ assistantWithToolCall("Read", { path: "new.ts" }),
51
+ ],
52
+ });
53
+ expect(r).toContain('* Read "old.ts"');
54
+ expect(r).toContain('* Read "new.ts"');
55
+ expect(r).toContain("Next step");
56
+ });
57
+
58
+ it("outstanding context is volatile (fresh only)", () => {
59
+ const previousSummary = "[Outstanding Context]\n- old blocker\n\n---\n\n[user]\nhi";
60
+ const r = compile({
61
+ previousSummary,
62
+ messages: [userMsg("continue")],
63
+ });
64
+ expect(r).not.toContain("old blocker");
65
+ });
66
+
67
+ it("caps long brief transcript with rolling window", () => {
68
+ // Build a very long previous transcript
69
+ const longTranscript = Array.from({ length: 200 }, (_, i) =>
70
+ `[user]\nmessage ${i}`
71
+ ).join("\n\n");
72
+ const previousSummary = `[Session Goal]\n- goal\n\n---\n\n${longTranscript}`;
73
+ const r = compile({
74
+ previousSummary,
75
+ messages: [userMsg("latest")],
76
+ });
77
+ expect(r).toContain("earlier lines omitted");
78
+ expect(r).toContain("latest");
79
+ });
80
+ });
@@ -0,0 +1,31 @@
1
+ import { describe, it, expect } from "bun:test";
2
+ import { textParts, textOf, clip, firstLine } from "../src/core/content";
3
+
4
+ describe("textParts", () => {
5
+ it("returns [] for undefined content", () => {
6
+ expect(textParts(undefined as any)).toEqual([]);
7
+ });
8
+
9
+ it("returns [] for null content", () => {
10
+ expect(textParts(null as any)).toEqual([]);
11
+ });
12
+
13
+ it("wraps string content", () => {
14
+ expect(textParts("hello")).toEqual(["hello"]);
15
+ });
16
+
17
+ it("extracts text parts from array content", () => {
18
+ const content = [
19
+ { type: "text" as const, text: "first" },
20
+ { type: "toolCall" as const, name: "x", id: "1", arguments: {} },
21
+ { type: "text" as const, text: "second" },
22
+ ];
23
+ expect(textParts(content)).toEqual(["first", "second"]);
24
+ });
25
+ });
26
+
27
+ describe("textOf", () => {
28
+ it("returns empty string for undefined content", () => {
29
+ expect(textOf(undefined as any)).toBe("");
30
+ });
31
+ });
@@ -0,0 +1,86 @@
1
+ import { describe, it, expect } from "bun:test";
2
+ import { extractGoals } from "../src/extract/goals";
3
+ import type { NormalizedBlock } from "../src/types";
4
+
5
+ describe("extractGoals", () => {
6
+ it("returns empty for no blocks", () => {
7
+ expect(extractGoals([])).toEqual([]);
8
+ });
9
+
10
+ it("returns empty when no user blocks", () => {
11
+ const blocks: NormalizedBlock[] = [
12
+ { kind: "assistant", text: "hello" },
13
+ ];
14
+ expect(extractGoals(blocks)).toEqual([]);
15
+ });
16
+
17
+ it("extracts first user message lines as goals", () => {
18
+ const blocks: NormalizedBlock[] = [
19
+ { kind: "user", text: "Fix login bug\nCheck auth flow" },
20
+ ];
21
+ const goals = extractGoals(blocks);
22
+ expect(goals).toEqual(["Fix login bug", "Check auth flow"]);
23
+ });
24
+
25
+ it("takes up to 6 lines from first user block", () => {
26
+ const blocks: NormalizedBlock[] = [
27
+ { kind: "user", text: "fix the login bug\ncheck auth flow\nupdate the tests\nrefactor utils\nclean up" },
28
+ ];
29
+ expect(extractGoals(blocks)).toHaveLength(5);
30
+ });
31
+
32
+ it("ignores subsequent user blocks", () => {
33
+ const blocks: NormalizedBlock[] = [
34
+ { kind: "user", text: "first goal" },
35
+ { kind: "assistant", text: "ok" },
36
+ { kind: "user", text: "second request" },
37
+ ];
38
+ expect(extractGoals(blocks)).toEqual(["first goal"]);
39
+ });
40
+
41
+ it("detects scope change with explicit pivot keywords", () => {
42
+ const blocks: NormalizedBlock[] = [
43
+ { kind: "user", text: "Fix login bug" },
44
+ { kind: "assistant", text: "ok" },
45
+ { kind: "user", text: "Actually, instead let's refactor the auth module" },
46
+ ];
47
+ const goals = extractGoals(blocks);
48
+ expect(goals).toContain("Fix login bug");
49
+ expect(goals).toContain("[Scope change]");
50
+ expect(goals.some((g) => g.includes("refactor"))).toBe(true);
51
+ });
52
+
53
+ it("detects scope change from new task statements", () => {
54
+ const blocks: NormalizedBlock[] = [
55
+ { kind: "user", text: "Fix login bug" },
56
+ { kind: "assistant", text: "done" },
57
+ { kind: "user", text: "Now implement the user registration flow" },
58
+ ];
59
+ const goals = extractGoals(blocks);
60
+ expect(goals).toContain("[Scope change]");
61
+ });
62
+
63
+ it("keeps latest scope change only", () => {
64
+ const blocks: NormalizedBlock[] = [
65
+ { kind: "user", text: "Fix login bug" },
66
+ { kind: "assistant", text: "done" },
67
+ { kind: "user", text: "Actually, fix the signup page instead" },
68
+ { kind: "assistant", text: "ok" },
69
+ { kind: "user", text: "Change of plan, implement password reset" },
70
+ ];
71
+ const goals = extractGoals(blocks);
72
+ const scopeIdx = goals.indexOf("[Scope change]");
73
+ expect(goals[scopeIdx + 1]).toContain("password reset");
74
+ });
75
+
76
+ it("skips noise short user messages as goals", () => {
77
+ const blocks: NormalizedBlock[] = [
78
+ { kind: "user", text: "ok" },
79
+ { kind: "assistant", text: "hello" },
80
+ { kind: "user", text: "Fix the authentication module" },
81
+ ];
82
+ const goals = extractGoals(blocks);
83
+ expect(goals[0]).toContain("Fix the authentication");
84
+ expect(goals.some((g) => g === "ok")).toBe(false);
85
+ });
86
+ });
@@ -0,0 +1,30 @@
1
+ import { describe, it, expect } from "bun:test";
2
+ import { extractPreferences } from "../src/extract/preferences";
3
+ import type { NormalizedBlock } from "../src/types";
4
+
5
+ describe("extractPreferences", () => {
6
+ it("returns empty for no blocks", () => {
7
+ expect(extractPreferences([])).toEqual([]);
8
+ });
9
+
10
+ it("captures preference patterns from user", () => {
11
+ const blocks: NormalizedBlock[] = [
12
+ { kind: "user", text: "I prefer TypeScript over JavaScript" },
13
+ ];
14
+ expect(extractPreferences(blocks).length).toBe(1);
15
+ });
16
+
17
+ it("ignores assistant blocks", () => {
18
+ const blocks: NormalizedBlock[] = [
19
+ { kind: "assistant", text: "I always use best practices" },
20
+ ];
21
+ expect(extractPreferences(blocks)).toEqual([]);
22
+ });
23
+
24
+ it("captures please use pattern", () => {
25
+ const blocks: NormalizedBlock[] = [
26
+ { kind: "user", text: "please use bun instead of node" },
27
+ ];
28
+ expect(extractPreferences(blocks).length).toBe(1);
29
+ });
30
+ });
@@ -0,0 +1,61 @@
1
+ import { describe, it, expect } from "bun:test";
2
+ import { filterNoise } from "../src/core/filter-noise";
3
+ import type { NormalizedBlock } from "../src/types";
4
+
5
+ describe("filterNoise", () => {
6
+ it("removes thinking blocks", () => {
7
+ const blocks: NormalizedBlock[] = [
8
+ { kind: "thinking", text: "hmm", redacted: false },
9
+ { kind: "assistant", text: "hello" },
10
+ ];
11
+ expect(filterNoise(blocks)).toEqual([{ kind: "assistant", text: "hello" }]);
12
+ });
13
+
14
+ it("removes noise tool calls and results", () => {
15
+ const blocks: NormalizedBlock[] = [
16
+ { kind: "tool_call", name: "TodoWrite", args: {} },
17
+ { kind: "tool_result", name: "TodoWrite", text: "ok", isError: false },
18
+ { kind: "tool_call", name: "Read", args: { path: "x.ts" } },
19
+ ];
20
+ const result = filterNoise(blocks);
21
+ expect(result).toHaveLength(1);
22
+ expect(result[0]).toEqual({ kind: "tool_call", name: "Read", args: { path: "x.ts" } });
23
+ });
24
+
25
+ it("removes user blocks that are pure XML wrappers", () => {
26
+ const blocks: NormalizedBlock[] = [
27
+ { kind: "user", text: "<system-reminder>some noise</system-reminder>" },
28
+ { kind: "user", text: "Fix the bug" },
29
+ ];
30
+ const result = filterNoise(blocks);
31
+ expect(result).toHaveLength(1);
32
+ expect(result[0]).toEqual({ kind: "user", text: "Fix the bug" });
33
+ });
34
+
35
+ it("cleans XML wrappers from user text but keeps real content", () => {
36
+ const blocks: NormalizedBlock[] = [
37
+ { kind: "user", text: "<system-reminder>noise</system-reminder>\nFix the login" },
38
+ ];
39
+ const result = filterNoise(blocks);
40
+ expect(result).toHaveLength(1);
41
+ expect((result[0] as any).text).toBe("Fix the login");
42
+ });
43
+
44
+ it("removes known noise strings", () => {
45
+ const blocks: NormalizedBlock[] = [
46
+ { kind: "user", text: "Continue from where you left off." },
47
+ { kind: "user", text: "real task" },
48
+ ];
49
+ const result = filterNoise(blocks);
50
+ expect(result).toHaveLength(1);
51
+ expect((result[0] as any).text).toBe("real task");
52
+ });
53
+
54
+ it("preserves non-noise tool calls", () => {
55
+ const blocks: NormalizedBlock[] = [
56
+ { kind: "tool_call", name: "Edit", args: { path: "a.ts" } },
57
+ { kind: "tool_result", name: "Edit", text: "ok", isError: false },
58
+ ];
59
+ expect(filterNoise(blocks)).toHaveLength(2);
60
+ });
61
+ });
@@ -0,0 +1,61 @@
1
+ import type { Message } from "@mariozechner/pi-ai";
2
+
3
+ const ts = Date.now();
4
+ const assistBase = {
5
+ api: "messages" as any,
6
+ provider: "anthropic" as any,
7
+ model: "test",
8
+ usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
9
+ timestamp: ts,
10
+ };
11
+
12
+ export const userMsg = (text: string): Message => ({
13
+ role: "user",
14
+ content: text,
15
+ timestamp: ts,
16
+ });
17
+
18
+ export const assistantText = (text: string): Message => ({
19
+ role: "assistant",
20
+ content: [{ type: "text", text }],
21
+ ...assistBase,
22
+ stopReason: "stop",
23
+ });
24
+
25
+ export const assistantWithThinking = (
26
+ text: string,
27
+ thinking: string,
28
+ ): Message => ({
29
+ role: "assistant",
30
+ content: [
31
+ { type: "thinking", thinking },
32
+ { type: "text", text },
33
+ ],
34
+ ...assistBase,
35
+ stopReason: "stop",
36
+ });
37
+
38
+ export const assistantWithToolCall = (
39
+ name: string,
40
+ args: Record<string, unknown>,
41
+ ): Message => ({
42
+ role: "assistant",
43
+ content: [{ type: "toolCall", id: "tc_1", name, arguments: args }],
44
+ ...assistBase,
45
+ stopReason: "toolUse",
46
+ });
47
+
48
+ export const toolResult = (
49
+ name: string,
50
+ text: string,
51
+ isError = false,
52
+ ): Message => ({
53
+ role: "toolResult",
54
+ toolCallId: "tc_1",
55
+ toolName: name,
56
+ content: [{ type: "text", text }],
57
+ isError,
58
+ timestamp: ts,
59
+ });
60
+
61
+
@@ -0,0 +1,30 @@
1
+ import { describe, it, expect } from "bun:test";
2
+ import { formatRecallOutput } from "../src/core/format-recall";
3
+ import type { RenderedEntry } from "../src/core/render-entries";
4
+
5
+ describe("formatRecallOutput", () => {
6
+ it("shows no-match message with query", () => {
7
+ const r = formatRecallOutput([], "xyz");
8
+ expect(r).toContain('No matches for "xyz"');
9
+ });
10
+
11
+ it("shows no-entries message without query", () => {
12
+ expect(formatRecallOutput([])).toContain("No entries");
13
+ });
14
+
15
+ it("formats entries with index and role", () => {
16
+ const entries: RenderedEntry[] = [
17
+ { index: 0, role: "user", summary: "hello" },
18
+ ];
19
+ const r = formatRecallOutput(entries);
20
+ expect(r).toContain("#0 [user] hello");
21
+ });
22
+
23
+ it("shows match count with query", () => {
24
+ const entries: RenderedEntry[] = [
25
+ { index: 2, role: "assistant", summary: "done" },
26
+ ];
27
+ const r = formatRecallOutput(entries, "done");
28
+ expect(r).toContain('Found 1 matches for "done"');
29
+ });
30
+ });
@@ -0,0 +1,62 @@
1
+ import { describe, it, expect } from "bun:test";
2
+ import { formatSummary } from "../src/core/format";
3
+ import type { SectionData } from "../src/sections";
4
+
5
+ const empty: SectionData = {
6
+ sessionGoal: [],
7
+ outstandingContext: [],
8
+ filesAndChanges: [],
9
+ commits: [],
10
+ userPreferences: [],
11
+ briefTranscript: "",
12
+ transcriptEntries: [],
13
+ };
14
+
15
+ describe("formatSummary", () => {
16
+ it("returns empty string for all-empty sections", () => {
17
+ expect(formatSummary(empty)).toBe("");
18
+ });
19
+
20
+ it("formats a single header section", () => {
21
+ const data = {
22
+ ...empty,
23
+ sessionGoal: ["fix auth bug"],
24
+ };
25
+ const r = formatSummary(data);
26
+ expect(r).toContain("[Session Goal]");
27
+ expect(r).toContain("- fix auth bug");
28
+ });
29
+
30
+ it("separates header and brief transcript with ---", () => {
31
+ const data = {
32
+ ...empty,
33
+ sessionGoal: ["goal"],
34
+ briefTranscript: "[user]\ndo something",
35
+ };
36
+ const r = formatSummary(data);
37
+ expect(r).toContain("[Session Goal]");
38
+ expect(r).toContain("---");
39
+ expect(r).toContain("[user]\ndo something");
40
+ });
41
+
42
+ it("renders brief transcript alone when no header sections", () => {
43
+ const data = {
44
+ ...empty,
45
+ briefTranscript: "[user]\nhi\n\n[assistant]\nhello",
46
+ };
47
+ const r = formatSummary(data);
48
+ expect(r).toContain("[user]\nhi\n\n[assistant]\nhello");
49
+ });
50
+
51
+ it("joins multiple header sections with blank line", () => {
52
+ const data = {
53
+ ...empty,
54
+ sessionGoal: ["goal"],
55
+ outstandingContext: ["blocker"],
56
+ };
57
+ const r = formatSummary(data);
58
+ expect(r).toContain("[Session Goal]");
59
+ expect(r).toContain("[Outstanding Context]");
60
+ expect(r).toContain("\n\n");
61
+ });
62
+ });
@@ -0,0 +1,33 @@
1
+ import { describe, it, expect } from "bun:test";
2
+ import { getActiveLineageEntryIds } from "../src/core/lineage";
3
+
4
+ describe("getActiveLineageEntryIds", () => {
5
+ it("returns IDs from active branch", () => {
6
+ const ids = getActiveLineageEntryIds({
7
+ getBranch: () => [{ id: "a" }, { id: "b" }, { id: "c" }],
8
+ });
9
+ expect([...ids]).toEqual(["a", "b", "c"]);
10
+ });
11
+
12
+ it("falls back to getEntries when getBranch throws", () => {
13
+ const ids = getActiveLineageEntryIds({
14
+ getBranch: () => {
15
+ throw new Error("boom");
16
+ },
17
+ getEntries: () => [{ id: "x" }, { id: "y" }],
18
+ });
19
+ expect([...ids]).toEqual(["x", "y"]);
20
+ });
21
+
22
+ it("returns empty set when both branch and entries are unavailable", () => {
23
+ const ids = getActiveLineageEntryIds({
24
+ getBranch: () => {
25
+ throw new Error("boom");
26
+ },
27
+ getEntries: () => {
28
+ throw new Error("boom2");
29
+ },
30
+ });
31
+ expect(ids.size).toBe(0);
32
+ });
33
+ });
@@ -0,0 +1,51 @@
1
+ import { describe, it, expect } from "bun:test";
2
+ import { mkdtempSync, writeFileSync, rmSync } from "fs";
3
+ import { tmpdir } from "os";
4
+ import { join } from "path";
5
+ import { loadAllMessages } from "../src/core/load-messages";
6
+
7
+ describe("loadAllMessages", () => {
8
+ it("loads all message entries when no lineage filter is provided", () => {
9
+ const dir = mkdtempSync(join(tmpdir(), "pi-vcc-load-all-"));
10
+ const file = join(dir, "session.jsonl");
11
+ try {
12
+ const lines = [
13
+ JSON.stringify({ type: "session", id: "s1" }),
14
+ JSON.stringify({ type: "message", id: "m1", message: { role: "user", content: "u1" } }),
15
+ JSON.stringify({ type: "custom", id: "c1", customType: "x", data: {} }),
16
+ JSON.stringify({ type: "message", id: "m2", message: { role: "assistant", content: [{ type: "text", text: "a1" }] } }),
17
+ JSON.stringify({ type: "message", id: "m3", message: { role: "toolResult", toolName: "read", content: [{ type: "text", text: "ok" }] } }),
18
+ ];
19
+ writeFileSync(file, lines.join("\n") + "\n", "utf8");
20
+
21
+ const loaded = loadAllMessages(file, false);
22
+ expect(loaded.rendered).toHaveLength(3);
23
+ expect(loaded.rawMessages).toHaveLength(3);
24
+ expect(loaded.entryIds).toEqual(["m1", "m2", "m3"]);
25
+ expect(loaded.rendered.map((e) => e.index)).toEqual([0, 1, 2]);
26
+ } finally {
27
+ rmSync(dir, { recursive: true, force: true });
28
+ }
29
+ });
30
+
31
+ it("filters messages by allowed lineage entry IDs and preserves original message index", () => {
32
+ const dir = mkdtempSync(join(tmpdir(), "pi-vcc-load-filter-"));
33
+ const file = join(dir, "session.jsonl");
34
+ try {
35
+ const lines = [
36
+ JSON.stringify({ type: "message", id: "m1", message: { role: "user", content: "u1" } }),
37
+ JSON.stringify({ type: "message", id: "m2", message: { role: "assistant", content: [{ type: "text", text: "a1" }] } }),
38
+ JSON.stringify({ type: "message", id: "m3", message: { role: "user", content: "u2" } }),
39
+ ];
40
+ writeFileSync(file, lines.join("\n") + "\n", "utf8");
41
+
42
+ const loaded = loadAllMessages(file, false, new Set(["m2"]));
43
+ expect(loaded.rendered).toHaveLength(1);
44
+ expect(loaded.rawMessages).toHaveLength(1);
45
+ expect(loaded.entryIds).toEqual(["m2"]);
46
+ expect(loaded.rendered[0].index).toBe(1);
47
+ } finally {
48
+ rmSync(dir, { recursive: true, force: true });
49
+ }
50
+ });
51
+ });