@posthog/agent 1.1.0 → 1.2.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.
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Persists task execution progress to PostHog so clients can poll for updates.
3
+ *
4
+ * The reporter is intentionally best-effort – failures are logged but never
5
+ * allowed to break the agent execution flow.
6
+ */
7
+ class TaskProgressReporter {
8
+ posthogAPI;
9
+ logger;
10
+ progressRecord;
11
+ taskId;
12
+ outputLog = [];
13
+ totalSteps;
14
+ lastLogEntry;
15
+ constructor(posthogAPI, logger) {
16
+ this.posthogAPI = posthogAPI;
17
+ this.logger = logger.child('TaskProgressReporter');
18
+ }
19
+ get progressId() {
20
+ return this.progressRecord?.id;
21
+ }
22
+ async start(taskId, metadata = {}) {
23
+ if (!this.posthogAPI) {
24
+ return;
25
+ }
26
+ this.taskId = taskId;
27
+ this.totalSteps = metadata.totalSteps;
28
+ try {
29
+ const record = await this.posthogAPI.createTaskProgress(taskId, {
30
+ status: 'started',
31
+ current_step: 'initializing',
32
+ total_steps: metadata.totalSteps ?? 0,
33
+ completed_steps: 0,
34
+ workflow_id: metadata.workflowId,
35
+ workflow_run_id: metadata.workflowRunId,
36
+ activity_id: metadata.activityId,
37
+ output_log: '',
38
+ });
39
+ this.progressRecord = record;
40
+ this.outputLog = record.output_log ? record.output_log.split('\n') : [];
41
+ this.logger.debug('Created task progress record', { taskId, progressId: record.id });
42
+ }
43
+ catch (error) {
44
+ this.logger.warn('Failed to create task progress record', { taskId, error: error.message });
45
+ }
46
+ }
47
+ async stageStarted(stageKey, stageIndex) {
48
+ await this.update({
49
+ status: 'in_progress',
50
+ current_step: stageKey,
51
+ completed_steps: Math.min(stageIndex, this.totalSteps ?? stageIndex),
52
+ }, `Stage started: ${stageKey}`);
53
+ }
54
+ async stageCompleted(stageKey, completedStages) {
55
+ await this.update({
56
+ status: 'in_progress',
57
+ current_step: stageKey,
58
+ completed_steps: Math.min(completedStages, this.totalSteps ?? completedStages),
59
+ }, `Stage completed: ${stageKey}`);
60
+ }
61
+ async branchCreated(stageKey, branchName) {
62
+ await this.appendLog(`Branch created (${stageKey}): ${branchName}`);
63
+ }
64
+ async commitMade(stageKey, kind) {
65
+ await this.appendLog(`Commit made (${stageKey}, ${kind})`);
66
+ }
67
+ async pullRequestCreated(stageKey, prUrl) {
68
+ await this.appendLog(`Pull request created (${stageKey}): ${prUrl}`);
69
+ }
70
+ async noNextStage(stageKey) {
71
+ await this.appendLog(stageKey
72
+ ? `No next stage available after '${stageKey}'. Execution halted.`
73
+ : 'No next stage available. Execution halted.');
74
+ }
75
+ async complete() {
76
+ await this.update({ status: 'completed', completed_steps: this.totalSteps }, 'Workflow execution completed');
77
+ }
78
+ async fail(error) {
79
+ const message = typeof error === 'string' ? error : error.message;
80
+ await this.update({ status: 'failed', error_message: message }, `Workflow execution failed: ${message}`);
81
+ }
82
+ async appendLog(line) {
83
+ await this.update({}, line);
84
+ }
85
+ async recordEvent(event) {
86
+ if (!this.posthogAPI || !this.progressId || !this.taskId) {
87
+ return;
88
+ }
89
+ switch (event.type) {
90
+ case 'token':
91
+ case 'message_delta':
92
+ case 'content_block_start':
93
+ case 'content_block_stop':
94
+ case 'compact_boundary':
95
+ case 'tool_call':
96
+ case 'tool_result':
97
+ case 'message_start':
98
+ case 'message_stop':
99
+ case 'metric':
100
+ case 'artifact':
101
+ // Skip verbose streaming artifacts from persistence
102
+ return;
103
+ case 'file_write':
104
+ await this.appendLog(this.formatFileWriteEvent(event));
105
+ return;
106
+ case 'diff':
107
+ await this.appendLog(this.formatDiffEvent(event));
108
+ return;
109
+ case 'status':
110
+ // Status events are covered by dedicated progress updates
111
+ return;
112
+ case 'error':
113
+ await this.appendLog(`[error] ${event.message}`);
114
+ return;
115
+ case 'done': {
116
+ const cost = event.totalCostUsd !== undefined ? ` cost=$${event.totalCostUsd.toFixed(2)}` : '';
117
+ await this.appendLog(`[done] duration=${event.durationMs ?? 'unknown'}ms turns=${event.numTurns ?? 'unknown'}${cost}`);
118
+ return;
119
+ }
120
+ case 'init':
121
+ // Omit verbose init messages from persisted log
122
+ return;
123
+ case 'user_message': {
124
+ const summary = this.summarizeUserMessage(event.content);
125
+ if (summary) {
126
+ await this.appendLog(summary);
127
+ }
128
+ return;
129
+ }
130
+ default:
131
+ // For any unfamiliar event types, avoid spamming the log.
132
+ return;
133
+ }
134
+ }
135
+ async update(update, logLine) {
136
+ if (!this.posthogAPI || !this.progressId || !this.taskId) {
137
+ return;
138
+ }
139
+ if (logLine) {
140
+ if (logLine !== this.lastLogEntry) {
141
+ this.outputLog.push(logLine);
142
+ this.lastLogEntry = logLine;
143
+ }
144
+ update.output_log = this.outputLog.join('\n');
145
+ }
146
+ try {
147
+ const record = await this.posthogAPI.updateTaskProgress(this.taskId, this.progressId, update);
148
+ // Sync local cache with server response to avoid drift if server modifies values
149
+ this.progressRecord = record;
150
+ if (record.output_log !== undefined && record.output_log !== null) {
151
+ this.outputLog = record.output_log ? record.output_log.split('\n') : [];
152
+ }
153
+ }
154
+ catch (error) {
155
+ this.logger.warn('Failed to update task progress record', {
156
+ taskId: this.taskId,
157
+ progressId: this.progressId,
158
+ error: error.message,
159
+ });
160
+ }
161
+ }
162
+ summarizeUserMessage(content) {
163
+ if (!content) {
164
+ return null;
165
+ }
166
+ const trimmed = content.trim();
167
+ if (!trimmed) {
168
+ return null;
169
+ }
170
+ const fileUpdateMatch = trimmed.match(/The file\s+([^\s]+)\s+has been updated/i);
171
+ if (fileUpdateMatch) {
172
+ return `[user] file updated: ${fileUpdateMatch[1]}`;
173
+ }
174
+ if (/Todos have been modified/i.test(trimmed)) {
175
+ return '[todo] list updated';
176
+ }
177
+ const diffMatch = trimmed.match(/diff --git a\/([^\s]+) b\/([^\s]+)/);
178
+ if (diffMatch) {
179
+ return `[diff] ${diffMatch[2] ?? diffMatch[1]}`;
180
+ }
181
+ const gitStatusMatch = trimmed.match(/^On branch ([^\n]+)/);
182
+ if (gitStatusMatch) {
183
+ return `[git] status ${gitStatusMatch[1]}`;
184
+ }
185
+ if (/This Bash command contains multiple operations/i.test(trimmed)) {
186
+ return '[approval] multi-step command pending';
187
+ }
188
+ if (/This command requires approval/i.test(trimmed)) {
189
+ return '[approval] command awaiting approval';
190
+ }
191
+ if (/^Exit plan mode\?/i.test(trimmed)) {
192
+ return null;
193
+ }
194
+ if (trimmed.includes('node_modules')) {
195
+ return null;
196
+ }
197
+ if (trimmed.includes('total ') && trimmed.includes('drwx')) {
198
+ return null;
199
+ }
200
+ if (trimmed.includes('→')) {
201
+ return null;
202
+ }
203
+ if (trimmed.split('\n').length > 2) {
204
+ return null;
205
+ }
206
+ const normalized = trimmed.replace(/\s+/g, ' ');
207
+ const maxLen = 120;
208
+ if (!normalized) {
209
+ return null;
210
+ }
211
+ const preview = normalized.length > maxLen ? `${normalized.slice(0, maxLen)}…` : normalized;
212
+ return `[user] ${preview}`;
213
+ }
214
+ formatFileWriteEvent(event) {
215
+ const size = event.bytes !== undefined ? ` (${event.bytes} bytes)` : '';
216
+ return `[file] wrote ${event.path}${size}`;
217
+ }
218
+ formatDiffEvent(event) {
219
+ const summary = event.summary
220
+ ? event.summary.trim()
221
+ : this.truncateMultiline(event.patch ?? '', 160);
222
+ return `[diff] ${event.file}${summary ? ` | ${summary}` : ''}`;
223
+ }
224
+ truncateMultiline(text, max = 160) {
225
+ if (!text) {
226
+ return '';
227
+ }
228
+ const compact = text.replace(/\s+/g, ' ').trim();
229
+ return compact.length > max ? `${compact.slice(0, max)}…` : compact;
230
+ }
231
+ }
232
+
233
+ export { TaskProgressReporter };
234
+ //# sourceMappingURL=task-progress-reporter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"task-progress-reporter.js","sources":["../../src/task-progress-reporter.ts"],"sourcesContent":["import type { Logger } from './utils/logger.js';\nimport type { PostHogAPIClient, TaskProgressRecord, TaskProgressUpdate } from './posthog-api.js';\nimport type { AgentEvent } from './types.js';\n\ninterface ProgressMetadata {\n workflowId?: string;\n workflowRunId?: string;\n activityId?: string;\n totalSteps?: number;\n}\n\n/**\n * Persists task execution progress to PostHog so clients can poll for updates.\n *\n * The reporter is intentionally best-effort – failures are logged but never\n * allowed to break the agent execution flow.\n */\nexport class TaskProgressReporter {\n private posthogAPI?: PostHogAPIClient;\n private logger: Logger;\n private progressRecord?: TaskProgressRecord;\n private taskId?: string;\n private outputLog: string[] = [];\n private totalSteps?: number;\n private lastLogEntry?: string;\n\n constructor(posthogAPI: PostHogAPIClient | undefined, logger: Logger) {\n this.posthogAPI = posthogAPI;\n this.logger = logger.child('TaskProgressReporter');\n }\n\n get progressId(): string | undefined {\n return this.progressRecord?.id;\n }\n\n async start(taskId: string, metadata: ProgressMetadata = {}): Promise<void> {\n if (!this.posthogAPI) {\n return;\n }\n\n this.taskId = taskId;\n this.totalSteps = metadata.totalSteps;\n\n try {\n const record = await this.posthogAPI.createTaskProgress(taskId, {\n status: 'started',\n current_step: 'initializing',\n total_steps: metadata.totalSteps ?? 0,\n completed_steps: 0,\n workflow_id: metadata.workflowId,\n workflow_run_id: metadata.workflowRunId,\n activity_id: metadata.activityId,\n output_log: '',\n });\n this.progressRecord = record;\n this.outputLog = record.output_log ? record.output_log.split('\\n') : [];\n this.logger.debug('Created task progress record', { taskId, progressId: record.id });\n } catch (error) {\n this.logger.warn('Failed to create task progress record', { taskId, error: (error as Error).message });\n }\n }\n\n async stageStarted(stageKey: string, stageIndex: number): Promise<void> {\n await this.update({\n status: 'in_progress',\n current_step: stageKey,\n completed_steps: Math.min(stageIndex, this.totalSteps ?? stageIndex),\n }, `Stage started: ${stageKey}`);\n }\n\n async stageCompleted(stageKey: string, completedStages: number): Promise<void> {\n await this.update({\n status: 'in_progress',\n current_step: stageKey,\n completed_steps: Math.min(completedStages, this.totalSteps ?? completedStages),\n }, `Stage completed: ${stageKey}`);\n }\n\n async branchCreated(stageKey: string, branchName: string): Promise<void> {\n await this.appendLog(`Branch created (${stageKey}): ${branchName}`);\n }\n\n async commitMade(stageKey: string, kind: 'plan' | 'implementation'): Promise<void> {\n await this.appendLog(`Commit made (${stageKey}, ${kind})`);\n }\n\n async pullRequestCreated(stageKey: string, prUrl: string): Promise<void> {\n await this.appendLog(`Pull request created (${stageKey}): ${prUrl}`);\n }\n\n async noNextStage(stageKey?: string): Promise<void> {\n await this.appendLog(\n stageKey\n ? `No next stage available after '${stageKey}'. Execution halted.`\n : 'No next stage available. Execution halted.'\n );\n }\n\n async complete(): Promise<void> {\n await this.update({ status: 'completed', completed_steps: this.totalSteps }, 'Workflow execution completed');\n }\n\n async fail(error: Error | string): Promise<void> {\n const message = typeof error === 'string' ? error : error.message;\n await this.update({ status: 'failed', error_message: message }, `Workflow execution failed: ${message}`);\n }\n\n async appendLog(line: string): Promise<void> {\n await this.update({}, line);\n }\n\n async recordEvent(event: AgentEvent): Promise<void> {\n if (!this.posthogAPI || !this.progressId || !this.taskId) {\n return;\n }\n\n switch (event.type) {\n case 'token':\n case 'message_delta':\n case 'content_block_start':\n case 'content_block_stop':\n case 'compact_boundary':\n case 'tool_call':\n case 'tool_result':\n case 'message_start':\n case 'message_stop':\n case 'metric':\n case 'artifact':\n // Skip verbose streaming artifacts from persistence\n return;\n\n case 'file_write':\n await this.appendLog(this.formatFileWriteEvent(event));\n return;\n\n case 'diff':\n await this.appendLog(this.formatDiffEvent(event));\n return;\n\n case 'status':\n // Status events are covered by dedicated progress updates\n return;\n\n case 'error':\n await this.appendLog(`[error] ${event.message}`);\n return;\n\n case 'done': {\n const cost = event.totalCostUsd !== undefined ? ` cost=$${event.totalCostUsd.toFixed(2)}` : '';\n await this.appendLog(\n `[done] duration=${event.durationMs ?? 'unknown'}ms turns=${event.numTurns ?? 'unknown'}${cost}`\n );\n return;\n }\n\n case 'init':\n // Omit verbose init messages from persisted log\n return;\n\n case 'user_message': {\n const summary = this.summarizeUserMessage(event.content);\n if (summary) {\n await this.appendLog(summary);\n }\n return;\n }\n\n default:\n // For any unfamiliar event types, avoid spamming the log.\n return;\n }\n }\n\n private async update(update: TaskProgressUpdate, logLine?: string): Promise<void> {\n if (!this.posthogAPI || !this.progressId || !this.taskId) {\n return;\n }\n\n if (logLine) {\n if (logLine !== this.lastLogEntry) {\n this.outputLog.push(logLine);\n this.lastLogEntry = logLine;\n }\n update.output_log = this.outputLog.join('\\n');\n }\n\n try {\n const record = await this.posthogAPI.updateTaskProgress(this.taskId, this.progressId, update);\n // Sync local cache with server response to avoid drift if server modifies values\n this.progressRecord = record;\n if (record.output_log !== undefined && record.output_log !== null) {\n this.outputLog = record.output_log ? record.output_log.split('\\n') : [];\n }\n } catch (error) {\n this.logger.warn('Failed to update task progress record', {\n taskId: this.taskId,\n progressId: this.progressId,\n error: (error as Error).message,\n });\n }\n }\n\n private summarizeUserMessage(content?: string): string | null {\n if (!content) {\n return null;\n }\n const trimmed = content.trim();\n if (!trimmed) {\n return null;\n }\n\n const fileUpdateMatch = trimmed.match(/The file\\s+([^\\s]+)\\s+has been updated/i);\n if (fileUpdateMatch) {\n return `[user] file updated: ${fileUpdateMatch[1]}`;\n }\n\n if (/Todos have been modified/i.test(trimmed)) {\n return '[todo] list updated';\n }\n\n const diffMatch = trimmed.match(/diff --git a\\/([^\\s]+) b\\/([^\\s]+)/);\n if (diffMatch) {\n return `[diff] ${diffMatch[2] ?? diffMatch[1]}`;\n }\n\n const gitStatusMatch = trimmed.match(/^On branch ([^\\n]+)/);\n if (gitStatusMatch) {\n return `[git] status ${gitStatusMatch[1]}`;\n }\n\n if (/This Bash command contains multiple operations/i.test(trimmed)) {\n return '[approval] multi-step command pending';\n }\n\n if (/This command requires approval/i.test(trimmed)) {\n return '[approval] command awaiting approval';\n }\n\n if (/^Exit plan mode\\?/i.test(trimmed)) {\n return null;\n }\n\n if (trimmed.includes('node_modules')) {\n return null;\n }\n\n if (trimmed.includes('total ') && trimmed.includes('drwx')) {\n return null;\n }\n\n if (trimmed.includes('→')) {\n return null;\n }\n\n if (trimmed.split('\\n').length > 2) {\n return null;\n }\n\n const normalized = trimmed.replace(/\\s+/g, ' ');\n const maxLen = 120;\n if (!normalized) {\n return null;\n }\n const preview = normalized.length > maxLen ? `${normalized.slice(0, maxLen)}…` : normalized;\n return `[user] ${preview}`;\n }\n\n private formatFileWriteEvent(event: Extract<AgentEvent, { type: 'file_write' }>): string {\n const size = event.bytes !== undefined ? ` (${event.bytes} bytes)` : '';\n return `[file] wrote ${event.path}${size}`;\n }\n\n private formatDiffEvent(event: Extract<AgentEvent, { type: 'diff' }>): string {\n const summary = event.summary\n ? event.summary.trim()\n : this.truncateMultiline(event.patch ?? '', 160);\n return `[diff] ${event.file}${summary ? ` | ${summary}` : ''}`;\n }\n\n private truncateMultiline(text: string, max = 160): string {\n if (!text) {\n return '';\n }\n const compact = text.replace(/\\s+/g, ' ').trim();\n return compact.length > max ? `${compact.slice(0, max)}…` : compact;\n }\n}\n"],"names":[],"mappings":"AAWA;;;;;AAKG;MACU,oBAAoB,CAAA;AACvB,IAAA,UAAU;AACV,IAAA,MAAM;AACN,IAAA,cAAc;AACd,IAAA,MAAM;IACN,SAAS,GAAa,EAAE;AACxB,IAAA,UAAU;AACV,IAAA,YAAY;IAEpB,WAAA,CAAY,UAAwC,EAAE,MAAc,EAAA;AAClE,QAAA,IAAI,CAAC,UAAU,GAAG,UAAU;QAC5B,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,sBAAsB,CAAC;IACpD;AAEA,IAAA,IAAI,UAAU,GAAA;AACZ,QAAA,OAAO,IAAI,CAAC,cAAc,EAAE,EAAE;IAChC;AAEA,IAAA,MAAM,KAAK,CAAC,MAAc,EAAE,WAA6B,EAAE,EAAA;AACzD,QAAA,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE;YACpB;QACF;AAEA,QAAA,IAAI,CAAC,MAAM,GAAG,MAAM;AACpB,QAAA,IAAI,CAAC,UAAU,GAAG,QAAQ,CAAC,UAAU;AAErC,QAAA,IAAI;YACF,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,kBAAkB,CAAC,MAAM,EAAE;AAC9D,gBAAA,MAAM,EAAE,SAAS;AACjB,gBAAA,YAAY,EAAE,cAAc;AAC5B,gBAAA,WAAW,EAAE,QAAQ,CAAC,UAAU,IAAI,CAAC;AACrC,gBAAA,eAAe,EAAE,CAAC;gBAClB,WAAW,EAAE,QAAQ,CAAC,UAAU;gBAChC,eAAe,EAAE,QAAQ,CAAC,aAAa;gBACvC,WAAW,EAAE,QAAQ,CAAC,UAAU;AAChC,gBAAA,UAAU,EAAE,EAAE;AACf,aAAA,CAAC;AACF,YAAA,IAAI,CAAC,cAAc,GAAG,MAAM;YAC5B,IAAI,CAAC,SAAS,GAAG,MAAM,CAAC,UAAU,GAAG,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE;AACvE,YAAA,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,8BAA8B,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,CAAC,EAAE,EAAE,CAAC;QACtF;QAAE,OAAO,KAAK,EAAE;AACd,YAAA,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,uCAAuC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAG,KAAe,CAAC,OAAO,EAAE,CAAC;QACxG;IACF;AAEA,IAAA,MAAM,YAAY,CAAC,QAAgB,EAAE,UAAkB,EAAA;QACrD,MAAM,IAAI,CAAC,MAAM,CAAC;AAChB,YAAA,MAAM,EAAE,aAAa;AACrB,YAAA,YAAY,EAAE,QAAQ;AACtB,YAAA,eAAe,EAAE,IAAI,CAAC,GAAG,CAAC,UAAU,EAAE,IAAI,CAAC,UAAU,IAAI,UAAU,CAAC;AACrE,SAAA,EAAE,CAAA,eAAA,EAAkB,QAAQ,CAAA,CAAE,CAAC;IAClC;AAEA,IAAA,MAAM,cAAc,CAAC,QAAgB,EAAE,eAAuB,EAAA;QAC5D,MAAM,IAAI,CAAC,MAAM,CAAC;AAChB,YAAA,MAAM,EAAE,aAAa;AACrB,YAAA,YAAY,EAAE,QAAQ;AACtB,YAAA,eAAe,EAAE,IAAI,CAAC,GAAG,CAAC,eAAe,EAAE,IAAI,CAAC,UAAU,IAAI,eAAe,CAAC;AAC/E,SAAA,EAAE,CAAA,iBAAA,EAAoB,QAAQ,CAAA,CAAE,CAAC;IACpC;AAEA,IAAA,MAAM,aAAa,CAAC,QAAgB,EAAE,UAAkB,EAAA;QACtD,MAAM,IAAI,CAAC,SAAS,CAAC,CAAA,gBAAA,EAAmB,QAAQ,CAAA,GAAA,EAAM,UAAU,CAAA,CAAE,CAAC;IACrE;AAEA,IAAA,MAAM,UAAU,CAAC,QAAgB,EAAE,IAA+B,EAAA;QAChE,MAAM,IAAI,CAAC,SAAS,CAAC,CAAA,aAAA,EAAgB,QAAQ,CAAA,EAAA,EAAK,IAAI,CAAA,CAAA,CAAG,CAAC;IAC5D;AAEA,IAAA,MAAM,kBAAkB,CAAC,QAAgB,EAAE,KAAa,EAAA;QACtD,MAAM,IAAI,CAAC,SAAS,CAAC,CAAA,sBAAA,EAAyB,QAAQ,CAAA,GAAA,EAAM,KAAK,CAAA,CAAE,CAAC;IACtE;IAEA,MAAM,WAAW,CAAC,QAAiB,EAAA;AACjC,QAAA,MAAM,IAAI,CAAC,SAAS,CAClB;cACI,CAAA,+BAAA,EAAkC,QAAQ,CAAA,oBAAA;cAC1C,4CAA4C,CACjD;IACH;AAEA,IAAA,MAAM,QAAQ,GAAA;AACZ,QAAA,MAAM,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,WAAW,EAAE,eAAe,EAAE,IAAI,CAAC,UAAU,EAAE,EAAE,8BAA8B,CAAC;IAC9G;IAEA,MAAM,IAAI,CAAC,KAAqB,EAAA;AAC9B,QAAA,MAAM,OAAO,GAAG,OAAO,KAAK,KAAK,QAAQ,GAAG,KAAK,GAAG,KAAK,CAAC,OAAO;AACjE,QAAA,MAAM,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,EAAE,OAAO,EAAE,EAAE,8BAA8B,OAAO,CAAA,CAAE,CAAC;IAC1G;IAEA,MAAM,SAAS,CAAC,IAAY,EAAA;QAC1B,MAAM,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,IAAI,CAAC;IAC7B;IAEA,MAAM,WAAW,CAAC,KAAiB,EAAA;AACjC,QAAA,IAAI,CAAC,IAAI,CAAC,UAAU,IAAI,CAAC,IAAI,CAAC,UAAU,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE;YACxD;QACF;AAEA,QAAA,QAAQ,KAAK,CAAC,IAAI;AAChB,YAAA,KAAK,OAAO;AACZ,YAAA,KAAK,eAAe;AACpB,YAAA,KAAK,qBAAqB;AAC1B,YAAA,KAAK,oBAAoB;AACzB,YAAA,KAAK,kBAAkB;AACvB,YAAA,KAAK,WAAW;AAChB,YAAA,KAAK,aAAa;AAClB,YAAA,KAAK,eAAe;AACpB,YAAA,KAAK,cAAc;AACnB,YAAA,KAAK,QAAQ;AACb,YAAA,KAAK,UAAU;;gBAEb;AAEF,YAAA,KAAK,YAAY;gBACf,MAAM,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,oBAAoB,CAAC,KAAK,CAAC,CAAC;gBACtD;AAEF,YAAA,KAAK,MAAM;gBACT,MAAM,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC;gBACjD;AAEF,YAAA,KAAK,QAAQ;;gBAEX;AAEF,YAAA,KAAK,OAAO;gBACV,MAAM,IAAI,CAAC,SAAS,CAAC,CAAA,QAAA,EAAW,KAAK,CAAC,OAAO,CAAA,CAAE,CAAC;gBAChD;YAEF,KAAK,MAAM,EAAE;gBACX,MAAM,IAAI,GAAG,KAAK,CAAC,YAAY,KAAK,SAAS,GAAG,CAAA,OAAA,EAAU,KAAK,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE;gBAC9F,MAAM,IAAI,CAAC,SAAS,CAClB,mBAAmB,KAAK,CAAC,UAAU,IAAI,SAAS,YAAY,KAAK,CAAC,QAAQ,IAAI,SAAS,GAAG,IAAI,CAAA,CAAE,CACjG;gBACD;YACF;AAEA,YAAA,KAAK,MAAM;;gBAET;YAEF,KAAK,cAAc,EAAE;gBACnB,MAAM,OAAO,GAAG,IAAI,CAAC,oBAAoB,CAAC,KAAK,CAAC,OAAO,CAAC;gBACxD,IAAI,OAAO,EAAE;AACX,oBAAA,MAAM,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC;gBAC/B;gBACA;YACF;AAEA,YAAA;;gBAEE;;IAEN;AAEQ,IAAA,MAAM,MAAM,CAAC,MAA0B,EAAE,OAAgB,EAAA;AAC/D,QAAA,IAAI,CAAC,IAAI,CAAC,UAAU,IAAI,CAAC,IAAI,CAAC,UAAU,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE;YACxD;QACF;QAEA,IAAI,OAAO,EAAE;AACX,YAAA,IAAI,OAAO,KAAK,IAAI,CAAC,YAAY,EAAE;AACjC,gBAAA,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC;AAC5B,gBAAA,IAAI,CAAC,YAAY,GAAG,OAAO;YAC7B;YACA,MAAM,CAAC,UAAU,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC;QAC/C;AAEA,QAAA,IAAI;AACF,YAAA,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,kBAAkB,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,UAAU,EAAE,MAAM,CAAC;;AAE7F,YAAA,IAAI,CAAC,cAAc,GAAG,MAAM;AAC5B,YAAA,IAAI,MAAM,CAAC,UAAU,KAAK,SAAS,IAAI,MAAM,CAAC,UAAU,KAAK,IAAI,EAAE;gBACjE,IAAI,CAAC,SAAS,GAAG,MAAM,CAAC,UAAU,GAAG,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE;YACzE;QACF;QAAE,OAAO,KAAK,EAAE;AACd,YAAA,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,uCAAuC,EAAE;gBACxD,MAAM,EAAE,IAAI,CAAC,MAAM;gBACnB,UAAU,EAAE,IAAI,CAAC,UAAU;gBAC3B,KAAK,EAAG,KAAe,CAAC,OAAO;AAChC,aAAA,CAAC;QACJ;IACF;AAEQ,IAAA,oBAAoB,CAAC,OAAgB,EAAA;QAC3C,IAAI,CAAC,OAAO,EAAE;AACZ,YAAA,OAAO,IAAI;QACb;AACA,QAAA,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,EAAE;QAC9B,IAAI,CAAC,OAAO,EAAE;AACZ,YAAA,OAAO,IAAI;QACb;QAEA,MAAM,eAAe,GAAG,OAAO,CAAC,KAAK,CAAC,yCAAyC,CAAC;QAChF,IAAI,eAAe,EAAE;AACnB,YAAA,OAAO,wBAAwB,eAAe,CAAC,CAAC,CAAC,EAAE;QACrD;AAEA,QAAA,IAAI,2BAA2B,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE;AAC7C,YAAA,OAAO,qBAAqB;QAC9B;QAEA,MAAM,SAAS,GAAG,OAAO,CAAC,KAAK,CAAC,oCAAoC,CAAC;QACrE,IAAI,SAAS,EAAE;YACb,OAAO,CAAA,OAAA,EAAU,SAAS,CAAC,CAAC,CAAC,IAAI,SAAS,CAAC,CAAC,CAAC,CAAA,CAAE;QACjD;QAEA,MAAM,cAAc,GAAG,OAAO,CAAC,KAAK,CAAC,qBAAqB,CAAC;QAC3D,IAAI,cAAc,EAAE;AAClB,YAAA,OAAO,gBAAgB,cAAc,CAAC,CAAC,CAAC,EAAE;QAC5C;AAEA,QAAA,IAAI,iDAAiD,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE;AACnE,YAAA,OAAO,uCAAuC;QAChD;AAEA,QAAA,IAAI,iCAAiC,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE;AACnD,YAAA,OAAO,sCAAsC;QAC/C;AAEA,QAAA,IAAI,oBAAoB,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE;AACtC,YAAA,OAAO,IAAI;QACb;AAEA,QAAA,IAAI,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC,EAAE;AACpC,YAAA,OAAO,IAAI;QACb;AAEA,QAAA,IAAI,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE;AAC1D,YAAA,OAAO,IAAI;QACb;AAEA,QAAA,IAAI,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE;AACzB,YAAA,OAAO,IAAI;QACb;QAEA,IAAI,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE;AAClC,YAAA,OAAO,IAAI;QACb;QAEA,MAAM,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC;QAC/C,MAAM,MAAM,GAAG,GAAG;QAClB,IAAI,CAAC,UAAU,EAAE;AACf,YAAA,OAAO,IAAI;QACb;QACA,MAAM,OAAO,GAAG,UAAU,CAAC,MAAM,GAAG,MAAM,GAAG,CAAA,EAAG,UAAU,CAAC,KAAK,CAAC,CAAC,EAAE,MAAM,CAAC,GAAG,GAAG,UAAU;QAC3F,OAAO,CAAA,OAAA,EAAU,OAAO,CAAA,CAAE;IAC5B;AAEQ,IAAA,oBAAoB,CAAC,KAAkD,EAAA;AAC7E,QAAA,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,KAAK,SAAS,GAAG,CAAA,EAAA,EAAK,KAAK,CAAC,KAAK,CAAA,OAAA,CAAS,GAAG,EAAE;AACvE,QAAA,OAAO,gBAAgB,KAAK,CAAC,IAAI,CAAA,EAAG,IAAI,EAAE;IAC5C;AAEQ,IAAA,eAAe,CAAC,KAA4C,EAAA;AAClE,QAAA,MAAM,OAAO,GAAG,KAAK,CAAC;AACpB,cAAE,KAAK,CAAC,OAAO,CAAC,IAAI;AACpB,cAAE,IAAI,CAAC,iBAAiB,CAAC,KAAK,CAAC,KAAK,IAAI,EAAE,EAAE,GAAG,CAAC;AAClD,QAAA,OAAO,UAAU,KAAK,CAAC,IAAI,CAAA,EAAG,OAAO,GAAG,CAAA,GAAA,EAAM,OAAO,CAAA,CAAE,GAAG,EAAE,EAAE;IAChE;AAEQ,IAAA,iBAAiB,CAAC,IAAY,EAAE,GAAG,GAAG,GAAG,EAAA;QAC/C,IAAI,CAAC,IAAI,EAAE;AACT,YAAA,OAAO,EAAE;QACX;AACA,QAAA,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE;QAChD,OAAO,OAAO,CAAC,MAAM,GAAG,GAAG,GAAG,CAAA,EAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,GAAG,OAAO;IACrE;AACD;;;;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@posthog/agent",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "TypeScript agent framework wrapping Claude Agent SDK with Git-based workflow for PostHog tasks",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
package/src/agent.ts CHANGED
@@ -14,6 +14,7 @@ import { AgentRegistry } from './agent-registry.js';
14
14
  import { WorkflowRegistry } from './workflow-registry.js';
15
15
  import { StageExecutor } from './stage-executor.js';
16
16
  import { PromptBuilder } from './prompt-builder.js';
17
+ import { TaskProgressReporter } from './task-progress-reporter.js';
17
18
 
18
19
  export class Agent {
19
20
  private workingDirectory: string;
@@ -28,6 +29,7 @@ export class Agent {
28
29
  private agentRegistry: AgentRegistry;
29
30
  private workflowRegistry: WorkflowRegistry;
30
31
  private stageExecutor: StageExecutor;
32
+ private progressReporter: TaskProgressReporter;
31
33
  public debug: boolean;
32
34
 
33
35
  constructor(config: AgentConfig = {}) {
@@ -64,6 +66,8 @@ export class Agent {
64
66
  logger: this.logger.child('PromptBuilder')
65
67
  });
66
68
  this.stageExecutor = new StageExecutor(this.agentRegistry, this.logger, promptBuilder);
69
+ this.stageExecutor.setEventHandler((event) => this.emitEvent(event));
70
+ this.progressReporter = new TaskProgressReporter(this.posthogAPI, this.logger);
67
71
  }
68
72
 
69
73
  /**
@@ -82,6 +86,7 @@ export class Agent {
82
86
  if (!workflow) {
83
87
  throw new Error(`Workflow ${workflowId} not found`);
84
88
  }
89
+ const orderedStages = [...workflow.stages].sort((a, b) => a.position - b.position);
85
90
 
86
91
  // Ensure task is assigned to workflow and positioned at first stage
87
92
  if (this.posthogAPI) {
@@ -103,9 +108,13 @@ export class Agent {
103
108
  const executionId = this.taskManager.generateExecutionId();
104
109
  this.logger.info('Starting workflow execution', { taskId: task.id, workflowId, executionId });
105
110
  this.taskManager.startExecution(task.id, 'plan_and_build', executionId);
111
+ await this.progressReporter.start(task.id, {
112
+ workflowId,
113
+ workflowRunId: executionId,
114
+ totalSteps: orderedStages.length,
115
+ });
106
116
 
107
117
  try {
108
- const orderedStages = [...workflow.stages].sort((a, b) => a.position - b.position);
109
118
  let startIndex = 0;
110
119
  const currentStageId = (task as any).current_stage as string | undefined;
111
120
 
@@ -114,7 +123,10 @@ export class Agent {
114
123
  const currIdx = orderedStages.findIndex(s => s.id === currentStageId);
115
124
  const atLastStage = currIdx >= 0 && currIdx === orderedStages.length - 1;
116
125
  if (atLastStage) {
117
- this.emitEvent(this.eventTransformer.createStatusEvent('no_next_stage', { stage: orderedStages[currIdx].key }));
126
+ const finalStageKey = orderedStages[currIdx]?.key;
127
+ this.emitEvent(this.eventTransformer.createStatusEvent('no_next_stage', { stage: finalStageKey }));
128
+ await this.progressReporter.noNextStage(finalStageKey);
129
+ await this.progressReporter.complete();
118
130
  this.taskManager.completeExecution(executionId, { task, workflow });
119
131
  return { task, workflow };
120
132
  }
@@ -135,7 +147,9 @@ export class Agent {
135
147
 
136
148
  for (let i = startIndex; i < orderedStages.length; i++) {
137
149
  const stage = orderedStages[i];
150
+ await this.progressReporter.stageStarted(stage.key, i);
138
151
  await this.executeStage(task, stage, options);
152
+ await this.progressReporter.stageCompleted(stage.key, i + 1);
139
153
  if (options.autoProgress) {
140
154
  const hasNext = i < orderedStages.length - 1;
141
155
  if (hasNext) {
@@ -143,9 +157,11 @@ export class Agent {
143
157
  }
144
158
  }
145
159
  }
160
+ await this.progressReporter.complete();
146
161
  this.taskManager.completeExecution(executionId, { task, workflow });
147
162
  return { task, workflow };
148
163
  } catch (error) {
164
+ await this.progressReporter.fail(error as Error);
149
165
  this.taskManager.failExecution(executionId, error as Error);
150
166
  throw error;
151
167
  }
@@ -167,10 +183,12 @@ export class Agent {
167
183
  const planningBranch = await this.createPlanningBranch(task.id);
168
184
  await this.updateTaskBranch(task.id, planningBranch);
169
185
  this.emitEvent(this.eventTransformer.createStatusEvent('branch_created', { stage: stage.key, branch: planningBranch }));
186
+ await this.progressReporter.branchCreated(stage.key, planningBranch);
170
187
  } else if (!isPlanning && !isManual && shouldCreateImplBranch) {
171
188
  const implBranch = await this.createImplementationBranch(task.id);
172
189
  await this.updateTaskBranch(task.id, implBranch);
173
190
  this.emitEvent(this.eventTransformer.createStatusEvent('branch_created', { stage: stage.key, branch: implBranch }));
191
+ await this.progressReporter.branchCreated(stage.key, implBranch);
174
192
  }
175
193
 
176
194
  const result = await this.stageExecutor.execute(task, stage, options);
@@ -179,6 +197,7 @@ export class Agent {
179
197
  await this.writePlan(task.id, result.plan);
180
198
  await this.commitPlan(task.id, task.title);
181
199
  this.emitEvent(this.eventTransformer.createStatusEvent('commit_made', { stage: stage.key, kind: 'plan' }));
200
+ await this.progressReporter.commitMade(stage.key, 'plan');
182
201
  }
183
202
 
184
203
  if (isManual) {
@@ -193,12 +212,14 @@ export class Agent {
193
212
  await this.updateTaskBranch(task.id, implBranch);
194
213
  branchName = implBranch;
195
214
  this.emitEvent(this.eventTransformer.createStatusEvent('branch_created', { stage: stage.key, branch: implBranch }));
215
+ await this.progressReporter.branchCreated(stage.key, implBranch);
196
216
  }
197
217
  try {
198
218
  const prUrl = await this.createPullRequest(task.id, branchName, task.title, task.description);
199
219
  await this.updateTaskBranch(task.id, branchName);
200
- await this.attachPullRequestToTask(task.id, prUrl);
220
+ await this.attachPullRequestToTask(task.id, prUrl, branchName);
201
221
  this.emitEvent(this.eventTransformer.createStatusEvent('pr_created', { stage: stage.key, prUrl }));
222
+ await this.progressReporter.pullRequestCreated(stage.key, prUrl);
202
223
  } catch {}
203
224
  }
204
225
  // Do not auto-progress on manual stages
@@ -211,6 +232,7 @@ export class Agent {
211
232
  const planSummary = existingPlan ? existingPlan.split('\n')[0] : undefined;
212
233
  await this.commitImplementation(task.id, task.title, planSummary);
213
234
  this.emitEvent(this.eventTransformer.createStatusEvent('commit_made', { stage: stage.key, kind: 'implementation' }));
235
+ await this.progressReporter.commitMade(stage.key, 'implementation');
214
236
  }
215
237
 
216
238
  // PR creation on complete stage (or if explicitly requested), regardless of whether edits occurred
@@ -222,8 +244,9 @@ export class Agent {
222
244
  try {
223
245
  const prUrl = await this.createPullRequest(task.id, branchName, task.title, task.description);
224
246
  await this.updateTaskBranch(task.id, branchName);
225
- await this.attachPullRequestToTask(task.id, prUrl);
247
+ await this.attachPullRequestToTask(task.id, prUrl, branchName);
226
248
  this.emitEvent(this.eventTransformer.createStatusEvent('pr_created', { stage: stage.key, prUrl }));
249
+ await this.progressReporter.pullRequestCreated(stage.key, prUrl);
227
250
  } catch {}
228
251
  }
229
252
  }
@@ -271,6 +294,10 @@ export class Agent {
271
294
  }
272
295
  return this.posthogAPI.fetchTask(taskId);
273
296
  }
297
+
298
+ getPostHogClient(): PostHogAPIClient | undefined {
299
+ return this.posthogAPI;
300
+ }
274
301
 
275
302
  async listTasks(filters?: {
276
303
  repository?: string;
@@ -367,8 +394,8 @@ Generated by PostHog Agent`;
367
394
  return prUrl;
368
395
  }
369
396
 
370
- async attachPullRequestToTask(taskId: string, prUrl: string): Promise<void> {
371
- this.logger.info('Attaching PR to task', { taskId, prUrl });
397
+ async attachPullRequestToTask(taskId: string, prUrl: string, branchName?: string): Promise<void> {
398
+ this.logger.info('Attaching PR to task', { taskId, prUrl, branchName });
372
399
 
373
400
  if (!this.posthogAPI) {
374
401
  const error = new Error('PostHog API not configured. Cannot attach PR to task.');
@@ -376,7 +403,7 @@ Generated by PostHog Agent`;
376
403
  throw error;
377
404
  }
378
405
 
379
- await this.posthogAPI.updateTask(taskId, { github_pr_url: prUrl });
406
+ await this.posthogAPI.attachTaskPullRequest(taskId, prUrl, branchName);
380
407
  this.logger.debug('PR attached to task', { taskId, prUrl });
381
408
  }
382
409
 
@@ -389,7 +416,7 @@ Generated by PostHog Agent`;
389
416
  throw error;
390
417
  }
391
418
 
392
- await this.posthogAPI.updateTask(taskId, { github_branch: branchName });
419
+ await this.posthogAPI.setTaskBranch(taskId, branchName);
393
420
  this.logger.debug('Task branch updated', { taskId, branchName });
394
421
  }
395
422
 
@@ -419,6 +446,12 @@ Generated by PostHog Agent`;
419
446
  // Log all events except tokens (too verbose)
420
447
  this.logger.debug('Emitting event', { type: event.type, ts: event.ts });
421
448
  }
449
+ const persistPromise = this.progressReporter.recordEvent(event);
450
+ if (persistPromise && typeof persistPromise.then === 'function') {
451
+ persistPromise.catch((error: Error) =>
452
+ this.logger.debug('Failed to persist agent event', { message: error.message })
453
+ );
454
+ }
422
455
  this.onEvent?.(event);
423
456
  }
424
457
  }
@@ -118,15 +118,14 @@ export class EventTransformer {
118
118
 
119
119
  // Handle user messages
120
120
  if (sdkMessage.type === 'user') {
121
- const content = sdkMessage.message.content;
122
- const textContent = Array.isArray(content)
123
- ? content.find(c => c.type === 'text')?.text
124
- : typeof content === 'string' ? content : '';
125
-
121
+ const textContent = this.extractUserContent(sdkMessage.message?.content);
122
+ if (!textContent) {
123
+ return null;
124
+ }
126
125
  return {
127
126
  ...baseEvent,
128
127
  type: 'user_message',
129
- content: textContent || '',
128
+ content: textContent,
130
129
  isSynthetic: sdkMessage.isSynthetic
131
130
  };
132
131
  }
@@ -186,4 +185,59 @@ export class EventTransformer {
186
185
  ...additionalData
187
186
  };
188
187
  }
189
- }
188
+
189
+ private extractUserContent(content: unknown): string | null {
190
+ if (!content) {
191
+ return null;
192
+ }
193
+
194
+ if (typeof content === 'string') {
195
+ const trimmed = content.trim();
196
+ return trimmed.length > 0 ? trimmed : null;
197
+ }
198
+
199
+ if (Array.isArray(content)) {
200
+ const parts: string[] = [];
201
+ for (const block of content) {
202
+ const extracted = this.extractUserContent(block);
203
+ if (extracted) {
204
+ parts.push(extracted);
205
+ } else if (block && typeof block === 'object') {
206
+ const candidate = this.extractFromObject(block as Record<string, unknown>);
207
+ if (candidate) {
208
+ parts.push(candidate);
209
+ }
210
+ }
211
+ }
212
+ const text = parts.join('\n').trim();
213
+ return text.length > 0 ? text : null;
214
+ }
215
+
216
+ if (typeof content === 'object') {
217
+ return this.extractFromObject(content as Record<string, unknown>);
218
+ }
219
+
220
+ return null;
221
+ }
222
+
223
+ private extractFromObject(value: Record<string, unknown>): string | null {
224
+ const preferredKeys = ['text', 'input_text', 'input', 'markdown', 'content', 'message'];
225
+ for (const key of preferredKeys) {
226
+ if (typeof value[key] === 'string') {
227
+ const trimmed = (value[key] as string).trim();
228
+ if (trimmed.length > 0) {
229
+ return trimmed;
230
+ }
231
+ }
232
+ }
233
+
234
+ for (const entry of Object.values(value)) {
235
+ const extracted = this.extractUserContent(entry);
236
+ if (extracted) {
237
+ return extracted;
238
+ }
239
+ }
240
+
241
+ return null;
242
+ }
243
+ }
@@ -26,6 +26,36 @@ interface TaskProgressResponse {
26
26
  message?: string;
27
27
  }
28
28
 
29
+ export interface TaskProgressRecord {
30
+ id: string;
31
+ task: string;
32
+ status: "started" | "in_progress" | "completed" | "failed";
33
+ current_step?: string | null;
34
+ completed_steps?: number | null;
35
+ total_steps?: number | null;
36
+ progress_percentage?: number | null;
37
+ output_log?: string | null;
38
+ error_message?: string | null;
39
+ workflow_id?: string | null;
40
+ workflow_run_id?: string | null;
41
+ activity_id?: string | null;
42
+ created_at: string;
43
+ updated_at: string;
44
+ completed_at?: string | null;
45
+ }
46
+
47
+ export interface TaskProgressUpdate {
48
+ status?: TaskProgressRecord["status"];
49
+ current_step?: string | null;
50
+ completed_steps?: number | null;
51
+ total_steps?: number | null;
52
+ output_log?: string | null;
53
+ error_message?: string | null;
54
+ workflow_id?: string | null;
55
+ workflow_run_id?: string | null;
56
+ activity_id?: string | null;
57
+ }
58
+
29
59
  export class PostHogAPIClient {
30
60
  private config: PostHogAPIConfig;
31
61
  private _teamId: number | null = null;
@@ -137,11 +167,60 @@ export class PostHogAPIClient {
137
167
  });
138
168
  }
139
169
 
170
+ async setTaskBranch(taskId: string, branch: string): Promise<Task> {
171
+ const teamId = await this.getTeamId();
172
+ return this.apiRequest<Task>(`/api/projects/${teamId}/tasks/${taskId}/set_branch/`, {
173
+ method: "POST",
174
+ body: JSON.stringify({ branch }),
175
+ });
176
+ }
177
+
178
+ async attachTaskPullRequest(taskId: string, prUrl: string, branch?: string): Promise<Task> {
179
+ const teamId = await this.getTeamId();
180
+ const payload: Record<string, string> = { pr_url: prUrl };
181
+ if (branch) {
182
+ payload.branch = branch;
183
+ }
184
+ return this.apiRequest<Task>(`/api/projects/${teamId}/tasks/${taskId}/attach_pr/`, {
185
+ method: "POST",
186
+ body: JSON.stringify(payload),
187
+ });
188
+ }
189
+
140
190
  async getTaskProgress(taskId: string): Promise<TaskProgressResponse> {
141
191
  const teamId = await this.getTeamId();
142
192
  return this.apiRequest<TaskProgressResponse>(`/api/projects/${teamId}/tasks/${taskId}/progress/`);
143
193
  }
144
194
 
195
+ async createTaskProgress(
196
+ taskId: string,
197
+ payload: TaskProgressUpdate & { status: TaskProgressRecord["status"] }
198
+ ): Promise<TaskProgressRecord> {
199
+ const teamId = await this.getTeamId();
200
+ return this.apiRequest<TaskProgressRecord>(`/api/projects/${teamId}/task_progress/`, {
201
+ method: "POST",
202
+ body: JSON.stringify({
203
+ ...payload,
204
+ task: taskId,
205
+ }),
206
+ });
207
+ }
208
+
209
+ async updateTaskProgress(
210
+ taskId: string,
211
+ progressId: string,
212
+ payload: TaskProgressUpdate
213
+ ): Promise<TaskProgressRecord> {
214
+ const teamId = await this.getTeamId();
215
+ return this.apiRequest<TaskProgressRecord>(`/api/projects/${teamId}/task_progress/${progressId}/`, {
216
+ method: "PATCH",
217
+ body: JSON.stringify({
218
+ ...payload,
219
+ task: taskId,
220
+ }),
221
+ });
222
+ }
223
+
145
224
  // Workflow endpoints
146
225
  async fetchWorkflow(workflowId: string): Promise<WorkflowDefinition> {
147
226
  const teamId = await this.getTeamId();