space-architect 7.0.0 → 8.0.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.
@@ -178,18 +178,10 @@ module Space::Architect
178
178
  nn = s["ordinal"] ? format("%02d", s["ordinal"]) : "-"
179
179
  lane_list = s["lanes"] || []
180
180
  lanes_str = lane_list.map do |l|
181
- h = l["harness"] || "claude-code"
182
- m = l["model"] || Harness.default_model_for(h)
181
+ h = l["harness"] || "pi"
182
+ m = l["model"] || Harness::DEFAULT_MODEL
183
183
  eff = l["effort"] ? "·#{l['effort']}" : ""
184
- # #89/AC8: the resolved tool grant, visible next to the other resolved
185
- # values — only when it diverges from the harness default, to keep the
186
- # common case (no lane touches it) uncluttered. Meaningless outside
187
- # claude-code (no equivalent grant mechanism), so shown only there.
188
- tools = if h == "claude-code"
189
- resolved = Harness::ClaudeCodeHarness.resolve_tools(replace: l["allowed_tools"], append: l["append_allowed_tools"])
190
- resolved == Harness::ClaudeCodeHarness::ALLOWED_TOOLS ? "" : "·tools:#{resolved}"
191
- end
192
- "#{l['name']}(#{l['repo']}·#{h}·#{m}#{eff}#{tools})"
184
+ "#{l['name']}(#{l['repo']}·#{h}·#{m}#{eff})"
193
185
  end.join(", ")
194
186
  lanes = lane_list.any? { |l| l["variant"] } ? "variant: #{lanes_str}" : lanes_str
195
187
  lanes = "#{lanes} → winner: #{s['winner']}" if s["winner"]
@@ -262,17 +254,29 @@ module Space::Architect
262
254
 
263
255
  private
264
256
 
257
+ # I01: inline output is bounded — one identity line per gate, ≤ 20
258
+ # lines of tail output — because the full report always lands in the
259
+ # transcript #render_gate points at. The summary block is emitted last
260
+ # (after the disclaimer) so a `tail` of stdout captures it whole.
265
261
  def render_rehearsal(result)
266
262
  if result[:empty]
267
263
  reason = result[:placeholder] ? "the scaffold placeholder '#{ArchitectProject::AC1_PLACEHOLDER}' with no active gate" : "no gates drafted yet"
268
264
  terminal.say "EMPTY — #{reason}. Nothing to rehearse; the pre-freeze look is still stamped."
269
- else
270
- terminal.say rehearsal_header(result)
271
- result[:gates].each { |g| render_gate(g) }
272
- render_scope_asymmetry(result[:scope_asymmetry])
265
+ terminal.say ""
266
+ terminal.say discrimination_note
267
+ return
273
268
  end
269
+
270
+ terminal.say rehearsal_header(result)
271
+ result[:gates].each { |g| render_gate(g, result[:transcript]) }
272
+ render_scope_asymmetry(result[:scope_asymmetry])
274
273
  terminal.say ""
275
- terminal.say "Discrimination report only — this runs and reports; it never judges whether these gates are good, bad, or ready."
274
+ terminal.say discrimination_note
275
+ render_summary(result)
276
+ end
277
+
278
+ def discrimination_note
279
+ "Discrimination report only — this runs and reports; it never judges whether these gates are good, bad, or ready."
276
280
  end
277
281
 
278
282
  # #90/AC7: with a single defaultable repo, name it as before; a cross-repo
@@ -286,17 +290,45 @@ module Space::Architect
286
290
  end
287
291
  end
288
292
 
289
- def render_gate(g)
293
+ # I01/AC "bounded terminal report": identity (AC label, gate id, verdict,
294
+ # exit code) always lands on one stdout line, however many lines `cmd`
295
+ # spans — the command itself is shown as at most its first line,
296
+ # truncated; the full command lives in the transcript.
297
+ CMD_LINE_MAX = 100
298
+ OUTPUT_LINE_CAP = 20
299
+
300
+ def render_gate(g, transcript_path)
290
301
  terminal.say ""
291
- terminal.say "── #{g[:ac].empty? ? "(gate)" : g[:ac]}: #{g[:cmd]} (exit #{g[:exit_code].inspect}) [#{g[:rehearsal].to_s.upcase}]"
302
+ terminal.say "── #{g[:ac].empty? ? "(gate)" : g[:ac]} #{g[:id]}: #{truncated_cmd(g[:cmd])} (exit #{g[:exit_code].inspect}) [#{g[:rehearsal].to_s.upcase}]"
292
303
  terminal.say " dir: #{terminal.path(g[:dir])}"
293
304
  terminal.say " reason: #{g[:reason]}" unless g[:reason].to_s.empty?
305
+ if g[:shared_with]&.any?
306
+ terminal.say " shared execution with: #{g[:shared_with].join(', ')} (identical cmd+dir, captured once)"
307
+ end
294
308
  if g[:rehearsal] == :broken
295
309
  terminal.say " BROKEN is advisory, not certain — a correct RED can look broken (e.g. a file the lane " \
296
310
  "hasn't written yet). Confirm before treating it as a defect."
297
311
  end
298
- terminal.say g[:stdout].rstrip unless g[:stdout].strip.empty?
299
- terminal.say g[:stderr].rstrip unless g[:stderr].strip.empty?
312
+ render_gate_output(g, transcript_path)
313
+ end
314
+
315
+ def truncated_cmd(cmd)
316
+ first, *rest = cmd.to_s.each_line.map(&:chomp)
317
+ clipped = first.length > CMD_LINE_MAX ? "#{first[0, CMD_LINE_MAX]}…" : first
318
+ rest.any? || clipped != first ? "#{clipped} …" : clipped
319
+ end
320
+
321
+ def render_gate_output(g, transcript_path)
322
+ combined = [g[:stdout], g[:stderr]].map { |s| s.to_s.rstrip }.reject(&:empty?).join("\n")
323
+ return if combined.empty?
324
+
325
+ lines = combined.lines(chomp: true)
326
+ if lines.size > OUTPUT_LINE_CAP
327
+ terminal.say lines.last(OUTPUT_LINE_CAP).join("\n")
328
+ terminal.say " … #{lines.size - OUTPUT_LINE_CAP} more line(s) elided — full output in #{terminal.path(transcript_path)}"
329
+ else
330
+ terminal.say lines.join("\n")
331
+ end
300
332
  end
301
333
 
302
334
  # I12/AC3-AC4: reports scope asymmetry only — never affects RED/GREEN/
@@ -325,6 +357,22 @@ module Space::Architect
325
357
  report[:not_analyzable].each { |na| terminal.say " #{na[:id]}: #{na[:reason]}" }
326
358
  end
327
359
 
360
+ # I01: always-on, positioned last so a `tail` of stdout captures it
361
+ # whole — one line per gate (AC label, gate id, verdict; reason
362
+ # appended for RED/BROKEN), a tally by verdict, and the transcript path.
363
+ def render_summary(result)
364
+ terminal.say ""
365
+ terminal.say "Summary:"
366
+ result[:gates].each do |g|
367
+ line = " #{g[:ac].empty? ? "(gate)" : g[:ac]} #{g[:id]} [#{g[:rehearsal].to_s.upcase}]"
368
+ line += " — #{g[:reason]}" if %i[red broken].include?(g[:rehearsal]) && !g[:reason].to_s.empty?
369
+ terminal.say line
370
+ end
371
+ tally = result[:gates].group_by { |g| g[:rehearsal] }.transform_values(&:size)
372
+ terminal.say " #{%i[green red broken].filter_map { |v| "#{v.to_s.upcase}: #{tally[v]}" if tally[v] }.join(', ')}"
373
+ terminal.say " transcript: #{terminal.path(result[:transcript])}"
374
+ end
375
+
328
376
  def render_record(result)
329
377
  return "> Rehearsed #{result[:iteration]} — no gates drafted; nothing to record." if result[:empty]
330
378
 
@@ -437,107 +485,68 @@ module Space::Architect
437
485
  argument :lane, required: true, desc: "Lane name"
438
486
  argument :space, required: false, desc: "Space identifier (default: $PWD)"
439
487
  option :prompt, default: nil, desc: "Read the lane prompt from this file (copied byte-for-byte to build/<id>-<lane>/prompt.md)"
440
- option :model, default: nil, desc: "Builder model to pin (default: the lane's stored model, else space.yaml project.model, else the per-harness sensible default). Any provider/tier; pin a full id, not a floating alias"
488
+ option :model, default: nil, desc: "Builder model to pin (default: the lane's stored model, else space.yaml project.model, else the pi default #{Harness::DEFAULT_MODEL}). Any provider/tier; pin a full id, not a floating alias"
441
489
  option :max_turns, default: "200", desc: "Max turns for the builder"
442
- option :harness, default: nil, desc: "Harness override (claude-code, opencode, pi)"
443
- option :allowed_tools, default: nil, desc: "Comma-separated tool list — replaces the claude-code --allowedTools grant for this dispatch (default: Read,Edit,Write,Grep,Glob,Bash,WebSearch,WebFetch). The lane's frozen allowed_tools: does the same with no flag; this flag wins over that key. Meaningless for opencode/pi"
444
- option :append_allowed_tools, default: nil, desc: "Comma-separated tool list — appends to the claude-code --allowedTools grant for this dispatch (to the flag/lane replace value, or the default). The lane's frozen append_allowed_tools: does the same with no flag; this flag wins over that key. Meaningless for opencode/pi"
445
- option :effort, default: nil, desc: "Thinking/reasoning effort level — alias for --thinking/--reasoning (off, minimal, low, medium, high, xhigh, max); translated + clamped to the lane's harness"
446
- option :thinking, default: nil, desc: "Thinking/reasoning effort level — alias for --effort/--reasoning (off, minimal, low, medium, high, xhigh, max); translated + clamped to the lane's harness"
447
- option :reasoning, default: nil, desc: "Thinking/reasoning effort level — alias for --effort/--thinking (off, minimal, low, medium, high, xhigh, max); translated + clamped to the lane's harness"
448
- option :force_effort, default: nil, desc: "Force the literal level onto the harness flag, skipping architect's clamp — alias for --force-thinking/--force-reasoning (dispatch only; the binary's rejection is final)"
449
- option :force_thinking, default: nil, desc: "Force the literal level onto the harness flag, skipping architect's clamp — alias for --force-effort/--force-reasoning (dispatch only; the binary's rejection is final)"
450
- option :force_reasoning, default: nil, desc: "Force the literal level onto the harness flag, skipping architect's clamp — alias for --force-effort/--force-thinking (dispatch only; the binary's rejection is final)"
490
+ option :harness, default: nil, desc: "Harness (pi)"
491
+ option :effort, default: nil, desc: "Thinking/reasoning effort level — alias for --thinking/--reasoning (off, minimal, low, medium, high, xhigh, max)"
492
+ option :thinking, default: nil, desc: "Thinking/reasoning effort level — alias for --effort/--reasoning (off, minimal, low, medium, high, xhigh, max)"
493
+ option :reasoning, default: nil, desc: "Thinking/reasoning effort level — alias for --effort/--thinking (off, minimal, low, medium, high, xhigh, max)"
494
+ option :force_effort, default: nil, desc: "Force the literal level onto the --thinking flag, skipping architect's validation — alias for --force-thinking/--force-reasoning (dispatch only; the binary's rejection is final)"
495
+ option :force_thinking, default: nil, desc: "Force the literal level onto the --thinking flag, skipping architect's validation — alias for --force-effort/--force-reasoning (dispatch only; the binary's rejection is final)"
496
+ option :force_reasoning, default: nil, desc: "Force the literal level onto the --thinking flag, skipping architect's validation — alias for --force-effort/--force-thinking (dispatch only; the binary's rejection is final)"
451
497
  option :quiet, type: :boolean, default: false, desc: "Suppress thinking-translation and harness run-time warn lines (liveness/push) on $stderr for this dispatch"
452
498
  option :detach, type: :boolean, default: false, desc: "Detach the builder process (returns immediately with PID; poll report for completion)"
453
499
  option :timeout, default: "14400", desc: "Wall-clock timeout in seconds (0 disables; default 4h); foreground only"
454
500
  option :push_url, default: nil, desc: "HTTP endpoint for streaming push (POST body to this URL)"
455
501
  option :push_token, default: nil, desc: "Bearer token for push endpoint authorization"
456
502
  option :push_host, default: nil, desc: "Base URL of the ingest server; the CLI creates a run via POST <host>/runs and streams to /runs/<id>/ingest (requires --push-token)"
457
- option :as_job, type: :boolean, default: false, desc: "Submit the run as a job to the space-server's queue instead of running locally (sandboxed executor; requires --host/--token/--backend-url)"
458
- option :host, default: nil, desc: "Base URL of the space-server (required with --as-job)"
459
- option :token, default: nil, desc: "Bearer token for authorization (required with --as-job)"
460
- option :backend_url, default: nil, desc: "Base URL of the harness backend the sandboxed job talks to (required with --as-job)"
461
- option :job_model, default: nil, desc: "Model pinned for the sandboxed job's harness — distinct from --model, which is meaningless against a non-Anthropic backend"
462
- option :api_key_ref, default: nil, desc: "op:// reference resolved into ANTHROPIC_API_KEY server-side (--as-job only; omit for keyless backends)"
463
503
 
464
504
  def call(iteration:, lane:, space: nil, prompt: nil, model: nil,
465
505
  max_turns: "200", harness: nil, effort: nil, thinking: nil, reasoning: nil,
466
- allowed_tools: nil, append_allowed_tools: nil,
467
506
  force_effort: nil, force_thinking: nil, force_reasoning: nil, quiet: false, detach: false,
468
- timeout: "14400", push_url: nil, push_token: nil, push_host: nil,
469
- as_job: false, host: nil, token: nil, backend_url: nil, job_model: nil, api_key_ref: nil, **opts)
507
+ timeout: "14400", push_url: nil, push_token: nil, push_host: nil, **opts)
470
508
  setup_terminal(**opts.slice(:color, :colors))
471
509
  handle_errors do
472
510
  level = resolve_thinking_alias(effort: effort, thinking: thinking, reasoning: reasoning)
473
511
  forced_level = resolve_force_thinking_alias(force_effort: force_effort,
474
512
  force_thinking: force_thinking, force_reasoning: force_reasoning)
475
513
 
476
- if as_job
477
- Jobs.require_credentials!(host, token)
478
- raise Space::Core::Error, "--backend-url is required with --as-job" unless backend_url
479
- raise Space::Core::Error, "--job-model is required with --as-job" unless job_model
480
- raise Space::Core::Error, "--as-job cannot be combined with --push-url/--push-token/--push-host/--detach" \
481
- if push_url || push_token || push_host || detach
482
- end
483
-
484
514
  render(store.find(space)) do |sp|
485
515
  project = ArchitectProject.new(space: sp)
486
516
 
487
- if as_job
488
- kwargs = { host: host, token: token, backend_url: backend_url, max_turns: max_turns.to_i }
489
- kwargs[:prompt] = prompt if prompt
490
- kwargs[:model] = model if model
491
- kwargs[:harness] = harness if harness
492
- kwargs[:effort] = forced_level || level if forced_level || level
493
- kwargs[:allowed_tools] = allowed_tools if allowed_tools
494
- kwargs[:append_allowed_tools] = append_allowed_tools if append_allowed_tools
495
- kwargs[:force] = true if forced_level
496
- kwargs[:quiet] = true if quiet
497
- kwargs[:job_model] = job_model if job_model
498
- kwargs[:api_key_ref] = api_key_ref if api_key_ref
499
- res = project.dispatch_as_job(iteration, lane, **kwargs)
500
- terminal.say "Prompt: #{prompt} → #{terminal.path(res[:prompt_copied])}" if res[:prompt_copied]
501
- terminal.say "Job: #{res[:job_id]}"
502
- terminal.say "Watch: architect jobs watch #{res[:job_id]} --host #{host} --token #{token}"
517
+ kwargs = { max_turns: max_turns.to_i, detach: detach }
518
+ kwargs[:prompt] = prompt if prompt
519
+ kwargs[:model] = model if model
520
+ kwargs[:harness] = harness if harness
521
+ kwargs[:effort] = forced_level || level if forced_level || level
522
+ kwargs[:force] = true if forced_level
523
+ kwargs[:quiet] = true if quiet
524
+ kwargs[:timeout] = timeout.to_i unless detach
525
+ kwargs[:push_url] = push_url if push_url
526
+ kwargs[:push_token] = push_token if push_token
527
+ kwargs[:push_host] = push_host if push_host
528
+ res = project.dispatch(iteration, lane, **kwargs)
529
+ terminal.say "Prompt: #{prompt} → #{terminal.path(res[:prompt_copied])}" if res[:prompt_copied]
530
+ if detach
531
+ terminal.say "PID: #{res[:pid]}"
532
+ terminal.say "Run log: #{terminal.path(res[:run_log])}"
533
+ terminal.say "Report: #{terminal.path(res[:report])}"
534
+ terminal.say "Dispatched detached — poll #{terminal.path(res[:report])} for completion"
503
535
  CLI.record_outcome(Outcome.new(exit_code: 0))
536
+ elsif res[:timed_out]
537
+ terminal.say "Run log: #{terminal.path(res[:run_log])}"
538
+ terminal.say "Report: #{terminal.path(res[:report])}"
539
+ terminal.say "Builder TIMED OUT after #{timeout}s — process group killed. Re-dispatch (lanes are cheap)."
540
+ CLI.record_outcome(Outcome.new(exit_code: res[:exit_code]))
504
541
  else
505
- kwargs = { max_turns: max_turns.to_i, detach: detach }
506
- kwargs[:prompt] = prompt if prompt
507
- kwargs[:model] = model if model
508
- kwargs[:harness] = harness if harness
509
- kwargs[:effort] = forced_level || level if forced_level || level
510
- kwargs[:allowed_tools] = allowed_tools if allowed_tools
511
- kwargs[:append_allowed_tools] = append_allowed_tools if append_allowed_tools
512
- kwargs[:force] = true if forced_level
513
- kwargs[:quiet] = true if quiet
514
- kwargs[:timeout] = timeout.to_i unless detach
515
- kwargs[:push_url] = push_url if push_url
516
- kwargs[:push_token] = push_token if push_token
517
- kwargs[:push_host] = push_host if push_host
518
- res = project.dispatch(iteration, lane, **kwargs)
519
- terminal.say "Prompt: #{prompt} → #{terminal.path(res[:prompt_copied])}" if res[:prompt_copied]
520
- if detach
521
- terminal.say "PID: #{res[:pid]}"
522
- terminal.say "Run log: #{terminal.path(res[:run_log])}"
523
- terminal.say "Report: #{terminal.path(res[:report])}"
524
- terminal.say "Dispatched detached — poll #{terminal.path(res[:report])} for completion"
525
- CLI.record_outcome(Outcome.new(exit_code: 0))
526
- elsif res[:timed_out]
527
- terminal.say "Run log: #{terminal.path(res[:run_log])}"
528
- terminal.say "Report: #{terminal.path(res[:report])}"
529
- terminal.say "Builder TIMED OUT after #{timeout}s — process group killed. Re-dispatch (lanes are cheap)."
530
- CLI.record_outcome(Outcome.new(exit_code: res[:exit_code]))
531
- else
532
- terminal.say "Run log: #{terminal.path(res[:run_log])}"
533
- terminal.say "Report: #{terminal.path(res[:report])}"
534
- terminal.say "Ingest URL: #{res[:push_url]}" if res[:push_url]
535
- terminal.say "Builder exited with status #{res[:exit_code]}"
536
- unless res[:report].exist? && !res[:report].read.strip.empty?
537
- terminal.say "WARNING: no report at #{terminal.path(res[:report])} — the lane produced no deliverable."
538
- end
539
- CLI.record_outcome(Outcome.new(exit_code: res[:exit_code]))
542
+ terminal.say "Run log: #{terminal.path(res[:run_log])}"
543
+ terminal.say "Report: #{terminal.path(res[:report])}"
544
+ terminal.say "Ingest URL: #{res[:push_url]}" if res[:push_url]
545
+ terminal.say "Builder exited with status #{res[:exit_code]}"
546
+ unless res[:report].exist? && !res[:report].read.strip.empty?
547
+ terminal.say "WARNING: no report at #{terminal.path(res[:report])} — the lane produced no deliverable."
540
548
  end
549
+ CLI.record_outcome(Outcome.new(exit_code: res[:exit_code]))
541
550
  end
542
551
  end
543
552
  end
@@ -802,7 +811,7 @@ module Space::Architect
802
811
  class InstallSkills < BaseCommand
803
812
  desc "Install bundled skills (architect, architect-research, architect-vocabulary) for a harness"
804
813
  phase 53, "Project"
805
- option :provider, default: "claude", desc: "Harness: claude, codex, opencode, pi"
814
+ option :provider, default: "claude", desc: "Skill install target (default: claude; validated by the installer)"
806
815
  option :project, type: :boolean, default: false, desc: "Install to CWD instead of global"
807
816
  option :force, type: :boolean, default: false, desc: "Overwrite existing skills that differ"
808
817
  option :dry_run, type: :boolean, default: false, desc: "Print what would happen without writing files"
@@ -830,11 +839,11 @@ module Space::Architect
830
839
  argument :lane, required: true, desc: "Lane name"
831
840
  argument :space, required: false, desc: "Space identifier (default: $PWD)"
832
841
  option :base, default: nil, desc: "Base ref (default: HEAD of repo)"
833
- option :harness, default: nil, desc: "Harness (claude-code, opencode, pi; default: space.yaml project.harness, else claude-code)"
834
- option :model, default: nil, desc: "Model; a trailing :<level> suffix (e.g. foo:high) is parsed into --effort (default: space.yaml project.model, else the per-harness sensible default)"
835
- option :effort, default: nil, desc: "Thinking/reasoning effort level — alias for --thinking/--reasoning (off, minimal, low, medium, high, xhigh, max); translated + clamped to the lane's harness at dispatch time"
836
- option :thinking, default: nil, desc: "Thinking/reasoning effort level — alias for --effort/--reasoning (off, minimal, low, medium, high, xhigh, max); translated + clamped to the lane's harness at dispatch time"
837
- option :reasoning, default: nil, desc: "Thinking/reasoning effort level — alias for --effort/--thinking (off, minimal, low, medium, high, xhigh, max); translated + clamped to the lane's harness at dispatch time"
842
+ option :harness, default: nil, desc: "Harness (pi; default: space.yaml project.harness, else pi)"
843
+ option :model, default: nil, desc: "Model; a trailing :<level> suffix (e.g. foo:high) is parsed into --effort (default: space.yaml project.model, else the pi default #{Harness::DEFAULT_MODEL})"
844
+ option :effort, default: nil, desc: "Thinking/reasoning effort level — alias for --thinking/--reasoning (off, minimal, low, medium, high, xhigh, max)"
845
+ option :thinking, default: nil, desc: "Thinking/reasoning effort level — alias for --effort/--reasoning (off, minimal, low, medium, high, xhigh, max)"
846
+ option :reasoning, default: nil, desc: "Thinking/reasoning effort level — alias for --effort/--thinking (off, minimal, low, medium, high, xhigh, max)"
838
847
  option :quiet, type: :boolean, default: false, desc: "Suppress the thinking-translation inform line on $stderr"
839
848
  option :touch, default: nil, desc: "Comma-separated file globs the lane may touch (records its touch_set for in-bounds + merge checks)"
840
849
  option :force, type: :boolean, default: false, desc: "Clear and re-create a stale (unregistered) worktree directory"
@@ -914,7 +923,7 @@ module Space::Architect
914
923
  argument :repo, required: true, desc: "Repo name (under repos/)"
915
924
  argument :iteration, required: true, desc: "Iteration name"
916
925
  argument :space, required: false, desc: "Space identifier (default: $PWD)"
917
- option :pairs, required: true, desc: "Comma-separated harness[:model] pairs (e.g. claude-code,opencode:fireworks-ai/accounts/fireworks/models/glm-5p2)"
926
+ option :pairs, required: true, desc: "Comma-separated harness[:model] pairs (e.g. pi,pi:#{Harness::DEFAULT_MODEL})"
918
927
  option :base, default: nil, desc: "Base ref (default: HEAD of repo)"
919
928
  option :prompt, default: nil, desc: "Prompt file to fan-out byte-identical to each variant"
920
929
 
@@ -8,7 +8,7 @@ module Space::Architect
8
8
  desc "Dispatch detached read-only research lanes (one per prompt file)"
9
9
  argument :prompts, type: :array, required: true,
10
10
  desc: "Prompt file(s) to dispatch (space-separated paths)"
11
- option :model, default: nil, desc: "Researcher model override (default: the reference default claude-sonnet-4-6)"
11
+ option :model, default: nil, desc: "Researcher model override (default: the pi default #{Space::Architect::Harness::DEFAULT_MODEL})"
12
12
  option :max_turns, default: "40", desc: "Max turns per researcher"
13
13
 
14
14
  def call(prompts:, model: nil, max_turns: "40", **opts)