@@ -25,14 +25,24 @@ pub const AgentStep = struct {
2525 issue_number : u32 ,
2626 step_name : []const u8 ,
2727 step_type : StepType ,
28- thought : ? []const u8 = null ,
2928 action : ? []const u8 = null ,
29+ labels : ? []const []const u8 = null ,
30+ files : ? []const []const u8 = null ,
31+ metrics : ? Metrics = null ,
32+ thought : ? []const u8 = null ,
3033 result : ? []const u8 = null ,
3134 error_message : ? []const u8 = null ,
3235 timestamp : i64 = 0 ,
36+
37+ pub const Metrics = struct {
38+ status : ? []const u8 = null ,
39+ files_changed : ? u32 = null ,
40+ lines_added : ? u32 = null ,
41+ files_touched : ? u32 = null ,
42+ };
3343};
3444
35- /// Log an agent step to Queen JSONL format
45+ /// Log an agent step to Queen JSONL format (proper JSON with escaping)
3646pub fn logStep (allocator : Allocator , step : AgentStep ) ! void {
3747 const logs_dir = ".trinity/logs" ;
3848 std .fs .cwd ().makePath (logs_dir ) catch {};
@@ -70,43 +80,130 @@ pub fn logStep(allocator: Allocator, step: AgentStep) !void {
7080 step .step_name ,
7181 });
7282
73- // Build JSON manually (Zig 0.15 compatible)
74- var buf = try std .ArrayList (u8 ).initCapacity (allocator , 256 );
83+ // Build JSON with proper escaping
84+ var buf = try std .ArrayList (u8 ).initCapacity (allocator , 1024 );
7585 defer buf .deinit (allocator );
7686 const w = buf .writer (allocator );
7787
7888 try w .writeAll ("{" );
79- try w .print ("\" episode_id\" :\" {s}\" ," , .{episode_id });
80- try w .print ("\" agent\" :\" {s}\" ," , .{step .agent });
89+ try w .writeAll ("\" episode_id\" :\" " );
90+ try w .writeAll (escapeString (allocator , episode_id ));
91+ try w .writeAll ("\" ," );
92+ try w .writeAll ("\" agent\" :\" " );
93+ try w .writeAll (escapeString (allocator , step .agent ));
94+ try w .writeAll ("\" ," );
8195 try w .print ("\" episode_type\" :\" {s}\" ," , .{episode_type });
82- try w .print ("\" timestamp\" :\" {d}\" ," , .{ts });
83- try w .print ("\" title\" :\" {s}\" " , .{title });
84- try w .print ("\" correlation_id\" :\" {d}\" ," , .{step .issue_number });
96+ try w .print ("\" timestamp\" :{d}," , .{ts });
97+ try w .writeAll ("\" title\" :\" " );
98+ try w .writeAll (escapeString (allocator , title ));
99+ try w .writeAll ("\" ," );
100+ try w .print ("\" correlation_id\" :{d}," , .{step .issue_number });
85101 try w .writeAll ("\" data\" :{" );
102+
103+ // Build data object
104+ try w .writeAll ("{" );
86105 try w .print ("\" domain\" :\" github_issue\" " , .{});
87106
88107 if (step .action ) | a | {
89- try w .print (",\" action\" :\" {s}" , .{a });
108+ try w .writeAll (",\" action\" :\" " );
109+ try w .writeAll (escapeString (allocator , a ));
110+ try w .writeAll ("\" " );
111+ }
112+ if (step .labels ) | labels | {
113+ try w .writeAll (",\" labels\" :[" );
114+ for (labels , 0.. ) | label , i | {
115+ if (i > 0 ) try w .writeAll ("," );
116+ const escaped = escapeString (allocator , label );
117+ try w .writeAll ("\" " );
118+ try w .writeAll (escaped );
119+ try w .writeAll ("\" " );
120+ }
121+ try w .writeAll ("]" );
122+ }
123+ if (step .files ) | files | {
124+ try w .writeAll (",\" files\" :[" );
125+ for (files , 0.. ) | file , i | {
126+ if (i > 0 ) try w .writeAll ("," );
127+ const escaped = escapeString (allocator , file );
128+ try w .writeAll ("\" " );
129+ try w .writeAll (escaped );
130+ try w .writeAll ("\" " );
131+ }
132+ try w .writeAll ("]" );
133+ }
134+ if (step .metrics ) | m | {
135+ try w .writeAll (",\" metrics\" :{" );
136+ var need_comma = false ;
137+ if (m .status ) | s | {
138+ try w .writeAll ("\" status\" :\" " );
139+ try w .writeAll (escapeString (allocator , s ));
140+ try w .writeAll ("\" " );
141+ need_comma = true ;
142+ }
143+ if (m .files_changed ) | fc | {
144+ if (need_comma ) try w .writeAll ("," );
145+ try w .print ("\" files_changed\" :{d}" , .{fc });
146+ need_comma = true ;
147+ }
148+ if (m .lines_added ) | la | {
149+ if (need_comma ) try w .writeAll ("," );
150+ try w .print ("\" lines_added\" :{d}" , .{la });
151+ need_comma = true ;
152+ }
153+ if (m .files_touched ) | ft | {
154+ if (need_comma ) try w .writeAll ("," );
155+ try w .print ("\" files_touched\" :{d}" , .{ft });
156+ }
157+ try w .writeAll ("}" );
90158 }
91159 if (step .thought ) | t | {
92- try w .print (",\" thought\" :\" {s}" , .{t });
160+ try w .writeAll (",\" thought\" :\" " );
161+ try w .writeAll (escapeString (allocator , t ));
162+ try w .writeAll ("\" " );
93163 }
94164 if (step .result ) | r | {
95- try w .print (",\" next_step\" :\" {s}" , .{r });
165+ try w .writeAll (",\" next_step\" :\" " );
166+ try w .writeAll (escapeString (allocator , r ));
167+ try w .writeAll ("\" " );
96168 }
97169 if (step .error_message ) | e | {
98- try w .print (",\" error\" :\" {s}" , .{e });
170+ try w .writeAll (",\" error\" :\" " );
171+ try w .writeAll (escapeString (allocator , e ));
172+ try w .writeAll ("\" " );
99173 }
100174
101- try w .writeAll ("}}\n " );
175+ try w .writeAll ("}" ); // Close data object
176+ try w .writeAll ("}" ); // Close episode object
102177
103178 // Open file for append
104179 const file = try std .fs .cwd ().createFile (path , .{ .truncate = false });
105180 defer file .close ();
106181 try file .seekFromEnd (0 );
107182
108- // Write JSON
183+ // Write JSON with newline
109184 try file .writeAll (buf .items );
185+ try file .writeAll ("\n " );
186+ }
187+
188+ /// Escape JSON string (minimal: quotes, backslashes, newlines)
189+ /// Returns escaped string (caller owns memory)
190+ fn escapeString (allocator : Allocator , s : []const u8 ) []const u8 {
191+ var escaped = std .ArrayList (u8 ).initCapacity (allocator , s .len + s .len / 4 ) catch return s ;
192+ defer escaped .deinit (allocator );
193+ const w = escaped .writer (allocator );
194+
195+ for (s ) | c | {
196+ switch (c ) {
197+ '\\ ' = > w .writeAll ("\\\\ " ) catch {},
198+ '"' = > w .writeAll ("\\ \" " ) catch {},
199+ '\n ' = > w .writeAll ("\\ n" ) catch {},
200+ '\r ' = > w .writeAll ("\\ r" ) catch {},
201+ '\t ' = > w .writeAll ("\\ t" ) catch {},
202+ else = > w .writeByte (c ) catch {},
203+ }
204+ }
205+
206+ return escaped .toOwnedSlice (allocator ) catch s ;
110207}
111208
112209/// Convenience: log step start
@@ -141,3 +238,62 @@ pub fn logStepError(allocator: Allocator, agent: []const u8, issue: u32, step_na
141238 .error_message = error_msg ,
142239 });
143240}
241+
242+ // ═════════════════════════════════════════════════════════════════════════════
243+ // GitHub Episode API — for γ agent to log issue work
244+ // ═════════════════════════════════════════════════════════════════════════════
245+
246+ /// Start working on a GitHub issue
247+ pub fn logGitHubIssueStart (allocator : Allocator , agent : []const u8 , issue_number : u32 , title : []const u8 , labels : []const []const u8 ) ! void {
248+ try logStep (allocator , .{
249+ .agent = agent ,
250+ .issue_number = issue_number ,
251+ .step_name = title ,
252+ .step_type = .start ,
253+ .action = "issue.start" ,
254+ .labels = labels ,
255+ });
256+ }
257+
258+ /// Record a step within an issue
259+ pub fn logGitHubIssueStep (allocator : Allocator , agent : []const u8 , issue_number : u32 , description : []const u8 , files : []const []const u8 ) ! void {
260+ try logStep (allocator , .{
261+ .agent = agent ,
262+ .issue_number = issue_number ,
263+ .step_name = description ,
264+ .step_type = .act ,
265+ .action = "issue.step" ,
266+ .files = files ,
267+ });
268+ }
269+
270+ /// Complete an issue successfully
271+ pub fn logGitHubIssueComplete (allocator : Allocator , agent : []const u8 , issue_number : u32 , status : []const u8 , files_changed : u32 , lines_added : u32 ) ! void {
272+ try logStep (allocator , .{
273+ .agent = agent ,
274+ .issue_number = issue_number ,
275+ .step_name = "Issue complete" ,
276+ .step_type = .success ,
277+ .action = "issue.complete" ,
278+ .metrics = .{
279+ .status = status ,
280+ .files_changed = files_changed ,
281+ .lines_added = lines_added ,
282+ },
283+ });
284+ }
285+
286+ /// Log issue failure
287+ pub fn logGitHubIssueFail (allocator : Allocator , agent : []const u8 , issue_number : u32 , error_message : []const u8 , files_touched : u32 ) ! void {
288+ try logStep (allocator , .{
289+ .agent = agent ,
290+ .issue_number = issue_number ,
291+ .step_name = "Issue failed" ,
292+ .step_type = .@"error" ,
293+ .action = "issue.fail" ,
294+ .error_message = error_message ,
295+ .metrics = .{
296+ .files_touched = files_touched ,
297+ },
298+ });
299+ }
0 commit comments