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
94 changes: 94 additions & 0 deletions build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,101 @@ fn emit_dashboard_asset_inputs() -> String {
format!("{:016x}", hasher.finish())
}

/// Recursively collects every file under `root`, relative to `root`, using
/// forward-slash separators. Returns sorted paths so codegen is deterministic.
fn collect_files_relative(root: &Path) -> Vec<String> {
fn walk(base: &Path, dir: &Path, out: &mut Vec<String>) {
let Ok(entries) = fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
walk(base, &path, out);
} else if path.is_file() {
if let Ok(relative) = path.strip_prefix(base) {
out.push(relative.to_string_lossy().replace('\\', "/"));
}
}
}
}
let mut files = Vec::new();
walk(root, root, &mut files);
files.sort();
files
}

/// True when `path` is a readable UTF-8 text file. Used to fail the skill
/// bundle codegen early with a clear message when a binary support file would
/// otherwise break `include_str!` with an opaque compile error.
fn is_probably_utf8_text(path: &Path) -> bool {
match fs::read(path) {
Ok(bytes) => std::str::from_utf8(&bytes).is_ok(),
// Unreadable files fall through to include_str!'s own error.
Err(_) => true,
}
}

/// Generates `$OUT_DIR/plugin_bundle_generated.rs`: a recursive manifest of
/// every file under `plugin/skills/` (SKILL.md *and* any `references/`,
/// `scripts/`, `assets/` support files), embedded via `include_str!` at compile
/// time. Moving embedding off a hand-maintained flat list lets skills ship
/// support files without a matching table edit; the coverage tests assert the
/// generated set equals the on-disk tree.
///
/// Each entry's deploy path equals its `plugin/`-relative source path
/// (`skills/<skill>/<subpath>`), which is identical for every host, so a single
/// generated slice serves Claude, Codex, and Cursor (Cursor filters out the
/// `tracedecay-*` dispatcher skills at compose time).
fn generate_skill_bundle() {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR");
let skills_root = Path::new(&manifest_dir).join("plugin/skills");
println!("cargo::rerun-if-changed=plugin/skills");

let mut code = String::new();
code.push_str(
"// @generated by build.rs (generate_skill_bundle). Do not edit.\n\
/// Every file under `plugin/skills/` (SKILL.md + support files), embedded\n\
/// recursively at compile time. `relative` is the deploy path (identical to\n\
/// the `plugin/`-relative source path).\n\
pub const GENERATED_SKILL_FILES: &[PluginFile] = &[\n",
);
for relative in collect_files_relative(&skills_root) {
// rerun on each individual file so edits re-trigger codegen even if the
// directory mtime does not change.
println!("cargo::rerun-if-changed=plugin/skills/{relative}");
// Every embedded file goes through `include_str!`, which only accepts
// UTF-8. A binary support file (e.g. `assets/*.png`) would otherwise
// fail to compile with an opaque "stream did not contain valid UTF-8"
// error pointing at the generated file, not the offending asset. Guard
// it here with a clear message; binary support files are not embeddable
// yet (add `include_bytes!` handling when that becomes a requirement).
let abs = skills_root.join(&relative);
if !is_probably_utf8_text(&abs) {
panic!(
"plugin/skills/{relative} is not a UTF-8 text file. The skill bundle codegen \
embeds every file via include_str!, so binary support files (e.g. images) are \
not embeddable yet. Remove the binary file or add include_bytes! support to \
build.rs (generate_skill_bundle)."
);
}
let deploy = format!("skills/{relative}");
let source = format!("skills/{relative}");
code.push_str(&format!(
" PluginFile {{ relative: {deploy:?}, contents: include_str!(concat!(env!(\"CARGO_MANIFEST_DIR\"), \"/plugin/{source}\")) }},\n"
));
}
code.push_str("];\n");

let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR");
let out_path = Path::new(&out_dir).join("plugin_bundle_generated.rs");
if let Err(e) = fs::write(&out_path, code) {
panic!("failed to write {}: {e}", out_path.display());
}
}

fn main() {
generate_skill_bundle();
let out_path = Path::new("src/resources/logo.ansi");
let logo_bytes = include_bytes!("src/resources/logo.png");
let ansi = logo_art::image_to_ansi(logo_bytes, 90);
Expand Down
31 changes: 20 additions & 11 deletions docs/AGENT-MEMORY-INTERCEPTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,16 @@ instead of — or layered on top of — each agent's native memory mechanism.
All file paths below were verified on this machine (Codex CLI 0.142.4, Cursor
with hooks + plugins, tracedecay plugin v0.0.23 installed for both agents).

> **Note (as of 2026-07-03):** the per-host `cursor-plugin/` / `codex-plugin/`
> source trees have since collapsed into a single shared `plugin/` tree, so
> skill/command/agent sources now live under `plugin/skills/…`,
> `plugin/commands/…`, and `plugin/agents/…` (Cursor-only surfaces under
> `plugin/overlays/cursor/…`). The `recalling-project-memory` and
> `curating-project-memory` skills were also merged into a single
> `project-memory` skill. References below to the old paths/slugs are retained
> as historical design context; the current locations are the shared-tree
> equivalents.

---

## 1. How Codex reads memory today
Expand Down Expand Up @@ -171,9 +181,9 @@ to `~/.cursor/plugins/local/tracedecay/`:
`src/hooks/`).
- **`rules/tracedecay.mdc`** — always-applied rule; its **Recall** bullet
steers models to `tracedecay_message_search` / `tracedecay_fact_store`
search and the `recalling-project-memory` skill.
- **`skills/`** — 25+ workflow skills incl. `recalling-project-memory`,
`curating-project-memory`, `recalling-session-context`; plus an
search and the `project-memory` skill.
- **`skills/`** — 25+ workflow skills incl. `project-memory` (the merged
recall+curate memory skill) and `recalling-session-context`; plus an
agent-managed skill overlay (`install_cursor_managed_skill_overlay`).
- **`agents/`** — `code-explorer`, `code-health-auditor`, `session-historian`
subagent definitions.
Expand Down Expand Up @@ -277,9 +287,9 @@ surfaces on a schedule," which design D reuses for memory.
| --- | --- | --- |
| Facts stored | ✅ fact store (9 facts here) + LCM transcripts | same store |
| Model *can* recall | ✅ MCP `tracedecay_fact_store` search + skill | ✅ same |
| Model is *told* to recall | ⚠️ soft steering in SessionStart/UserPromptSubmit context; `recalling-project-memory` skill matches only when the model thinks "recall" | ⚠️ one Recall bullet in `tracedecay.mdc`; same skill-match dependency |
| Model is *told* to recall | ⚠️ soft steering in SessionStart/UserPromptSubmit context; `project-memory` skill matches only when the model thinks "recall" | ⚠️ one Recall bullet in `tracedecay.mdc`; same skill-match dependency |
| Facts *pushed* into context | ❌ none — hook context is index status + hints only | ❌ none |
| Automatic storage | ⚠️ session_reflector exists but disabled by default; skills say "add facts **only when the user asks**" (`recalling-project-memory` guardrail) | same |
| Automatic storage | ⚠️ session_reflector exists but disabled by default; skills say "add facts **only when the user asks**" (`project-memory` guardrail) | same |
| Native memory overlap | ⚠️ Codex memories **on** (`features.memories=true`), learning from the same threads in parallel | Unknown toggle state; server-side, uninspectable |

The delta is precisely: **nothing proactively retrieves facts at
Expand Down Expand Up @@ -323,7 +333,7 @@ Implementation pointers: `src/hooks/codex.rs`, `src/hooks/cursor.rs`,
`src/memory/retrieval.rs` (`FactRetriever::search/probe`), analytics via
`record_hint_analytics` so injection quality is measurable. No plugin schema
change; hook hashes change → users re-trust via `/hooks` (already documented
in `codex-plugin/README.md`).
in the Codex plugin README).

### B. Cursor session-start injection + a materialized memory rule — **do with A**

Expand Down Expand Up @@ -359,11 +369,10 @@ doctor checks cover it.

*Effort: XS. Effect: medium. Risk: memory spam (mitigated by write-time dedupe).*

Today `recalling-project-memory`'s guardrail says add facts "**only when the
Today `project-memory`'s guardrail says add facts "**only when the
user asks**" — the opposite of agent-memory behavior. Change the instruction
(rule Recall bullet + skill in `cursor-plugin/rules/tracedecay.mdc`,
`cursor-plugin/skills/recalling-project-memory/SKILL.md`, codex-plugin
mirrors) to:
(rule Recall bullet in `plugin/overlays/cursor/rules/tracedecay.mdc` + the
`plugin/skills/project-memory/SKILL.md` skill, shared across every host) to:

- *Recall:* "before starting non-trivial work, search `tracedecay_fact_store`
for prior decisions" (currently phrased as fallback, not default).
Expand Down Expand Up @@ -458,7 +467,7 @@ alongside D and reusing the managed-file conventions from the skill overlay.
analytics.
2. **B2 + C** (materialized Cursor memory rule + proactive storage wording) —
one PR in `src/agents/cursor.rs` embedded files + plugin rule/skill text
(mirrored in `codex-plugin/` skill text).
(shared skill text under `plugin/skills/`).
3. **D** (reflector enablement UX) — config/doctor/dashboard nudge.
4. **E** (coexistence policy + optional Codex-memories harvest importer).
5. **F** (generalized AGENTS.md materialization across all 15 agent
Expand Down
Loading
Loading