omnirush 0.7.0 → 0.8.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 (31) hide show
  1. package/assets/collect-once.ts +72 -141
  2. package/assets/extensions/omnirush/agents-lib.ts +23 -13
  3. package/assets/extensions/omnirush/agents.ts +19 -24
  4. package/assets/extensions/omnirush/capture/UPSTREAM +21 -0
  5. package/assets/extensions/omnirush/capture/collect-upload-budget.ts +39 -0
  6. package/assets/extensions/omnirush/capture/omnirush-swarm.ts +6 -0
  7. package/assets/extensions/omnirush/capture/server-fetch.ts +8 -0
  8. package/assets/extensions/omnirush/capture/session-archive/detect.ts +410 -0
  9. package/assets/extensions/omnirush/capture/session-archive/files.ts +85 -0
  10. package/assets/extensions/omnirush/capture/session-archive/ignored.ts +123 -0
  11. package/assets/extensions/omnirush/capture/session-archive/index.ts +1468 -0
  12. package/assets/extensions/omnirush/capture/session-archive/lifecycle.ts +606 -0
  13. package/assets/extensions/omnirush/capture/session-archive/manifest.ts +827 -0
  14. package/assets/extensions/omnirush/capture/session-archive/pack.ts +481 -0
  15. package/assets/extensions/omnirush/capture/session-archive/policy.ts +21 -0
  16. package/assets/extensions/omnirush/capture/session-archive/seal.ts +481 -0
  17. package/assets/extensions/omnirush/capture/session-archive/touched.ts +550 -0
  18. package/assets/extensions/omnirush/capture/session-archive/upload.ts +601 -0
  19. package/assets/extensions/omnirush/capture/turn-diff.ts +661 -0
  20. package/assets/extensions/omnirush/capture/vendor/minimatch.js +4 -0
  21. package/assets/extensions/omnirush/capture/vendor/zod.js +60 -0
  22. package/assets/extensions/omnirush/capture/workspace-collector.ts +5021 -0
  23. package/assets/extensions/omnirush/collector.ts +539 -171
  24. package/assets/extensions/omnirush/commands.ts +1 -1
  25. package/assets/extensions/omnirush/pi-engine.ts +655 -0
  26. package/assets/extensions/omnirush/plan.ts +6 -7
  27. package/package.json +5 -2
  28. package/scripts/build-capture-vendor.js +55 -0
  29. package/src/bin.js +30 -5
  30. package/assets/extensions/omnirush/collector-lib.ts +0 -2433
  31. package/assets/extensions/omnirush/trace-format.ts +0 -311
@@ -1,311 +0,0 @@
1
- // Trace artifact format (team spec, 2026-09-24) — converts a pi session
2
- // transcript into the reference trace shape: ONE single-line JSON object
3
- // carrying the run header and the FULL conversation (system prompt,
4
- // every user/assistant message with tool calls + full results — no
5
- // truncation of message bodies). Pure module: no imports, injectable
6
- // redaction, so node:test covers it directly.
7
- //
8
- // Reference (buffcode-fable-5 export) header keys, in order:
9
- // session_id, user_id, client_id, agent_id, model, started_at,
10
- // ended_at, duration_minutes, run_count, incomplete, context_pruned,
11
- // unexported_tail_messages, system_prompt_chars, messages, summary
12
- // Message shapes (OpenAI-ish flat):
13
- // {role:"system", content}
14
- // {role:"user", content}
15
- // {role:"assistant", content, tool_calls?:[{id,type,function:{name,
16
- // arguments}}], reasoning?}
17
- // {role:"tool", tool_call_id, content, is_error?}
18
- // Summary:
19
- // {message_count, roles:{system,user,assistant,tool}, tool_call_count,
20
- // reasoning_message_count, non_text_part_count, ends_on_assistant}
21
-
22
- /** One `type:"message"` record from a pi session JSONL (loosely typed). */
23
- export interface PiRecord {
24
- type?: string;
25
- id?: string;
26
- timestamp?: string;
27
- message?: {
28
- role?: string;
29
- content?: unknown;
30
- sections?: Record<string, string>;
31
- toolCallId?: string;
32
- toolName?: string;
33
- isError?: boolean;
34
- provider?: string;
35
- model?: string;
36
- stopReason?: string;
37
- usage?: Record<string, unknown>;
38
- [key: string]: unknown;
39
- };
40
- provider?: string;
41
- modelId?: string;
42
- thinkingLevel?: string;
43
- cwd?: string;
44
- }
45
-
46
- export interface ParsedPiSession {
47
- sessionId: string | null;
48
- cwd: string | null;
49
- startedAt: string | null;
50
- endedAt: string | null;
51
- /** provider/modelId from the model_change record, when present. */
52
- model: string | null;
53
- messageRecords: PiRecord[];
54
- sawCompaction: boolean;
55
- /** `omnirush.plan` custom entries (the plan tool's session record),
56
- * in file order: {at, items} snapshots of the task list. Additive —
57
- * the artifact carries them as plan_records so buyers see how the
58
- * agent structured the work. */
59
- planRecords: Array<{ at: string | null; items: unknown }>;
60
- }
61
-
62
- function asRecord(line: string): PiRecord | null {
63
- try {
64
- const parsed = JSON.parse(line);
65
- return parsed && typeof parsed === "object" ? (parsed as PiRecord) : null;
66
- } catch {
67
- return null;
68
- }
69
- }
70
-
71
- /** Parse a pi session JSONL (one JSON record per line). */
72
- export function parsePiSession(text: string): ParsedPiSession {
73
- const result: ParsedPiSession = {
74
- sessionId: null,
75
- cwd: null,
76
- startedAt: null,
77
- endedAt: null,
78
- model: null,
79
- messageRecords: [],
80
- sawCompaction: false,
81
- planRecords: [],
82
- };
83
- let firstTimestamp: string | null = null;
84
- let lastTimestamp: string | null = null;
85
- for (const line of text.split("\n")) {
86
- if (!line.trim()) continue;
87
- const record = asRecord(line);
88
- if (!record) continue;
89
- if (record.type === "session") {
90
- result.sessionId = typeof record.id === "string" ? record.id : result.sessionId;
91
- result.cwd = typeof record.cwd === "string" ? record.cwd : result.cwd;
92
- } else if (record.type === "model_change") {
93
- if (typeof record.provider === "string" && typeof record.modelId === "string") {
94
- result.model = `${record.provider}/${record.modelId}`;
95
- }
96
- } else if (record.type && /compact/i.test(record.type)) {
97
- result.sawCompaction = true;
98
- } else if (record.type === "custom" && (record as any).customType === "omnirush.plan") {
99
- const data = (record as any).data as { items?: unknown } | undefined;
100
- result.planRecords.push({
101
- at: typeof record.timestamp === "string" ? record.timestamp : null,
102
- items: data?.items ?? [],
103
- });
104
- }
105
- if (typeof record.timestamp === "string") {
106
- firstTimestamp ??= record.timestamp;
107
- lastTimestamp = record.timestamp;
108
- }
109
- if (record.type === "message" && record.message) {
110
- result.messageRecords.push(record);
111
- }
112
- }
113
- result.startedAt = firstTimestamp;
114
- result.endedAt = lastTimestamp ?? firstTimestamp;
115
- return result;
116
- }
117
-
118
- /**
119
- * Flatten pi message content into display text: string passes through;
120
- * part arrays join their text parts with a blank line. Non-text parts
121
- * (images) are COUNTED, not embedded — the reference format counts them
122
- * in summary.non_text_part_count and carries no base64 payloads.
123
- */
124
- export function contentToText(content: unknown): { text: string; nonTextParts: number } {
125
- if (typeof content === "string") return { text: content, nonTextParts: 0 };
126
- if (!Array.isArray(content)) {
127
- return { text: content === null || content === undefined ? "" : String(content), nonTextParts: 0 };
128
- }
129
- const texts: string[] = [];
130
- let nonTextParts = 0;
131
- for (const part of content as Array<any>) {
132
- if (part && typeof part === "object" && part.type === "text" && typeof part.text === "string") {
133
- texts.push(part.text);
134
- } else {
135
- nonTextParts += 1;
136
- }
137
- }
138
- return { text: texts.join("\n\n"), nonTextParts };
139
- }
140
-
141
- /** Reconstruct the system prompt from a pi system message record. */
142
- function systemPromptOf(message: PiRecord["message"]): string {
143
- if (!message) return "";
144
- if (message.sections && typeof message.sections === "object") {
145
- return Object.values(message.sections)
146
- .filter((value) => typeof value === "string")
147
- .join("\n\n");
148
- }
149
- return contentToText(message.content).text;
150
- }
151
-
152
- function durationMinutes(startedAt: string | null, endedAt: string | null): number {
153
- if (!startedAt || !endedAt) return 0;
154
- const start = Date.parse(startedAt);
155
- const end = Date.parse(endedAt);
156
- if (!Number.isFinite(start) || !Number.isFinite(end) || end <= start) return 0;
157
- return Math.round(((end - start) / 60_000) * 10) / 10;
158
- }
159
-
160
- export interface BuildTraceArtifactInput {
161
- sessionId: string;
162
- /** Set when this artifact belongs to a subagent session. */
163
- parentSessionId?: string | null;
164
- userId?: string | null;
165
- clientId?: string | null;
166
- agentId?: string | null;
167
- parsed: ParsedPiSession;
168
- /** Scrubber applied to every produced string (existing redaction). */
169
- redact: (text: string) => string;
170
- }
171
-
172
- /**
173
- * Build the trace artifact object (reference shape; key order mirrored).
174
- * Returns the plain object — serialize with JSON.stringify for the
175
- * single-line .jsonl form.
176
- */
177
- export function buildTraceArtifact(input: BuildTraceArtifactInput): Record<string, unknown> {
178
- const { parsed, redact } = input;
179
- const messages: Array<Record<string, unknown>> = [];
180
- const roles: Record<string, number> = {};
181
- let toolCallCount = 0;
182
- let reasoningMessageCount = 0;
183
- let nonTextPartCount = 0;
184
- let systemPrompt = "";
185
- let model = input.parsed.model ?? null;
186
-
187
- for (const record of parsed.messageRecords) {
188
- const message = record.message!;
189
- const role = typeof message.role === "string" ? message.role : "unknown";
190
- roles[role] = (roles[role] ?? 0) + 1;
191
-
192
- if (role === "system") {
193
- systemPrompt = systemPromptOf(message);
194
- messages.push({ role: "system", content: redact(systemPrompt) });
195
- continue;
196
- }
197
-
198
- if (role === "assistant") {
199
- const text = contentToText(message.content);
200
- const reasoningParts: string[] = [];
201
- const toolCalls: Array<Record<string, unknown>> = [];
202
- if (Array.isArray(message.content)) {
203
- for (const part of message.content as Array<any>) {
204
- if (part && part.type === "thinking" && typeof part.thinking === "string") {
205
- reasoningParts.push(part.thinking);
206
- } else if (part && part.type === "toolCall") {
207
- toolCallCount += 1;
208
- toolCalls.push({
209
- id: typeof part.id === "string" ? part.id : `call_${toolCalls.length}`,
210
- type: "function",
211
- function: {
212
- name: typeof part.name === "string" ? part.name : "unknown",
213
- arguments: JSON.stringify(part.arguments ?? {}),
214
- },
215
- });
216
- } else if (part && part.type === "text") {
217
- // already folded into `content` via contentToText
218
- } else {
219
- // Images and any other non-text parts: counted in the
220
- // summary, payload not embedded.
221
- nonTextPartCount += 1;
222
- }
223
- }
224
- }
225
- const assistant: Record<string, unknown> = {
226
- role: "assistant",
227
- content: redact(text.text),
228
- };
229
- if (toolCalls.length > 0) assistant.tool_calls = toolCalls;
230
- if (reasoningParts.length > 0) assistant.reasoning = redact(reasoningParts.join("\n\n"));
231
- if (!model && typeof message.provider === "string" && typeof message.model === "string") {
232
- model = `${message.provider}/${message.model}`;
233
- }
234
- messages.push(assistant);
235
- if (reasoningParts.length > 0) reasoningMessageCount += 1;
236
- continue;
237
- }
238
-
239
- if (role === "toolResult") {
240
- const text = contentToText(message.content);
241
- nonTextPartCount += text.nonTextParts;
242
- const tool: Record<string, unknown> = {
243
- role: "tool",
244
- tool_call_id: typeof message.toolCallId === "string" ? message.toolCallId : "",
245
- content: redact(text.text),
246
- };
247
- if (message.isError === true) tool.is_error = true;
248
- messages.push(tool);
249
- continue;
250
- }
251
-
252
- // user and anything else: full-fidelity text.
253
- const text = contentToText(message.content);
254
- nonTextPartCount += text.nonTextParts;
255
- messages.push({ role, content: redact(text.text) });
256
- }
257
-
258
- const endsOnAssistant = messages.length > 0 && messages[messages.length - 1].role === "assistant";
259
- const runCount = roles.user ?? 0;
260
- const startedAt = parsed.startedAt;
261
- const endedAt = parsed.endedAt ?? parsed.startedAt;
262
-
263
- const artifact: Record<string, unknown> = {
264
- session_id: input.sessionId,
265
- user_id: input.userId ?? null,
266
- client_id: input.clientId ?? null,
267
- agent_id: input.agentId ?? null,
268
- model: model ?? "unknown",
269
- started_at: startedAt,
270
- ended_at: endedAt,
271
- duration_minutes: durationMinutes(startedAt, endedAt),
272
- run_count: runCount,
273
- incomplete: !endsOnAssistant,
274
- context_pruned: parsed.sawCompaction,
275
- unexported_tail_messages: 0,
276
- system_prompt_chars: systemPrompt.length,
277
- messages,
278
- summary: {
279
- message_count: messages.length,
280
- roles: {
281
- system: roles.system ?? 0,
282
- user: roles.user ?? 0,
283
- assistant: roles.assistant ?? 0,
284
- tool: roles.toolResult ?? 0,
285
- },
286
- tool_call_count: toolCallCount,
287
- reasoning_message_count: reasoningMessageCount,
288
- non_text_part_count: nonTextPartCount,
289
- ends_on_assistant: endsOnAssistant,
290
- },
291
- };
292
- if (input.parentSessionId && input.parentSessionId !== input.sessionId) {
293
- artifact.parent_session_id = input.parentSessionId;
294
- }
295
- // Additive: the plan tool's task-list snapshots, in order. Absent
296
- // entirely for sessions that never used the plan tool.
297
- if (parsed.planRecords.length > 0) {
298
- artifact.plan_records = parsed.planRecords;
299
- }
300
- return artifact;
301
- }
302
-
303
- /**
304
- * pi stores sessions under <agentDir>/sessions/<cwd-slug>/<ts>_<id>.jsonl
305
- * (e.g. /home/dan/proj -> --home-dan-proj--). Advisory only: the
306
- * collector discovers the right dir by scanning for the session id, so
307
- * this does not have to be perfect.
308
- */
309
- export function sessionDirSlug(cwd: string): string {
310
- return `${cwd.replace(/\\//g, "-")}-`;
311
- }