ultimate-pi 0.4.0 → 0.5.0

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 (94) hide show
  1. package/.agents/skills/harness-decisions/SKILL.md +15 -0
  2. package/.agents/skills/scrapling-web/SKILL.md +45 -40
  3. package/.agents/skills/wiki-autoresearch/SKILL.md +3 -3
  4. package/.pi/PACKAGING.md +3 -2
  5. package/.pi/SYSTEM.md +12 -13
  6. package/.pi/agents/pi-pi/agent-expert.md +3 -3
  7. package/.pi/extensions/harness-web-guard.ts +95 -0
  8. package/.pi/extensions/harness-web-tools.ts +209 -0
  9. package/.pi/extensions/lib/harness-vcc-settings.ts +50 -0
  10. package/.pi/extensions/lib/harness-web/run-cli.ts +92 -0
  11. package/.pi/extensions/ultimate-pi-vcc.ts +17 -0
  12. package/.pi/harness/docs/adrs/0030-inhouse-vcc-compaction.md +40 -0
  13. package/.pi/harness/docs/adrs/README.md +1 -0
  14. package/.pi/harness/env.harness.template +3 -1
  15. package/.pi/prompts/harness-setup.md +48 -2
  16. package/.pi/scripts/harness-cli-verify.sh +12 -3
  17. package/.pi/scripts/harness-searxng-bootstrap.mjs +270 -0
  18. package/.pi/scripts/harness-web-search.md +24 -5
  19. package/.pi/scripts/harness-web.py +24 -7
  20. package/.pi/scripts/harness_web/config.py +37 -3
  21. package/.pi/scripts/harness_web/output.py +8 -2
  22. package/.pi/scripts/harness_web/search.py +22 -0
  23. package/.pi/scripts/harness_web/search_ddg.py +1 -5
  24. package/.pi/scripts/harness_web/search_searxng.py +100 -0
  25. package/.pi/scripts/vendor-pi-vcc-settings.stub.ts +8 -0
  26. package/.pi/scripts/vendor-sync-pi-vcc.sh +40 -0
  27. package/.pi/settings.example.json +1 -6
  28. package/CHANGELOG.md +20 -6
  29. package/THIRD_PARTY_NOTICES.md +8 -22
  30. package/package.json +7 -6
  31. package/vendor/pi-vcc/README.md +215 -0
  32. package/vendor/pi-vcc/UPSTREAM_PIN.md +12 -0
  33. package/vendor/pi-vcc/demo.gif +0 -0
  34. package/vendor/pi-vcc/index.ts +12 -0
  35. package/vendor/pi-vcc/package.json +26 -0
  36. package/vendor/pi-vcc/scripts/audit-sessions.ts +88 -0
  37. package/vendor/pi-vcc/scripts/benchmark-real-sessions.ts +25 -0
  38. package/vendor/pi-vcc/scripts/compare-before-after.ts +36 -0
  39. package/vendor/pi-vcc/scripts/dump-branch-output.ts +20 -0
  40. package/vendor/pi-vcc/src/commands/pi-vcc.ts +36 -0
  41. package/vendor/pi-vcc/src/commands/vcc-recall.ts +65 -0
  42. package/vendor/pi-vcc/src/core/brief.ts +381 -0
  43. package/vendor/pi-vcc/src/core/build-sections.ts +79 -0
  44. package/vendor/pi-vcc/src/core/content.ts +60 -0
  45. package/vendor/pi-vcc/src/core/filter-noise.ts +42 -0
  46. package/vendor/pi-vcc/src/core/format-recall.ts +27 -0
  47. package/vendor/pi-vcc/src/core/format.ts +49 -0
  48. package/vendor/pi-vcc/src/core/lineage.ts +26 -0
  49. package/vendor/pi-vcc/src/core/load-messages.ts +41 -0
  50. package/vendor/pi-vcc/src/core/normalize.ts +66 -0
  51. package/vendor/pi-vcc/src/core/recall-scope.ts +14 -0
  52. package/vendor/pi-vcc/src/core/render-entries.ts +55 -0
  53. package/vendor/pi-vcc/src/core/report.ts +237 -0
  54. package/vendor/pi-vcc/src/core/sanitize.ts +5 -0
  55. package/vendor/pi-vcc/src/core/search-entries.ts +221 -0
  56. package/vendor/pi-vcc/src/core/settings.ts +8 -0
  57. package/vendor/pi-vcc/src/core/skill-collapse.ts +35 -0
  58. package/vendor/pi-vcc/src/core/summarize.ts +157 -0
  59. package/vendor/pi-vcc/src/core/tool-args.ts +14 -0
  60. package/vendor/pi-vcc/src/details.ts +7 -0
  61. package/vendor/pi-vcc/src/extract/commits.ts +69 -0
  62. package/vendor/pi-vcc/src/extract/files.ts +80 -0
  63. package/vendor/pi-vcc/src/extract/goals.ts +79 -0
  64. package/vendor/pi-vcc/src/extract/preferences.ts +55 -0
  65. package/vendor/pi-vcc/src/hooks/before-compact.ts +314 -0
  66. package/vendor/pi-vcc/src/sections.ts +12 -0
  67. package/vendor/pi-vcc/src/tools/recall.ts +109 -0
  68. package/vendor/pi-vcc/src/types.ts +14 -0
  69. package/vendor/pi-vcc/tests/before-compact-hook.test.ts +204 -0
  70. package/vendor/pi-vcc/tests/before-compact.test.ts +145 -0
  71. package/vendor/pi-vcc/tests/brief.test.ts +206 -0
  72. package/vendor/pi-vcc/tests/build-sections.test.ts +59 -0
  73. package/vendor/pi-vcc/tests/compile.test.ts +80 -0
  74. package/vendor/pi-vcc/tests/content.test.ts +31 -0
  75. package/vendor/pi-vcc/tests/extract-goals.test.ts +86 -0
  76. package/vendor/pi-vcc/tests/extract-preferences.test.ts +30 -0
  77. package/vendor/pi-vcc/tests/filter-noise.test.ts +61 -0
  78. package/vendor/pi-vcc/tests/fixtures.ts +61 -0
  79. package/vendor/pi-vcc/tests/format-recall.test.ts +30 -0
  80. package/vendor/pi-vcc/tests/format.test.ts +62 -0
  81. package/vendor/pi-vcc/tests/lineage.test.ts +33 -0
  82. package/vendor/pi-vcc/tests/load-messages.test.ts +51 -0
  83. package/vendor/pi-vcc/tests/normalize.test.ts +97 -0
  84. package/vendor/pi-vcc/tests/real-sessions.test.ts +38 -0
  85. package/vendor/pi-vcc/tests/recall-expand.test.ts +15 -0
  86. package/vendor/pi-vcc/tests/recall-scope.test.ts +32 -0
  87. package/vendor/pi-vcc/tests/recall-tool-scope.test.ts +67 -0
  88. package/vendor/pi-vcc/tests/render-entries.test.ts +62 -0
  89. package/vendor/pi-vcc/tests/report.test.ts +44 -0
  90. package/vendor/pi-vcc/tests/sanitize.test.ts +24 -0
  91. package/vendor/pi-vcc/tests/search-entries.test.ts +144 -0
  92. package/vendor/pi-vcc/tests/support/load-session.ts +23 -0
  93. package/vendor/pi-vcc/tests/support/real-sessions.ts +51 -0
  94. package/.pi/pi-vcc-config.json +0 -4
@@ -0,0 +1,145 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { buildOwnCut } from "../src/hooks/before-compact";
3
+
4
+ const msg = (id: string, role: "user" | "assistant" | "toolResult", content = "x") => ({
5
+ id,
6
+ type: "message",
7
+ message: { role, content },
8
+ });
9
+
10
+ const comp = (id: string, firstKeptEntryId?: string) => ({
11
+ id,
12
+ type: "compaction",
13
+ firstKeptEntryId,
14
+ });
15
+
16
+ describe("buildOwnCut", () => {
17
+ test("no prior compaction: cuts at last user message", () => {
18
+ const r = buildOwnCut([
19
+ msg("m1", "user", "a"),
20
+ msg("m2", "assistant", "b"),
21
+ msg("m3", "user", "c"),
22
+ msg("m4", "assistant", "d"),
23
+ ]);
24
+ expect(r.ok).toBe(true);
25
+ if (!r.ok) return;
26
+ expect(r.firstKeptEntryId).toBe("m3");
27
+ expect(r.messages).toHaveLength(2);
28
+ expect(r.compactAll).toBe(false);
29
+ });
30
+
31
+ test("cancels with too_few_live_messages when liveMessages <= 2", () => {
32
+ const r = buildOwnCut([
33
+ comp("c1", "m1"),
34
+ msg("m1", "user", "x"),
35
+ msg("m2", "assistant", "y"),
36
+ ]);
37
+ expect(r.ok).toBe(false);
38
+ if (r.ok) return;
39
+ expect(r.reason).toBe("too_few_live_messages");
40
+ });
41
+
42
+ test("orphan firstKeptEntryId triggers recovery (collect after compaction)", () => {
43
+ // Prev compaction set firstKeptEntryId to a non-existent id (e.g. "" sentinel
44
+ // from a previous compact-all). Recovery should collect msgs after compaction.
45
+ const r = buildOwnCut([
46
+ msg("old1", "user", "old"),
47
+ msg("old2", "assistant", "old"),
48
+ comp("c1", "ORPHAN_ID"),
49
+ msg("m1", "user", "a"),
50
+ msg("m2", "assistant", "b"),
51
+ msg("m3", "user", "c"),
52
+ msg("m4", "assistant", "d"),
53
+ ]);
54
+ expect(r.ok).toBe(true);
55
+ if (!r.ok) return;
56
+ expect(r.firstKeptEntryId).toBe("m3");
57
+ expect(r.messages).toHaveLength(2);
58
+ });
59
+
60
+ test("resumes from firstKeptEntryId after prior compaction", () => {
61
+ const r = buildOwnCut([
62
+ msg("old1", "user", "old"),
63
+ msg("old2", "assistant", "old"),
64
+ comp("c1", "m1"),
65
+ msg("m1", "user", "a"),
66
+ msg("m2", "assistant", "b"),
67
+ msg("m3", "user", "c"),
68
+ msg("m4", "assistant", "d"),
69
+ ]);
70
+ expect(r.ok).toBe(true);
71
+ if (!r.ok) return;
72
+ expect(r.firstKeptEntryId).toBe("m3");
73
+ expect(r.messages).toHaveLength(2);
74
+ });
75
+
76
+ test("single user prompt + autonomous tail: compact all", () => {
77
+ // The Discord scenario: user types 1 prompt, agent runs autonomously
78
+ // (assistant + toolResult interleaved). No user > idx 0.
79
+ const r = buildOwnCut([
80
+ msg("m1", "user", "go"),
81
+ msg("m2", "assistant", "calling tool"),
82
+ msg("m3", "toolResult", "result"),
83
+ msg("m4", "assistant", "more"),
84
+ msg("m5", "toolResult", "result2"),
85
+ msg("m6", "assistant", "done"),
86
+ ]);
87
+ expect(r.ok).toBe(true);
88
+ if (!r.ok) return;
89
+ expect(r.compactAll).toBe(true);
90
+ expect(r.firstKeptEntryId).toBe("");
91
+ expect(r.messages).toHaveLength(6);
92
+ });
93
+
94
+ test("no user message: compact-all instead of cancelling", () => {
95
+ // When there are enough live messages but none are from the user
96
+ // (e.g., long assistant/tool chain), compact all rather than
97
+ // cancelling and leaving the session unrecoverable.
98
+ const r = buildOwnCut([
99
+ msg("m1", "assistant", "a"),
100
+ msg("m2", "assistant", "b"),
101
+ msg("m3", "assistant", "c"),
102
+ ]);
103
+ expect(r.ok).toBe(true);
104
+ if (!r.ok) return;
105
+ expect(r.compactAll).toBe(true);
106
+ expect(r.firstKeptEntryId).toBe("");
107
+ expect(r.messages).toHaveLength(3);
108
+ });
109
+
110
+ test("compact-all then more chat: orphan recovery + normal cut", () => {
111
+ // After a compact-all (firstKeptEntryId=""), user chats more turns,
112
+ // next compaction should orphan-recover and find multiple users.
113
+ const r = buildOwnCut([
114
+ msg("o1", "user", "old"),
115
+ msg("o2", "assistant", "old"),
116
+ comp("c1", ""), // sentinel from prior compact-all
117
+ msg("u1", "user", "new1"),
118
+ msg("a1", "assistant", "reply1"),
119
+ msg("u2", "user", "new2"),
120
+ msg("a2", "assistant", "reply2"),
121
+ msg("u3", "user", "new3"),
122
+ msg("a3", "assistant", "reply3"),
123
+ ]);
124
+ expect(r.ok).toBe(true);
125
+ if (!r.ok) return;
126
+ expect(r.compactAll).toBe(false);
127
+ expect(r.firstKeptEntryId).toBe("u3");
128
+ expect(r.messages).toHaveLength(4); // u1, a1, u2, a2
129
+ });
130
+
131
+ test("compact-all then single user msg + autonomous: compact all again", () => {
132
+ const r = buildOwnCut([
133
+ msg("o1", "user", "old"),
134
+ comp("c1", ""),
135
+ msg("u1", "user", "okay"),
136
+ msg("a1", "assistant", "x"),
137
+ msg("t1", "toolResult", "y"),
138
+ msg("a2", "assistant", "z"),
139
+ ]);
140
+ expect(r.ok).toBe(true);
141
+ if (!r.ok) return;
142
+ expect(r.compactAll).toBe(true);
143
+ expect(r.firstKeptEntryId).toBe("");
144
+ });
145
+ });
@@ -0,0 +1,206 @@
1
+ import { describe, it, expect } from "bun:test";
2
+ import { compileBrief } from "../src/core/brief";
3
+ import type { NormalizedBlock } from "../src/types";
4
+
5
+ describe("compileBrief", () => {
6
+ it("returns empty string for no blocks", () => {
7
+ expect(compileBrief([])).toBe("");
8
+ });
9
+
10
+ it("renders user and assistant text", () => {
11
+ const blocks: NormalizedBlock[] = [
12
+ { kind: "user", text: "fix auth bug" },
13
+ { kind: "assistant", text: "Let me look at the auth module." },
14
+ ];
15
+ const r = compileBrief(blocks);
16
+ expect(r).toContain("[user]");
17
+ expect(r).toContain("fix auth bug");
18
+ expect(r).toContain("[assistant]");
19
+ expect(r).toContain("Let me look at the auth module.");
20
+ });
21
+
22
+ it("strips filler prefixes but preserves meaningful lead-ins", () => {
23
+ const blocks: NormalizedBlock[] = [
24
+ { kind: "assistant", text: "Okay, I found the root cause." },
25
+ { kind: "assistant", text: "Actually, the issue is in middleware." },
26
+ { kind: "assistant", text: "Let me check the logs." },
27
+ ];
28
+ const r = compileBrief(blocks);
29
+ expect(r).toContain("I found the root cause.");
30
+ expect(r).toContain("the issue is in middleware.");
31
+ expect(r).toContain("Let me check the logs.");
32
+ });
33
+
34
+ it("collapses tool calls to one-liners under [assistant]", () => {
35
+ const blocks: NormalizedBlock[] = [
36
+ { kind: "assistant", text: "Let me check." },
37
+ { kind: "tool_call", name: "Read", args: { file_path: "auth.ts" } },
38
+ { kind: "tool_call", name: "Edit", args: { file_path: "auth.ts" } },
39
+ ];
40
+ const r = compileBrief(blocks);
41
+ expect(r).toContain('* Read "auth.ts"');
42
+ expect(r).toContain('* Edit "auth.ts"');
43
+ // Should merge into single [assistant] section
44
+ const matches = r.match(/\[assistant\]/g);
45
+ expect(matches?.length).toBe(1);
46
+ });
47
+
48
+ it("hides non-error tool results", () => {
49
+ const blocks: NormalizedBlock[] = [
50
+ { kind: "tool_result", name: "Read", text: "const x = 1;\nconst y = 2;\n// lots of code", isError: false },
51
+ ];
52
+ const r = compileBrief(blocks);
53
+ expect(r).toBe("");
54
+ });
55
+
56
+ it("shows tool errors with first line", () => {
57
+ const blocks: NormalizedBlock[] = [
58
+ { kind: "tool_result", name: "bash", text: "FAIL auth.test.ts\nexpected 200 got 401", isError: true },
59
+ ];
60
+ const r = compileBrief(blocks);
61
+ expect(r).toContain("[tool_error] bash");
62
+ expect(r).toContain("FAIL auth.test.ts");
63
+ });
64
+
65
+ it("hides thinking blocks", () => {
66
+ const blocks: NormalizedBlock[] = [
67
+ { kind: "thinking", text: "Let me think about this...", redacted: false },
68
+ { kind: "assistant", text: "Here's what I found." },
69
+ ];
70
+ const r = compileBrief(blocks);
71
+ expect(r).not.toContain("think");
72
+ expect(r).toContain("Here's what I found.");
73
+ });
74
+
75
+ it("merges adjacent assistant sections", () => {
76
+ const blocks: NormalizedBlock[] = [
77
+ { kind: "assistant", text: "First part." },
78
+ { kind: "tool_call", name: "Read", args: { file_path: "a.ts" } },
79
+ // No user/tool_result between these — should merge
80
+ { kind: "assistant", text: "Second part." },
81
+ { kind: "tool_call", name: "Read", args: { file_path: "b.ts" } },
82
+ ];
83
+ const r = compileBrief(blocks);
84
+ const matches = r.match(/\[assistant\]/g);
85
+ expect(matches?.length).toBe(1);
86
+ });
87
+
88
+ it("does NOT merge assistant after user", () => {
89
+ const blocks: NormalizedBlock[] = [
90
+ { kind: "assistant", text: "First." },
91
+ { kind: "user", text: "Next task." },
92
+ { kind: "assistant", text: "Second." },
93
+ ];
94
+ const r = compileBrief(blocks);
95
+ const matches = r.match(/\[assistant\]/g);
96
+ expect(matches?.length).toBe(2);
97
+ });
98
+
99
+ it("truncates long user text", () => {
100
+ const longText = Array.from({ length: 300 }, (_, i) => `word${i}`).join(" ");
101
+ const blocks: NormalizedBlock[] = [
102
+ { kind: "user", text: longText },
103
+ ];
104
+ const r = compileBrief(blocks);
105
+ expect(r).toContain("(truncated)");
106
+ expect(r).not.toContain("word299");
107
+ });
108
+
109
+ it("truncates long assistant text", () => {
110
+ const longText = Array.from({ length: 300 }, (_, i) => `word${i}`).join(" ");
111
+ const blocks: NormalizedBlock[] = [
112
+ { kind: "assistant", text: longText },
113
+ ];
114
+ const r = compileBrief(blocks);
115
+ expect(r).toContain("(truncated)");
116
+ expect(r).not.toContain("word299");
117
+ });
118
+
119
+ it("renders a realistic conversation flow", () => {
120
+ const blocks: NormalizedBlock[] = [
121
+ { kind: "user", text: "fix the login bug" },
122
+ { kind: "thinking", text: "I need to check...", redacted: false },
123
+ { kind: "assistant", text: "Let me investigate." },
124
+ { kind: "tool_call", name: "Read", args: { file_path: "login.ts" } },
125
+ { kind: "tool_result", name: "Read", text: "export function login() { ... }", isError: false },
126
+ { kind: "tool_call", name: "bash", args: { command: "npm test" } },
127
+ { kind: "tool_result", name: "bash", text: "FAIL: login test\nExpected true, got false", isError: true },
128
+ { kind: "assistant", text: "The test is failing because..." },
129
+ { kind: "tool_call", name: "Edit", args: { file_path: "login.ts" } },
130
+ { kind: "tool_result", name: "Edit", text: "File edited successfully", isError: false },
131
+ { kind: "user", text: "test lại đi" },
132
+ { kind: "assistant", text: "Running tests again." },
133
+ { kind: "tool_call", name: "bash", args: { command: "npm test" } },
134
+ { kind: "tool_result", name: "bash", text: "All tests passed", isError: false },
135
+ ];
136
+ const r = compileBrief(blocks);
137
+
138
+ // Check structure
139
+ expect(r).toContain("[user]\nfix the login bug");
140
+ expect(r).toContain('[assistant]\nLet me investigate.\n* Read "login.ts"');
141
+ expect(r).toContain("[tool_error] bash\nFAIL: login test");
142
+ expect(r).toContain('[assistant]\nThe test is failing because...\n* Edit "login.ts"');
143
+ expect(r).toContain("[user]\ntest lại đi");
144
+ expect(r).toContain('[assistant]\nRunning tests again.\n* bash "npm test"');
145
+
146
+ // Hidden content
147
+ expect(r).not.toContain("think");
148
+ expect(r).not.toContain("export function login");
149
+ expect(r).not.toContain("File edited successfully");
150
+ expect(r).not.toContain("All tests passed");
151
+ });
152
+
153
+ // ── noise filtering tests (aligned with VCC) ──
154
+
155
+
156
+
157
+
158
+
159
+
160
+
161
+
162
+
163
+ it("suppresses blank lines between consecutive tool-only sections", () => {
164
+ const blocks: NormalizedBlock[] = [
165
+ { kind: "assistant", text: "Checking files." },
166
+ { kind: "tool_call", name: "Read", args: { file_path: "a.ts" } },
167
+ { kind: "tool_result", name: "Read", text: "...", isError: false },
168
+ // tool_result hidden → next tool_call starts new assistant section
169
+ // but since both are tool-only, no blank line between
170
+ { kind: "tool_call", name: "Read", args: { file_path: "b.ts" } },
171
+ { kind: "tool_result", name: "Read", text: "...", isError: false },
172
+ ];
173
+ const r = compileBrief(blocks);
174
+ // The first assistant section has text + tool, so it's NOT tool-only
175
+ // The second would be tool-only but merges into the first (adjacent assistant)
176
+ // So all under one [assistant]
177
+ expect(r.match(/\[assistant\]/g)?.length).toBe(1);
178
+ });
179
+
180
+ it("caps tool calls per [assistant] turn at 8 (keep tail)", () => {
181
+ const blocks: NormalizedBlock[] = [
182
+ { kind: "assistant", text: "Working." },
183
+ ];
184
+ for (let i = 1; i <= 12; i++) {
185
+ blocks.push({ kind: "tool_call", name: "bash", args: { command: `echo ${i}` } });
186
+ }
187
+ const r = compileBrief(blocks);
188
+ expect(r).toContain("(4 earlier tool-call entries omitted)");
189
+ // Last 8 (5..12) kept; first 4 dropped
190
+ expect(r).not.toContain("echo 1\"");
191
+ expect(r).not.toContain("echo 4\"");
192
+ expect(r).toContain("echo 5");
193
+ expect(r).toContain("echo 12");
194
+ });
195
+
196
+ it("does not cap when tool calls per turn <= 8", () => {
197
+ const blocks: NormalizedBlock[] = [{ kind: "assistant", text: "ok" }];
198
+ for (let i = 1; i <= 8; i++) {
199
+ blocks.push({ kind: "tool_call", name: "bash", args: { command: `c${i}` } });
200
+ }
201
+ const r = compileBrief(blocks);
202
+ expect(r).not.toContain("entries omitted");
203
+ expect(r).toContain("c1");
204
+ expect(r).toContain("c8");
205
+ });
206
+ });
@@ -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
+ });