diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a72e08a..f0e0740b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ versioning theo [SemVer](https://semver.org). **8sync rule:** mỗi PR cập nh ## [Unreleased] ### Fixed +- **The critic now actually answers for an away owner: rooms ask through AskUserQuestion.** Seen live on 2026-10-18: + - `delegate` was on (60 s), yet `su-code/DECISIONS.md` stayed empty. The R&D room had asked about BiRefNet as plain text at the end of a turn and waited. The delegate hook only sees an `AskUserQuestion` call, so the critic never got the question. + - Every room's `/goal`, loop tick and new `.claude/loop.md` now say: ask the owner through `AskUserQuestion` (the recommended option first), never in prose before stopping. + - Rooms already running pick this up after `8sync ccc auto up --clear-ss`. An existing `.claude/loop.md` is never overwritten; add the two lines from a fresh one by hand if you keep your own. +- **One Ship question per project.** The BA room shares the backlog lane with the lead and also asked the owner to Ship PR 20. Only the lead asks Ship now. +- **HQ explains why a Ship question waits.** It shows "Câu Ship (merge, release, deploy): chỉ anh duyệt", meaning the critic never answers it, however long the owner is away. A room waiting on a question typed as plain text is marked as one the critic cannot see. Ship staying with the owner is unchanged and intended. +### Fixed - **The site catches up with v0.100.6–v0.100.10.** - HQ's tabs (Tổng quan, Board, Flow, Preview) and the Máy chip are listed under "Open HQ". - The full team lists `arch`, with a pill and a short note on what it does (ADR from The System Design Primer before an `arch` card is built, a daily sourced radar, measured scale). diff --git a/apps/hq-ui/components/hq/ask-panel.tsx b/apps/hq-ui/components/hq/ask-panel.tsx index 89bc4390..2e2af6a9 100644 --- a/apps/hq-ui/components/hq/ask-panel.tsx +++ b/apps/hq-ui/components/hq/ask-panel.tsx @@ -112,6 +112,15 @@ function PermitCard({ r, p, sid, now, token }: { r: AskRoom; p: Permit; sid: str } /** What happens if the owner does not click: the hook decided it when it posted the request. */ +/** Ship (merge, release, deploy) is the owner's alone: the critic never answers it, however long the owner is away. */ +export const SHIP_NOTE = "Câu Ship (merge, release, deploy): chỉ anh duyệt. Critic không trả lời thay câu này dù anh vắng bao lâu."; +/** A question typed as text, not through AskUserQuestion: the delegate hook cannot see it. */ +export const PROSE_NOTE = "Phòng hỏi bằng chữ thường nên critic không trả lời thay được."; + +export function isShip(a: { kind: string; questions?: { header?: string | null }[] } | null | undefined): boolean { + return a?.kind === "question" && (a.questions ?? []).some((q) => (q.header ?? "").trim().toLowerCase() === "ship"); +} + export function silence(p: Pick, left: number): string { if (p.then === "allow") { return left > 0 ? `Không bấm thì sau ${left} s phòng tự cho phép và làm tiếp (việc thường trong dự án).` : "Hết giờ: phòng đã tự cho phép và làm tiếp."; @@ -195,6 +204,7 @@ function AskCard({ r, now, token, onOpen }: { r: AskRoom; now: number; token: st ))} + {isShip(a) &&

{SHIP_NOTE}

} ) : a?.kind === "permission" ? (

@@ -202,7 +212,7 @@ function AskCard({ r, now, token, onOpen }: { r: AskRoom; now: number; token: st {a.what && {a.what}}

) : ( -

{r.waiting ?? "Claude Code đang chờ một câu trả lời"}: mở terminal để xem câu hỏi.

+

{r.waiting ?? "Claude Code đang chờ một câu trả lời"}: mở terminal để xem câu hỏi. {PROSE_NOTE}

)}

diff --git a/apps/hq-ui/tests/permit.test.ts b/apps/hq-ui/tests/permit.test.ts index 1b4e1c32..4bab622c 100644 --- a/apps/hq-ui/tests/permit.test.ts +++ b/apps/hq-ui/tests/permit.test.ts @@ -75,4 +75,15 @@ describe("PermitCard markup", () => { // a request from an older 8sync (no `then`) always skipped expect(silence({ then: null, why: null }, 5)).toContain("bỏ bước này,"); }); + + it("a Ship question says it waits for the owner only; a prose question says the critic cannot see it", async () => { + const { isShip, SHIP_NOTE, PROSE_NOTE } = await import("@/components/hq/ask-panel"); + expect(isShip({ kind: "question", questions: [{ header: "Ship" }] })).toBe(true); + expect(isShip({ kind: "question", questions: [{ header: "Layout" }, { header: " ship " }] })).toBe(true); + expect(isShip({ kind: "question", questions: [{ header: "Layout" }] })).toBe(false); + expect(isShip({ kind: "permission" })).toBe(false); + expect(isShip(null)).toBe(false); + expect(SHIP_NOTE).toContain("chỉ anh duyệt"); + expect(PROSE_NOTE).toContain("critic không trả lời thay"); + }); }); diff --git a/crates/cli/src/verbs/ccc/auto.rs b/crates/cli/src/verbs/ccc/auto.rs index c82512d3..6aaebead 100644 --- a/crates/cli/src/verbs/ccc/auto.rs +++ b/crates/cli/src/verbs/ccc/auto.rs @@ -327,6 +327,8 @@ Mỗi nhịp: Không bao giờ: merge, push main, tag, release, publish, deploy khi owner chưa trả lời Ship; đọc hay in secret; xoá dữ liệu; nhắn người ngoài. Câu hỏi Ship, secret, tiền, high-risk chờ owner. Không có việc hợp vai → R&D cho mục Next kế tiếp, không ngồi không. +Hỏi owner luôn bằng tool AskUserQuestion (option khuyến nghị đứng đầu), không hỏi bằng chữ cuối lượt rồi dừng: owner vắng 60 s +thì critic chỉ trả lời thay được câu hỏi qua AskUserQuestion. Câu Ship chỉ phòng lead hỏi. "; /// Write [`LOOP_MD`] at `/.claude/loop.md` unless something is already there (the owner's @@ -350,10 +352,16 @@ pub(super) fn write_loop_md(root: &Path) -> Result<(PathBuf, bool)> { /// does: `CronCreate`) that follows `.claude/loop.md` for the room's role. The stop check stays in /// the cron prompt itself, so an edited loop.md cannot drop it: `off` or `pause` /// delete the loop on its next tick, the `cap` ([`idle::cap`]) rests it. Re-running the command replaces the job (a fresh 7 days). +/// How every room asks the owner. The delegate hook (`8sync ccc hook delegate`) only sees an +/// AskUserQuestion call; a question typed as plain text at the end of a turn just waits, so the critic +/// never gets it (seen live 2026-10-18: rd asked about BiRefNet in prose and sat). One Ship question +/// per project: two rooms asking it doubles the owner's work (ba and lead both asked for PR 20). +pub(super) const ASK_RULE: &str = "Cần owner quyết → gọi tool AskUserQuestion (≤4 câu, option khuyến nghị đứng đầu có \"(Recommended)\"), KHÔNG hỏi bằng chữ ở cuối lượt rồi dừng: owner vắng 60 s thì critic chỉ trả lời thay được câu hỏi qua AskUserQuestion. Câu Ship (merge/release/deploy) chỉ phòng lead hỏi."; + /// 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 ready --check \"\" --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 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 --squash --match-head-commit ` → `8sync ccc auto done --evidence \"merged \"`", + "backlog" => "card backlog: viết AC + MỘT lệnh check (exit 0 = đạt) → `8sync ccc auto move ready --check \"\" --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 ready`); chỉ phòng lead (BA bỏ qua bước này): card approved → gom MỌI card approved vào MỘT câu hỏi Ship cho owner; owner duyệt → `gh pr merge --squash --match-head-commit ` → `8sync ccc auto done --evidence \"merged \"`", "ready" => "card building: worktree `.claude/worktrees/card-` nhánh `card--`, `/ship` card tới commit (test + gate), push nhánh, `gh pr create` → `8sync ccc auto move qa --pr `; làm dở → `8sync ccc auto note \"…\"` (giữ claim)", "qa" => "card qa: checkout nhánh PR, chạy `check` + UAT thật, ảnh chụp lưu `qa/evidence/card--.png` (HQ Preview hiện) → đạt `8sync ccc auto move review`; trượt → `8sync ccc auto move ready --note \"[qa] \"`", "review" => "card review: review đối kháng diff PR (bug, test thiếu, bảo mật, quy ước) → ổn `8sync ccc auto move approved --sha `; lỗi → `8sync ccc auto move ready --note \"[review] \"`", @@ -376,7 +384,7 @@ fn tick_prompt(name: &str) -> String { None => "2) Vai không giữ cột board: bước 3. ".into(), }; format!( - "{LOOP_TAG} {name} (vai {role}): 1) chạy `8sync ccc auto status --json`; on=false hoặc paused=true → CronDelete job {LOOP_TAG} này và dừng; `cap` khác null → nghỉ nhịp này (giữ loop). {board}3) `target` khác null → nhịp này làm phần hợp vai {role} cho đích đó; đạt (có `check` thì lệnh đó exit 0) → `8sync ccc auto done --evidence \"\"`. 4) Không có đích thì làm MỘT nhịp theo .claude/loop.md của dự án cho vai {role} (thiếu file → lấy MỘT việc hợp vai từ su-code/STATE.md mục Next, tự chọn skill: code → /ship, ≤3 file → /do, R&D → /deep-research). Không merge/push main/tag/deploy khi owner chưa trả lời Ship." + "{LOOP_TAG} {name} (vai {role}): 1) chạy `8sync ccc auto status --json`; on=false hoặc paused=true → CronDelete job {LOOP_TAG} này và dừng; `cap` khác null → nghỉ nhịp này (giữ loop). {board}3) `target` khác null → nhịp này làm phần hợp vai {role} cho đích đó; đạt (có `check` thì lệnh đó exit 0) → `8sync ccc auto done --evidence \"\"`. 4) Không có đích thì làm MỘT nhịp theo .claude/loop.md của dự án cho vai {role} (thiếu file → lấy MỘT việc hợp vai từ su-code/STATE.md mục Next, tự chọn skill: code → /ship, ≤3 file → /do, R&D → /deep-research). Không merge/push main/tag/deploy khi owner chưa trả lời Ship. {ASK_RULE}" ) } @@ -1680,6 +1688,24 @@ mod tests { } } + #[test] + fn rooms_ask_the_owner_through_the_tool_and_only_the_lead_asks_ship() { + // The delegate hook sees AskUserQuestion only; a question in prose never reaches the critic. + for name in ["lead", "rd", "arch", "dev", "qa"] { + assert!(tick_prompt(name).contains(ASK_RULE), "{name}"); + } + assert!(ASK_RULE.contains("AskUserQuestion") && ASK_RULE.contains("KHÔNG hỏi bằng chữ") && ASK_RULE.contains("chỉ phòng lead hỏi")); + assert!(LOOP_MD.contains("AskUserQuestion") && LOOP_MD.contains("Câu Ship chỉ phòng lead hỏi")); + // BA shares the backlog lane but leaves the Ship question to the lead. + assert!(lane_playbook("backlog").contains("chỉ phòng lead (BA bỏ qua bước này)")); + for (name, lane) in [("lead", "backlog"), ("ba", "backlog"), ("dev", "ready"), ("qa", "qa"), ("review", "review")] { + let g = super::super::team::goal_prompt(name, lane, 10); + assert!(g.contains(ASK_RULE), "{name}"); + // Claude Code caps a /goal condition at 4,000 characters (https://code.claude.com/docs/en/goal). + assert!(g.chars().count() <= 4000, "{name}: {} chars", g.chars().count()); + } + } + #[test] fn the_architect_tick_runs_its_playbook_and_the_lead_waits_for_an_adr() { let t = tick_prompt("arch");