pi-messenger 0.7.3

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 (45) hide show
  1. package/ARCHITECTURE.md +244 -0
  2. package/CHANGELOG.md +418 -0
  3. package/README.md +394 -0
  4. package/banner.png +0 -0
  5. package/config-overlay.ts +172 -0
  6. package/config.ts +178 -0
  7. package/crew/agents/crew-docs-scout.md +55 -0
  8. package/crew/agents/crew-gap-analyst.md +105 -0
  9. package/crew/agents/crew-github-scout.md +111 -0
  10. package/crew/agents/crew-interview-generator.md +79 -0
  11. package/crew/agents/crew-plan-sync.md +64 -0
  12. package/crew/agents/crew-practice-scout.md +62 -0
  13. package/crew/agents/crew-repo-scout.md +65 -0
  14. package/crew/agents/crew-reviewer.md +58 -0
  15. package/crew/agents/crew-web-scout.md +85 -0
  16. package/crew/agents/crew-worker.md +95 -0
  17. package/crew/agents.ts +200 -0
  18. package/crew/handlers/interview.ts +211 -0
  19. package/crew/handlers/plan.ts +358 -0
  20. package/crew/handlers/review.ts +341 -0
  21. package/crew/handlers/status.ts +257 -0
  22. package/crew/handlers/sync.ts +232 -0
  23. package/crew/handlers/task.ts +511 -0
  24. package/crew/handlers/work.ts +289 -0
  25. package/crew/id-allocator.ts +44 -0
  26. package/crew/index.ts +229 -0
  27. package/crew/state.ts +116 -0
  28. package/crew/store.ts +480 -0
  29. package/crew/types.ts +164 -0
  30. package/crew/utils/artifacts.ts +65 -0
  31. package/crew/utils/config.ts +104 -0
  32. package/crew/utils/discover.ts +170 -0
  33. package/crew/utils/install.ts +373 -0
  34. package/crew/utils/progress.ts +107 -0
  35. package/crew/utils/result.ts +16 -0
  36. package/crew/utils/truncate.ts +79 -0
  37. package/crew-overlay.ts +259 -0
  38. package/handlers.ts +799 -0
  39. package/index.ts +591 -0
  40. package/lib.ts +232 -0
  41. package/overlay.ts +687 -0
  42. package/package.json +20 -0
  43. package/skills/pi-messenger-crew/SKILL.md +140 -0
  44. package/store.ts +1068 -0
  45. package/tsconfig.json +19 -0
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Crew - Progress Tracking
3
+ *
4
+ * Real-time visibility into agent execution via --mode json event parsing.
5
+ */
6
+
7
+ export interface AgentProgress {
8
+ agent: string;
9
+ status: "pending" | "running" | "completed" | "failed";
10
+ currentTool?: string;
11
+ currentToolArgs?: string;
12
+ recentTools: Array<{ tool: string; args: string; endMs: number }>;
13
+ tokens: number;
14
+ durationMs: number;
15
+ error?: string;
16
+ }
17
+
18
+ // Event types from pi's --mode json output
19
+ interface PiEvent {
20
+ type: string;
21
+ toolName?: string;
22
+ args?: Record<string, unknown>;
23
+ message?: {
24
+ role: string;
25
+ usage?: { input?: number; output?: number; cacheRead?: number; cacheWrite?: number };
26
+ model?: string;
27
+ content?: Array<{ type: string; text?: string }>;
28
+ errorMessage?: string;
29
+ };
30
+ }
31
+
32
+ export function createProgress(agent: string): AgentProgress {
33
+ return {
34
+ agent,
35
+ status: "pending",
36
+ recentTools: [],
37
+ tokens: 0,
38
+ durationMs: 0,
39
+ };
40
+ }
41
+
42
+ export function parseJsonlLine(line: string): PiEvent | null {
43
+ if (!line.trim()) return null;
44
+ try {
45
+ return JSON.parse(line);
46
+ } catch {
47
+ return null;
48
+ }
49
+ }
50
+
51
+ export function updateProgress(progress: AgentProgress, event: PiEvent, startTime: number): void {
52
+ progress.durationMs = Date.now() - startTime;
53
+
54
+ switch (event.type) {
55
+ case "tool_execution_start":
56
+ progress.status = "running";
57
+ progress.currentTool = event.toolName;
58
+ progress.currentToolArgs = extractArgsPreview(event.args);
59
+ break;
60
+
61
+ case "tool_execution_end":
62
+ if (progress.currentTool) {
63
+ progress.recentTools.unshift({
64
+ tool: progress.currentTool,
65
+ args: progress.currentToolArgs ?? "",
66
+ endMs: Date.now(),
67
+ });
68
+ if (progress.recentTools.length > 5) progress.recentTools.pop();
69
+ }
70
+ progress.currentTool = undefined;
71
+ progress.currentToolArgs = undefined;
72
+ break;
73
+
74
+ case "message_end":
75
+ if (event.message?.usage) {
76
+ progress.tokens += (event.message.usage.input ?? 0) + (event.message.usage.output ?? 0);
77
+ }
78
+ if (event.message?.errorMessage) {
79
+ progress.error = event.message.errorMessage;
80
+ }
81
+ break;
82
+ }
83
+ }
84
+
85
+ function extractArgsPreview(args?: Record<string, unknown>): string {
86
+ if (!args) return "";
87
+ const previewKeys = ["command", "path", "file_path", "pattern", "query"];
88
+ for (const key of previewKeys) {
89
+ if (args[key] && typeof args[key] === "string") {
90
+ const value = args[key] as string;
91
+ return value.length > 60 ? `${value.slice(0, 57)}...` : value;
92
+ }
93
+ }
94
+ return "";
95
+ }
96
+
97
+ export function getFinalOutput(messages: PiEvent[]): string {
98
+ for (let i = messages.length - 1; i >= 0; i--) {
99
+ const msg = messages[i];
100
+ if (msg.type === "message_end" && msg.message?.role === "assistant") {
101
+ for (const part of msg.message.content ?? []) {
102
+ if (part.type === "text" && part.text) return part.text;
103
+ }
104
+ }
105
+ }
106
+ return "";
107
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Crew - Result Formatter
3
+ *
4
+ * Helper for consistent tool result formatting.
5
+ */
6
+
7
+ /**
8
+ * Format a tool result with text content and structured details.
9
+ * Matches the pattern used throughout pi-messenger handlers.
10
+ */
11
+ export function result(text: string, details: Record<string, unknown>) {
12
+ return {
13
+ content: [{ type: "text" as const, text }],
14
+ details
15
+ };
16
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Crew - Output Truncation
3
+ *
4
+ * Prevents token explosion from verbose agent outputs.
5
+ */
6
+
7
+ export interface MaxOutputConfig {
8
+ bytes?: number;
9
+ lines?: number;
10
+ }
11
+
12
+ export interface TruncationResult {
13
+ text: string;
14
+ truncated: boolean;
15
+ originalBytes?: number;
16
+ originalLines?: number;
17
+ artifactPath?: string;
18
+ }
19
+
20
+ export const DEFAULT_MAX_OUTPUT: Required<MaxOutputConfig> = {
21
+ bytes: 200 * 1024, // 200KB
22
+ lines: 5000,
23
+ };
24
+
25
+ export function formatBytes(bytes: number): string {
26
+ if (bytes < 1024) return `${bytes}B`;
27
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
28
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
29
+ }
30
+
31
+ /**
32
+ * Truncate output to fit within limits.
33
+ */
34
+ export function truncateOutput(
35
+ output: string,
36
+ config: MaxOutputConfig,
37
+ artifactPath?: string
38
+ ): TruncationResult {
39
+ const maxBytes = config.bytes ?? DEFAULT_MAX_OUTPUT.bytes;
40
+ const maxLines = config.lines ?? DEFAULT_MAX_OUTPUT.lines;
41
+
42
+ const lines = output.split("\n");
43
+ const bytes = Buffer.byteLength(output, "utf-8");
44
+
45
+ if (bytes <= maxBytes && lines.length <= maxLines) {
46
+ return { text: output, truncated: false };
47
+ }
48
+
49
+ // Truncate by lines first
50
+ let truncatedLines = lines.length > maxLines ? lines.slice(0, maxLines) : lines;
51
+ let result = truncatedLines.join("\n");
52
+
53
+ // Then truncate by bytes if still too large
54
+ if (Buffer.byteLength(result, "utf-8") > maxBytes) {
55
+ // Binary search for the right cut point
56
+ let low = 0, high = result.length;
57
+ while (low < high) {
58
+ const mid = Math.floor((low + high + 1) / 2);
59
+ if (Buffer.byteLength(result.slice(0, mid), "utf-8") <= maxBytes) {
60
+ low = mid;
61
+ } else {
62
+ high = mid - 1;
63
+ }
64
+ }
65
+ result = result.slice(0, low);
66
+ }
67
+
68
+ const keptLines = result.split("\n").length;
69
+ const fullOutputHint = artifactPath ? ` - full output at ${artifactPath}` : "";
70
+ const marker = `[TRUNCATED: ${keptLines}/${lines.length} lines, ${formatBytes(Buffer.byteLength(result))}/${formatBytes(bytes)}${fullOutputHint}]\n`;
71
+
72
+ return {
73
+ text: marker + result,
74
+ truncated: true,
75
+ originalBytes: bytes,
76
+ originalLines: lines.length,
77
+ artifactPath,
78
+ };
79
+ }
@@ -0,0 +1,259 @@
1
+ /**
2
+ * Crew Overlay - Task Visualization
3
+ *
4
+ * Renders the Crew tab content for the messenger overlay.
5
+ * Shows flat task list under PRD name with status and dependencies.
6
+ */
7
+
8
+ import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
9
+ import type { Theme } from "@mariozechner/pi-coding-agent";
10
+ import * as crewStore from "./crew/store.js";
11
+ import { autonomousState } from "./crew/state.js";
12
+ import type { Task } from "./crew/types.js";
13
+
14
+ // Status icons
15
+ const STATUS_ICONS: Record<string, string> = {
16
+ done: "✓",
17
+ in_progress: "●",
18
+ todo: "○",
19
+ blocked: "✗",
20
+ };
21
+
22
+ export interface CrewViewState {
23
+ scrollOffset: number;
24
+ selectedTaskIndex: number;
25
+ }
26
+
27
+ export function createCrewViewState(): CrewViewState {
28
+ return {
29
+ scrollOffset: 0,
30
+ selectedTaskIndex: 0,
31
+ };
32
+ }
33
+
34
+ /**
35
+ * Render the crew overview content - flat task list under PRD.
36
+ */
37
+ export function renderCrewContent(
38
+ theme: Theme,
39
+ cwd: string,
40
+ width: number,
41
+ height: number,
42
+ viewState: CrewViewState
43
+ ): string[] {
44
+ const lines: string[] = [];
45
+ const plan = crewStore.getPlan(cwd);
46
+
47
+ if (!plan) {
48
+ return renderEmptyState(theme, width, height);
49
+ }
50
+
51
+ const tasks = crewStore.getTasks(cwd);
52
+
53
+ // Header: PRD with progress
54
+ const pct = plan.task_count > 0 ? Math.round((plan.completed_count / plan.task_count) * 100) : 0;
55
+ const progressText = `[${plan.completed_count}/${plan.task_count}]`;
56
+ const prdLine = `📋 ${plan.prd}`;
57
+ const prdWidth = visibleWidth(prdLine);
58
+ const progressWidth = visibleWidth(progressText);
59
+ const padding = Math.max(1, width - prdWidth - progressWidth - 2);
60
+
61
+ lines.push(prdLine + " ".repeat(padding) + theme.fg("accent", progressText));
62
+ lines.push("");
63
+
64
+ // Task list
65
+ if (tasks.length === 0) {
66
+ lines.push(theme.fg("dim", " (no tasks yet)"));
67
+ } else {
68
+ for (let i = 0; i < tasks.length; i++) {
69
+ const task = tasks[i];
70
+ const taskLine = renderTaskLine(theme, task, i === viewState.selectedTaskIndex, width);
71
+ lines.push(taskLine);
72
+ }
73
+ }
74
+
75
+ // Add legend
76
+ lines.push("");
77
+ lines.push(renderLegend(theme, width));
78
+
79
+ // Ensure we fill the height
80
+ while (lines.length < height) {
81
+ lines.push("");
82
+ }
83
+
84
+ // Handle scrolling if content exceeds height
85
+ if (lines.length > height) {
86
+ const startIdx = Math.min(viewState.scrollOffset, lines.length - height);
87
+ return lines.slice(startIdx, startIdx + height);
88
+ }
89
+
90
+ return lines.slice(0, height);
91
+ }
92
+
93
+ /**
94
+ * Render the status bar for autonomous mode.
95
+ */
96
+ export function renderCrewStatusBar(theme: Theme, cwd: string, width: number): string {
97
+ const plan = crewStore.getPlan(cwd);
98
+
99
+ if (!plan) {
100
+ return theme.fg("dim", "No active plan");
101
+ }
102
+
103
+ if (!autonomousState.active) {
104
+ // Show plan progress
105
+ const progress = `${plan.completed_count}/${plan.task_count}`;
106
+ const ready = crewStore.getReadyTasks(cwd);
107
+ const readyText = ready.length > 0 ? ` │ ${ready.length} ready` : "";
108
+ return truncateToWidth(
109
+ `📋 ${plan.prd}: ${progress} tasks${readyText}`,
110
+ width
111
+ );
112
+ }
113
+
114
+ // Autonomous mode active
115
+ const progress = `${plan.completed_count}/${plan.task_count}`;
116
+
117
+ // Calculate elapsed time
118
+ let elapsed = "";
119
+ if (autonomousState.startedAt) {
120
+ const startTime = new Date(autonomousState.startedAt).getTime();
121
+ const elapsedMs = Date.now() - startTime;
122
+ const minutes = Math.floor(elapsedMs / 60000);
123
+ const seconds = Math.floor((elapsedMs % 60000) / 1000);
124
+ elapsed = `${minutes}:${seconds.toString().padStart(2, "0")}`;
125
+ }
126
+
127
+ const readyTasks = crewStore.getReadyTasks(cwd);
128
+
129
+ const parts = [
130
+ `Wave ${autonomousState.waveNumber}`,
131
+ `${progress} tasks`,
132
+ `${readyTasks.length} ready`,
133
+ ];
134
+
135
+ if (elapsed) {
136
+ parts.push(`⏱️ ${elapsed}`);
137
+ }
138
+
139
+ return truncateToWidth(
140
+ theme.fg("accent", "● AUTO ") + parts.join(" │ "),
141
+ width
142
+ );
143
+ }
144
+
145
+ // =============================================================================
146
+ // Private Helpers
147
+ // =============================================================================
148
+
149
+ function renderEmptyState(theme: Theme, width: number, height: number): string[] {
150
+ const lines: string[] = [];
151
+ const msg = "No active plan";
152
+ const hint = "Use pi_messenger({ action: \"plan\" })";
153
+
154
+ const padTop = Math.floor((height - 3) / 2);
155
+ for (let i = 0; i < padTop; i++) lines.push("");
156
+
157
+ const pad1 = " ".repeat(Math.max(0, Math.floor((width - msg.length) / 2)));
158
+ lines.push(pad1 + msg);
159
+ lines.push("");
160
+ const pad2 = " ".repeat(Math.max(0, Math.floor((width - hint.length) / 2)));
161
+ lines.push(pad2 + theme.fg("dim", hint));
162
+
163
+ while (lines.length < height) lines.push("");
164
+ return lines;
165
+ }
166
+
167
+ function renderTaskLine(
168
+ theme: Theme,
169
+ task: Task,
170
+ isSelected: boolean,
171
+ width: number
172
+ ): string {
173
+ const icon = STATUS_ICONS[task.status] ?? "?";
174
+ const selectIndicator = isSelected ? theme.fg("accent", "▸ ") : " ";
175
+
176
+ // Color the icon based on status
177
+ let coloredIcon: string;
178
+ switch (task.status) {
179
+ case "done":
180
+ coloredIcon = theme.fg("accent", icon);
181
+ break;
182
+ case "in_progress":
183
+ coloredIcon = theme.fg("warning", icon);
184
+ break;
185
+ case "blocked":
186
+ coloredIcon = theme.fg("error", icon);
187
+ break;
188
+ default:
189
+ coloredIcon = theme.fg("dim", icon);
190
+ }
191
+
192
+ // Build task suffix (assigned agent or dependencies)
193
+ let suffix = "";
194
+ if (task.status === "in_progress" && task.assigned_to) {
195
+ suffix = ` (${task.assigned_to})`;
196
+ } else if (task.status === "todo" && task.depends_on.length > 0) {
197
+ suffix = ` → deps: ${task.depends_on.join(", ")}`;
198
+ } else if (task.status === "blocked" && task.blocked_reason) {
199
+ // Truncate block reason
200
+ const reason = task.blocked_reason.slice(0, 20);
201
+ suffix = ` [${reason}${task.blocked_reason.length > 20 ? "…" : ""}]`;
202
+ }
203
+
204
+ const line = `${selectIndicator}${coloredIcon} ${task.id} ${task.title}`;
205
+ const fullLine = line + theme.fg("dim", suffix);
206
+
207
+ return truncateToWidth(fullLine, width);
208
+ }
209
+
210
+ function renderLegend(theme: Theme, width: number): string {
211
+ const items = [
212
+ `${theme.fg("accent", STATUS_ICONS.done)} done`,
213
+ `${theme.fg("warning", STATUS_ICONS.in_progress)} in_progress`,
214
+ `${theme.fg("dim", STATUS_ICONS.todo)} todo`,
215
+ `${theme.fg("error", STATUS_ICONS.blocked)} blocked`,
216
+ ];
217
+
218
+ const legend = "Legend: " + items.join(" ");
219
+ return truncateToWidth(theme.fg("dim", legend), width);
220
+ }
221
+
222
+ /**
223
+ * Navigate to next/prev task.
224
+ */
225
+ export function navigateTask(viewState: CrewViewState, direction: 1 | -1, taskCount: number): void {
226
+ if (taskCount === 0) return;
227
+ viewState.selectedTaskIndex = Math.max(
228
+ 0,
229
+ Math.min(taskCount - 1, viewState.selectedTaskIndex + direction)
230
+ );
231
+ }
232
+
233
+ /**
234
+ * Get the currently selected task ID.
235
+ */
236
+ export function getSelectedTaskId(cwd: string, viewState: CrewViewState): string | null {
237
+ const tasks = crewStore.getTasks(cwd);
238
+ if (viewState.selectedTaskIndex >= 0 && viewState.selectedTaskIndex < tasks.length) {
239
+ return tasks[viewState.selectedTaskIndex].id;
240
+ }
241
+ return null;
242
+ }
243
+
244
+ // Legacy exports for compatibility (no-ops now that epics are removed)
245
+ export function toggleEpicExpansion(_viewState: CrewViewState, _epicId: string): void {
246
+ // No-op - epics removed
247
+ }
248
+
249
+ export function navigateEpic(viewState: CrewViewState, direction: 1 | -1, _epicCount: number): void {
250
+ // Redirect to task navigation
251
+ const cwd = process.cwd();
252
+ const tasks = crewStore.getTasks(cwd);
253
+ navigateTask(viewState, direction, tasks.length);
254
+ }
255
+
256
+ export function getSelectedEpicId(_cwd: string, _viewState: CrewViewState): string | null {
257
+ // No-op - epics removed
258
+ return null;
259
+ }