diff --git a/CHANGELOG.md b/CHANGELOG.md index 5485986c..0da205b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ versioning theo [SemVer](https://semver.org). **8sync rule:** mỗi PR cập nh ## [Unreleased] +## [0.110.41] — 2026-10-22 +### Fixed +- **`auto merge` không kẹt cả board vì một card chờ Ship** (owner: "bị kẹt hoài vậy auto ship chứ"; live zus/ide: 19/27 card approved có PR đã merge từ hôm trước nhưng card ngồi yên vì 8 card không merge được làm cả lệnh dừng): chưa có quyền tự merge, `auto merge` vẫn land những việc không cần quyền — PR đã merge → card done, conflict/base sai → về ready/review — rồi MỚI hỏi owner MỘT câu Ship cho đúng những card cần `gh pr merge` thật (hoặc chưa có PR). Không bao giờ tự merge khi chưa có quyền. ## [0.110.40] — 2026-10-22 ### Added - **Một team cho nhiều dự án: `8sync ccc auto up --company` / `auto on --company`** (owner 2026-10-22: "1 cty rooms/roles agent nhận nhiều dự án … chứ hiện tại mỗi dự án 1 hạm đội riêng"; đo 2026-10-22: 17 phòng rảnh giữ 9,4 GB cho hai dự án). Cờ `company` trong auto.json (giữ qua on/off/pause như `teams`): phòng hết card ở dự án mình thì `auto claim` lấy card gấp nhất (P0 trước, rồi thứ tự scope) ở dự án khác trong scope của auto, `auto wait` thức dậy khi dự án khác có card; card về kèm `root` và hướng dẫn `env -C 8sync ccc auto … --as ` (board, inbox, grant vẫn theo từng repo; dự án bị pin bằng lease vẫn bị bỏ qua). `auto status --json` thêm `company`. Test auto +1 (round trip, chọn theo prio, bỏ qua dự án pin/không board), wait +2 (card dự án khác, dự án mình ưu tiên). HQ nhóm "Công ty" sẽ ở bản sau. diff --git a/Cargo.lock b/Cargo.lock index 9cecb7d1..a3473054 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -200,7 +200,7 @@ dependencies = [ [[package]] name = "ccc-core" -version = "0.110.40" +version = "0.110.41" dependencies = [ "anyhow", "dirs", @@ -1503,7 +1503,7 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "su-code" -version = "0.110.40" +version = "0.110.41" dependencies = [ "anyhow", "axum", diff --git a/Cargo.toml b/Cargo.toml index 2b60b1ce..7c4f917a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ resolver = "2" members = ["crates/cli", "crates/ccc-core"] [workspace.package] -version = "0.110.40" +version = "0.110.41" edition = "2021" license = "PolyForm-Noncommercial-1.0.0" 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 c782945b..62383648 100644 --- a/assets/claude/.claude-plugin/plugin.json +++ b/assets/claude/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "8sync-kit", "displayName": "8sync kit", - "version": "0.110.40", + "version": "0.110.41", "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://get.8syncdev.com", diff --git a/crates/cli/src/verbs/ccc/merge.rs b/crates/cli/src/verbs/ccc/merge.rs index 3a52c0b1..b600a58f 100644 --- a/crates/cli/src/verbs/ccc/merge.rs +++ b/crates/cli/src/verbs/ccc/merge.rs @@ -299,6 +299,11 @@ struct Ctx<'a> { /// This project's `owner/repo` ([`origin_repo`]), read only for a card without a PR field. origin: &'a dyn Fn() -> Option, gh: Gh<'a>, + /// Whether this run may really `gh pr merge`. The ungranted `auto merge` (owner 2026-10-22: + /// "bị kẹt hoài vậy … auto ship chứ") still lands what needs no permission — a PR already + /// merged is marked done, a conflict goes back to ready — and collects the rest for the one + /// Ship question instead of stalling the whole board on it. + merge_allowed: bool, } /// Pure: the PR a card without a `pr` field names in its notes, newest note first: `zus-backend#19 @@ -419,6 +424,7 @@ fn land(t: &Target, c: &Ctx, first: bool) -> Outcome { } }; Ok(match decide(&sha, &v, base_merged, main) { + Step::Merge(head) if !c.merge_allowed => ("ask", None, Some("PR mở, chờ owner Duyệt tất cả (Ship)".into())), Step::Merge(head) => { // GitHub refuses to merge a draft; review already approved this head (4 failed // merges on 3d-game, 2026-10-20). @@ -489,20 +495,39 @@ pub(super) fn run(as_json: bool, home: &Path, root: &Path, who: Option<&str>, no if let Some(lane) = who.and_then(super::room::find).and_then(|r| r.lane).filter(|l| *l != "backlog") { bail!("`auto merge` là việc của phòng lead, không phải phòng cột {lane}"); } + // Never guess `main`: on a master repo that would merge stacked PRs into the wrong branch. + let Some(main) = default_branch(root) else { bail!(NO_DEFAULT) }; + let gh = |args: &[&str]| gh(root, args); + let main_of = |repo: &str| default_branch_of(root, Some(repo)); + let origin = || origin_repo(root); if super::grant::self_merge(&super::grant::dir(home), root, now).is_none() { let approved: Vec = target::list(home, root)?.into_iter().filter(|t| t.state == "open" && t.lane == "approved").collect(); if approved.is_empty() { println!("không có card approved"); return Ok(()); } - bail!("{}", ship_question(&approved)); + // Free outcomes first (owner 2026-10-22: 19 of 27 approved cards on zus/ide had PRs merged + // on GitHub the day before and sat in approved because one unmergeable card made the whole + // run bail): done / back / retarget need no merge permission. What really needs + // `gh pr merge` (or has no PR at all) goes into the owner's ONE Ship question. + let c = Ctx { home, root, who: Some("lead"), now, main: &main, main_of: &main_of, origin: &origin, gh: &gh, merge_allowed: false }; + let mut left: Vec = Vec::new(); + for t in &approved { + let mut o = land(t, &c, true); + if o.action == "deferred" { + o = land(t, &c, false); + } + match o.action.as_ref() { + "ask" | "wait" | "failed" => left.push(t.clone()), + a => println!("{a}: {}", line(&o)), + } + } + if left.is_empty() { + return Ok(()); + } + bail!("{}", ship_question(&left)); } - // Never guess `main`: on a master repo that would merge stacked PRs into the wrong branch. - let Some(main) = default_branch(root) else { bail!(NO_DEFAULT) }; - let gh = |args: &[&str]| gh(root, args); - let main_of = |repo: &str| default_branch_of(root, Some(repo)); - let origin = || origin_repo(root); - run_with(as_json, &Ctx { home, root, who, now, main: &main, main_of: &main_of, origin: &origin, gh: &gh }) + run_with(as_json, &Ctx { home, root, who, now, main: &main, main_of: &main_of, origin: &origin, gh: &gh, merge_allowed: true }) } /// HQ's revive tick (every 2 min): land approved cards in code while the owner's self-merge grant @@ -520,7 +545,7 @@ pub(super) fn background(home: &Path, root: &Path, now: u64) -> Result