Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[workspace]

Check failure on line 1 in Cargo.toml

View check run for this annotation

Autter.dev / autter/review-gate

🔴 High · Build failed: autter

`cargo build` failed in `.` — this PR breaks the **autter** build. ``` error: rustc 1.85.0 is not supported by the following packages: ratatui@0.30.0 requires rustc 1.86.0 ratatui-core@0.1.0 requires rustc 1.86.0 ratatui-crossterm@0.1.0 requires rustc 1.86.0 ratatui-widgets@0.3.0 requires rustc 1.86.0 time@0.3.47 requires rustc 1.88.0 time-core@0.1.8 requires rustc 1.88.0 zip@8.6.0 requires rustc 1.88 Either upgrade rustc or select compatible dependency versions with `cargo update <name>@<current-ver> --precise <compatible-ver>` where `<compatible-ver>` is the latest version supporting rustc 1.85.0 ``` <!-- autter:failure-kind:code --> Suggested fix: The autter build (`cargo build` in `.`) fails with: error: rustc 1.85.0 is not supported by the following packages: ratatui@0.30.0 requires rustc 1.86.0 ratatui-core@0.1.0 requires rustc 1.86.0 ratatui-crossterm@0.1.0 requires rustc 1.86.0 ratatui-widgets@0.3.0 requires rustc 1.86.0 time@0.3.47 requires rustc 1.88.0 time-core@0.1.8 requires rustc 1.88.0 zip@8.6.0 requires rustc 1.88 Either upgrade rustc or select compatible dependency. Fix the code so the build passes.

Check failure on line 1 in Cargo.toml

View check run for this annotation

Autter.dev / autter/review-gate

🔴 High · Tests failed: autter

`cargo test` failed in `.` — this PR breaks the **autter** test suite. ``` error: rustc 1.85.0 is not supported by the following packages: ratatui@0.30.0 requires rustc 1.86.0 ratatui-core@0.1.0 requires rustc 1.86.0 ratatui-crossterm@0.1.0 requires rustc 1.86.0 ratatui-widgets@0.3.0 requires rustc 1.86.0 time@0.3.47 requires rustc 1.88.0 time-core@0.1.8 requires rustc 1.88.0 zip@8.6.0 requires rustc 1.88 Either upgrade rustc or select compatible dependency versions with `cargo update <name>@<current-ver> --precise <compatible-ver>` where `<compatible-ver>` is the latest version supporting rustc 1.85.0 ``` <!-- autter:failure-kind:code --> **Failing tests** - `agent-support/vscode/src/test/extension.test.ts` — the file failed before any test reported — # const err = new Error(message); - `agent-support/vscode/src/test/semver.test.ts` — the file failed before any test reported — # suite("Semver Utilities", () => { - `agent-support/vscode/src/test/vscode-hooks.test.ts` — the file failed before any test reported — # suite("VS Code Hook Gating", () => { Suggested fix: The autter test suite (`cargo test` in `.`) fails with: error: rustc 1.85.0 is not supported by the following packages: ratatui@0.30.0 requires rustc 1.86.0 ratatui-core@0.1.0 requires rustc 1.86.0 ratatui-crossterm@0.1.0 requires rustc 1.86.0 ratatui-widgets@0.3.0 requires rustc 1.86.0 time@0.3.47 requires rustc 1.88.0 time-core@0.1.8 requires rustc 1.88.0 zip@8.6.0 requires rustc 1.88 Either upgrade rustc or select compatible dependency. Fix the code (or the test if it is genuinely stale) so the suite passes.
# The CLI writes authorship notes and prompt CAS objects straight to each org's
# own Postgres (connection URL comes from the access-token JWT's `org_db_url`
# claim), so there is no separate backend crate to build or deploy.
Expand All @@ -8,7 +8,7 @@

[package]
name = "autter"
version = "1.7.0"
version = "1.7.1"
edition = "2024"
default-run = "autter"

Expand Down
2 changes: 1 addition & 1 deletion INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ New terminals pick it up automatically. Restart your IDE (not just its terminal

```bash
autter --version
autter doctor # v1.7.0+ — focused setup validation (exits 1 on failure)
autter doctor # v1.7.1+ — focused setup validation (exits 1 on failure)
autter debug # full support dump (always exits 0)
```

Expand Down
36 changes: 32 additions & 4 deletions src/commands/autter_handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -540,16 +540,44 @@
);
}

// How long to wait for a freshly-spawned daemon to accept connections when a

Check failure on line 543 in src/commands/autter_handlers.rs

View check run for this annotation

Autter.dev / autter/review-gate

🔴 High · Source changes without matching tests

This source file was changed but no sibling test file is added or modified anywhere in the PR. Blast radius — skipping this guardrail cascades to the downstream usage that depends on this file: functions `handle_autter`, `synthesize_hook_input_from_cli_args`, `handle_checkpoint`, `report_checkpoint_usage_error`, `checkpoint_ts`, `new`, `to_checkpoint_line_stats`, `CheckpointValues`; dependent files `crate::authorship::ignore::effective_ignore_patterns`, `crate::authorship::internal_db::InternalDatabase`, `crate::authorship::range_authorship`, `crate::authorship::stats::stats_command`, `crate::commands`, `crate::config`, `crate::daemon::ControlRequest`, `crate::git::find_repository`. Suggested fix: Add or update a sibling test (`*.test.*`, `*_test.*`, or `__tests__/`) that exercises the new behavior in `src/commands/autter_handlers.rs` around line 543. Cover the happy path AND at least one failure case; without a test, a regression here will only be caught in production. Blast radius — skipping this guardrail cascades to the downstream usage that depends on this file: functions `handle_autter`, `synthesize_hook_input_from_cli_args`, `handle_checkpoint`, `report_checkpoint_usage_error`, `checkpoint_ts`, `new`, `to_checkpoint_line_stats`, `CheckpointValues`; dependent files `crate::authorship::ignore::effective_ignore_patterns`, `crate::authorship::internal_db::InternalDatabase`, `crate::authorship::range_authorship`, `crate::authorship::stats::stats_command`, `crate::commands`, `crate::config`, `crate::daemon::ControlRequest`, `crate::git::find_repository`.
// send fails. Paid only when the daemon is down — the exact case that used
// to silently drop a whole session's checkpoints — so a one-off spawn cost
// beats losing the data.
const CHECKPOINT_DAEMON_SPAWN_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);

let mut sent_count = 0u64;
let mut active_socket = config.control_socket_path.clone();
let mut tried_spawn = false;
for request in requests {
let t_send = std::time::Instant::now();
let control_request = ControlRequest::CheckpointRun {
request: Box::new(request),
};
let send_result = crate::daemon::send_control_request_fire_and_forget(
&config.control_socket_path,
&control_request,
);
let mut send_result =
crate::daemon::send_control_request_fire_and_forget(&active_socket, &control_request);

// A failed send almost always means the daemon is down (crashed, never
// started, or restarting). Rather than dropping the checkpoint — which
// silently loses the session's steps — spawn the daemon and retry once.
// `ensure_daemon_running` is a no-op when the daemon is already up, so a
// transient blip is retried too. Only attempt the spawn once per call.
if send_result.is_err() && !tried_spawn {
tried_spawn = true;
match crate::commands::daemon::ensure_daemon_running(CHECKPOINT_DAEMON_SPAWN_TIMEOUT) {
Ok(spawned) => {
active_socket = spawned.control_socket_path;
send_result = crate::daemon::send_control_request_fire_and_forget(
&active_socket,
&control_request,
);
}
Err(e) => {
eprintln!("Background worker unavailable, checkpoint dropped: {}", e);
std::process::exit(0);
}
}
}
if perf {
eprintln!(
"[perf] checkpoint: ipc_send={:.1}ms",
Expand Down
39 changes: 39 additions & 0 deletions src/daemon/checkpoint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,38 @@
}
}

/// Serialize the line ranges a single checkpoint (step) touched in one file, as a

Check failure on line 48 in src/daemon/checkpoint.rs

View check run for this annotation

Autter.dev / autter/review-gate

🔴 High · Source changes without matching tests

This source file was changed but no sibling test file is added or modified anywhere in the PR. Blast radius — skipping this guardrail cascades to the downstream usage that depends on this file: functions `to_checkpoint_line_stats`, `PreviousFileState`, `checkpoint_ts`, `new`, `CheckpointValues`; dependent files `crate::authorship::attribution_tracker::{ Attribution, AttributionTracker, INITIAL_ATTRIBUTION_TS, LineAttribution, }`, `crate::authorship::authorship_log_serialization::generate_session_id`, `crate::authorship::authorship_log_serialization::generate_short_hash`, `crate::authorship::imara_diff_utils::{ LineChangeTag, compute_line_changes, normalize_line_endings, }`, `crate::authorship::working_log::AgentId`, `crate::authorship::working_log::CheckpointKind`, `crate::authorship::working_log::{Checkpoint, WorkingLogEntry}`, `crate::commands::checkpoint_agent::orchestrator::CheckpointRequest`. Suggested fix: Add or update a sibling test (`*.test.*`, `*_test.*`, or `__tests__/`) that exercises the new behavior in `src/daemon/checkpoint.rs` around line 48. Cover the happy path AND at least one failure case; without a test, a regression here will only be caught in production. Blast radius — skipping this guardrail cascades to the downstream usage that depends on this file: functions `to_checkpoint_line_stats`, `PreviousFileState`, `checkpoint_ts`, `new`, `CheckpointValues`; dependent files `crate::authorship::attribution_tracker::{ Attribution, AttributionTracker, INITIAL_ATTRIBUTION_TS, LineAttribution, }`, `crate::authorship::authorship_log_serialization::generate_session_id`, `crate::authorship::authorship_log_serialization::generate_short_hash`, `crate::authorship::imara_diff_utils::{ LineChangeTag, compute_line_changes, normalize_line_endings, }`, `crate::authorship::working_log::AgentId`, `crate::authorship::working_log::CheckpointKind`, `crate::authorship::working_log::{Checkpoint, WorkingLogEntry}`, `crate::commands::checkpoint_agent::orchestrator::CheckpointRequest`.

Check warning on line 48 in src/daemon/checkpoint.rs

View check run for this annotation

Autter.dev / autter/review-gate

🟠 Medium · Missing linked tracker issue

This PR's title and body do not reference any tracker issue (GitHub `#123`, Jira/Linear `KEY-123`, or `Fixes/Closes/Resolves`). Suggested fix: In the PR description, add a reference to the tracker issue this change implements (GitHub `#123`, Jira/Linear `PROJ-456`, or a `Fixes/Closes/Resolves` marker). Reviewers anchor on `src/daemon/checkpoint.rs` around line 48 need that context to understand why this change exists and what success looks like. Why it matters: reviewers and on-call engineers need the linked issue to understand the why behind a change months from now.
/// compact JSON array of `[start, end]` pairs (1-indexed, inclusive), sorted and
/// with overlapping/adjacent ranges merged. A checkpoint's own lines are the
/// attributions whose `author_id` embeds this checkpoint's `trace_id` (AI kinds
/// use `s_<session>::t_<trace>`). Returns `None` when nothing is attributable to

Check notice on line 52 in src/daemon/checkpoint.rs

View check run for this annotation

Autter.dev / autter/review-gate

💡 Suggestion · Batch size limit not detected

This handler appears to accept an unbounded array/list/batch input without a maximum item-count limit. Blast radius — abusing this cascades to the downstream usage that depends on this file: functions `to_checkpoint_line_stats`, `PreviousFileState`, `checkpoint_ts`, `new`, `CheckpointValues`; dependent files `crate::authorship::attribution_tracker::{ Attribution, AttributionTracker, INITIAL_ATTRIBUTION_TS, LineAttribution, }`, `crate::authorship::authorship_log_serialization::generate_session_id`, `crate::authorship::authorship_log_serialization::generate_short_hash`, `crate::authorship::imara_diff_utils::{ LineChangeTag, compute_line_changes, normalize_line_endings, }`, `crate::authorship::working_log::AgentId`, `crate::authorship::working_log::CheckpointKind`, `crate::authorship::working_log::{Checkpoint, WorkingLogEntry}`, `crate::commands::checkpoint_agent::orchestrator::CheckpointRequest`. Suggested fix: In `src/daemon/checkpoint.rs` around line 52, this endpoint accepts an unbounded array/list/batch without a maximum item count. Enforce a max-items limit (validate length before processing, reject oversized payloads with 413/400, and consider chunked processing) so a single request cannot exhaust resources. Blast radius — abusing this cascades to the downstream usage that depends on this file: functions `to_checkpoint_line_stats`, `PreviousFileState`, `checkpoint_ts`, `new`, `CheckpointValues`; dependent files `crate::authorship::attribution_tracker::{ Attribution, AttributionTracker, INITIAL_ATTRIBUTION_TS, LineAttribution, }`, `crate::authorship::authorship_log_serialization::generate_session_id`, `crate::authorship::authorship_log_serialization::generate_short_hash`, `crate::authorship::imara_diff_utils::{ LineChangeTag, compute_line_changes, normalize_line_endings, }`, `crate::authorship::working_log::AgentId`, `crate::authorship::working_log::CheckpointKind`, `crate::authorship::working_log::{Checkpoint, WorkingLogEntry}`, `crate::commands::checkpoint_agent::orchestrator::CheckpointRequest`.
/// this step (e.g. plain human saves), so the metric field stays empty.
fn serialize_touched_ranges(entry: &WorkingLogEntry, trace_id: &str) -> Option<String> {
if trace_id.is_empty() {
return None;
}
let mut ranges: Vec<(u32, u32)> = entry
.line_attributions
.iter()
.filter(|la| la.author_id.contains(trace_id))
.map(|la| (la.start_line, la.end_line))
.collect();
if ranges.is_empty() {
return None;
}
ranges.sort_unstable();
let mut merged: Vec<[u32; 2]> = Vec::with_capacity(ranges.len());
for (start, end) in ranges {
match merged.last_mut() {
Some(last) if start <= last[1].saturating_add(1) => {
last[1] = last[1].max(end);
}
_ => merged.push([start, end]),
}
}
serde_json::to_string(&merged).ok()
}

/// Latest checkpoint state needed to process a file in the next checkpoint.
#[derive(Debug, Clone)]
struct PreviousFileState {
Expand Down Expand Up @@ -389,6 +421,13 @@
if let Some(ek) = edit_kind {
values = values.edit_kind(ek);
}
// Record the exact line ranges this step touched, so per-session step
// detail survives even when a later checkpoint overwrites these lines
// (the authorship note would drop them). Attributions carrying this
// checkpoint's trace_id are the lines it authored.
if let Some(ranges) = serialize_touched_ranges(entry, &trace_id) {
values = values.line_ranges(ranges);
}

let file_attrs = attrs.clone().author(&checkpoint.author);
crate::metrics::record(values, file_attrs);
Expand Down
23 changes: 23 additions & 0 deletions src/metrics/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,7 @@
pub const LINES_DELETED_SLOC: usize = 6; // u32 - for this file
pub const TOOL_USE_ID: usize = 7; // String - nullable
pub const EDIT_KIND: usize = 8; // String - nullable ("file_edit" | "bash")
pub const LINE_RANGES: usize = 9; // String - nullable; JSON `[[start,end],…]` (1-indexed, inclusive) touched by this step in this file

Check failure on line 455 in src/metrics/events.rs

View check run for this annotation

Autter.dev / autter/review-gate

🔴 High · Source changes without matching tests

This source file was changed but no sibling test file is added or modified anywhere in the PR. Blast radius — skipping this guardrail cascades to the downstream usage that depends on this file: functions `checkpoint_ts`, `human_additions`, `ai_additions`, `tool_model_pairs`, `git_diff_deleted_lines`, `ai_accepted`, `new`, `to_checkpoint_line_stats`; dependent files `super::pos_encoded::{ PosEncoded, PosField, sparse_get_string, sparse_get_u32, sparse_get_u64, sparse_get_vec_string, sparse_get_vec_u32, sparse_set, string_to_json, u32_to_json, u64_to_json, vec_string_to_json, vec_u32_to_json, }`, `super::types::{EventValues, MetricEventId, SparseArray}`. Suggested fix: Add or update a sibling test (`*.test.*`, `*_test.*`, or `__tests__/`) that exercises the new behavior in `src/metrics/events.rs` around line 455. Cover the happy path AND at least one failure case; without a test, a regression here will only be caught in production. Blast radius — skipping this guardrail cascades to the downstream usage that depends on this file: functions `checkpoint_ts`, `human_additions`, `ai_additions`, `tool_model_pairs`, `git_diff_deleted_lines`, `ai_accepted`, `new`, `to_checkpoint_line_stats`; dependent files `super::pos_encoded::{ PosEncoded, PosField, sparse_get_string, sparse_get_u32, sparse_get_u64, sparse_get_vec_string, sparse_get_vec_u32, sparse_set, string_to_json, u32_to_json, u64_to_json, vec_string_to_json, vec_u32_to_json, }`, `super::types::{EventValues, MetricEventId, SparseArray}`.
}

/// Values for Event ID 4: checkpoint
Expand All @@ -471,6 +472,7 @@
/// | 6 | lines_deleted_sloc | u32 |
/// | 7 | external_tool_use_id | String (nullable) |
/// | 8 | edit_kind | String (nullable) |
/// | 9 | line_ranges | String (nullable) — JSON `[[start,end],…]` touched by this step |
#[derive(Debug, Clone, Default)]
pub struct CheckpointValues {
pub checkpoint_ts: PosField<u64>,
Expand All @@ -482,6 +484,10 @@
pub lines_deleted_sloc: PosField<u32>,
pub external_tool_use_id: PosField<String>,
pub edit_kind: PosField<String>,
/// JSON `[[start,end],…]` (1-indexed, inclusive) of the line ranges this
/// checkpoint/step touched in this file. Empty/absent on older clients and
/// on checkpoints that touched no attributable lines (e.g. human saves).
pub line_ranges: PosField<String>,
}

impl CheckpointValues {
Expand Down Expand Up @@ -587,6 +593,17 @@
self.edit_kind = Some(None);
self
}

pub fn line_ranges(mut self, value: impl Into<String>) -> Self {
self.line_ranges = Some(Some(value.into()));
self
}

#[allow(dead_code)]
pub fn line_ranges_null(mut self) -> Self {
self.line_ranges = Some(None);
self
}
}

impl PosEncoded for CheckpointValues {
Expand Down Expand Up @@ -634,6 +651,11 @@
checkpoint_pos::EDIT_KIND,
string_to_json(&self.edit_kind),
);
sparse_set(
&mut map,
checkpoint_pos::LINE_RANGES,
string_to_json(&self.line_ranges),
);

map
}
Expand All @@ -649,6 +671,7 @@
lines_deleted_sloc: sparse_get_u32(arr, checkpoint_pos::LINES_DELETED_SLOC),
external_tool_use_id: sparse_get_string(arr, checkpoint_pos::TOOL_USE_ID),
edit_kind: sparse_get_string(arr, checkpoint_pos::EDIT_KIND),
line_ranges: sparse_get_string(arr, checkpoint_pos::LINE_RANGES),
}
}
}
Expand Down
Loading