From 26dfd6ee528779a866aed7567d6a9e7947d399e5 Mon Sep 17 00:00:00 2001 From: 8syncdev Date: Thu, 15 Oct 2026 14:59:18 +0700 Subject: [PATCH 1/2] fix(learn): resolve the trending dir from inside a su-code memory directory Running `8sync learn trending` with cwd = /su-code (the memory dir, which carries its own AGENTS.md) made root detection stop there and write su-code/su-code/research/trending/.md. `trending_dir()` now treats a su-code memory dir without a VCS/manifest of its own as part of its parent project, keeps a repo literally named su-code as a normal root, and resolves from any subfolder. Found by the R&D session in UAT of v0.91.0. Co-Authored-By: Claude Fable 5.1 --- CHANGELOG.md | 2 ++ crates/cli/src/verbs/learn.rs | 62 ++++++++++++++++++++++++++++++++--- 2 files changed, 59 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b5f905c..a53dbfd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ Mọi thay đổi đáng kể của `8sync` ghi vào đây. Format theo [Keep a versioning theo [SemVer](https://semver.org). **8sync rule:** mỗi PR cập nhật mục `Unreleased`. ## [Unreleased] +### Fixed +- `8sync learn trending` run from inside a project's `su-code/` memory directory (which carries its own AGENTS.md) wrote to `su-code/su-code/research/trending/`; the trending dir is now resolved by `trending_dir()`: a `su-code` memory dir without a VCS/manifest of its own belongs to its parent project, a repo that is itself named `su-code` stays a normal root, and any subfolder of the project resolves to `/su-code/research/trending/.md` (test `learn_trending_dir_resolves_from_memory_dir_and_subfolders`; reported by the R&D session in UAT of v0.91.0). ## [0.91.0] — 2026-10-15 ### Added diff --git a/crates/cli/src/verbs/learn.rs b/crates/cli/src/verbs/learn.rs index 2bf67ac7..3d39028c 100644 --- a/crates/cli/src/verbs/learn.rs +++ b/crates/cli/src/verbs/learn.rs @@ -7,7 +7,7 @@ //! The response is untrusted data: only a few typed fields are parsed and the description is //! truncated before it reaches the markdown table. -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::process::Command; use std::time::{SystemTime, UNIX_EPOCH}; @@ -83,10 +83,8 @@ fn trending(lang: Option, days: u32, top: u32, json: bool, out: Option

p, None => { - let root = crate::verbs::skill::discover::detect_current_project_root() - .or_else(|| std::env::current_dir().ok()) - .ok_or_else(|| anyhow!("không xác định được thư mục project"))?; - root.join("su-code/research/trending") + let cwd = std::env::current_dir().context("không xác định được thư mục hiện tại")?; + trending_dir(&cwd) } }; let path = dir.join(format!("{today}.md")); @@ -115,6 +113,33 @@ fn trending(lang: Option, days: u32, top: u32, json: bool, out: Option

/su-code/research/trending` for the project that contains `start`. +/// +/// Walks up from `start`. A `su-code` *memory* directory (STATE.md / KNOWLEDGE.md / skills inside, +/// but no VCS or manifest of its own, because `8sync .` seeds AGENTS.md there) belongs to its +/// parent project, so running from inside it must not nest `su-code/su-code`. A repo that is +/// itself named `su-code` (has `.git` / `Cargo.toml`) is a normal project root. +pub(crate) fn trending_dir(start: &Path) -> PathBuf { + const PROJECT: [&str; 6] = [".git", ".hg", "Cargo.toml", "package.json", "pyproject.toml", "go.mod"]; + const MEMORY: [&str; 4] = ["STATE.md", "KNOWLEDGE.md", "skills.toml", "skills"]; + let has = |p: &Path, names: &[&str]| names.iter().any(|n| p.join(n).exists()); + let mut p = start.to_path_buf(); + loop { + let named_memory = p.file_name().is_some_and(|n| n == "su-code"); + if named_memory && !has(&p, &PROJECT) && has(&p, &MEMORY) { + if let Some(parent) = p.parent() { + return parent.join("su-code/research/trending"); + } + } + if has(&p, &PROJECT) || p.join("AGENTS.md").exists() || p.join("CLAUDE.md").exists() || has(&p.join("su-code"), &MEMORY) { + return p.join("su-code/research/trending"); + } + if !p.pop() { + return start.join("su-code/research/trending"); + } + } +} + /// Repos the R&D session should not be handed (adult content shows up in star-sorted search). fn is_noise(r: &Repo) -> bool { let hay = format!("{} {}", r.full_name, r.description.as_deref().unwrap_or("")).to_lowercase(); @@ -307,6 +332,33 @@ mod tests { assert!(valid_full_name("a-b.c_d/e.f-g_h") && !valid_full_name("a//b") && !valid_full_name("a/b/c") && !valid_full_name("ab")); } + #[test] + fn learn_trending_dir_resolves_from_memory_dir_and_subfolders() { + let tmp = std::env::temp_dir().join(format!("8sync-learn-root-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&tmp); + // Project seeded by `8sync .`: no VCS, CLAUDE.md at root, memory dir with its own AGENTS.md. + let proj = tmp.join("proj"); + std::fs::create_dir_all(proj.join("su-code")).unwrap(); + std::fs::write(proj.join("CLAUDE.md"), "#").unwrap(); + std::fs::write(proj.join("su-code/AGENTS.md"), "#").unwrap(); + std::fs::write(proj.join("su-code/STATE.md"), "#").unwrap(); + std::fs::create_dir_all(proj.join("apps/web/src")).unwrap(); + let want = proj.join("su-code/research/trending"); + assert_eq!(trending_dir(&proj.join("su-code")), want, "cwd = memory dir must not nest su-code/su-code"); + assert_eq!(trending_dir(&proj), want); + assert_eq!(trending_dir(&proj.join("apps/web/src")), want); + // A repo that is itself called su-code (this one) is a normal root. + let repo = tmp.join("su-code"); + std::fs::create_dir_all(repo.join("su-code")).unwrap(); + std::fs::create_dir_all(repo.join(".git")).unwrap(); + std::fs::write(repo.join("Cargo.toml"), "").unwrap(); + std::fs::write(repo.join("su-code/STATE.md"), "#").unwrap(); + assert_eq!(trending_dir(&repo), repo.join("su-code/research/trending")); + assert_eq!(trending_dir(&repo.join("su-code")), repo.join("su-code/research/trending")); + assert_eq!(trending_dir(&repo.join("crates/cli")), repo.join("su-code/research/trending")); + let _ = std::fs::remove_dir_all(&tmp); + } + #[test] fn learn_date_math() { assert_eq!(ymd_from_days(0), "1970-01-01"); From 158022a1447c7868a1a92620f2144145b9a756b1 Mon Sep 17 00:00:00 2001 From: 8syncdev Date: Thu, 15 Oct 2026 15:13:06 +0700 Subject: [PATCH 2/2] =?UTF-8?q?release:=20v0.91.1=20=E2=80=94=20learn=20tr?= =?UTF-8?q?ending=20resolves=20the=20dir=20from=20inside=20su-code/?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 --- CHANGELOG.md | 2 ++ Cargo.lock | 4 ++-- Cargo.toml | 2 +- assets/claude/.claude-plugin/plugin.json | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a53dbfd4..bf744456 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ Mọi thay đổi đáng kể của `8sync` ghi vào đây. Format theo [Keep a versioning theo [SemVer](https://semver.org). **8sync rule:** mỗi PR cập nhật mục `Unreleased`. ## [Unreleased] + +## [0.91.1] — 2026-10-15 ### Fixed - `8sync learn trending` run from inside a project's `su-code/` memory directory (which carries its own AGENTS.md) wrote to `su-code/su-code/research/trending/`; the trending dir is now resolved by `trending_dir()`: a `su-code` memory dir without a VCS/manifest of its own belongs to its parent project, a repo that is itself named `su-code` stays a normal root, and any subfolder of the project resolves to `/su-code/research/trending/.md` (test `learn_trending_dir_resolves_from_memory_dir_and_subfolders`; reported by the R&D session in UAT of v0.91.0). diff --git a/Cargo.lock b/Cargo.lock index 878d43f9..ece94abe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -194,7 +194,7 @@ dependencies = [ [[package]] name = "ccc-core" -version = "0.91.0" +version = "0.91.1" dependencies = [ "anyhow", "serde", @@ -1398,7 +1398,7 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "su-code" -version = "0.91.0" +version = "0.91.1" dependencies = [ "anyhow", "axum", diff --git a/Cargo.toml b/Cargo.toml index c588d9eb..501c6b0e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ resolver = "2" members = ["crates/cli", "crates/ccc-core"] [workspace.package] -version = "0.91.0" +version = "0.91.1" edition = "2021" license = "MIT" authors = ["8sync-dev <8sync.dev.1111@gmail.com>"] diff --git a/assets/claude/.claude-plugin/plugin.json b/assets/claude/.claude-plugin/plugin.json index 314acef9..c04cad65 100644 --- a/assets/claude/.claude-plugin/plugin.json +++ b/assets/claude/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "8sync-kit", - "version": "0.91.0", + "version": "0.91.1", "description": "8 Sync kit for Claude Code: role-routed subagents (explore, task, quick-task, reviewer, designer, researcher, verifier, pm, architect, qa, critic) and the /ship /rules /sec /harden skills. Hooks, rules and the full skill set come from `8sync ccc kit`.", "author": { "name": "8 Sync", "url": "https://8syncdev.com" }, "homepage": "https://github.com/8-Sync-Dev/su-code",