ultimate-pi 0.3.1 → 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 (184) hide show
  1. package/.agents/skills/harness-decisions/SKILL.md +37 -0
  2. package/.agents/skills/harness-governor/SKILL.md +1 -1
  3. package/.agents/skills/harness-orchestration/SKILL.md +54 -0
  4. package/.agents/skills/harness-plan/SKILL.md +4 -3
  5. package/.agents/skills/harness-sentrux-setup/SKILL.md +57 -0
  6. package/.agents/skills/scrapling-web/SKILL.md +93 -0
  7. package/.pi/PACKAGING.md +1 -0
  8. package/.pi/SYSTEM.md +13 -15
  9. package/.pi/agents/harness/adversary.md +3 -0
  10. package/.pi/agents/harness/evaluator.md +3 -0
  11. package/.pi/agents/harness/executor.md +4 -1
  12. package/.pi/agents/harness/meta-optimizer.md +2 -1
  13. package/.pi/agents/harness/planner.md +22 -1
  14. package/.pi/agents/harness/sentrux-bootstrap.md +42 -0
  15. package/.pi/agents/harness/tie-breaker.md +2 -0
  16. package/.pi/extensions/harness-ask-user.ts +74 -0
  17. package/.pi/extensions/harness-subagents.ts +9 -0
  18. package/.pi/extensions/lib/ask-user/dialog.ts +260 -0
  19. package/.pi/extensions/lib/ask-user/fallback.ts +78 -0
  20. package/.pi/extensions/lib/ask-user/render.ts +66 -0
  21. package/.pi/extensions/lib/ask-user/schema.ts +69 -0
  22. package/.pi/extensions/lib/ask-user/types.ts +41 -0
  23. package/.pi/extensions/lib/ask-user/validate-core.mjs +79 -0
  24. package/.pi/extensions/lib/ask-user/validate.ts +92 -0
  25. package/.pi/extensions/lib/harness-subagents/agent-loader.ts +126 -0
  26. package/.pi/extensions/lib/harness-subagents/agent-manifest.ts +119 -0
  27. package/.pi/extensions/lib/harness-subagents/agent-parser.ts +87 -0
  28. package/.pi/extensions/lib/harness-subagents/blackboard-tool.ts +118 -0
  29. package/.pi/extensions/lib/harness-subagents/blackboard.ts +175 -0
  30. package/.pi/extensions/lib/harness-subagents/spawn-policy.ts +27 -0
  31. package/.pi/extensions/lib/harness-subagents/types-blackboard.ts +27 -0
  32. package/.pi/extensions/lib/harness-subagents/vendored/agent-manager.ts +553 -0
  33. package/.pi/extensions/lib/harness-subagents/vendored/agent-runner.ts +637 -0
  34. package/.pi/extensions/lib/harness-subagents/vendored/agent-types.ts +175 -0
  35. package/.pi/extensions/lib/harness-subagents/vendored/context.ts +59 -0
  36. package/.pi/extensions/lib/harness-subagents/vendored/cross-extension-rpc.ts +134 -0
  37. package/.pi/extensions/lib/harness-subagents/vendored/custom-agents.ts +5 -0
  38. package/.pi/extensions/lib/harness-subagents/vendored/default-agents.ts +123 -0
  39. package/.pi/extensions/lib/harness-subagents/vendored/env.ts +43 -0
  40. package/.pi/extensions/lib/harness-subagents/vendored/group-join.ts +144 -0
  41. package/.pi/extensions/lib/harness-subagents/vendored/index.ts +2447 -0
  42. package/.pi/extensions/lib/harness-subagents/vendored/invocation-config.ts +52 -0
  43. package/.pi/extensions/lib/harness-subagents/vendored/memory.ts +182 -0
  44. package/.pi/extensions/lib/harness-subagents/vendored/model-resolver.ts +92 -0
  45. package/.pi/extensions/lib/harness-subagents/vendored/output-file.ts +115 -0
  46. package/.pi/extensions/lib/harness-subagents/vendored/prompts.ts +103 -0
  47. package/.pi/extensions/lib/harness-subagents/vendored/schedule-store.ts +177 -0
  48. package/.pi/extensions/lib/harness-subagents/vendored/schedule.ts +416 -0
  49. package/.pi/extensions/lib/harness-subagents/vendored/settings.ts +210 -0
  50. package/.pi/extensions/lib/harness-subagents/vendored/skill-loader.ts +108 -0
  51. package/.pi/extensions/lib/harness-subagents/vendored/types.ts +187 -0
  52. package/.pi/extensions/lib/harness-subagents/vendored/ui/agent-widget.ts +637 -0
  53. package/.pi/extensions/lib/harness-subagents/vendored/ui/conversation-viewer.ts +324 -0
  54. package/.pi/extensions/lib/harness-subagents/vendored/ui/schedule-menu.ts +110 -0
  55. package/.pi/extensions/lib/harness-subagents/vendored/usage.ts +71 -0
  56. package/.pi/extensions/lib/harness-subagents/vendored/worktree.ts +195 -0
  57. package/.pi/extensions/lib/harness-vcc-settings.ts +50 -0
  58. package/.pi/extensions/ultimate-pi-vcc.ts +17 -0
  59. package/.pi/harness/README.md +2 -1
  60. package/.pi/harness/agents.manifest.json +80 -0
  61. package/.pi/harness/docs/adrs/0009-sentrux-rules-lifecycle.md +9 -5
  62. package/.pi/harness/docs/adrs/0030-inhouse-vcc-compaction.md +40 -0
  63. package/.pi/harness/docs/adrs/README.md +1 -0
  64. package/.pi/harness/env.harness.template +28 -0
  65. package/.pi/harness/sentrux/architecture.manifest.json +6 -1
  66. package/.pi/prompts/harness-auto.md +2 -2
  67. package/.pi/prompts/harness-plan.md +2 -2
  68. package/.pi/prompts/harness-router-tune.md +2 -2
  69. package/.pi/prompts/harness-run.md +1 -0
  70. package/.pi/prompts/harness-setup.md +179 -340
  71. package/.pi/scripts/README.md +6 -1
  72. package/.pi/scripts/harness-agents-manifest.mjs +123 -0
  73. package/.pi/scripts/harness-cli-verify.sh +60 -11
  74. package/.pi/scripts/harness-generate-model-router.mjs +242 -0
  75. package/.pi/scripts/harness-graphify-bootstrap.sh +1 -6
  76. package/.pi/scripts/harness-resolve-up-pkg.mjs +71 -0
  77. package/.pi/scripts/harness-seed-project-contracts.mjs +33 -1
  78. package/.pi/scripts/harness-sentrux-bootstrap.mjs +146 -0
  79. package/.pi/scripts/harness-sync-env.mjs +148 -0
  80. package/.pi/scripts/harness-verify.mjs +19 -0
  81. package/.pi/scripts/harness-web-search.md +33 -0
  82. package/.pi/scripts/harness-web.py +177 -0
  83. package/.pi/scripts/harness_web/__init__.py +1 -0
  84. package/.pi/scripts/harness_web/config.py +80 -0
  85. package/.pi/scripts/harness_web/output.py +55 -0
  86. package/.pi/scripts/harness_web/scrape.py +120 -0
  87. package/.pi/scripts/harness_web/search_ddg.py +106 -0
  88. package/.pi/scripts/release.sh +338 -0
  89. package/.pi/scripts/sentrux-rules-sync.mjs +29 -7
  90. package/.pi/scripts/vendor-pi-vcc-settings.stub.ts +8 -0
  91. package/.pi/scripts/vendor-sync-pi-vcc.sh +40 -0
  92. package/.pi/settings.example.json +1 -7
  93. package/.sentrux/rules.toml +1 -1
  94. package/AGENTS.md +1 -1
  95. package/CHANGELOG.md +14 -0
  96. package/THIRD_PARTY_NOTICES.md +8 -0
  97. package/package.json +16 -12
  98. package/vendor/pi-vcc/README.md +215 -0
  99. package/vendor/pi-vcc/UPSTREAM_PIN.md +12 -0
  100. package/vendor/pi-vcc/demo.gif +0 -0
  101. package/vendor/pi-vcc/index.ts +12 -0
  102. package/vendor/pi-vcc/package.json +26 -0
  103. package/vendor/pi-vcc/scripts/audit-sessions.ts +88 -0
  104. package/vendor/pi-vcc/scripts/benchmark-real-sessions.ts +25 -0
  105. package/vendor/pi-vcc/scripts/compare-before-after.ts +36 -0
  106. package/vendor/pi-vcc/scripts/dump-branch-output.ts +20 -0
  107. package/vendor/pi-vcc/src/commands/pi-vcc.ts +36 -0
  108. package/vendor/pi-vcc/src/commands/vcc-recall.ts +65 -0
  109. package/vendor/pi-vcc/src/core/brief.ts +381 -0
  110. package/vendor/pi-vcc/src/core/build-sections.ts +79 -0
  111. package/vendor/pi-vcc/src/core/content.ts +60 -0
  112. package/vendor/pi-vcc/src/core/filter-noise.ts +42 -0
  113. package/vendor/pi-vcc/src/core/format-recall.ts +27 -0
  114. package/vendor/pi-vcc/src/core/format.ts +49 -0
  115. package/vendor/pi-vcc/src/core/lineage.ts +26 -0
  116. package/vendor/pi-vcc/src/core/load-messages.ts +41 -0
  117. package/vendor/pi-vcc/src/core/normalize.ts +66 -0
  118. package/vendor/pi-vcc/src/core/recall-scope.ts +14 -0
  119. package/vendor/pi-vcc/src/core/render-entries.ts +55 -0
  120. package/vendor/pi-vcc/src/core/report.ts +237 -0
  121. package/vendor/pi-vcc/src/core/sanitize.ts +5 -0
  122. package/vendor/pi-vcc/src/core/search-entries.ts +221 -0
  123. package/vendor/pi-vcc/src/core/settings.ts +8 -0
  124. package/vendor/pi-vcc/src/core/skill-collapse.ts +35 -0
  125. package/vendor/pi-vcc/src/core/summarize.ts +157 -0
  126. package/vendor/pi-vcc/src/core/tool-args.ts +14 -0
  127. package/vendor/pi-vcc/src/details.ts +7 -0
  128. package/vendor/pi-vcc/src/extract/commits.ts +69 -0
  129. package/vendor/pi-vcc/src/extract/files.ts +80 -0
  130. package/vendor/pi-vcc/src/extract/goals.ts +79 -0
  131. package/vendor/pi-vcc/src/extract/preferences.ts +55 -0
  132. package/vendor/pi-vcc/src/hooks/before-compact.ts +314 -0
  133. package/vendor/pi-vcc/src/sections.ts +12 -0
  134. package/vendor/pi-vcc/src/tools/recall.ts +109 -0
  135. package/vendor/pi-vcc/src/types.ts +14 -0
  136. package/vendor/pi-vcc/tests/before-compact-hook.test.ts +204 -0
  137. package/vendor/pi-vcc/tests/before-compact.test.ts +145 -0
  138. package/vendor/pi-vcc/tests/brief.test.ts +206 -0
  139. package/vendor/pi-vcc/tests/build-sections.test.ts +59 -0
  140. package/vendor/pi-vcc/tests/compile.test.ts +80 -0
  141. package/vendor/pi-vcc/tests/content.test.ts +31 -0
  142. package/vendor/pi-vcc/tests/extract-goals.test.ts +86 -0
  143. package/vendor/pi-vcc/tests/extract-preferences.test.ts +30 -0
  144. package/vendor/pi-vcc/tests/filter-noise.test.ts +61 -0
  145. package/vendor/pi-vcc/tests/fixtures.ts +61 -0
  146. package/vendor/pi-vcc/tests/format-recall.test.ts +30 -0
  147. package/vendor/pi-vcc/tests/format.test.ts +62 -0
  148. package/vendor/pi-vcc/tests/lineage.test.ts +33 -0
  149. package/vendor/pi-vcc/tests/load-messages.test.ts +51 -0
  150. package/vendor/pi-vcc/tests/normalize.test.ts +97 -0
  151. package/vendor/pi-vcc/tests/real-sessions.test.ts +38 -0
  152. package/vendor/pi-vcc/tests/recall-expand.test.ts +15 -0
  153. package/vendor/pi-vcc/tests/recall-scope.test.ts +32 -0
  154. package/vendor/pi-vcc/tests/recall-tool-scope.test.ts +67 -0
  155. package/vendor/pi-vcc/tests/render-entries.test.ts +62 -0
  156. package/vendor/pi-vcc/tests/report.test.ts +44 -0
  157. package/vendor/pi-vcc/tests/sanitize.test.ts +24 -0
  158. package/vendor/pi-vcc/tests/search-entries.test.ts +144 -0
  159. package/vendor/pi-vcc/tests/support/load-session.ts +23 -0
  160. package/vendor/pi-vcc/tests/support/real-sessions.ts +51 -0
  161. package/.agents/skills/firecrawl/SKILL.md +0 -150
  162. package/.agents/skills/firecrawl/rules/install.md +0 -82
  163. package/.agents/skills/firecrawl/rules/security.md +0 -26
  164. package/.agents/skills/firecrawl-agent/SKILL.md +0 -57
  165. package/.agents/skills/firecrawl-build-interact/SKILL.md +0 -67
  166. package/.agents/skills/firecrawl-build-onboarding/SKILL.md +0 -102
  167. package/.agents/skills/firecrawl-build-onboarding/references/auth-flow.md +0 -39
  168. package/.agents/skills/firecrawl-build-onboarding/references/project-setup.md +0 -20
  169. package/.agents/skills/firecrawl-build-onboarding/references/sdk-installation.md +0 -17
  170. package/.agents/skills/firecrawl-build-scrape/SKILL.md +0 -68
  171. package/.agents/skills/firecrawl-build-search/SKILL.md +0 -68
  172. package/.agents/skills/firecrawl-crawl/SKILL.md +0 -58
  173. package/.agents/skills/firecrawl-download/SKILL.md +0 -69
  174. package/.agents/skills/firecrawl-interact/SKILL.md +0 -83
  175. package/.agents/skills/firecrawl-map/SKILL.md +0 -50
  176. package/.agents/skills/firecrawl-parse/SKILL.md +0 -61
  177. package/.agents/skills/firecrawl-scrape/SKILL.md +0 -68
  178. package/.agents/skills/firecrawl-search/SKILL.md +0 -59
  179. package/.pi/pi-vcc-config.json +0 -4
  180. package/firecrawl/.env.template +0 -62
  181. package/firecrawl/README.md +0 -49
  182. package/firecrawl/docker-compose.yaml +0 -201
  183. package/firecrawl/searxng/searxng.env +0 -3
  184. package/firecrawl/searxng/settings.yml +0 -85
@@ -0,0 +1,49 @@
1
+ import type { SectionData } from "../sections";
2
+
3
+ const section = (title: string, items: string[]): string => {
4
+ if (items.length === 0) return "";
5
+ const body = items.map((i) => `- ${i}`).join("\n");
6
+ return `[${title}]\n${body}`;
7
+ };
8
+
9
+ const BRIEF_MAX_LINES = 120;
10
+
11
+ export const capBrief = (text: string): string => {
12
+ const lines = text.split("\n");
13
+ if (lines.length <= BRIEF_MAX_LINES) return text;
14
+ const omitted = lines.length - BRIEF_MAX_LINES;
15
+ const kept = lines.slice(-BRIEF_MAX_LINES);
16
+ // Find first section header to avoid cutting mid-section
17
+ const firstHeader = kept.findIndex((l) => /^\[.+\]/.test(l));
18
+ const clean = firstHeader > 0 ? kept.slice(firstHeader) : kept;
19
+ return `...(${omitted} earlier lines omitted)\n\n${clean.join("\n")}`;
20
+ };
21
+
22
+ export const RECALL_NOTE =
23
+ "Use `vcc_recall` to search for prior work, decisions, and context from before this summary. " +
24
+ "Do not redo work already completed.";
25
+
26
+ export const formatSummary = (data: SectionData): string => {
27
+ const headerParts = [
28
+ section("Session Goal", data.sessionGoal),
29
+ section("Files And Changes", data.filesAndChanges),
30
+ section("Commits", data.commits),
31
+ section("Outstanding Context", data.outstandingContext),
32
+ section("User Preferences", data.userPreferences),
33
+ ].filter(Boolean);
34
+
35
+ const parts: string[] = [];
36
+ if (headerParts.length > 0) {
37
+ parts.push(headerParts.join("\n\n"));
38
+ }
39
+ if (data.briefTranscript) {
40
+ parts.push(capBrief(data.briefTranscript));
41
+ }
42
+
43
+ if (parts.length === 0) return "";
44
+
45
+ // NOTE: RECALL_NOTE is intentionally NOT appended here.
46
+ // It is appended once by `compile()` at the very end, after merge-with-previous,
47
+ // to avoid the note compounding inside the brief transcript across compactions.
48
+ return parts.join("\n\n---\n\n");
49
+ };
@@ -0,0 +1,26 @@
1
+ export interface LineageEntryLike {
2
+ id?: string;
3
+ }
4
+
5
+ export interface LineageSessionManagerLike {
6
+ getBranch: () => LineageEntryLike[];
7
+ getEntries?: () => LineageEntryLike[];
8
+ }
9
+
10
+ export const getActiveLineageEntryIds = (sessionManager: LineageSessionManagerLike): Set<string> => {
11
+ try {
12
+ const branch = sessionManager.getBranch() ?? [];
13
+ if (branch.length > 0) {
14
+ return new Set(branch.map((e) => e.id).filter((id): id is string => Boolean(id)));
15
+ }
16
+ } catch {
17
+ // fall through to defensive fallback
18
+ }
19
+
20
+ try {
21
+ const all = sessionManager.getEntries?.() ?? [];
22
+ return new Set(all.map((e) => e.id).filter((id): id is string => Boolean(id)));
23
+ } catch {
24
+ return new Set();
25
+ }
26
+ };
@@ -0,0 +1,41 @@
1
+ import { readFileSync } from "fs";
2
+ import type { Message } from "@mariozechner/pi-ai";
3
+ import { renderMessage, type RenderedEntry } from "./render-entries";
4
+
5
+ export interface LoadedMessages {
6
+ rendered: RenderedEntry[];
7
+ rawMessages: Message[];
8
+ entryIds: string[];
9
+ }
10
+
11
+ export const loadAllMessages = (
12
+ sessionFile: string,
13
+ full: boolean,
14
+ allowedEntryIds?: Set<string>,
15
+ ): LoadedMessages => {
16
+ const content = readFileSync(sessionFile, "utf-8");
17
+ const entries: any[] = [];
18
+ for (const line of content.split("\n")) {
19
+ if (!line.trim()) continue;
20
+ try { entries.push(JSON.parse(line)); } catch {}
21
+ }
22
+ const rendered: RenderedEntry[] = [];
23
+ const rawMessages: Message[] = [];
24
+ const entryIds: string[] = [];
25
+
26
+ let messageIndex = 0;
27
+ for (const e of entries) {
28
+ const isMessage = e.type === "message" && e.message;
29
+ if (!isMessage) continue;
30
+
31
+ const allowed = !allowedEntryIds || allowedEntryIds.has(e.id);
32
+ if (allowed) {
33
+ rendered.push(renderMessage(e.message, messageIndex, full));
34
+ rawMessages.push(e.message);
35
+ entryIds.push(String(e.id));
36
+ }
37
+ messageIndex++;
38
+ }
39
+
40
+ return { rendered, rawMessages, entryIds };
41
+ };
@@ -0,0 +1,66 @@
1
+ import type { Message } from "@mariozechner/pi-ai";
2
+ import type { NormalizedBlock } from "../types";
3
+ import { textOf } from "./content";
4
+ import { sanitize } from "./sanitize";
5
+
6
+ const normalizeOne = (msg: Message, msgIndex: number): NormalizedBlock[] => {
7
+ if (msg.role === "user") {
8
+ const blocks: NormalizedBlock[] = [];
9
+ const text = sanitize(textOf(msg.content));
10
+ if (text) blocks.push({ kind: "user", text, sourceIndex: msgIndex });
11
+ if (msg.content && typeof msg.content !== "string") {
12
+ for (const part of msg.content) {
13
+ if (part.type === "image") {
14
+ blocks.push({ kind: "user", text: `[image: ${part.mimeType}]`, sourceIndex: msgIndex });
15
+ }
16
+ }
17
+ }
18
+ return blocks.length > 0 ? blocks : [{ kind: "user", text: "", sourceIndex: msgIndex }];
19
+ }
20
+
21
+ if (msg.role === "toolResult") {
22
+ return [{
23
+ kind: "tool_result",
24
+ name: msg.toolName,
25
+ text: sanitize(textOf(msg.content)),
26
+ isError: msg.isError,
27
+ sourceIndex: msgIndex,
28
+ }];
29
+ }
30
+
31
+ if (msg.role === "assistant") {
32
+ if (!msg.content) return [];
33
+ if (typeof msg.content === "string") {
34
+ return [{ kind: "assistant", text: sanitize(msg.content), sourceIndex: msgIndex }];
35
+ }
36
+
37
+ const blocks: NormalizedBlock[] = [];
38
+ for (const part of msg.content) {
39
+ if (part.type === "text") {
40
+ blocks.push({ kind: "assistant", text: sanitize(part.text), sourceIndex: msgIndex });
41
+ } else if (part.type === "thinking") {
42
+ blocks.push({
43
+ kind: "thinking",
44
+ text: sanitize(part.thinking),
45
+ redacted: part.redacted ?? false,
46
+ sourceIndex: msgIndex,
47
+ });
48
+ } else if (part.type === "toolCall") {
49
+ blocks.push({
50
+ kind: "tool_call",
51
+ name: part.name,
52
+ args: part.arguments,
53
+ sourceIndex: msgIndex,
54
+ });
55
+ }
56
+ }
57
+ return blocks;
58
+ }
59
+
60
+ return [];
61
+ };
62
+
63
+ export const normalize = (messages: Message[]): NormalizedBlock[] =>
64
+ messages.flatMap((msg, i) => normalizeOne(msg, i));
65
+
66
+
@@ -0,0 +1,14 @@
1
+ export type RecallScope = "lineage" | "all";
2
+
3
+ const SCOPE_RE = /\bscope:(lineage|all)\b/i;
4
+
5
+ export const normalizeRecallScope = (scope?: unknown): RecallScope =>
6
+ typeof scope === "string" && scope.toLowerCase() === "all" ? "all" : "lineage";
7
+
8
+ export const parseRecallScope = (text: string): { scope: RecallScope; text: string } => {
9
+ const match = text.match(SCOPE_RE);
10
+ return {
11
+ scope: normalizeRecallScope(match?.[1]),
12
+ text: text.replace(SCOPE_RE, "").replace(/\s+/g, " ").trim(),
13
+ };
14
+ };
@@ -0,0 +1,55 @@
1
+ import type { Message } from "@mariozechner/pi-ai";
2
+ import { clip, textOf } from "./content";
3
+ import { summarizeToolArgs } from "./tool-args";
4
+ import { extractPath } from "./tool-args";
5
+
6
+ export interface RenderedEntry {
7
+ index: number;
8
+ role: string;
9
+ summary: string;
10
+ files?: string[];
11
+ }
12
+
13
+ const toolCalls = (content: Message["content"]): string => {
14
+ if (!content || typeof content === "string") return "";
15
+ return content
16
+ .filter((c) => c.type === "toolCall")
17
+ .map((c) => `${c.name}(${summarizeToolArgs(c.arguments)})`)
18
+ .join(", ");
19
+ };
20
+
21
+ const extractFilesFromContent = (content: Message["content"]): string[] => {
22
+ if (!content || typeof content === "string") return [];
23
+ return content
24
+ .filter((c) => c.type === "toolCall")
25
+ .map((c) => extractPath(c.arguments))
26
+ .filter((p): p is string => p !== null);
27
+ };
28
+
29
+ export const renderMessage = (msg: Message, index: number, full = false): RenderedEntry => {
30
+ if (msg.role === "user") {
31
+ return { index, role: "user", summary: full ? textOf(msg.content) : clip(textOf(msg.content), 300) };
32
+ }
33
+ if (msg.role === "toolResult") {
34
+ const prefix = msg.isError ? "ERROR " : "";
35
+ const text = full ? textOf(msg.content) : clip(textOf(msg.content), 200);
36
+ return {
37
+ index, role: "tool_result",
38
+ summary: `${prefix}[${msg.toolName}] ${text}`,
39
+ };
40
+ }
41
+ // bashExecution has command+output instead of content
42
+ if ((msg as any).role === "bashExecution") {
43
+ const cmd = (msg as any).command ?? "";
44
+ const out = (msg as any).output ?? "";
45
+ const text = full ? `$ ${cmd}\n${out}` : clip(`$ ${cmd}\n${out}`, 300);
46
+ return { index, role: "bash", summary: text };
47
+ }
48
+ const text = full ? textOf(msg.content) : clip(textOf(msg.content), 300);
49
+ const tools = toolCalls(msg.content);
50
+ const files = extractFilesFromContent(msg.content);
51
+ const summary = tools ? `${tools}\n${text}` : text;
52
+ return { index, role: "assistant", summary, ...(files.length > 0 && { files }) };
53
+ };
54
+
55
+
@@ -0,0 +1,237 @@
1
+ import type { Message } from "@mariozechner/pi-ai";
2
+ import { buildSections } from "./build-sections";
3
+ import { clip } from "./content";
4
+ import { normalize } from "./normalize";
5
+ import { renderMessage } from "./render-entries";
6
+ import { searchEntries } from "./search-entries";
7
+ import { type CompileInput, compile } from "./summarize";
8
+
9
+ const SECTION_HEADERS = ["Session Goal", "Files And Changes", "Commits", "Outstanding Context"];
10
+
11
+ interface RoleCounts {
12
+ user: number;
13
+ assistant: number;
14
+ toolResult: number;
15
+ }
16
+
17
+ interface BlockCounts {
18
+ user: number;
19
+ assistant: number;
20
+ toolCalls: number;
21
+ toolResults: number;
22
+ thinking: number;
23
+ }
24
+
25
+ export interface RecallProbe {
26
+ label: string;
27
+ sourceText: string;
28
+ query: string;
29
+ summaryMentioned: boolean;
30
+ recallHits: number;
31
+ }
32
+
33
+ export interface CompactReport {
34
+ summary: string;
35
+ before: {
36
+ messageCount: number;
37
+ roleCounts: RoleCounts;
38
+ blockCounts: BlockCounts;
39
+ inputChars: number;
40
+ estimatedTokens: number;
41
+ topFiles: string[];
42
+ preview: string;
43
+ };
44
+ after: {
45
+ summaryLength: number;
46
+ estimatedTokens: number;
47
+ sectionCount: number;
48
+ summaryPreview: string;
49
+ goalsCount: number;
50
+ blockersCount: number;
51
+ briefTranscriptLines: number;
52
+ };
53
+ compression: {
54
+ charsBefore: number;
55
+ charsAfter: number;
56
+ ratio: number;
57
+ messagesBefore: number;
58
+ };
59
+ recall: {
60
+ probes: RecallProbe[];
61
+ };
62
+ }
63
+
64
+ const estimateTokensFromChars = (chars: number): number =>
65
+ Math.ceil(chars / 4);
66
+
67
+ const countRoles = (messages: Message[]): RoleCounts => {
68
+ const counts: RoleCounts = { user: 0, assistant: 0, toolResult: 0 };
69
+ for (const msg of messages) {
70
+ if (msg.role === "user") counts.user += 1;
71
+ else if (msg.role === "assistant") counts.assistant += 1;
72
+ else if (msg.role === "toolResult") counts.toolResult += 1;
73
+ }
74
+ return counts;
75
+ };
76
+
77
+ const countBlocks = (messages: Message[]): BlockCounts => {
78
+ const counts: BlockCounts = {
79
+ user: 0,
80
+ assistant: 0,
81
+ toolCalls: 0,
82
+ toolResults: 0,
83
+ thinking: 0,
84
+ };
85
+
86
+ for (const block of normalize(messages)) {
87
+ if (block.kind === "user") counts.user += 1;
88
+ else if (block.kind === "assistant") counts.assistant += 1;
89
+ else if (block.kind === "tool_call") counts.toolCalls += 1;
90
+ else if (block.kind === "tool_result") counts.toolResults += 1;
91
+ else if (block.kind === "thinking") counts.thinking += 1;
92
+ }
93
+
94
+ return counts;
95
+ };
96
+
97
+ const inputCharsOf = (messages: Message[]): number =>
98
+ messages
99
+ .map((msg, index) => renderMessage(msg, index, true).summary.length)
100
+ .reduce((sum, len) => sum + len, 0);
101
+
102
+ const topFilesOf = (messages: Message[]): string[] => {
103
+ const files = new Set<string>();
104
+ for (const block of normalize(messages)) {
105
+ if (block.kind === "tool_call") {
106
+ for (const key of ["path", "file_path", "filePath", "file"]) {
107
+ const val = block.args[key];
108
+ if (typeof val === "string") { files.add(val); break; }
109
+ }
110
+ }
111
+ }
112
+ return [...files].slice(0, 10);
113
+ };
114
+
115
+ const previewOf = (messages: Message[], edgeCount = 3): string => {
116
+ const rendered = messages.map((msg, index) => renderMessage(msg, index));
117
+ if (rendered.length === 0) return "(empty)";
118
+ if (rendered.length <= edgeCount * 2) {
119
+ return rendered
120
+ .map((entry) => `#${entry.index} [${entry.role}] ${clip(entry.summary, 220)}`)
121
+ .join("\n");
122
+ }
123
+
124
+ const first = rendered.slice(0, edgeCount);
125
+ const last = rendered.slice(-edgeCount);
126
+ return [
127
+ ...first.map((entry) => `#${entry.index} [${entry.role}] ${clip(entry.summary, 220)}`),
128
+ "...",
129
+ ...last.map((entry) => `#${entry.index} [${entry.role}] ${clip(entry.summary, 220)}`),
130
+ ].join("\n");
131
+ };
132
+
133
+ const sectionCountOf = (summary: string): number =>
134
+ SECTION_HEADERS.filter((header) => summary.includes(`[${header}]`)).length;
135
+
136
+ const briefLineCountOf = (summary: string): number => {
137
+ const sep = "\n\n---\n\n";
138
+ const idx = summary.indexOf(sep);
139
+ if (idx < 0) return 0;
140
+ return summary.slice(idx + sep.length).split("\n").length;
141
+ };
142
+
143
+ const queryTermsOf = (text: string): string[] =>
144
+ (text.match(/[\p{L}\p{N}_./-]{3,}/gu) ?? [])
145
+ .map((part) => part.trim())
146
+ .filter(Boolean);
147
+
148
+ const queryOf = (text: string): string => {
149
+ const terms = queryTermsOf(text);
150
+ return terms.slice(0, 6).join(" ");
151
+ };
152
+
153
+ const matchesQuery = (text: string, query: string): boolean => {
154
+ const hay = text.toLowerCase();
155
+ return query
156
+ .toLowerCase()
157
+ .split(/\s+/)
158
+ .filter(Boolean)
159
+ .every((term) => hay.includes(term));
160
+ };
161
+
162
+ const probesOf = (messages: Message[], summary: string): RecallProbe[] => {
163
+ const blocks = normalize(messages);
164
+ const data = buildSections({ blocks });
165
+
166
+ // Find first file from tool calls
167
+ let firstFile = "";
168
+ for (const b of blocks) {
169
+ if (b.kind === "tool_call") {
170
+ for (const key of ["path", "file_path", "filePath", "file"]) {
171
+ if (typeof b.args[key] === "string") { firstFile = b.args[key] as string; break; }
172
+ }
173
+ if (firstFile) break;
174
+ }
175
+ }
176
+
177
+ const rawProbes = [
178
+ { label: "goal", text: data.sessionGoal[0] ?? "" },
179
+ { label: "file", text: firstFile },
180
+ { label: "problem", text: data.outstandingContext[0] ?? "" },
181
+ ];
182
+
183
+ const rendered = messages.map((msg, index) => renderMessage(msg, index));
184
+
185
+ return rawProbes
186
+ .map(({ label, text }) => {
187
+ const sourceText = text.trim();
188
+ const query = queryOf(sourceText);
189
+ if (!query) return null;
190
+ return {
191
+ label,
192
+ sourceText,
193
+ query,
194
+ summaryMentioned: matchesQuery(summary, query),
195
+ recallHits: searchEntries(rendered, query).length,
196
+ };
197
+ })
198
+ .filter((probe): probe is RecallProbe => probe !== null);
199
+ };
200
+
201
+ export const buildCompactReport = (input: CompileInput): CompactReport => {
202
+ const summary = compile(input);
203
+ const data = buildSections({ blocks: normalize(input.messages) });
204
+ const inputChars = inputCharsOf(input.messages);
205
+ const topFiles = topFilesOf(input.messages);
206
+
207
+ return {
208
+ summary,
209
+ before: {
210
+ messageCount: input.messages.length,
211
+ roleCounts: countRoles(input.messages),
212
+ blockCounts: countBlocks(input.messages),
213
+ inputChars,
214
+ estimatedTokens: estimateTokensFromChars(inputChars),
215
+ topFiles,
216
+ preview: previewOf(input.messages),
217
+ },
218
+ after: {
219
+ summaryLength: summary.length,
220
+ estimatedTokens: estimateTokensFromChars(summary.length),
221
+ sectionCount: sectionCountOf(summary),
222
+ summaryPreview: summary,
223
+ goalsCount: data.sessionGoal.length,
224
+ blockersCount: data.outstandingContext.length,
225
+ briefTranscriptLines: briefLineCountOf(summary),
226
+ },
227
+ compression: {
228
+ charsBefore: inputChars,
229
+ charsAfter: summary.length,
230
+ ratio: summary.length === 0 ? 0 : Number((inputChars / summary.length).toFixed(2)),
231
+ messagesBefore: input.messages.length,
232
+ },
233
+ recall: {
234
+ probes: probesOf(input.messages, summary),
235
+ },
236
+ };
237
+ };
@@ -0,0 +1,5 @@
1
+ const ANSI_RE = /\x1b\[[0-9;]*[A-Za-z]/g;
2
+ const CTRL_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f]/g;
3
+
4
+ export const sanitize = (text: string): string =>
5
+ text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(ANSI_RE, "").replace(CTRL_RE, "");