-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Feat/Add a durable SlopLedger that makes invisible architectural residue visible and queryable across agent sessions
#2161
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
6bc8363
3cfb265
3e07399
d107433
f700076
8928e1d
9c5bf7d
9943fe5
e902681
ff1a8cd
a73da58
721a979
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -699,6 +699,47 @@ pub fn theme(app: &mut App, arg: Option<&str>) -> CommandResult { | |||||||||
| } | ||||||||||
| } | ||||||||||
|
|
||||||||||
| /// `/slop [query|export]` — inspect or export the slop ledger (#2127). | ||||||||||
| /// With no arguments, prints a summary. `query` shows filtered results; | ||||||||||
| /// `export` outputs the full ledger as Markdown. | ||||||||||
| pub fn slop(_app: &mut App, arg: Option<&str>) -> CommandResult { | ||||||||||
| let arg = arg.map(str::trim).unwrap_or(""); | ||||||||||
| let ledger = match crate::slop_ledger::SlopLedger::load() { | ||||||||||
| Ok(l) => l, | ||||||||||
| Err(e) => return CommandResult::error(format!("Failed to load slop ledger: {e}")), | ||||||||||
| }; | ||||||||||
|
|
||||||||||
| match arg { | ||||||||||
| "" => CommandResult::message(ledger.summary()), | ||||||||||
| "query" | "q" => { | ||||||||||
| if ledger.is_empty() { | ||||||||||
| return CommandResult::message("Slop ledger is empty."); | ||||||||||
| } | ||||||||||
| let mut out = String::new(); | ||||||||||
| for entry in &ledger.query(&Default::default()) { | ||||||||||
| use std::fmt::Write; | ||||||||||
| let _ = writeln!( | ||||||||||
| out, | ||||||||||
| "[{}] {} ({:?} | {:?}) — {}", | ||||||||||
| &entry.id[..8], | ||||||||||
|
Comment on lines
+723
to
+724
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||
| entry.bucket.as_str(), | ||||||||||
| entry.severity, | ||||||||||
| entry.status, | ||||||||||
| entry.title | ||||||||||
| ); | ||||||||||
| } | ||||||||||
| CommandResult::message(out) | ||||||||||
| } | ||||||||||
| "export" | "e" => { | ||||||||||
| let md = ledger.export_markdown(None, None); | ||||||||||
| CommandResult::message(md) | ||||||||||
| } | ||||||||||
| _ => CommandResult::error(format!( | ||||||||||
| "Unknown /slop action '{arg}'. Use /slop, /slop query, or /slop export." | ||||||||||
| )), | ||||||||||
| } | ||||||||||
| } | ||||||||||
|
|
||||||||||
| /// Manage workspace-level trust and the per-path allowlist. | ||||||||||
| /// | ||||||||||
| /// Subcommands: | ||||||||||
|
|
||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -331,6 +331,11 @@ pub struct Engine { | |
| /// Diagnostics collected during the current step's tool calls. Drained | ||
| /// and forwarded as a synthetic user message before the next API call. | ||
| pending_lsp_blocks: Vec<crate::lsp::DiagnosticBlock>, | ||
| /// Cached SlopLedger gate block so `refresh_system_prompt` doesn't hit | ||
| /// the filesystem on every turn (#2127). `None` = not yet loaded; | ||
| /// `Some(None)` = loaded, no open entries; `Some(Some(...))` = loaded, | ||
| /// gate block ready. | ||
| slop_ledger_gate_cache: Option<Option<String>>, | ||
| } | ||
|
|
||
| // === Internal tool helpers === | ||
|
|
@@ -564,6 +569,7 @@ impl Engine { | |
| turn_counter: 0, | ||
| lsp_manager, | ||
| pending_lsp_blocks: Vec::new(), | ||
| slop_ledger_gate_cache: None, | ||
| workshop_vars, | ||
| sandbox_backend, | ||
| }; | ||
|
|
@@ -1840,8 +1846,37 @@ impl Engine { | |
| }, | ||
| self.session.approval_mode, | ||
| ); | ||
| let stable_prompt = | ||
| let mut stable_prompt = | ||
| merge_system_prompts(Some(&base), self.session.compaction_summary_prompt.clone()); | ||
|
|
||
| // SlopLedger completion-gate: inject unresolved slop entries into the | ||
| // system prompt so the agent can autonomously review them before | ||
| // claiming the task is done (#2127). Cached to avoid filesystem I/O on | ||
| // every turn — only re-loaded when the cache is empty (first call or | ||
| // after invalidation). | ||
| let gate_block = match &self.slop_ledger_gate_cache { | ||
| Some(cached) => cached.clone(), | ||
| None => { | ||
| let loaded = crate::slop_ledger::SlopLedger::load() | ||
| .ok() | ||
| .and_then(|ledger| { | ||
| if ledger.has_open_entries() { | ||
| ledger.completion_gate_summary() | ||
| } else { | ||
| None | ||
| } | ||
| }); | ||
| self.slop_ledger_gate_cache = Some(loaded.clone()); | ||
| loaded | ||
| } | ||
| }; | ||
| if let Some(ref block) = gate_block { | ||
| if let Some(SystemPrompt::Text(prompt_text)) = &mut stable_prompt { | ||
| prompt_text.push_str("\n\n"); | ||
| prompt_text.push_str(block); | ||
| } | ||
| } | ||
|
Comment on lines
+1857
to
+1878
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
|
||
| let stable_hash = system_prompt_hash(stable_prompt.as_ref()); | ||
| if self.session.system_prompt_override { | ||
| self.session.last_system_prompt_hash = Some(stable_hash); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Slicing
&entry.id[..8]directly can panic if the ID is shorter than 8 bytes or sliced on an invalid UTF-8 boundary. Use.get(..8)to safely slice the ID.