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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ versioning theo [SemVer](https://semver.org). **8sync rule:** mỗi PR cập nh

## [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 `<project>/su-code/research/trending/<date>.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
- **`8sync learn trending`** (Rust): the most-starred repos created in the last N days (`--lang`, `--days 7`, `--top 25`, `--json`) through GitHub's official search API — not the trending page, which must not be scraped — written to `su-code/research/trending/<date>.md` with a "Có ích cho 8sync vì" column for the R&D room to fill. Unauthenticated, no token read; adult-content repos are dropped.
Expand Down
4 changes: 2 additions & 2 deletions 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
Expand Up @@ -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 <[email protected]>"]
Expand Down
2 changes: 1 addition & 1 deletion assets/claude/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
62 changes: 57 additions & 5 deletions crates/cli/src/verbs/learn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -83,10 +83,8 @@ fn trending(lang: Option<String>, days: u32, top: u32, json: bool, out: Option<P
let dir = match out {
Some(p) => 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"));
Expand Down Expand Up @@ -115,6 +113,33 @@ fn trending(lang: Option<String>, days: u32, top: u32, json: bool, out: Option<P
Ok(())
}

/// `<project>/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();
Expand Down Expand Up @@ -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");
Expand Down
Loading