NAME
Langertha::Raider::CLI::REPL - Internal interactive loop of the raider CLI
VERSION
version 0.504
SYNOPSIS
# Internal to Langertha-Raider -- no API promise.
Langertha::Raider::CLI::REPL->new(app => $app, output => $out)->run(@first_prompt);
DESCRIPTION
Internal module. Its interface may change without notice.
The raider REPL: banner, line reading, slash commands via Langertha::Raider::CLI::Commands, every other line a prompt via Langertha::Raider::CLI::Runner, until /quit, /exit, :q, quit, exit or the end of input.
A line !CMD runs CMD in the shell and nothing else ("run_shell"); a line ?CMD runs it and then sends the command, its exit status and its output to the model as the next prompt ("run_shell_prompt"). A line that is only ! or ? is a prompt.
On a terminal, lines come from Term::ReadLine::Gnu (line editing, ~/.raider_history), else the REPL prints its own prompt and reads the line itself.
While it waits for a line the REPL runs the application's loop (ADR 0027 §4), so background subagents go on working: with Term::ReadLine::Gnu through its callback interface (the characters fed from a watcher on the loop), otherwise by running the loop until the input is readable ("read_line"). When a background subagent of the main conversation ends while nothing is typed, its notification starts a turn of its own, shown as background agent agent_1 finished ("run_notifications"); notifications that are due when a line is submitted go in front of it, in the same turn, each in a <system-reminder> ("run_prompt"). At the end of input that is not a terminal the REPL waits for its background subagents and delivers their notifications before it ends ("finish_background"); leaving it any other way -- /quit, Ctrl-D, a second Ctrl-C, SIGTERM -- stops them ("stop_background" in Langertha::Raider::Application). The first Ctrl-C leaves them running. However the REPL is left, a worktree the session still works in is closed (ADR 0032 §5, "close_worktree_session" in Langertha::Raider::Application): removed when enter_worktree made it and it has no changes, else kept and named in a meta line before bye..
The first SIGINT (Ctrl-C) while a prompt runs cancels that turn ("cancel_run" in Langertha::Raider::CLI::Runner): the tool commands still running are ended, the raid stops at its next safe point, turn cancelled is printed, the session journal ends the run as cancelled, and the REPL reads the next line. At the prompt the first Ctrl-C only warns. A second one within two seconds, or a SIGTERM, leaves the REPL with exit status 0, after ending the tool commands still running ("terminate_children" in Langertha::Raider::CLI::Runner). While a !CMD or ?CMD runs, raider ignores SIGINT, so Ctrl-C ends the command alone. When "in" is not a terminal, lines are read from it as they are, without prompt, so piped input ends the REPL at its end.
app
The Langertha::Raider::CLI. Required.
output
The Langertha::Raider::CLI::Output to print to. Required.
in
Filehandle lines are read from when it is not a terminal. Defaults to STDIN.
can_ask
Whether a person answers the model's questions (the ask_user tool, "ask_user"), approves tool calls ("permission_request") and reviews plans ("plan_review"). Defaults to whether "in" is a terminal; without one the tool refuses and the model decides itself, and a call the permission policy asks about is refused.
active_profiles
Agent profiles to show in the banner (claude, openai).
saved_profiles
Profiles saved to the config file by this start, marked (saved).
customize_prompt
Start with the prompt-builder (--customize-prompt).
session_store
The Langertha::Raider::SessionStore a new session is created in, with the first prompt. Without one (--no-session) nothing is recorded.
session
The Langertha::Raider::Session the prompts are recorded in, once there is one. A /clear is recorded there too, as history.cleared, so resuming the session starts with an empty history again.
notes
Lines printed right after the banner: what resuming "session" found ("restore" in Langertha::Raider::CLI::Sessions).
banner
$repl->banner($readline_impl);
Prints the logo and the live configuration: mission source, profiles, layout mode (ADR 0019) and the memory files it loads, the listed skills, packs, ignored agent files, engine, model, API key, root.
run
$repl->run(@first_prompt);
Runs the REPL; @first_prompt (the words given on the command line) is sent first. Returns when the user leaves.
run_notifications
my $ok = $repl->run_notifications;
$ok = $repl->run_notifications(wait_background => 1);
A turn of the notifications that wait for the main conversation (ADR 0027 §4): a meta line for each (background agent agent_1 finished), then one prompt of them all, in the order their work ended, recorded with notification 1. Further options go to "run_prompt" in Langertha::Raider::CLI::Runner. Then the prompt of every cron job that came due (ADR 0031) runs as a turn of its own through "run_prompt", after a meta line that names the job -- not with wait_background. Returns what the runner returns, true when nothing was due.
finish_background
At the end of input that is not a terminal: as long as background subagents of the main conversation run or their notifications wait, waits for them on the loop ("wait_for_notifications" in Langertha::Raider::Application) and delivers them in a turn that waits for the rest ("run_notifications" with wait_background).
read_line
my $line = $repl->read_line($in); # input that is not a terminal
my $line = $repl->read_line($in, 'raider> '); # a terminal without Term::ReadLine::Gnu
One line from $in without stopping the application's loop (ADR 0027 §4): what is there is read without blocking, and while no whole line is, the loop runs until $in is readable. Returns the line, undef at the end of the input, or a reference (\'notifications') when notifications of background work wait for the main conversation, or the prompt of a cron job is due (ADR 0031), and no part of a line has come yet -- the REPL then runs them as a turn. With $prompt, that is printed first (and a line break before such a turn).
ask_user
my $answer = $repl->ask_user($question, \@options);
Puts the model's question to the person: the question, the options numbered, then a line read from "in". A number that names an option answers with that option's text, any other line is the answer as typed. Returns undef when the input is closed or the line is empty. The REPL registers this as the application's on_ask_user when it can ask.
permission_request
my $answer = $repl->permission_request({ tool => 'bash', arguments => { command => 'make' },
reason => '...', rule => 'Bash(make)' });
Puts a tool call the permission policy asks about to the person (ADR 0021): the tool and what it would do (the command, the path, the URL), why it asks, then the choices -- 1 yes, once; 2 yes, for this session, naming the rule that is added (only when there is one); 3 no. y and n work too. A "no" asks for an optional reason, which the model gets. Returns { answer => 'once' | 'session' | 'no', reason => ... }; a closed input is a "no". The REPL registers this as the application's on_permission_request when it can ask -- for every raider of the application, the prompt builder's too.
approve_mcp_servers
$repl->approve_mcp_servers;
Before the first prompt: every project MCP server that waits for an approval (ADR 0029 §7) is put to the person once with "mcp_approval", through "ask_mcp_approvals" in Langertha::Raider::Application, and each answer is reported with where it was saved. When the REPL cannot ask (input is not a terminal), the servers stay off and "mcp_notes" in Langertha::Raider::Application says so.
mcp_approval
my $answer = $repl->mcp_approval($server); # 'approve', 'all', 'reject' or undef
Claude Code's three choices for a pending project server: 1 use this server, 2 use this and all future MCP servers of this project, 3 continue without it (y / n work too). A closed input is no answer: the server stays pending.
call_summary
my $text = $repl->call_summary({ tool => 'bash', arguments => { command => 'ls' } }); # 'ls'
What a call would do, in one line for the question: the command of bash, the path of a file tool, the url of web_fetch, the query of web_search, else the arguments as JSON -- cut at 200 characters.
plan_review
my $answer = $repl->plan_review($plan); # { approve => 1 } or { approve => 0, feedback => ... }
Shows the plan the model presents with exit_plan_mode and asks whether to accept it: y accepts (the permission mode becomes default), anything else asks for feedback, which the model gets. A closed input does not accept. Registered as on_plan_review when the REPL can ask.
run_prompt
$repl->run_prompt($text);
$repl->run_prompt($text, %run); # with run options (a /name run request)
Runs one prompt through the runner, recorded in "session". The first prompt with a "session_store" starts the session and names it. The notifications of background subagents that wait for the main conversation go in front of the prompt, each wrapped as Claude Code delivers one next to a genuine message ("reminder_text" in Langertha::Raider::Tasks, ADR 0027 §4), after a meta line for each. %run are the run options of "run_options" in Langertha::Raider::CLI::Runner.
run_slash
$repl->run_slash('/review src/App.pm');
A /... line (also the first prompt from the command line): resolved by "dispatch" in Langertha::Raider::CLI::Commands. A run request it returns (a catalogue entry, /init, a path such as /etc/hosts is broken) runs as the next prompt with its run options, recorded in "session" -- through "run_prompt", so notifications that wait go in front of it as in front of a typed line (ADR 0027 §4). A /clear, /reset or /new is recorded there as history.cleared.
run_shell
my $status = $repl->run_shell('git status');
The REPL's !CMD: runs the command with $SHELL -c (/bin/sh without SHELL) in the app's root, on the terminal raider runs on, so less or vim work. A non-zero exit status is printed, as exit status N, a command ended by a signal as interrupted (SIGINT) or killed by SIGTERM. Nothing goes to the model or into the session journal. Returns the wait status, or undef when the command could not be started.
run_shell_prompt
my $ok = $repl->run_shell_prompt('make test');
The REPL's ?CMD: runs the command as "run_shell" does, but with its standard output and standard error shown as they come and captured, and standard input from /dev/null. Then the command, its exit status and the captured output ("shell_prompt") go to the model through "run_prompt", as any prompt: the model answers, the session journal records the turn. A command ended by Ctrl-C (SIGINT) sends nothing, since Ctrl-C in the REPL cancels the turn. Returns what "run_prompt" returns, false when nothing was sent.
shell_prompt
my $prompt = $repl->shell_prompt($command, $wait_status, $output);
The prompt "run_shell_prompt" sends: where the command ran, the command, its exit status and its output. An output longer than "shell_output_limit" characters keeps its head and its tail, half the limit each, with a line [... N characters omitted ...] between them.
shell_output_limit
How many characters of a ?CMD output reach the model: 20000.
SEE ALSO
SUPPORT
Issues
Please report bugs and feature requests at https://github.com/Getty/langertha-raider/issues.
IRC
Join #langertha on irc.perl.org or message Getty directly.
CONTRIBUTING
Contributions are welcome! Please fork the repository and submit a pull request.
AUTHOR
Torsten Raudssus <torsten@raudssus.de> https://raudssus.de/
COPYRIGHT AND LICENSE
This software is copyright (c) 2026 by Torsten Raudssus.
This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.