Merge pull request #2161 from idling11/feat/slop-ledger
Feat/Add a durable `SlopLedger` that makes invisible architectural residue visible and queryable across agent sessions
This commit is contained in:
@@ -744,6 +744,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,
|
||||
"[{}] {} ({:?} | {:?}) — {}",
|
||||
crate::slop_ledger::short_id(&entry.id),
|
||||
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:
|
||||
|
||||
@@ -546,6 +546,13 @@ pub const COMMANDS: &[CommandInfo] = &[
|
||||
usage: "/cache [count|inspect|stats|warmup]",
|
||||
description_id: MessageId::CmdCacheDescription,
|
||||
},
|
||||
// Slop Ledger (#2127)
|
||||
CommandInfo {
|
||||
name: "slop",
|
||||
aliases: &["canzha"],
|
||||
usage: "/slop [query|export]",
|
||||
description_id: MessageId::CmdSlopDescription,
|
||||
},
|
||||
];
|
||||
|
||||
/// Execute a slash command
|
||||
@@ -621,6 +628,9 @@ pub fn execute(cmd: &str, app: &mut App) -> CommandResult {
|
||||
"balance" => balance::balance(app),
|
||||
"cache" => debug::cache(app, arg),
|
||||
|
||||
// Slop ledger (#2127)
|
||||
"slop" | "canzha" => config::slop(app, arg),
|
||||
|
||||
// ChangeLog command
|
||||
"change" => change::change(app, arg),
|
||||
"system" | "xitong" => debug::system_prompt(app),
|
||||
|
||||
@@ -12,7 +12,7 @@ use std::collections::{HashMap, HashSet};
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{Arc, Mutex as StdMutex};
|
||||
use std::time::{Duration, Instant};
|
||||
use std::time::{Duration, Instant, SystemTime};
|
||||
|
||||
use anyhow::Result;
|
||||
use futures_util::StreamExt;
|
||||
@@ -358,6 +358,10 @@ 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 keyed by the ledger file's modified time.
|
||||
/// This keeps prompt refreshes cheap while still noticing append/update
|
||||
/// writes from slop ledger tools during the same session.
|
||||
slop_ledger_gate_cache: Option<(Option<SystemTime>, Option<String>)>,
|
||||
}
|
||||
|
||||
// === Internal tool helpers ===
|
||||
@@ -599,6 +603,7 @@ impl Engine {
|
||||
turn_counter: 0,
|
||||
lsp_manager,
|
||||
pending_lsp_blocks: Vec::new(),
|
||||
slop_ledger_gate_cache: None,
|
||||
workshop_vars,
|
||||
sandbox_backend,
|
||||
};
|
||||
@@ -1907,8 +1912,20 @@ 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).
|
||||
let gate_block = self.slop_ledger_gate_block();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -1920,6 +1937,31 @@ impl Engine {
|
||||
}
|
||||
}
|
||||
|
||||
fn slop_ledger_gate_block(&mut self) -> Option<String> {
|
||||
let modified = crate::slop_ledger::SlopLedger::default_path()
|
||||
.ok()
|
||||
.and_then(|path| std::fs::metadata(path).ok())
|
||||
.and_then(|metadata| metadata.modified().ok());
|
||||
|
||||
if let Some((cached_modified, cached_block)) = &self.slop_ledger_gate_cache
|
||||
&& *cached_modified == modified
|
||||
{
|
||||
return cached_block.clone();
|
||||
}
|
||||
|
||||
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((modified, loaded.clone()));
|
||||
loaded
|
||||
}
|
||||
|
||||
fn merge_compaction_summary(&mut self, summary_prompt: Option<SystemPrompt>) {
|
||||
if summary_prompt.is_none() {
|
||||
return;
|
||||
|
||||
@@ -67,6 +67,14 @@ impl Engine {
|
||||
.with_parallel_tool()
|
||||
.with_recall_archive_tool();
|
||||
|
||||
// SlopLedger: plan mode only gets read-only query + export,
|
||||
// agent/yolo get the full set including append + update.
|
||||
builder = if mode == AppMode::Plan {
|
||||
builder.with_slop_ledger_read_only_tools()
|
||||
} else {
|
||||
builder.with_slop_ledger_tools()
|
||||
};
|
||||
|
||||
if mode != AppMode::Plan {
|
||||
builder = builder
|
||||
.with_rlm_tool(self.deepseek_client.clone(), self.session.model.clone())
|
||||
|
||||
@@ -303,6 +303,7 @@ pub enum MessageId {
|
||||
CmdSettingsDescription,
|
||||
CmdSkillDescription,
|
||||
CmdSkillsDescription,
|
||||
CmdSlopDescription,
|
||||
CmdStashDescription,
|
||||
CmdStatusDescription,
|
||||
CmdStatuslineDescription,
|
||||
@@ -563,6 +564,7 @@ pub const ALL_MESSAGE_IDS: &[MessageId] = &[
|
||||
MessageId::CmdSettingsDescription,
|
||||
MessageId::CmdSkillDescription,
|
||||
MessageId::CmdSkillsDescription,
|
||||
MessageId::CmdSlopDescription,
|
||||
MessageId::CmdStashDescription,
|
||||
MessageId::CmdStatusDescription,
|
||||
MessageId::CmdStatuslineDescription,
|
||||
@@ -1046,6 +1048,7 @@ fn english(id: MessageId) -> &'static str {
|
||||
MessageId::CmdSkillsDescription => {
|
||||
"List local skills (filter by `/skills <prefix>`; --remote browses the curated registry)"
|
||||
}
|
||||
MessageId::CmdSlopDescription => "Inspect or export the SlopLedger",
|
||||
MessageId::CmdStashDescription => {
|
||||
"Park or restore a composer draft (Ctrl+S to push, /stash list/pop)"
|
||||
}
|
||||
@@ -1457,6 +1460,7 @@ fn vietnamese(id: MessageId) -> Option<&'static str> {
|
||||
MessageId::CmdSkillsDescription => {
|
||||
"Liệt kê các kỹ năng cục bộ (lọc bằng `/skills <tiền_tố>`; --remote để duyệt kho lưu trữ được kiểm duyệt)"
|
||||
}
|
||||
MessageId::CmdSlopDescription => "Kiểm tra hoặc xuất SlopLedger",
|
||||
MessageId::CmdStashDescription => {
|
||||
"Tạm cất hoặc khôi phục bản nháp (Ctrl+S để cất, /stash list/pop để xem/lấy ra)"
|
||||
}
|
||||
@@ -1880,6 +1884,7 @@ fn japanese(id: MessageId) -> Option<&'static str> {
|
||||
MessageId::CmdSkillsDescription => {
|
||||
"ローカルスキルを一覧表示(`/skills <prefix>` で絞り込み、--remote で精選レジストリを参照)"
|
||||
}
|
||||
MessageId::CmdSlopDescription => "Inspect or export the SlopLedger",
|
||||
MessageId::CmdStashDescription => {
|
||||
"コンポーザーの下書きを退避/復元(Ctrl+S で退避、/stash list|pop)"
|
||||
}
|
||||
@@ -2248,6 +2253,7 @@ fn chinese_simplified(id: MessageId) -> Option<&'static str> {
|
||||
MessageId::CmdSkillsDescription => {
|
||||
"列出本地技能(用 `/skills <prefix>` 按名称前缀过滤,--remote 浏览精选注册表)"
|
||||
}
|
||||
MessageId::CmdSlopDescription => "Inspect or export the SlopLedger",
|
||||
MessageId::CmdStashDescription => "暂存或恢复输入草稿(Ctrl+S 暂存,/stash list|pop)",
|
||||
MessageId::CmdStatusDescription => "显示当前运行状态",
|
||||
MessageId::CmdStatuslineDescription => "配置底栏要显示哪些条目",
|
||||
@@ -2612,6 +2618,7 @@ fn portuguese_brazil(id: MessageId) -> Option<&'static str> {
|
||||
MessageId::CmdSkillsDescription => {
|
||||
"Listar skills locais (filtre com `/skills <prefixo>`; --remote navega pelo registro curado)"
|
||||
}
|
||||
MessageId::CmdSlopDescription => "Inspect or export the SlopLedger",
|
||||
MessageId::CmdStashDescription => {
|
||||
"Estacionar ou restaurar rascunho do compositor (Ctrl+S estaciona, /stash list|pop)"
|
||||
}
|
||||
@@ -3030,6 +3037,7 @@ fn spanish_latin_america(id: MessageId) -> Option<&'static str> {
|
||||
MessageId::CmdSkillsDescription => {
|
||||
"Listar skills locales (filtra con `/skills <prefijo>`; --remote navega el registro curado)"
|
||||
}
|
||||
MessageId::CmdSlopDescription => "Inspect or export the SlopLedger",
|
||||
MessageId::CmdStashDescription => {
|
||||
"Estacionar o restaurar borrador del compositor (Ctrl+S estaciona, /stash list|pop)"
|
||||
}
|
||||
|
||||
@@ -68,6 +68,7 @@ mod settings;
|
||||
mod shell_dispatcher;
|
||||
mod skill_state;
|
||||
mod skills;
|
||||
mod slop_ledger;
|
||||
mod snapshot;
|
||||
mod task_manager;
|
||||
#[cfg(test)]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -733,6 +733,30 @@ impl ToolRegistryBuilder {
|
||||
self.with_tool(Arc::new(RememberTool))
|
||||
}
|
||||
|
||||
/// Include the slop ledger tools (#2127) — durable tracking of
|
||||
/// unresolved architectural residue: append, query, update, export.
|
||||
/// Registered unconditionally; the ledger JSON file is auto-created
|
||||
/// on first append.
|
||||
#[must_use]
|
||||
pub fn with_slop_ledger_tools(self) -> Self {
|
||||
use crate::slop_ledger::{
|
||||
SlopLedgerAppendTool, SlopLedgerExportTool, SlopLedgerQueryTool, SlopLedgerUpdateTool,
|
||||
};
|
||||
self.with_tool(Arc::new(SlopLedgerAppendTool))
|
||||
.with_tool(Arc::new(SlopLedgerQueryTool))
|
||||
.with_tool(Arc::new(SlopLedgerUpdateTool))
|
||||
.with_tool(Arc::new(SlopLedgerExportTool))
|
||||
}
|
||||
|
||||
/// Read-only subset of slop ledger tools (#2127) for plan mode:
|
||||
/// only query and export — no append or update.
|
||||
#[must_use]
|
||||
pub fn with_slop_ledger_read_only_tools(self) -> Self {
|
||||
use crate::slop_ledger::{SlopLedgerExportTool, SlopLedgerQueryTool};
|
||||
self.with_tool(Arc::new(SlopLedgerQueryTool))
|
||||
.with_tool(Arc::new(SlopLedgerExportTool))
|
||||
}
|
||||
|
||||
/// Include the `notify` tool — model-callable desktop notification
|
||||
/// (#1322). Routes through the existing `tui::notifications` OSC 9 /
|
||||
/// BEL pipeline so the user's `[notifications].method` config is
|
||||
|
||||
@@ -1605,6 +1605,25 @@ async fn run_event_loop(
|
||||
// composer receipt), regardless of notification method
|
||||
// or platform.
|
||||
if status == crate::core::events::TurnOutcomeStatus::Completed {
|
||||
// SlopLedger completion-gate: after every completed
|
||||
// turn, check whether there are unresolved slop entries
|
||||
// the agent should address before claiming the task is
|
||||
// done (#2127). This runs autonomously — no tool call
|
||||
// required — so the agent can't forget to check.
|
||||
if let Ok(ledger) = crate::slop_ledger::SlopLedger::load()
|
||||
&& ledger.has_open_entries()
|
||||
{
|
||||
if let Some(gate_msg) = ledger.completion_gate_summary() {
|
||||
let short =
|
||||
gate_msg.lines().nth(4).unwrap_or("review before done");
|
||||
app.push_status_toast(
|
||||
format!("⚠️ SlopLedger: {short}"),
|
||||
crate::tui::app::StatusToastLevel::Warning,
|
||||
Some(12_000),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let tool_count = app.tool_evidence.len();
|
||||
let mut receipt = "✓ turn completed".to_string();
|
||||
if tool_count > 0 {
|
||||
|
||||
Reference in New Issue
Block a user