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
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,19 @@ 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]
### Added
- **An Architect room (solution architecture) in the full team.** `8sync ccc auto up --roles full` (HQ **Team đầy đủ**) now opens `arch` in place of the second builder. The team stays at 8 rooms.
- Room names `arch`, `architect`, `solution…` and `sa` map to it. It runs on Opus, on its own loop like `rd`.
- It holds no board lane, so a card labelled `arch` never waits on a room the team lacks.
- Cards labelled `arch` (new service, DB, queue, cache, scale, public API) get an ADR before they are built. The room runs `/sysdesign` (sections of The System Design Primer plus the stack's official docs, with back-of-envelope numbers), writes an ADR with at least 2 options and the trade-offs, then notes `ADR: …` on the card. The ADR goes where the repo already records decisions (`docs/adr/`, `DECISIONS.md`, `docs/*DECISION*.md`), else `docs/adr/card-<id>-<slug>.md`, never a parallel place.
- The lead moves an `arch` card to ready only after that note. With no `arch` room, the lead writes a short ADR itself.
- On cards in review, the room checks the diff against the ADR and `docs/adr/budgets.md`, and leaves an `[arch]` note. Review still decides.
- Every day it does three passes:
- a sourced tech radar (new versions, breaking changes, CVEs of the repo's own stack), saved to `su-code/research/radar-<date>.md`;
- a measured scale-health pass (bundle or binary size, build and test time, slow queries, RAM, cold start, cost per request) against the budgets. Hard-coded limits (concurrency, pools, timeouts) go into the budgets with their reason;
- a drift pass: decisions that contradict each other or the code, stale service lists or diagrams, and drifted `file:line` references get one reconciling ADR or a doc fix.
- The worst bottleneck becomes an `arch` card with a measuring `--check`. With no measured number, no card is opened.
- `auto add --label arch` is accepted. HQ shows an "kiến trúc" chip on those cards and an Architect icon for the room.

## [0.100.8] — 2026-10-18
### Fixed
Expand Down
3 changes: 2 additions & 1 deletion apps/hq-ui/components/hq/card-sheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const LABEL_TONE: Record<string, { tone: string; ink: string }> = {
bug: LANE_TONE.blocked,
feature: LANE_TONE.ready,
qa: LANE_TONE.qa,
arch: LANE_TONE.review,
};

export const LANE_VI: Record<LaneKey, string> = {
Expand All @@ -42,7 +43,7 @@ export const LANE_VI: Record<LaneKey, string> = {
blocked: "Blocked",
};

const LABEL_VI: Record<string, string> = { bug: "bug", feature: "feature", qa: "qa" };
const LABEL_VI: Record<string, string> = { bug: "bug", feature: "feature", qa: "qa", arch: "kiến trúc" };

/** CSS variables a toned element reads: `--tone` for dots and tints, `--tone-ink` for text. */
export function toneOf(t: { tone: string; ink: string } | undefined): CSSProperties | undefined {
Expand Down
2 changes: 1 addition & 1 deletion apps/hq-ui/components/hq/guide.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export const GUIDE_STEPS: GuideStep[] = [
body:
"Thêm thư mục ở \"Mở team ở repo mới\" bên trái (dự án đã có phòng tự hiện), rồi bấm \"Mở team\": 5 phòng nền chạy liên tục. " +
"lead và rd dùng Opus (claude-opus-5-5) để chia việc, nghiên cứu; dev và review dùng Sonnet (claude-sonnet-5-5) để code, duyệt; qa dùng Haiku (claude-haiku-5-5) để kiểm nhanh. " +
"\"Team đầy đủ\" mở 8 phòng (thêm ba, dev2, ui), tốn quota gần gấp 8 lần một phòng.",
"\"Team đầy đủ\" mở 8 phòng (thêm ba, arch, ui; arch viết ADR cho card nhãn arch và làm tech radar mỗi ngày), tốn quota gần gấp 8 lần một phòng.",
},
{
icon: SendIcon,
Expand Down
8 changes: 4 additions & 4 deletions apps/hq-ui/components/hq/team-controls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const DONE: Record<Action, string> = {
};
const OPENING: Record<Action, string> = {
up: "lead, dev, qa, review, rd",
full: "8 phòng: lead, ba, rd, dev, dev2, ui, qa, review",
full: "8 phòng: lead, ba, arch, rd, dev, ui, qa, review",
clean: "dọn rác, khởi động lại phòng chạy bản cũ",
down: "",
};
Expand Down Expand Up @@ -54,7 +54,7 @@ export function TeamControls({ root, name, token, after }: { root: string; name:
<Button type="button" size="sm" variant="ghost" disabled={busy != null} onClick={() => setAsk("clean")} title="Xoá session nền rác, khởi động lại phòng đang chạy bản cũ, rồi mở team (auto up --clear-ss)">
<SparklesIcon aria-hidden /> Dọn & mở lại
</Button>
<Button type="button" size="sm" variant="ghost" disabled={busy != null} onClick={() => setAsk("full")} title="8 phòng: lead, ba, rd, dev, dev2, ui, qa, review (auto up --roles full)">
<Button type="button" size="sm" variant="ghost" disabled={busy != null} onClick={() => setAsk("full")} title="8 phòng: lead, ba, arch, rd, dev, ui, qa, review (auto up --roles full)">
<UsersIcon aria-hidden /> Team đầy đủ
</Button>
<Button type="button" size="sm" variant="ghost" disabled={busy != null} onClick={() => setAsk("down")} title="Dừng các phòng nền, giữ hội thoại (auto down)">
Expand All @@ -68,7 +68,7 @@ export function TeamControls({ root, name, token, after }: { root: string; name:
{ask === "down"
? "Các phòng nền dừng lại, hội thoại vẫn giữ. Bấm “Mở lại team” để chạy tiếp từ chỗ cũ."
: ask === "full"
? "8 phòng chạy nền: lead và ba viết card, rd nghiên cứu, dev, dev2 và ui làm, qa kiểm, review duyệt. Mỗi phòng là một session riêng nên tốn quota gần gấp 8 lần một session; tới 90% cửa sổ 5 giờ/7 ngày thì cả team tự nghỉ."
? "8 phòng chạy nền: lead và ba viết card, arch thiết kế (ADR theo System Design Primer, tech radar mỗi ngày), rd nghiên cứu, dev và ui làm, qa kiểm, review duyệt. Mỗi phòng là một session riêng nên tốn quota gần gấp 8 lần một session; tới 90% cửa sổ 5 giờ/7 ngày thì cả team tự nghỉ."
: "Xoá session nền của dự án mà không phòng nào giữ, khởi động lại phòng đang chạy bản 8sync cũ (giữ hội thoại), rồi mở team. Session có worktree còn thay đổi chưa commit được giữ lại, không mất code."}
</DialogDescription>
</DialogHeader>
Expand Down Expand Up @@ -123,7 +123,7 @@ export function TeamStarter({ projects, token, after }: { projects: { path: stri
))}
</ul>
<p className="max-w-[75ch] text-xs text-muted-foreground text-pretty">
Mở team: 5 phòng nền (lead viết card, dev làm, qa kiểm, review duyệt, rd nghiên cứu). Team đầy đủ: thêm ba, dev2, ui (8 phòng). Thêm thư mục khác ở mục Projects bên trái.
Mở team: 5 phòng nền (lead viết card, dev làm, qa kiểm, review duyệt, rd nghiên cứu). Team đầy đủ: thêm ba, arch, ui (8 phòng). Thêm thư mục khác ở mục Projects bên trái.
</p>
</section>
);
Expand Down
1 change: 1 addition & 0 deletions apps/hq-ui/lib/now.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export const IDLE_FLAG_MS = 60_000;
*/
const ROOM_ROLES: { keys: string[]; role: RoleId; seat: SeatKey; lane: LaneKey | null }[] = [
{ keys: ["pm", "lead", "plan", "boss", "main", "ba", "analyst", "spec", "req", "po"], role: "plan", seat: "lead", lane: "backlog" },
{ keys: ["arch", "architect", "architecture", "sa", "solution"], role: "architect", seat: "rd", lane: null },
{ keys: ["rd", "r&d", "rnd", "research", "explore"], role: "rd", seat: "rd", lane: null },
{ keys: ["dev", "build", "builder", "code", "coder", "impl", "ui", "ux", "fe", "frontend", "web", "design", "hq", "api", "be", "backend", "server", "encore", "db", "core", "cli", "rust", "crate", "kit", "ccc"], role: "fullstack", seat: "dev", lane: "ready" },
{ keys: ["qa", "qc", "test", "tester", "e2e", "uat"], role: "tester", seat: "qa", lane: "qa" },
Expand Down
4 changes: 4 additions & 0 deletions apps/hq-ui/tests/now.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ describe("roomRole", () => {
expect(roomRole("reviewer")?.lane).toBe("review");
expect(roomRole("research-x")?.role).toBe("rd");
expect(roomRole("rd")?.lane).toBeNull();
// The full team's architect: its own icon, no lane (it designs cards labelled arch).
expect(roomRole("arch")).toMatchObject({ role: "architect", lane: null });
expect(roomRole("solution-architect")?.role).toBe("architect");
expect(roomRole("sale")).toBeNull();
expect(roomRole("zus")).toBeNull();
expect(roomRole(null)).toBeNull();
});
Expand Down
20 changes: 19 additions & 1 deletion crates/cli/src/verbs/ccc/auto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,9 @@ Mỗi nhịp:
- building: worktree `.claude/worktrees/card-<id>`, nhánh `card-<id>-<slug>`, `/ship` tới commit, push, PR → `move <id> qa --pr <số>`
- qa: `check` + UAT → `move <id> review`, trượt → `move <id> ready --note \"[qa] …\"`
- review: review đối kháng → `move <id> approved --sha <head>`, lỗi → `move <id> ready --note \"[review] …\"`
- arch (không giữ cột): card nhãn arch → `/sysdesign` → ADR `docs/adr/card-<id>-<slug>.md` → `auto note <id> \"ADR: …\"`
(lead chỉ chuyển card nhãn arch sang ready khi có note ADR); mỗi ngày tech radar có nguồn + đo sức khoẻ scale
theo `docs/adr/budgets.md`, chỗ nghẽn có số đo → card nhãn arch
Card bị trả về ready quá 2 lần tự sang blocked; claim im 2 giờ thì phòng khác lấy lại.
2. `target` khác null → việc của nhịp là phần hợp vai phòng cho đích đó: lead chia việc và giao, dev `/ship` phần mình,
qa chạy `check`/test, reviewer review, rd gỡ chỗ kẹt. Đạt đích (có `check` thì lệnh đó exit 0 trong nhịp này, không thì
Expand Down Expand Up @@ -350,7 +353,7 @@ pub(super) fn write_loop_md(root: &Path) -> Result<(PathBuf, bool)> {
/// What a room does with the card `auto claim` gives it, per lane (super-board's lane playbooks).
pub(super) fn lane_playbook(lane: &str) -> &'static str {
match lane {
"backlog" => "card backlog: viết AC + MỘT lệnh check (exit 0 = đạt) → `8sync ccc auto move <id> ready --check \"<lệnh>\" --note \"AC: …\"` (bug chặn người dùng hoặc owner nói gấp: thêm `--prio P0`, quan trọng `--prio P1`; dev nhận card gấp trước).Hết card backlog: `8sync ccc auto lint` → gỡ card blocked (`move <id> ready`), card approved → gom MỌI card approved vào MỘT câu hỏi Ship cho owner; owner duyệt → `gh pr merge <pr> --squash --match-head-commit <sha>` → `8sync ccc auto done <id> --evidence \"merged <sha>\"`",
"backlog" => "card backlog: viết AC + MỘT lệnh check (exit 0 = đạt) → `8sync ccc auto move <id> ready --check \"<lệnh>\" --note \"AC: …\"` (bug chặn người dùng hoặc owner nói gấp: thêm `--prio P0`, quan trọng `--prio P1`; dev nhận card gấp trước). Card thêm service/DB/queue/cache/scale/API công khai → gắn nhãn arch khi tạo (`auto add … --label arch`); card nhãn arch chỉ sang ready khi đã có note \"ADR: …\" (phòng arch viết; team không có phòng arch trong `auto status` → tự viết ADR ngắn bằng `/sysdesign`).Hết card backlog: `8sync ccc auto lint` → gỡ card blocked (`move <id> ready`), card approved → gom MỌI card approved vào MỘT câu hỏi Ship cho owner; owner duyệt → `gh pr merge <pr> --squash --match-head-commit <sha>` → `8sync ccc auto done <id> --evidence \"merged <sha>\"`",
"ready" => "card building: worktree `.claude/worktrees/card-<id>` nhánh `card-<id>-<slug>`, `/ship` card tới commit (test + gate), push nhánh, `gh pr create` → `8sync ccc auto move <id> qa --pr <số>`; làm dở → `8sync ccc auto note <id> \"…\"` (giữ claim)",
"qa" => "card qa: checkout nhánh PR, chạy `check` + UAT thật, ảnh chụp lưu `qa/evidence/card-<id>-<slug>.png` (HQ Preview hiện) → đạt `8sync ccc auto move <id> review`; trượt → `8sync ccc auto move <id> ready --note \"[qa] <lỗi + cách tái hiện>\"`",
"review" => "card review: review đối kháng diff PR (bug, test thiếu, bảo mật, quy ước) → ổn `8sync ccc auto move <id> approved --sha <head commit đã review>`; lỗi → `8sync ccc auto move <id> ready --note \"[review] <lỗi>\"`",
Expand All @@ -366,6 +369,10 @@ fn tick_prompt(name: &str) -> String {
"2) `8sync ccc auto claim --json --as {name}` (cột {lane} của vai {role}): `card` khác null → nhịp này chỉ làm card đó: {} (mọi lệnh `auto move|note` thêm `--as {name}`). Không có card → bước 3. ",
lane_playbook(lane)
),
None if super::room::find(name).is_some_and(|r| r.name == "Architect") => format!(
"2) Vai không giữ cột board; nhịp này: {} (mọi lệnh `auto note|add` thêm `--as {name}`). Không có việc nào trong đó → bước 3. ",
super::room::ARCH_PLAYBOOK
),
None => "2) Vai không giữ cột board: bước 3. ".into(),
};
format!(
Expand Down Expand Up @@ -1673,6 +1680,17 @@ mod tests {
}
}

#[test]
fn the_architect_tick_runs_its_playbook_and_the_lead_waits_for_an_adr() {
let t = tick_prompt("arch");
assert!(t.contains(super::super::room::ARCH_PLAYBOOK) && t.contains("--as arch"), "{t}");
assert!(!tick_prompt("rd").contains("/sysdesign"), "rd keeps its own loop");
let lead = lane_playbook("backlog");
assert!(lead.contains("--label arch") && lead.contains("ADR:") && lead.contains("/sysdesign"), "{lead}");
// arch is a card label `auto add` accepts.
assert!(target::LABELS.contains(&"arch"));
}

#[test]
fn targets_steer_every_tick_and_now_reaches_every_room() {
let h = home("target");
Expand Down
30 changes: 26 additions & 4 deletions crates/cli/src/verbs/ccc/room.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,21 @@ const fn r(keys: &'static [&'static str], name: &'static str, duty: &'static str
pub(super) const DEFAULT_TEAM: [&str; 5] = ["lead", "dev", "qa", "review", "rd"];

/// `auto up --roles full` (HQ "Team đầy đủ"): a whole product team, the 8-room cap. A BA writes
/// cards beside the lead, two builders and a UI/UX room take ready cards. Each room is its own
/// Claude Code session, so this spends quota about 8× a single session; the 90% rest still applies.
pub(super) const FULL_TEAM: [&str; 8] = ["lead", "ba", "rd", "dev", "dev2", "ui", "qa", "review"];
/// cards beside the lead, an architect designs the cards labelled `arch` (owner 2026-10-18: a
/// solution-architecture room that keeps up with the web and goes deep on system design), a builder
/// and a UI/UX room take ready cards. Each room is its own Claude Code session, so this spends quota
/// about 8× a single session; the 90% rest still applies.
pub(super) const FULL_TEAM: [&str; 8] = ["lead", "ba", "arch", "rd", "dev", "ui", "qa", "review"];

/// What the architect room does each tick instead of a board lane: it owns no lane, so a card
/// labelled `arch` never waits on a room the team does not have (the lead writes a short ADR then).
pub(super) const ARCH_PLAYBOOK: &str = "card nhãn `arch` (xem `8sync ccc auto board --json`) chưa có note \"ADR:\" → `/sysdesign` (đọc đúng mục System Design Primer + docs chính thức của stack, số ước lượng) → ADR (≥2 phương án, trade-off, chọn gì, vì sao, nguồn) ghi vào nơi repo đang ghi quyết định (`docs/adr/`, `DECISIONS.md`, `docs/*DECISION*.md`; chưa có → `docs/adr/card-<id>-<slug>.md`), không mở nơi song song → `8sync ccc auto note <id> \"ADR: <path> · chọn <x>\"`; card ở cột review → đọc diff, lệch ADR, vượt ngân sách trong `docs/adr/budgets.md` hay thêm thành phần không cần → `auto note <id> \"[arch] …\"` (review vẫn quyết). Hết việc card, mỗi ngày một lượt mỗi việc: (a) tech radar `/deep-research` (bản mới, breaking change, CVE của đúng stack repo) → `su-code/research/radar-<ngày>.md` có nguồn; (b) sức khoẻ scale: ĐO thật (kích thước bundle/binary, thời gian build/test, query chậm hay N+1, RAM, cold start, chi phí mỗi request) so với `docs/adr/budgets.md` (chưa có → viết bản đầu từ số đo hôm nay), chỗ nghẽn lớn nhất có số → card; giới hạn cứng trong code (concurrency, pool, timeout) đưa vào budgets kèm lý do; (c) lệch tài liệu: quyết định mâu thuẫn nhau hay với code, sơ đồ/danh sách service cũ, `file:line` trôi → một ADR hợp nhất hoặc sửa tài liệu. Mục cần làm → `8sync ccc auto add \"…\" --label arch --check \"<lệnh đo>\"`; không có số đo thì không mở card";

/// In match order. "qc" is a tester here, as Vietnamese teams use the word.
const ROLES: [Role; 12] = [
const ROLES: [Role; 13] = [
r(&["pm", "lead", "plan", "boss", "main"], "Lead/PM", "điều phối board: triage backlog (đích của owner) thành card ready có AC + lệnh check, gỡ card blocked, chia việc; merge chỉ sau câu Ship của owner", Some("opus"), Some("backlog")),
r(&["ba", "analyst", "spec", "req", "po"], "BA", "viết card: acceptance criteria + đúng một lệnh check, tách việc lớn thành card nhỏ, backlog → ready; không sửa code sản phẩm", Some("opus"), Some("backlog")),
r(&["arch", "architect", "architecture", "sa", "solution"], "Architect", "solution architecture suốt vòng đời: ADR cho card nhãn arch trước khi build (/sysdesign theo System Design Primer + docs chính thức), review kiến trúc diff ở cột review, mỗi ngày tech radar có nguồn + đo sức khoẻ scale theo docs/adr/budgets.md; không sửa code sản phẩm", Some("opus"), None),
r(&["rd", "r&d", "rnd", "research", "explore"], "R&D", "nghiên cứu: /deep-research câu hỏi mở → brief có nguồn trong su-code/research/ → /planning; chưa sửa code sản phẩm khi chưa có plan", Some("opus"), None),
r(&["dev", "build", "builder", "code", "coder", "impl"], "Dev/Builder", "nhận card ready: worktree .claude/worktrees/card-<id>, nhánh card-<id>-<slug>, code + test, PR; building → qa; không merge", Some("sonnet"), Some("ready")),
r(&["qa", "qc", "test", "tester", "e2e", "uat"], "QA", "chạy check/test/UAT của card trên nhánh PR; ảnh chụp UAT lưu `qa/evidence/card-<id>-<slug>.png` (HQ Preview hiện); đạt → review, lỗi → ready kèm ghi chú [qa]; viết test, không sửa code sản phẩm", Some("haiku"), Some("qa")),
Expand Down Expand Up @@ -221,6 +228,21 @@ mod tests {
assert_eq!(role("bake").0, "\"bake\"");
}

#[test]
fn the_architect_room_designs_without_holding_a_lane() {
for n in ["arch", "architect", "sa", "solution-architect", "arch2"] {
assert_eq!(role(n).0, "Architect", "{n}");
}
// "sa" is a whole token only; "sale" is not an architect.
assert_eq!(role("sale").0, "\"sale\"");
let a = find("arch").unwrap();
assert_eq!((a.lane, a.model), (None, Some("opus")), "no lane: an arch card never waits on a room the team lacks");
assert!(FULL_TEAM.contains(&"arch") && !DEFAULT_TEAM.contains(&"arch"));
for s in ["/sysdesign", "docs/adr/card-<id>", "DECISIONS.md", "không mở nơi song song", "lệch tài liệu", "ADR:", "tech radar", "su-code/research/radar-", "--label arch", "docs/adr/budgets.md", "ĐO thật", "không có số đo thì không mở card"] {
assert!(ARCH_PLAYBOOK.contains(s), "{s}");
}
}

#[test]
fn the_default_team_covers_every_lane_with_a_model() {
let lanes: Vec<_> = DEFAULT_TEAM.iter().map(|n| find(n).and_then(|r| r.lane)).collect();
Expand Down
3 changes: 2 additions & 1 deletion crates/cli/src/verbs/ccc/target.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ const REBUILD_CAP: u32 = 2;

/// Open lanes in board order; `done`/`dropped` live in `state`.
pub(super) const LANES: [&str; 7] = ["backlog", "ready", "building", "qa", "review", "approved", "blocked"];
pub(super) const LABELS: [&str; 3] = ["bug", "feature", "qa"];
/// `arch`: needs a design first; the lead moves it to ready only once a note starts with "ADR:".
pub(super) const LABELS: [&str; 4] = ["bug", "feature", "qa", "arch"];

/// Allowed moves (from → to). Done goes through `auto done`, which needs evidence.
fn allowed(from: &str, to: &str) -> bool {
Expand Down
Loading
Loading