11const std = @import ("std" );
2+ const builtin = @import ("builtin" );
23
34pub fn run (allocator : std.mem.Allocator , args : []const []const u8 ) ! void {
45 if (args .len == 0 ) {
@@ -33,6 +34,17 @@ pub fn run(allocator: std.mem.Allocator, args: []const []const u8) !void {
3334 const tracker_agent_id = getString (obj , "tracker_agent_id" ) orelse getString (obj , "instance_name" ) orelse "nullboiler" ;
3435 const tracker_success_trigger = getString (obj , "tracker_success_trigger" ) orelse "complete" ;
3536 const tracker_max_concurrent_tasks = getU32 (obj , "tracker_max_concurrent_tasks" ) orelse 1 ;
37+ const tracker_poll_interval_ms = getU32 (obj , "tracker_poll_interval_ms" ) orelse 10000 ;
38+ const tracker_lease_ttl_ms = getU32 (obj , "tracker_lease_ttl_ms" ) orelse 60000 ;
39+ const tracker_heartbeat_interval_ms = getU32 (obj , "tracker_heartbeat_interval_ms" ) orelse 30000 ;
40+ const tracker_stall_timeout_ms = getU32 (obj , "tracker_stall_timeout_ms" ) orelse 300000 ;
41+ const tracker_workspace_root = getString (obj , "tracker_workspace_root" ) orelse "workspaces" ;
42+ const tracker_subprocess_command = getString (obj , "tracker_subprocess_command" ) orelse "nullclaw" ;
43+ const tracker_subprocess_base_port = getU16 (obj , "tracker_subprocess_base_port" ) orelse 9200 ;
44+ const tracker_subprocess_health_check_retries = getU32 (obj , "tracker_subprocess_health_check_retries" ) orelse 10 ;
45+ const tracker_subprocess_max_turns = getU32 (obj , "tracker_subprocess_max_turns" ) orelse 20 ;
46+ const tracker_subprocess_turn_timeout_ms = getU32 (obj , "tracker_subprocess_turn_timeout_ms" ) orelse 600000 ;
47+ const tracker_subprocess_continuation_prompt = getString (obj , "tracker_subprocess_continuation_prompt" ) orelse "Continue working on this task. Your previous context is preserved." ;
3648 const workflows_dir = "workflows" ;
3749
3850 if (tracker_enabled and tracker_url != null and tracker_pipeline_id == null ) {
@@ -52,10 +64,22 @@ pub fn run(allocator: std.mem.Allocator, args: []const []const u8) !void {
5264 .concurrency = .{
5365 .max_concurrent_tasks = tracker_max_concurrent_tasks ,
5466 },
55- .poll_interval_ms = 5000 ,
56- .lease_ttl_ms = 120000 ,
57- .heartbeat_interval_ms = 30000 ,
67+ .poll_interval_ms = tracker_poll_interval_ms ,
68+ .stall_timeout_ms = tracker_stall_timeout_ms ,
69+ .lease_ttl_ms = tracker_lease_ttl_ms ,
70+ .heartbeat_interval_ms = tracker_heartbeat_interval_ms ,
5871 .workflows_dir = workflows_dir ,
72+ .workspace = .{
73+ .root = tracker_workspace_root ,
74+ },
75+ .subprocess = .{
76+ .command = tracker_subprocess_command ,
77+ .base_port = tracker_subprocess_base_port ,
78+ .health_check_retries = tracker_subprocess_health_check_retries ,
79+ .max_turns = tracker_subprocess_max_turns ,
80+ .turn_timeout_ms = tracker_subprocess_turn_timeout_ms ,
81+ .continuation_prompt = tracker_subprocess_continuation_prompt ,
82+ },
5983 },
6084 }, .{ .whitespace = .indent_2 , .emit_null_optional_fields = false })
6185 else
@@ -79,8 +103,10 @@ pub fn run(allocator: std.mem.Allocator, args: []const []const u8) !void {
79103 try writeFileAtHome (allocator , home , workflow_rel_path , workflow_json );
80104 }
81105
82- const stdout = std .fs .File .stdout ();
83- try stdout .writeAll ("{\" status\" :\" ok\" }\n " );
106+ if (! builtin .is_test ) {
107+ const stdout = std .fs .File .stdout ();
108+ try stdout .writeAll ("{\" status\" :\" ok\" }\n " );
109+ }
84110}
85111
86112fn buildDefaultWorkflow (
@@ -203,3 +229,131 @@ fn sanitizeFileComponent(allocator: std.mem.Allocator, value: []const u8) ![]con
203229 }
204230 return out ;
205231}
232+
233+ test "run writes tracker config and workflow with advanced settings" {
234+ var tmp = std .testing .tmpDir (.{});
235+ defer tmp .cleanup ();
236+
237+ const home = try tmp .dir .realpathAlloc (std .testing .allocator , "." );
238+ defer std .testing .allocator .free (home );
239+
240+ var arena = std .heap .ArenaAllocator .init (std .testing .allocator );
241+ defer arena .deinit ();
242+
243+ const input = try std .json .Stringify .valueAlloc (arena .allocator (), .{
244+ .home = home ,
245+ .port = 9091 ,
246+ .db_path = "tracker.db" ,
247+ .api_token = "local-token" ,
248+ .instance_name = "boiler-a" ,
249+ .tracker_enabled = "true" ,
250+ .tracker_url = "http://127.0.0.1:7700" ,
251+ .tracker_api_token = "tracker-secret" ,
252+ .tracker_pipeline_id = "pipeline.dev" ,
253+ .tracker_claim_role = "reviewer" ,
254+ .tracker_success_trigger = "ship" ,
255+ .tracker_max_concurrent_tasks = "2" ,
256+ .tracker_poll_interval_ms = "15000" ,
257+ .tracker_lease_ttl_ms = "90000" ,
258+ .tracker_heartbeat_interval_ms = "45000" ,
259+ .tracker_stall_timeout_ms = "123000" ,
260+ .tracker_workspace_root = "workspaces" ,
261+ .tracker_subprocess_command = "nullclaw" ,
262+ .tracker_subprocess_base_port = "9300" ,
263+ .tracker_subprocess_health_check_retries = "7" ,
264+ .tracker_subprocess_max_turns = "11" ,
265+ .tracker_subprocess_turn_timeout_ms = "700000" ,
266+ .tracker_subprocess_continuation_prompt = "Keep going." ,
267+ }, .{});
268+
269+ try run (arena .allocator (), &.{input });
270+
271+ const config_path = try std .fs .path .join (std .testing .allocator , &.{ home , "config.json" });
272+ defer std .testing .allocator .free (config_path );
273+ const config_file = try std .fs .openFileAbsolute (config_path , .{});
274+ defer config_file .close ();
275+ const config_bytes = try config_file .readToEndAlloc (arena .allocator (), 64 * 1024 );
276+
277+ const ConfigFile = struct {
278+ port : u16 ,
279+ db : []const u8 ,
280+ api_token : ? []const u8 = null ,
281+ tracker : struct {
282+ url : []const u8 ,
283+ api_token : ? []const u8 = null ,
284+ agent_id : []const u8 ,
285+ poll_interval_ms : u32 ,
286+ stall_timeout_ms : u32 ,
287+ lease_ttl_ms : u32 ,
288+ heartbeat_interval_ms : u32 ,
289+ workflows_dir : []const u8 ,
290+ concurrency : struct {
291+ max_concurrent_tasks : u32 ,
292+ },
293+ workspace : struct {
294+ root : []const u8 ,
295+ },
296+ subprocess : struct {
297+ command : []const u8 ,
298+ base_port : u16 ,
299+ health_check_retries : u32 ,
300+ max_turns : u32 ,
301+ turn_timeout_ms : u32 ,
302+ continuation_prompt : []const u8 ,
303+ },
304+ },
305+ };
306+
307+ const parsed_cfg = try std .json .parseFromSlice (ConfigFile , arena .allocator (), config_bytes , .{
308+ .allocate = .alloc_always ,
309+ .ignore_unknown_fields = true ,
310+ });
311+ defer parsed_cfg .deinit ();
312+
313+ try std .testing .expectEqual (@as (u16 , 9091 ), parsed_cfg .value .port );
314+ try std .testing .expectEqualStrings ("tracker.db" , parsed_cfg .value .db );
315+ try std .testing .expectEqualStrings ("local-token" , parsed_cfg .value .api_token .? );
316+ try std .testing .expectEqualStrings ("http://127.0.0.1:7700" , parsed_cfg .value .tracker .url );
317+ try std .testing .expectEqualStrings ("tracker-secret" , parsed_cfg .value .tracker .api_token .? );
318+ try std .testing .expectEqualStrings ("boiler-a" , parsed_cfg .value .tracker .agent_id );
319+ try std .testing .expectEqual (@as (u32 , 2 ), parsed_cfg .value .tracker .concurrency .max_concurrent_tasks );
320+ try std .testing .expectEqual (@as (u32 , 15000 ), parsed_cfg .value .tracker .poll_interval_ms );
321+ try std .testing .expectEqual (@as (u32 , 123000 ), parsed_cfg .value .tracker .stall_timeout_ms );
322+ try std .testing .expectEqual (@as (u32 , 90000 ), parsed_cfg .value .tracker .lease_ttl_ms );
323+ try std .testing .expectEqual (@as (u32 , 45000 ), parsed_cfg .value .tracker .heartbeat_interval_ms );
324+ try std .testing .expectEqualStrings ("workflows" , parsed_cfg .value .tracker .workflows_dir );
325+ try std .testing .expectEqualStrings ("workspaces" , parsed_cfg .value .tracker .workspace .root );
326+ try std .testing .expectEqualStrings ("nullclaw" , parsed_cfg .value .tracker .subprocess .command );
327+ try std .testing .expectEqual (@as (u16 , 9300 ), parsed_cfg .value .tracker .subprocess .base_port );
328+ try std .testing .expectEqual (@as (u32 , 7 ), parsed_cfg .value .tracker .subprocess .health_check_retries );
329+ try std .testing .expectEqual (@as (u32 , 11 ), parsed_cfg .value .tracker .subprocess .max_turns );
330+ try std .testing .expectEqual (@as (u32 , 700000 ), parsed_cfg .value .tracker .subprocess .turn_timeout_ms );
331+ try std .testing .expectEqualStrings ("Keep going." , parsed_cfg .value .tracker .subprocess .continuation_prompt );
332+
333+ const workflow_path = try std .fs .path .join (std .testing .allocator , &.{ home , "workflows" , "pipeline.dev.json" });
334+ defer std .testing .allocator .free (workflow_path );
335+ const workflow_file = try std .fs .openFileAbsolute (workflow_path , .{});
336+ defer workflow_file .close ();
337+ const workflow_bytes = try workflow_file .readToEndAlloc (arena .allocator (), 16 * 1024 );
338+
339+ const WorkflowFile = struct {
340+ pipeline_id : []const u8 ,
341+ claim_roles : []const []const u8 ,
342+ execution : []const u8 ,
343+ on_success : struct {
344+ transition_to : []const u8 ,
345+ },
346+ };
347+
348+ const parsed_workflow = try std .json .parseFromSlice (WorkflowFile , arena .allocator (), workflow_bytes , .{
349+ .allocate = .alloc_always ,
350+ .ignore_unknown_fields = true ,
351+ });
352+ defer parsed_workflow .deinit ();
353+
354+ try std .testing .expectEqualStrings ("pipeline.dev" , parsed_workflow .value .pipeline_id );
355+ try std .testing .expectEqual (@as (usize , 1 ), parsed_workflow .value .claim_roles .len );
356+ try std .testing .expectEqualStrings ("reviewer" , parsed_workflow .value .claim_roles [0 ]);
357+ try std .testing .expectEqualStrings ("subprocess" , parsed_workflow .value .execution );
358+ try std .testing .expectEqualStrings ("ship" , parsed_workflow .value .on_success .transition_to );
359+ }
0 commit comments