Generated by deep audit across all source files — April 2026
- Files:
executor.rs:60,104-107,373,storage.rs:477 ParsedHarnessOutputextractssession_idfrom harness JSON output. The DB schema has asession_idcolumn.create_execution_logacceptssession_id, butupdate_execution_loghas nosession_idparameter. On line 373 ofexecutor.rs,create_execution_logis called withsession_id: None. Result: session IDs are always lost — confirmed data loss.
- File:
runner.rs:617-618 find_resume_pointcorrectly returns steps withStepStatus::Aborted, but the reset condition only coversFailedandInProgress. AnAbortedstep at the resume point will NOT be reset toPending. Whenexecute_stepencounters it, it will either bail (becauseattempts >= max_attempts) or proceed with stale state.
- Files:
commands/agents.rs:74,93,145,hook_library.rs:115-117,344-392 agents_dir().join(format!("{name}.md"))andhooks_dir()?.join(format!("{name}.md"))accept unvalidated user input. A name like../../etc/shadowwould resolve outside the target directory, enabling arbitrary file read, write, or delete.
- File:
tui/app.rs:186-195 - Every
StepStatusvariant returns" "(two spaces). The correspondingstatus_icon()inoutput.rs:126-141correctly returns distinct Unicode icons (○,▶,✔,✘,⊘). The TUI step list shows no status iconography — every step looks identical before the number/title.
- File:
tui/input.rs:107 KeyCode::Char('c')inhandle_add_mode()unconditionally pushes'c'to the input buffer. The Ctrl+C handler exists only inhandle_normal_mode(). Users must press Esc, then q/Ctrl+C to quit from input mode.
- File:
import.rs:90-157 import_plan_from_data()creates a plan, updates status, creates steps in a loop, and attaches dependencies — all as separate operations. If any step creation or dependency attachment fails partway, the database is left with a partial plan inReadystatus with incomplete steps. No rollback mechanism.
- File:
harness.rs:153-172 {prompt}replacement happens beforeremove_agent_file_args. If the prompt text contains the literal string{agent_file}, the function will find it and incorrectly remove the preceding argument (e.g., the-pflag), silently corrupting the harness command line.
- File:
hook_library.rs:163-166 - The frontmatter parser strips everything from the first
#to end of line. Values likedescription: "Fix issue #123"becomedescription: "Fix issue. Since#is extremely common in issue references and URLs, this is a significant parsing bug.
- File:
executor.rs:681 - After
child.kill(), the code reads stdout/stderr but never callschild.wait(). On Unix, the killed process may not be reaped, potentially leaving a zombie. Thegraceful_shutdownpath correctly callschild.wait(), but the timeout path does not.
- File:
test_runner.rs:47 run_tests()usesstd::process::Command::output()which blocks the current thread. During test execution, the tokio runtime cannot process other async tasks (includingabort_rxsignal handling), so Ctrl+C during test execution won't be detected until after tests finish.
- File:
runner.rs:756-769 - Uses
std::process::Command(synchronous) while called from the asyncrun_plancall chain. This blocks the tokio runtime thread for the duration of the git checkout. All other git operations ingit.rsare also synchronous but called from non-async contexts.
- File:
executor.rs:327-337 - When abort is detected at this point,
attempthas been incremented butset_step_attemptshas NOT been called. TheStepResultreportsattempts_used = step.attempts + 1, but the database still showsstep.attempts. No execution log entry is created for this aborted attempt.
- Files:
main.rs:490-512,515-524 ralph resumeandralph skipmodify step status without acquiring a run lock. Concurrent invocations ofralph runandralph skipcould race on step state.
- File:
plan.rs:154-189 from_rowexpectsplan_harnessat position 9 andcreated_atat position 10, butALTER TABLE ADD COLUMNappendsplan_harnessat position 11 (afterupdated_at). This works today only because every SQL query explicitly reorders columns. A futureSELECT *would silently corrupt data.
- File:
frac_index.rs:120-123 key_between("0", "00")panics because the suffix recursion produces an emptyb_suffix. Input keys like this could arise from import/export. Should return an error instead of panicking.
- File:
db.rs:192-206 - Allows attaching the same hook to the same step/lifecycle multiple times, causing duplicate hook execution.
- File:
config.rs:72-81 - A config with
default_harness: "foobar"that doesn't exist in theharnessesmap loads successfully but fails at runtime with "Unknown harness" for every operation.
- File:
signal.rs:97 - The
RunLock::dropnever runs, leaving a stale row inrun_locks. Users must use--forceor manually clean up the DB row on next run.
- File:
run_lock.rs:121 - The
projectpath is interpolated directly into a suggestedDELETE FROM run_locks WHERE project = '{project}'SQL command. If the path contains a single quote (e.g.,/tmp/o'brien/project), the suggested command is broken. More concerning, blind copy-paste could be destructive.
- File:
run_lock.rs:113-133 - The acquire sequence (query PID → check liveness → delete → insert) is non-atomic. Two processes could both pass the PID check and insert their own rows concurrently. Should use an explicit SQLite transaction.
- File:
hook_library.rs:294-302 - Literal newlines inside descriptions will break the YAML frontmatter when round-tripped through
serialize_hook→parse_hook. YAML requires\nescape sequences inside double-quoted strings.
- File:
hook_library.rs:148-150 rest.find("\n---")matches\n---anywhere, including in diff output (--- a/old_file). The closing frontmatter delimiter should require---at column 0 followed by end-of-line.
- File:
storage.rs:400 - Every other status update uses
status.as_str(). If theStepStatus::Pendingstring representation ever changes, this query will insert an unparseable value, silently corrupting step state.
- File:
storage.rs:306-325 - All comparable update functions bail with "not found" when
affected == 0, but this one silently succeeds on a nonexistent step ID.
- File:
storage.rs:340-386 - Each UPDATE sets its own
updated_at = strftime(...), leading to non-atomic partial updates and inconsistent timestamps if any statement fails mid-way.
- File:
git.rs:83-94 git status --porcelainoutputs renames asXY old_path -> new_path. The code strips the first 3 characters withl.get(3..), returningold_path -> new_pathas a single string — an invalid file path.
- File:
git.rs:97-114 - Both
git diffandgit diff --cacheduse.unwrap_or_default(). A corrupt repository or lock contention produces an empty diff, masking real problems — the executor would think there are no changes.
- File:
hooks.rs:73-74 - Environment variables like
RALPH_PROJECT_DIRare set viacmd.env()(safe), but if a hook command references them via$RALPH_PROJECT_DIR, shell expansion processes them. A project path like/tmp/$(malicious)/projwould be expanded.
- File:
hooks.rs:80-83 cmd.output()blocks indefinitely. A hanging hook blocks the entire ralph run with no timeout mechanism.
- File:
preflight.rs:61-70 - Hardcoded ANSI escape codes (
\x1b[32m) are always emitted, bypassing theOutputContext.colorsystem. With--no-colororNO_COLOR, the preflight report still contains escape codes.
- File:
cli.rs:398-444 - The
titlepositional arg conflicts with--import-json, soralph step add --import-json file.json my-planfails because "my-plan" is parsed astitle. There's no way to target a non-active plan with bulk import.
- File:
main.rs:170-171 - The
planfield is explicitly ignored (let _ = plan;). Users targeting a specific plan will see it silently ignored.
- File:
plan_harness.rs:161,218 &config.harnesses[harness_name]will panic if the key doesn't exist. Should useget()with explicit error handling.
- File:
storage.rs:76-88 - Should use a SQL
WHERE status IN (...)query instead of fetching all plans and filtering in memory.
- File:
storage.rs:551 - When
limitisNone, the function silently caps results at 100 with no way for the caller to know results were truncated.
- File:
executor.rs:124-126 - Marked
#[allow(dead_code)]. When no changes are detected, the code usesFailureReason::TestFailedinstead.
- File:
executor.rs:862-873 - Only extracts
+++ b/lines. Deleted files and old names in renames are lost, potentially missing relevant context for the AI agent.
- File:
executor.rs:835-836 - Steps that failed (and were rolled back) could provide useful "here's what didn't work" context, but they're excluded.
- File:
runner.rs:646 - Accepted but never stored or displayed — dead code suggesting an incomplete feature.
- File:
runner.rs:154-156 - Steps that were complete before this run started are counted as "succeeded" in the result, inflating the metric.
- File:
runner.rs:898 - For a
Readyplan,final_statuswill beReady, which is semantically misleading for a "run result."
- File:
runner.rs:573-593 - Independent plans that don't depend on an in-progress plan won't get a chance to run.
- File:
runner.rs:730-737 git add -A && git commitstages and commits everything. No preview or opt-out for secrets, build artifacts, etc.
- File:
main.rs:367-368 - Warning says
--plan is ignored when --all is set, but there is no--planflag — it's a positional argument.
- File:
main.rs:64 - Violates Rust naming convention — suggests unused but is referenced 9+ times.
- File:
cli.rs:44-49,main.rs:80-93 - Users can pass these flags but they do nothing.
- File:
cli.rs:86-87 - No
conflicts_withorgroupconstraint. The code gives--allpriority, but only warns about--plan.
- File:
cli.rs:366-369 Addhasrequired = trueondepends_on,Removedoes not. The runtime error message is identical, but the timing of detection differs.
- Files:
commands/run.rs:148-155,commands/run.rs:29-38 cmd_status(JSON) prints"null".cmd_log(JSON) prints nothing. Should be consistent.
- File:
test_runner.rs:119-126 - When text fits within the limit, the original (possibly with trailing newline) is preserved. When truncated,
join("\n")produces no trailing newline.
- File:
frac_index.rs:10-17 - Invalid characters in sort keys cause a runtime panic. Should return a
Result.
- File:
plan.rs:357-370 - The
Archivedvariant's serialization roundtrip is never tested.
- File:
plan.rs:315-316 - Both could be true simultaneously (violating business logic) with no assertion.
- File:
db.rs:12-19 - Uses
constrather than computing fromMIGRATIONS.len(). If a migration is added without updating the constant, tests fail but production behavior is subtle.
- File:
config.rs:356-375 - Between
path.exists()andfs::write(), another process could create the file. Benign in practice since both write the same default config.
- File:
config.rs:311 - Zero seconds could reasonably be read as "immediate timeout." Should use
Option<u64>instead.
- File:
db.rs:28-35 - The database is created with default permissions, potentially readable by other users on shared systems.
- File:
git.rs:164-176 - Between
get_untracked_files()and deletion, the filesystem can change.
- File:
git.rs:145 let _ = git(...)discards all errors including permission denied and lock contention.
- File:
git.rs:121 - Modern git (2.23+) recommends
git restore .instead.
- File:
tui/ui.rs:75-77 - Causes viewport "jump" on each render. Should be stored in
Appand reused.
- File:
tui/ui.rs:136 - Doesn't respect user's
Config.max_retries_per_stepsetting.
- File:
output.rs:255-256 - Claims "and similar" but only accepts
y,Y,yes,Yes,YES— not mixed case likeyEs.
- File:
output.rs:45-51 - For a machine-readable stream, a serialization error should be fatal to avoid corrupt output.
- File:
output.rs:423-443 - If any code path uses
.into(), log output will silently be lost.
- File:
run_lock.rs:73-84 - Opens a brand-new
Connectionfor lock release, which could fail if the DB is temporarily unavailable.
- File:
run_lock.rs:114 - If a crashed ralph process's PID is reused by an unrelated process,
pid_is_alivereturns true and incorrectly refuses the lock.
- File:
signal.rs:38,47 - One test's
new()reset could race with another test's swap under--threads > 1.
- File:
signal.rs:69 - After spawning, there's no way to trigger graceful shutdown from application code.
- File:
commands/hooks.rs:178-208 - When some hooks collide, successfully imported hooks are on disk but the command returns a non-zero exit code.
- File:
commands/plan.rs:210-219 - The status filter takes precedence entirely, making
--archiveda no-op in combination with--status.
- File:
prompt.rs:228-237 - For diffs and test output, the most informative context is typically at the top. Callers may lose critical header information.
- File:
prompt.rs:153-183 - Steps 3, 7, and 10 would be labeled "Step 1", "Step 2", "Step 3" instead of their actual numbers.
- File:
commands/agents.rs:113-141 format!macro with\line continuations includes 9 spaces of source-code indentation in each output line.
- File:
main.rs:48 slug.filter(|s| !s.is_empty())meansralph status ""silently falls back to active plan lookup instead of producing an error.
- File:
commands/run.rs:253-284 --lines 50could produce up to 100 lines (50 stdout + 50 stderr), which may surprise users expecting a total cap.
- Files:
commands/plan.rs:452-487,commands/step.rs:501-557 ralph plan hooks my-plan | grep stepproduces no output because all text goes to stderr.
- File:
preflight.rs:164 - Custom harness auth requirements are never detected.
- File:
preflight.rs:98-101 - If the harness name doesn't exist in
config.harnesses, the auth check is silently skipped with no warning.
- File:
import.rs:90-157 ralph_rs_versionis included in exports but never checked on import. Future schema changes could silently drop fields.
- Files:
main.rs:348,cli.rs:80 ralph --harness A run --harness Buses harness A, not B. Counterintuitive — more-specific flags should override global ones.
- File:
hooks.rs:41 unwrap_or_default()converts permission errors, I/O failures, and corrupt files into an empty vector, indistinguishable from "no hooks configured."
- File:
hooks.rs:161-183,207-232 - Database errors, permission errors, and all other failures produce the same "Warning" output. Critical infrastructure problems go unnoticed.
- File:
storage.rs:248 - Misleading comment — the attempt bumping is done separately by
set_step_attempts.
- File:
db.rs:301-323 - Only V1 indexes are verified. V2/V3 index regressions would not be caught.
| Severity | Count |
|---|---|
| High | 8 |
| Medium | 25 |
| Low | 52 |
| Total | 85 |
- H2 — Add
StepStatus::Abortedto the reset condition inresume_plan - H1 — Add
session_idparameter toupdate_execution_logor remove the dead column - H3 — Validate agent/hook names reject path separators (
/,\,..) - H4 — Implement proper status indicators in TUI matching
output.rsicons - H5 — Handle Ctrl+C in
handle_add_modeby checkingKeyModifiers::CONTROL - H6 — Wrap import operations in a SQLite transaction
- H7 — Escape or check for
{agent_file}in prompt text before arg processing - H8 — Fix frontmatter comment parser to only strip
#outside quoted values