From 5f262479c4d99fd67029151a9197596d13afb2c2 Mon Sep 17 00:00:00 2001 From: dp Date: Tue, 14 Jul 2026 15:28:05 +0800 Subject: [PATCH 1/5] wip: role-skills From 6ed28207facc4aed611a5abbd6ee9f4eff5706b1 Mon Sep 17 00:00:00 2001 From: dp Date: Tue, 14 Jul 2026 16:21:28 +0800 Subject: [PATCH 2/5] feat: ship role specs through the hive plugin, retire the CLI skills group Role protocol text now lives in plugins/hive/skills/hive/references/ and reaches both Claude and Codex panes through the marketplace auto-update chain, so spec iteration no longer rides on a CLI release. The skills command group and core_assets/specs are removed outright (single-user cutover, no compatibility path); birth prompts, adoption hints, and init `next` fields route through the hive skill's references instead. --- AGENTS.md | 2 +- plugins/hive/skills/hive/SKILL.md | 36 ++---- .../hive/references}/advanced-routing.md | 0 .../hive/skills/hive/references}/core.md | 8 +- .../hive/skills/hive/references}/debug.md | 0 .../skills/hive/references}/duo-validator.md | 4 +- .../skills/hive/references}/duo-worker.md | 4 +- .../hive/references}/squad-challenger.md | 4 +- .../skills/hive/references}/squad-orch.md | 4 +- .../hive/references}/squad-validator.md | 4 +- .../skills/hive/references}/squad-worker.md | 4 +- src/hive/cli.py | 106 +++--------------- tests/cli/test_current_init_use.py | 2 +- tests/cli/test_duo_init.py | 34 +++--- tests/cli/test_output_contract.py | 8 -- tests/cli/test_skills_command.py | 60 ---------- tests/cli/test_squad_init.py | 8 +- tests/conftest.py | 2 +- 18 files changed, 69 insertions(+), 221 deletions(-) rename {src/hive/core_assets/specs => plugins/hive/skills/hive/references}/advanced-routing.md (100%) rename {src/hive/core_assets/specs => plugins/hive/skills/hive/references}/core.md (93%) rename {src/hive/core_assets/specs => plugins/hive/skills/hive/references}/debug.md (100%) rename {src/hive/core_assets/specs => plugins/hive/skills/hive/references}/duo-validator.md (98%) rename {src/hive/core_assets/specs => plugins/hive/skills/hive/references}/duo-worker.md (98%) rename {src/hive/core_assets/specs => plugins/hive/skills/hive/references}/squad-challenger.md (97%) rename {src/hive/core_assets/specs => plugins/hive/skills/hive/references}/squad-orch.md (98%) rename {src/hive/core_assets/specs => plugins/hive/skills/hive/references}/squad-validator.md (98%) rename {src/hive/core_assets/specs => plugins/hive/skills/hive/references}/squad-worker.md (98%) delete mode 100644 tests/cli/test_skills_command.py diff --git a/AGENTS.md b/AGENTS.md index 3350cca..880db47 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -45,7 +45,7 @@ Use Python 3.11+ with 4-space indentation and type hints where practical. Match Every CLI command should have at least one CLI test and complex flows should also have e2e coverage. Add unit tests for pure logic before relying on higher-level tests. Keep new tests in the correct layer and use shared fixtures from `tests/conftest.py` or helpers in `tests/e2e/_helpers.py`. -Do not test hand-written prose by locking exact words. Forbidden: tests that read repo-authored docs, specs, prompts, or skill text (`AGENTS.md`, `README.md`, `skills/**/SKILL.md`, `src/hive/core_assets/specs/*.md`) and assert that specific phrases or headings are present or absent. Review prose changes by reading the diff. +Do not test hand-written prose by locking exact words. Forbidden: tests that read repo-authored docs, specs, prompts, or skill text (`AGENTS.md`, `README.md`, `skills/**/SKILL.md`, `plugins/hive/skills/hive/references/*.md`) and assert that specific phrases or headings are present or absent. Review prose changes by reading the diff. Allowed: tests that read generated files, state files, JSON, scripts, or payloads to verify executable behavior. Prefer assertions on command exit codes, structured fields, files created, parser output, tmux side effects, and other runtime contracts. If prose must control behavior, move the contract into code or structured data and test that boundary instead of literal wording. diff --git a/plugins/hive/skills/hive/SKILL.md b/plugins/hive/skills/hive/SKILL.md index c7440c2..f320563 100644 --- a/plugins/hive/skills/hive/SKILL.md +++ b/plugins/hive/skills/hive/SKILL.md @@ -5,7 +5,7 @@ description: Hive 是 tmux 里的多 agent 协作 runtime。当收到 HIVE 消 # Hive — agent 协作入口 -Hive 让多个 agent 在 tmux 里用 `` 消息协作。这个文件只是发现入口;真正协议由 CLI 按当前角色取回。 +Hive 让多个 agent 在 tmux 里用 `` 消息协作。这个文件只是发现入口;真正协议在本 skill 目录的 `references/` 下,按当前角色读取。 ## 已在 team 里 @@ -17,24 +17,18 @@ hive team 看 `self`、`members`、`group`、`peer` 和当前 pane 状态。 -如果出生 prompt 或 init 输出给了角色,就只取那一份: +如果出生 prompt 或 init 输出给了角色,就只读那一份(路径相对本 skill 目录): -```bash -hive skills get duo-worker -hive skills get duo-validator -hive skills get squad-orch -hive skills get squad-challenger -hive skills get squad-worker -hive skills get squad-validator -``` +- `references/duo-worker.md` +- `references/duo-validator.md` +- `references/squad-orch.md` +- `references/squad-challenger.md` +- `references/squad-worker.md` +- `references/squad-validator.md` -这些角色 spec 都是自包含协议。取回一份、读完、照它做;不要再拼别的 role spec。 +这些角色 spec 都是自包含协议。读完一份、照它做;不要再拼别的 role spec。 -如果你只是被拉进已有 team、没有角色,取: - -```bash -hive skills get core -``` +如果你只是被拉进已有 team、没有角色,读 `references/core.md`。 没有待办时结束当前 turn,pane 保持打开等下一条 `` 注入。不要 `sleep` 轮询,不要自己翻库找活。 @@ -52,14 +46,8 @@ hive duo init hive squad init ``` -init 的 JSON 会给 `next`,例如 `hive skills get duo-worker` 或 `hive skills get squad-orch`。你自己跑 `next` 取当前 pane 的完整协议。 +init 的 JSON 会给 `next`,例如 `hive skill: read references/duo-worker.md`。按 `next` 读本 skill 目录下对应文件,取当前 pane 的完整协议。 ## 速查 -```bash -hive skills list -hive skills get debug -hive skills get advanced-routing -``` - -`debug` 和 `advanced-routing` 是按需逃生口;日常流程按当前角色 spec。 +`references/debug.md` 和 `references/advanced-routing.md` 是按需逃生口;日常流程按当前角色 spec。 diff --git a/src/hive/core_assets/specs/advanced-routing.md b/plugins/hive/skills/hive/references/advanced-routing.md similarity index 100% rename from src/hive/core_assets/specs/advanced-routing.md rename to plugins/hive/skills/hive/references/advanced-routing.md diff --git a/src/hive/core_assets/specs/core.md b/plugins/hive/skills/hive/references/core.md similarity index 93% rename from src/hive/core_assets/specs/core.md rename to plugins/hive/skills/hive/references/core.md index 975d35d..1d47550 100644 --- a/src/hive/core_assets/specs/core.md +++ b/plugins/hive/skills/hive/references/core.md @@ -34,7 +34,7 @@ hive reply dodo "ack, looking" - 标签里的 `body` 是短摘要。 - `artifact=` 是正文;需要细节时直接打开这个文件。 -- 以 `` block 为准。`hive thread` 只用于排障;需要时取 `hive skills get debug`。 +- 以 `` block 为准。`hive thread` 只用于排障;需要时读 hive skill 的 `references/debug.md`。 ### 发消息:send 还是 reply @@ -42,7 +42,7 @@ hive reply dodo "ack, looking" - 新话题用 `hive send "<短摘要>"`,例如派任务、提新问题、发新汇报。`send` 不接 `--reply-to`。 - 回应入站消息用 `hive reply "<回复>"`。不传 `--reply-to` 时,它会锚到最近一条来自该 agent 且你还没回过的入站消息。 -- 有 anchor msgId 但当前 pane 没有那条入站消息时,显式 `hive reply --reply-to "<回复>"`。接管 thread 的细节需要时取 `hive skills get advanced-routing`。 +- 有 anchor msgId 但当前 pane 没有那条入站消息时,显式 `hive reply --reply-to "<回复>"`。接管 thread 的细节需要时读 hive skill 的 `references/advanced-routing.md`。 不要因为“刚收到过对方消息”就用 `reply`。如果现在说的是新任务或新汇报,用 `send` 开新 thread。 @@ -113,5 +113,5 @@ source: ... 日常收发只用上面的通信底座。 -- 排障命令、delivery、thread、capture、inject、interrupt、kill:`hive skills get debug` -- active-turn fork、handoff 接管、复杂 thread routing:`hive skills get advanced-routing` +- 排障命令、delivery、thread、capture、inject、interrupt、kill:hive skill 的 `references/debug.md` +- active-turn fork、handoff 接管、复杂 thread routing:hive skill 的 `references/advanced-routing.md` diff --git a/src/hive/core_assets/specs/debug.md b/plugins/hive/skills/hive/references/debug.md similarity index 100% rename from src/hive/core_assets/specs/debug.md rename to plugins/hive/skills/hive/references/debug.md diff --git a/src/hive/core_assets/specs/duo-validator.md b/plugins/hive/skills/hive/references/duo-validator.md similarity index 98% rename from src/hive/core_assets/specs/duo-validator.md rename to plugins/hive/skills/hive/references/duo-validator.md index e70117b..1d486a0 100644 --- a/src/hive/core_assets/specs/duo-validator.md +++ b/plugins/hive/skills/hive/references/duo-validator.md @@ -21,7 +21,7 @@ - 标签里的 `body` 是短摘要。 - `artifact=` 是正文;需要细节时直接打开这个文件。 -- 以 `` block 为准。`hive thread` 只用于排障;需要时取 `hive skills get debug`。 +- 以 `` block 为准。`hive thread` 只用于排障;需要时读 hive skill 的 `references/debug.md`。 ### 发消息:send 还是 reply @@ -29,7 +29,7 @@ - 新话题用 `hive send "<短摘要>"`,例如派任务、提新问题、发新汇报。`send` 不接 `--reply-to`。 - 回应入站消息用 `hive reply "<回复>"`。不传 `--reply-to` 时,它会锚到最近一条来自该 agent 且你还没回过的入站消息。 -- 有 anchor msgId 但当前 pane 没有那条入站消息时,显式 `hive reply --reply-to "<回复>"`。接管 thread 的细节需要时取 `hive skills get advanced-routing`。 +- 有 anchor msgId 但当前 pane 没有那条入站消息时,显式 `hive reply --reply-to "<回复>"`。接管 thread 的细节需要时读 hive skill 的 `references/advanced-routing.md`。 不要因为“刚收到过对方消息”就用 `reply`。如果现在说的是新任务或新汇报,用 `send` 开新 thread。 diff --git a/src/hive/core_assets/specs/duo-worker.md b/plugins/hive/skills/hive/references/duo-worker.md similarity index 98% rename from src/hive/core_assets/specs/duo-worker.md rename to plugins/hive/skills/hive/references/duo-worker.md index df5bb3e..85b1f36 100644 --- a/src/hive/core_assets/specs/duo-worker.md +++ b/plugins/hive/skills/hive/references/duo-worker.md @@ -21,7 +21,7 @@ - 标签里的 `body` 是短摘要。 - `artifact=` 是正文;需要细节时直接打开这个文件。 -- 以 `` block 为准。`hive thread` 只用于排障;需要时取 `hive skills get debug`。 +- 以 `` block 为准。`hive thread` 只用于排障;需要时读 hive skill 的 `references/debug.md`。 ### 发消息:send 还是 reply @@ -29,7 +29,7 @@ - 新话题用 `hive send "<短摘要>"`,例如派任务、提新问题、发新汇报。`send` 不接 `--reply-to`。 - 回应入站消息用 `hive reply "<回复>"`。不传 `--reply-to` 时,它会锚到最近一条来自该 agent 且你还没回过的入站消息。 -- 有 anchor msgId 但当前 pane 没有那条入站消息时,显式 `hive reply --reply-to "<回复>"`。接管 thread 的细节需要时取 `hive skills get advanced-routing`。 +- 有 anchor msgId 但当前 pane 没有那条入站消息时,显式 `hive reply --reply-to "<回复>"`。接管 thread 的细节需要时读 hive skill 的 `references/advanced-routing.md`。 不要因为“刚收到过对方消息”就用 `reply`。如果现在说的是新任务或新汇报,用 `send` 开新 thread。 diff --git a/src/hive/core_assets/specs/squad-challenger.md b/plugins/hive/skills/hive/references/squad-challenger.md similarity index 97% rename from src/hive/core_assets/specs/squad-challenger.md rename to plugins/hive/skills/hive/references/squad-challenger.md index c0b6813..aa9d749 100644 --- a/src/hive/core_assets/specs/squad-challenger.md +++ b/plugins/hive/skills/hive/references/squad-challenger.md @@ -16,7 +16,7 @@ - 标签里的 `body` 是短摘要。 - `artifact=` 是正文;需要细节时直接打开这个文件。 -- 以 `` block 为准。`hive thread` 只用于排障;需要时取 `hive skills get debug`。 +- 以 `` block 为准。`hive thread` 只用于排障;需要时读 hive skill 的 `references/debug.md`。 ### 发消息:send 还是 reply @@ -24,7 +24,7 @@ - 新话题用 `hive send "<短摘要>"`,例如派任务、提新问题、发新汇报。`send` 不接 `--reply-to`。 - 回应入站消息用 `hive reply "<回复>"`。不传 `--reply-to` 时,它会锚到最近一条来自该 agent 且你还没回过的入站消息。 -- 有 anchor msgId 但当前 pane 没有那条入站消息时,显式 `hive reply --reply-to "<回复>"`。接管 thread 的细节需要时取 `hive skills get advanced-routing`。 +- 有 anchor msgId 但当前 pane 没有那条入站消息时,显式 `hive reply --reply-to "<回复>"`。接管 thread 的细节需要时读 hive skill 的 `references/advanced-routing.md`。 不要因为“刚收到过对方消息”就用 `reply`。如果现在说的是新任务或新汇报,用 `send` 开新 thread。 diff --git a/src/hive/core_assets/specs/squad-orch.md b/plugins/hive/skills/hive/references/squad-orch.md similarity index 98% rename from src/hive/core_assets/specs/squad-orch.md rename to plugins/hive/skills/hive/references/squad-orch.md index e55f312..519210b 100644 --- a/src/hive/core_assets/specs/squad-orch.md +++ b/plugins/hive/skills/hive/references/squad-orch.md @@ -23,7 +23,7 @@ - 标签里的 `body` 是短摘要。 - `artifact=` 是正文;需要细节时直接打开这个文件。 -- 以 `` block 为准。`hive thread` 只用于排障;需要时取 `hive skills get debug`。 +- 以 `` block 为准。`hive thread` 只用于排障;需要时读 hive skill 的 `references/debug.md`。 ### 发消息:send 还是 reply @@ -31,7 +31,7 @@ - 新话题用 `hive send "<短摘要>"`,例如派任务、提新问题、发新汇报。`send` 不接 `--reply-to`。 - 回应入站消息用 `hive reply "<回复>"`。不传 `--reply-to` 时,它会锚到最近一条来自该 agent 且你还没回过的入站消息。 -- 有 anchor msgId 但当前 pane 没有那条入站消息时,显式 `hive reply --reply-to "<回复>"`。接管 thread 的细节需要时取 `hive skills get advanced-routing`。 +- 有 anchor msgId 但当前 pane 没有那条入站消息时,显式 `hive reply --reply-to "<回复>"`。接管 thread 的细节需要时读 hive skill 的 `references/advanced-routing.md`。 不要因为“刚收到过对方消息”就用 `reply`。如果现在说的是新任务或新汇报,用 `send` 开新 thread。 diff --git a/src/hive/core_assets/specs/squad-validator.md b/plugins/hive/skills/hive/references/squad-validator.md similarity index 98% rename from src/hive/core_assets/specs/squad-validator.md rename to plugins/hive/skills/hive/references/squad-validator.md index b085882..91efa4e 100644 --- a/src/hive/core_assets/specs/squad-validator.md +++ b/plugins/hive/skills/hive/references/squad-validator.md @@ -21,7 +21,7 @@ - 标签里的 `body` 是短摘要。 - `artifact=` 是正文;需要细节时直接打开这个文件。 -- 以 `` block 为准。`hive thread` 只用于排障;需要时取 `hive skills get debug`。 +- 以 `` block 为准。`hive thread` 只用于排障;需要时读 hive skill 的 `references/debug.md`。 ### 发消息:send 还是 reply @@ -29,7 +29,7 @@ - 新话题用 `hive send "<短摘要>"`,例如派任务、提新问题、发新汇报。`send` 不接 `--reply-to`。 - 回应入站消息用 `hive reply "<回复>"`。不传 `--reply-to` 时,它会锚到最近一条来自该 agent 且你还没回过的入站消息。 -- 有 anchor msgId 但当前 pane 没有那条入站消息时,显式 `hive reply --reply-to "<回复>"`。接管 thread 的细节需要时取 `hive skills get advanced-routing`。 +- 有 anchor msgId 但当前 pane 没有那条入站消息时,显式 `hive reply --reply-to "<回复>"`。接管 thread 的细节需要时读 hive skill 的 `references/advanced-routing.md`。 不要因为“刚收到过对方消息”就用 `reply`。如果现在说的是新任务或新汇报,用 `send` 开新 thread。 diff --git a/src/hive/core_assets/specs/squad-worker.md b/plugins/hive/skills/hive/references/squad-worker.md similarity index 98% rename from src/hive/core_assets/specs/squad-worker.md rename to plugins/hive/skills/hive/references/squad-worker.md index b4f9a56..94146ae 100644 --- a/src/hive/core_assets/specs/squad-worker.md +++ b/plugins/hive/skills/hive/references/squad-worker.md @@ -21,7 +21,7 @@ - 标签里的 `body` 是短摘要。 - `artifact=` 是正文;需要细节时直接打开这个文件。 -- 以 `` block 为准。`hive thread` 只用于排障;需要时取 `hive skills get debug`。 +- 以 `` block 为准。`hive thread` 只用于排障;需要时读 hive skill 的 `references/debug.md`。 ### 发消息:send 还是 reply @@ -29,7 +29,7 @@ - 新话题用 `hive send "<短摘要>"`,例如派任务、提新问题、发新汇报。`send` 不接 `--reply-to`。 - 回应入站消息用 `hive reply "<回复>"`。不传 `--reply-to` 时,它会锚到最近一条来自该 agent 且你还没回过的入站消息。 -- 有 anchor msgId 但当前 pane 没有那条入站消息时,显式 `hive reply --reply-to "<回复>"`。接管 thread 的细节需要时取 `hive skills get advanced-routing`。 +- 有 anchor msgId 但当前 pane 没有那条入站消息时,显式 `hive reply --reply-to "<回复>"`。接管 thread 的细节需要时读 hive skill 的 `references/advanced-routing.md`。 不要因为“刚收到过对方消息”就用 `reply`。如果现在说的是新任务或新汇报,用 `send` 开新 thread。 diff --git a/src/hive/cli.py b/src/hive/cli.py index 743717a..4678579 100644 --- a/src/hive/cli.py +++ b/src/hive/cli.py @@ -931,8 +931,9 @@ def _classify_pane(pane: tmux.PaneInfo) -> tuple[str, str]: def _hive_join_message(agent_name: str, team_name: str) -> str: return ( f"You are '{agent_name}' in hive team '{team_name}'. " - "Context is pre-bound. Run `hive skills get core` first and follow " - "that protocol. Hive messages will arrive inline as " + "Context is pre-bound. Load the hive skill and read its " + "`references/core.md` first and follow that protocol. " + "Hive messages will arrive inline as " " ... blocks. " "Use `hive team` to inspect the team; reply on an existing thread with " "`hive reply \"...\"`; open a new thread with " @@ -2217,85 +2218,11 @@ def layout_cmd(preset: str): click.echo(json.dumps({"layout": preset, "window": window_target})) -# --- CLI-shipped skill specs --------------------------------------------- -# Thin discovery stub (skills/hive/SKILL.md) points here; the volatile -# protocol/topology guidance ships inside the package and is fetched on -# demand, so it can never drift from the installed CLI version. - -_SPEC_NAME_RE = re.compile(r"^[a-z0-9][a-z0-9-]*$") - - -def _spec_repo_dir() -> Path | None: - """Repo specs dir when running from a checkout; else None (packaged).""" - candidate = Path(__file__).resolve().parents[2] / "src" / "hive" / "core_assets" / "specs" - return candidate if candidate.is_dir() else None - - -def _read_spec(name: str) -> str | None: - repo = _spec_repo_dir() - if repo is not None: - path = repo / f"{name}.md" - return path.read_text(encoding="utf-8") if path.is_file() else None - from importlib import resources - - resource = resources.files("hive.core_assets").joinpath("specs", f"{name}.md") - try: - return resource.read_text(encoding="utf-8") - except (FileNotFoundError, OSError): - return None - - -def _list_specs() -> list[str]: - repo = _spec_repo_dir() - if repo is not None: - return sorted(p.stem for p in repo.glob("*.md")) - from importlib import resources - - names: list[str] = [] - try: - for entry in resources.files("hive.core_assets").joinpath("specs").iterdir(): - if entry.name.endswith(".md"): - names.append(entry.name[:-3]) - except (FileNotFoundError, OSError, NotADirectoryError): - pass - return sorted(names) - - -@cli.group("skills") -def skills_cmd(): - """CLI-shipped skill specs (version-locked, never drift). Start: `hive skills get core`.""" - - -@skills_cmd.command("get") -@click.argument("name") -def skills_get_cmd(name: str): - """Print spec NAME (e.g. `core`). Content always matches the installed CLI version.""" - if not _SPEC_NAME_RE.match(name): - _fail(f"invalid spec name '{name}' (lowercase letters, digits, dashes only)") - text = _read_spec(name) - if text is None: - available = ", ".join(_list_specs()) or "(none)" - _fail(f"unknown spec '{name}'. available: {available}") - click.echo(text) - - -@skills_cmd.command("list") -def skills_list_cmd(): - """List spec names available on this installed version.""" - click.echo(json.dumps({"specs": _list_specs()}, ensure_ascii=False, indent=2)) - - -@skills_cmd.command("ls", hidden=True) -def skills_ls_cmd(): - """Hidden alias of `hive skills list`.""" - skills_list_cmd.callback() - - def _inject_role_bootstrap(pane: str, role: str) -> bool: """Deliver the role bootstrap prompt to *pane* over its native transport. - Same text a spawned pane gets as its launch prompt (identity + - ``hive skills get `` + idle discipline) — adoption only changes the + Same text a spawned pane gets as its launch prompt (identity + the hive + skill reference to read + idle discipline) — adoption only changes the delivery channel, never the wording. Returns True when the pane's transport accepted it; False when the pane runs no known agent CLI or the transport refused (delivery is native-only, no keystroke fallback). @@ -2314,15 +2241,16 @@ def _inject_role_bootstrap(pane: str, role: str) -> bool: def _role_bootstrap_prompt(role: str) -> str: - """Spawn first-message for a no-human role pane: identity + the one command - that loads the role. The spec itself stays CLI-served — the spawned pane - runs ``hive skills get `` exactly like a dispatched pane does - (`_inject_role_bootstrap`), so there is no inlined spec snapshot to keep in - sync and the prompt stays short enough to inline into the launch command. + """Spawn first-message for a no-human role pane: identity + the one skill + reference that loads the role. The spec ships inside the hive plugin — + the spawned pane reads ``references/.md`` from the hive skill + exactly like a dispatched pane does (`_inject_role_bootstrap`), so there + is no inlined spec snapshot to keep in sync and the prompt stays short + enough to inline into the launch command. """ return ( - f"你是这个 team 的 {role}。先跑 `hive skills get {role}` 取你的角色协议 " - f"—— 照它做。没有待办时结束当前 turn,让 pane 开着接收第一条任务消息" + f"你是这个 team 的 {role}。用 hive skill 取你的角色协议:读该 skill 目录下的 " + f"`references/{role}.md` —— 照它做。没有待办时结束当前 turn,让 pane 开着接收第一条任务消息" f"(orch / peer 会发来);在那之前别自己找活、别翻库、别 `sleep` 轮询。" ) @@ -2681,8 +2609,8 @@ def _attach_duo_to_team(t: Team, *, placement: _DuoPlacement, ws: str) -> dict[s layout_mod.apply_adaptive(window) - # Hand the validator its role: a spawned validator already got `hive - # skills get duo-validator` as its startup prompt; an adopted idle + # Hand the validator its role: a spawned validator already got the + # duo-validator bootstrap prompt at launch; an adopted idle # neighbor gets it injected here. The worker pane is the agent running # this very command — its role load is returned as `next` for it to run # in-turn, never injected into its input box as a fake user message. @@ -2704,7 +2632,7 @@ def _attach_duo_to_team(t: Team, *, placement: _DuoPlacement, ws: str) -> dict[s "mode": mode, }, "dispatched": dispatched, - "next": "hive skills get duo-worker", + "next": "hive skill: read references/duo-worker.md", } @@ -3820,7 +3748,7 @@ def squad_init_cmd(peer_cli: str | None, squad_name: str | None, worker_cli: str "orch": {"pane": orch_pane, "name": orch_agent_name}, "challenger": {"pane": challenger_agent.pane_id, "name": challenger_agent_name}, "dispatched": dispatched, - "next": "hive skills get squad-orch", + "next": "hive skill: read references/squad-orch.md", }, indent=2)) diff --git a/tests/cli/test_current_init_use.py b/tests/cli/test_current_init_use.py index 4808c80..934df37 100644 --- a/tests/cli/test_current_init_use.py +++ b/tests/cli/test_current_init_use.py @@ -356,7 +356,7 @@ def test_init_creates_team_and_forms_duo(runner, configure_hive_home, monkeypatc assert payload["worker"]["name"] == "worker" assert payload["validator"]["name"] == "validator" assert payload["dispatched"] == ["validator"] - assert payload["next"] == "hive skills get duo-worker" + assert payload["next"] == "hive skill: read references/duo-worker.md" # The team is created and the current pane is remembered as the worker. from hive.team import Team diff --git a/tests/cli/test_duo_init.py b/tests/cli/test_duo_init.py index 6343026..38e53a4 100644 --- a/tests/cli/test_duo_init.py +++ b/tests/cli/test_duo_init.py @@ -94,22 +94,22 @@ def fake_spawn(**kwargs): assert spawned[0]["name"] == "validator" assert spawned[0]["cli"] == "claude" assert spawned[0]["skill"] == "none" - # validator's role bootstrap is a thin pointer: the spawned pane loads its - # spec itself via `hive skills get duo-validator` — the same CLI-served - # channel a dispatched pane uses — so no spec snapshot is inlined into the - # launch command or cached on disk. + # validator's role bootstrap is a thin pointer: the spawned pane reads + # references/duo-validator.md from the hive skill — the same + # plugin-shipped channel a dispatched pane uses — so no spec snapshot is + # inlined into the launch command or cached on disk. bootstrap = spawned[0]["prompt"] assert bootstrap == cli_mod._role_bootstrap_prompt("duo-validator") - assert "hive skills get duo-validator" in bootstrap + assert "references/duo-validator.md" in bootstrap assert "别 `sleep` 轮询" in bootstrap assert "别退出" not in bootstrap assert payload["dispatched"] == ["validator"] - assert payload["next"] == "hive skills get duo-worker" + assert payload["next"] == "hive skill: read references/duo-worker.md" # The worker runs init itself: nothing may be injected into its pane. worker_pane = payload["worker"]["pane"] - assert not [c for c in sent if c[0] == worker_pane and "skills get" in c[1]] + assert not [c for c in sent if c[0] == worker_pane and "references/" in c[1]] # Spawned validator gets its role via the launch prompt, not injection. - assert not [c for c in sent if "skills get duo-validator" in c[1]] + assert not [c for c in sent if "references/duo-validator.md" in c[1]] def test_role_bootstrap_prompts_do_not_tell_idle_agents_to_not_exit(configure_hive_home): @@ -162,13 +162,13 @@ def test_duo_init_two_panes_adopts_idle_antifamily_neighbor( assert spawned == [] # adopted the neighbor, did not spawn assert breaks == [] # 2-pane pairable → no break-out assert payload["dispatched"] == ["validator"] # adopted validator got its role injected - assert payload["next"] == "hive skills get duo-worker" + assert payload["next"] == "hive skill: read references/duo-worker.md" worker_pane = payload["worker"]["pane"] - assert not [c for c in sent if c[0] == worker_pane and "skills get" in c[1]] + assert not [c for c in sent if c[0] == worker_pane and "references/" in c[1]] # Positive control: the adopted idle neighbor gets the exact same # bootstrap prompt a spawned validator would get at launch. validator_pane = payload["validator"]["pane"] - injected = [c[1] for c in sent if c[0] == validator_pane and "skills get" in c[1]] + injected = [c[1] for c in sent if c[0] == validator_pane and "references/" in c[1]] assert injected == [cli_mod._role_bootstrap_prompt("duo-validator")] @@ -219,12 +219,12 @@ def fake_spawn(**kwargs): assert payload["validator"]["mode"] == "spawned" assert len(spawned) == 1 assert payload["dispatched"] == ["validator"] - assert payload["next"] == "hive skills get duo-worker" + assert payload["next"] == "hive skill: read references/duo-worker.md" # The worker runs init itself: nothing may be injected into its pane. worker_pane = payload["worker"]["pane"] - assert not [c for c in sent if c[0] == worker_pane and "skills get" in c[1]] + assert not [c for c in sent if c[0] == worker_pane and "references/" in c[1]] # Spawned validator gets its role via the launch prompt, not injection. - assert not [c for c in sent if "skills get duo-validator" in c[1]] + assert not [c for c in sent if "references/duo-validator.md" in c[1]] def test_duo_init_three_panes_breaks_out_then_spawns( @@ -261,12 +261,12 @@ def fake_spawn(**kwargs): assert payload["validator"]["mode"] == "spawned" assert len(spawned) == 1 assert payload["dispatched"] == ["validator"] - assert payload["next"] == "hive skills get duo-worker" + assert payload["next"] == "hive skill: read references/duo-worker.md" # The worker runs init itself: nothing may be injected into its pane. worker_pane = payload["worker"]["pane"] - assert not [c for c in sent if c[0] == worker_pane and "skills get" in c[1]] + assert not [c for c in sent if c[0] == worker_pane and "references/" in c[1]] # Spawned validator gets its role via the launch prompt, not injection. - assert not [c for c in sent if "skills get duo-validator" in c[1]] + assert not [c for c in sent if "references/duo-validator.md" in c[1]] def test_duo_init_breakout_names_team_from_final_window_not_origin( diff --git a/tests/cli/test_output_contract.py b/tests/cli/test_output_contract.py index 112bf74..1ec6748 100644 --- a/tests/cli/test_output_contract.py +++ b/tests/cli/test_output_contract.py @@ -181,13 +181,6 @@ def test_dash_h_still_reaches_status_tombstone(runner): # --- B5: ls aliases --- -def test_skills_ls_matches_skills_list(runner): - ls = runner.invoke(cli, ["skills", "ls"]) - lst = runner.invoke(cli, ["skills", "list"]) - assert ls.exit_code == lst.exit_code == 0 - assert json.loads(ls.output) == json.loads(lst.output) - - def test_plugin_ls_matches_plugin_list_in_all_modes(runner, configure_hive_home): configure_hive_home(tmux_inside=False) for extra in ([], ["--json"], ["--plain"]): @@ -198,5 +191,4 @@ def test_plugin_ls_matches_plugin_list_in_all_modes(runner, configure_hive_home) def test_ls_aliases_are_hidden(): - assert cli.commands["skills"].commands["ls"].hidden assert cli.commands["plugin"].commands["ls"].hidden diff --git a/tests/cli/test_skills_command.py b/tests/cli/test_skills_command.py deleted file mode 100644 index bd07872..0000000 --- a/tests/cli/test_skills_command.py +++ /dev/null @@ -1,60 +0,0 @@ -"""Tests for `hive skills get` / `hive skills list` — CLI-shipped spec serving. - -The hive base skill is a thin discovery stub; the volatile protocol ships -inside the package and is fetched on demand via `hive skills get `, so -it can never drift from the installed CLI version. -""" - -import json - -from hive.cli import cli - - -def test_skills_list_includes_core(runner): - result = runner.invoke(cli, ["skills", "list"]) - assert result.exit_code == 0, result.output - payload = json.loads(result.output) - assert "core" in payload["specs"] - - -def test_skills_get_core_serves_protocol(runner): - result = runner.invoke(cli, ["skills", "get", "core"]) - assert result.exit_code == 0, result.output - assert result.output.strip() - - -def test_skills_get_rejects_path_traversal(runner): - result = runner.invoke(cli, ["skills", "get", "../etc/passwd"]) - assert result.exit_code != 0 - assert "invalid spec name" in result.output - - -def test_skills_get_unknown_lists_available(runner): - result = runner.invoke(cli, ["skills", "get", "does-not-exist"]) - assert result.exit_code != 0 - assert "unknown spec" in result.output - assert "core" in result.output # error names the available specs - - -def test_skills_get_serves_role_specs(runner): - """Role content lives in CLI-served specs now (no per-role SKILL.md). Every - role a spawn dispatches `hive skills get ` for must be fetchable.""" - listed = json.loads(runner.invoke(cli, ["skills", "list"]).output)["specs"] - for role in ( - "squad-orch", "squad-challenger", "squad-worker", "squad-validator", - "duo-worker", "duo-validator", - ): - assert role in listed - result = runner.invoke(cli, ["skills", "get", role]) - assert result.exit_code == 0, result.output - assert result.output.strip() - - -def test_skills_get_serves_debug_and_advanced_routing(runner): - """debug + advanced-routing were promoted from skill-home references to - CLI-served specs, so core's pointers to them resolve through the same - `hive skills get` interface instead of dangling at an unreachable file.""" - for name in ("debug", "advanced-routing"): - result = runner.invoke(cli, ["skills", "get", name]) - assert result.exit_code == 0, result.output - assert result.output.strip() diff --git a/tests/cli/test_squad_init.py b/tests/cli/test_squad_init.py index ac28d2f..2be4304 100644 --- a/tests/cli/test_squad_init.py +++ b/tests/cli/test_squad_init.py @@ -66,10 +66,10 @@ def fake_spawn(**kwargs): assert payload["challenger"]["name"] == "peaky.challenger" assert payload["challenger"]["pane"] == "%101" assert payload["dispatched"] == ["peaky.challenger"] - assert payload["next"] == "hive skills get squad-orch" + assert payload["next"] == "hive skill: read references/squad-orch.md" # The orch runs init itself: nothing may be injected into its pane. orch_pane = payload["orch"]["pane"] - assert not [c for c in sent if c[0] == orch_pane and "skills get" in c[1]] + assert not [c for c in sent if c[0] == orch_pane and "references/" in c[1]] # Positive control: the spawned challenger gets its role via launch prompt. assert spawned[0]["prompt"] == cli_mod._role_bootstrap_prompt("squad-challenger") assert "board" not in payload @@ -151,9 +151,9 @@ def fake_spawn(**kwargs): assert payload["challenger"]["name"] == "peaky.challenger" assert spawned[0]["team_name"] == "dev-w88" # challenger spawned under the final-window team assert payload["dispatched"] == ["peaky.challenger"] - assert payload["next"] == "hive skills get squad-orch" + assert payload["next"] == "hive skill: read references/squad-orch.md" orch_pane = payload["orch"]["pane"] - assert not [c for c in sent if c[0] == orch_pane and "skills get" in c[1]] + assert not [c for c in sent if c[0] == orch_pane and "references/" in c[1]] assert sidecar_calls == [("/tmp/hive-dev-w88", "dev-w88", "dev:8", "@88")] diff --git a/tests/conftest.py b/tests/conftest.py index 925bb3c..1397a5b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -238,7 +238,7 @@ def _guard_break_pane(*_args, **_kwargs): "worker": {"pane": "%self", "name": "worker", "cli": "claude"}, "validator": {"pane": "%peer", "name": "validator", "cli": "codex", "mode": "spawned"}, "dispatched": ["validator"], - "next": "hive skills get duo-worker", + "next": "hive skill: read references/duo-worker.md", }, ) monkeypatch.delenv("TMUX_PANE", raising=False) From 555d598e0d0ecbd6757b9fc521bf776b47504b8f Mon Sep 17 00:00:00 2001 From: dp Date: Tue, 14 Jul 2026 16:38:15 +0800 Subject: [PATCH 3/5] docs: point the topology snapshot at the plugin reference mechanism --- hive-topology.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/hive-topology.html b/hive-topology.html index 1a1f783..51871cc 100644 --- a/hive-topology.html +++ b/hive-topology.html @@ -138,12 +138,12 @@

入口交互 · 单一选择

 你打 /hive  (任何 pane 都行)
    │
-   ▼  自动加载通信底座 (hive skills get core: send/reply/team)
+   ▼  自动加载通信底座 (hive skill 的 references/core.md: send/reply/team)
 [问] AskUserQuestion —— 唯一一次交互:"起哪种拓扑?"
    ├─ cell   worker + validator,你自己编排          (2 panes)
    └─ crew   orch + skeptic + 按需 worker-cell        (orch = 当前 pane)
    │
-   ▼  hive skills get <选的>  → 拉该拓扑的角色说明
+   ▼  读 hive skill 的 references/<选的>.md  → 拉该拓扑的角色说明
    ▼  跑对应 init:hive cell init  /  hive crew init  → 自动布 tmux + spawn 角色
    ▼  开干
 
@@ -164,7 +164,7 @@

✓ 已在 main(via #5,非本会话)

⧗ 待建(设计已定,未写码)

    -
  • hive skills get <core|cell|crew> —— 新命令,目前不存在。
  • +
  • references/<core|cell|crew>.md(hive skill 内)—— role spec 分发面,已从 CLI 命令改为插件 references。
  • hive cell init —— 把 cell 落成 worker+validator(固定角色)注意:当前 hive init 起的还是旧的对称 peergroup=peer、双方都写码互审),尚未改成本图里的 doer+validator。
  • 重写 /hive skill 成 AskUserQuestion(cell/crew) 选择器 + fetch + init。
From 5d6f3824017b2ff8f2780c46f093f937ca3bdaf3 Mon Sep 17 00:00:00 2001 From: dp Date: Wed, 15 Jul 2026 13:32:39 +0800 Subject: [PATCH 4/5] feat: role protocols become per-role skills loaded by name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each role ships as its own plugin skill (hive:duo-worker, hive:core, ...) instead of reference files inside one skill. Spawned panes get the role skill activated as line one of the launch turn (Agent.spawn skill kwarg), so the birth prompt shrinks to identity + idle discipline and no path is ever constructed by the model; dispatched running panes are told to invoke the skill by name. Resumed sessions keep skill=none — their protocol already lives in the session. --- AGENTS.md | 2 +- hive-topology.html | 6 +- .../SKILL.md} | 5 ++ .../references/core.md => core/SKILL.md} | 13 ++-- .../references/debug.md => debug/SKILL.md} | 5 ++ .../SKILL.md} | 9 ++- .../duo-worker.md => duo-worker/SKILL.md} | 9 ++- plugins/hive/skills/hive/SKILL.md | 24 +++---- .../SKILL.md} | 9 ++- .../squad-orch.md => squad-orch/SKILL.md} | 9 ++- .../SKILL.md} | 9 ++- .../squad-worker.md => squad-worker/SKILL.md} | 9 ++- src/hive/cli.py | 71 +++++++++++-------- tests/cli/test_current_init_use.py | 2 +- tests/cli/test_duo_init.py | 41 ++++++----- tests/cli/test_resume_command.py | 7 +- tests/cli/test_squad_init.py | 10 +-- tests/conftest.py | 2 +- 18 files changed, 150 insertions(+), 92 deletions(-) rename plugins/hive/skills/{hive/references/advanced-routing.md => advanced-routing/SKILL.md} (85%) rename plugins/hive/skills/{hive/references/core.md => core/SKILL.md} (93%) rename plugins/hive/skills/{hive/references/debug.md => debug/SKILL.md} (89%) rename plugins/hive/skills/{hive/references/duo-validator.md => duo-validator/SKILL.md} (95%) rename plugins/hive/skills/{hive/references/duo-worker.md => duo-worker/SKILL.md} (96%) rename plugins/hive/skills/{hive/references/squad-challenger.md => squad-challenger/SKILL.md} (93%) rename plugins/hive/skills/{hive/references/squad-orch.md => squad-orch/SKILL.md} (96%) rename plugins/hive/skills/{hive/references/squad-validator.md => squad-validator/SKILL.md} (96%) rename plugins/hive/skills/{hive/references/squad-worker.md => squad-worker/SKILL.md} (96%) diff --git a/AGENTS.md b/AGENTS.md index 880db47..0dd7bfd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -45,7 +45,7 @@ Use Python 3.11+ with 4-space indentation and type hints where practical. Match Every CLI command should have at least one CLI test and complex flows should also have e2e coverage. Add unit tests for pure logic before relying on higher-level tests. Keep new tests in the correct layer and use shared fixtures from `tests/conftest.py` or helpers in `tests/e2e/_helpers.py`. -Do not test hand-written prose by locking exact words. Forbidden: tests that read repo-authored docs, specs, prompts, or skill text (`AGENTS.md`, `README.md`, `skills/**/SKILL.md`, `plugins/hive/skills/hive/references/*.md`) and assert that specific phrases or headings are present or absent. Review prose changes by reading the diff. +Do not test hand-written prose by locking exact words. Forbidden: tests that read repo-authored docs, specs, prompts, or skill text (`AGENTS.md`, `README.md`, `skills/**/SKILL.md`) and assert that specific phrases or headings are present or absent. Review prose changes by reading the diff. Allowed: tests that read generated files, state files, JSON, scripts, or payloads to verify executable behavior. Prefer assertions on command exit codes, structured fields, files created, parser output, tmux side effects, and other runtime contracts. If prose must control behavior, move the contract into code or structured data and test that boundary instead of literal wording. diff --git a/hive-topology.html b/hive-topology.html index 51871cc..21d6124 100644 --- a/hive-topology.html +++ b/hive-topology.html @@ -138,12 +138,12 @@

入口交互 · 单一选择

 你打 /hive  (任何 pane 都行)
    │
-   ▼  自动加载通信底座 (hive skill 的 references/core.md: send/reply/team)
+   ▼  自动加载通信底座 (hive:core skill: send/reply/team)
 [问] AskUserQuestion —— 唯一一次交互:"起哪种拓扑?"
    ├─ cell   worker + validator,你自己编排          (2 panes)
    └─ crew   orch + skeptic + 按需 worker-cell        (orch = 当前 pane)
    │
-   ▼  读 hive skill 的 references/<选的>.md  → 拉该拓扑的角色说明
+   ▼  加载 hive:<选的> skill  → 拉该拓扑的角色说明
    ▼  跑对应 init:hive cell init  /  hive crew init  → 自动布 tmux + spawn 角色
    ▼  开干
 
@@ -164,7 +164,7 @@

✓ 已在 main(via #5,非本会话)

⧗ 待建(设计已定,未写码)

    -
  • references/<core|cell|crew>.md(hive skill 内)—— role spec 分发面,已从 CLI 命令改为插件 references。
  • +
  • hive:<core|cell|crew> —— role spec 分发面,已改为按 role 独立的插件 skill,按名加载。
  • hive cell init —— 把 cell 落成 worker+validator(固定角色)注意:当前 hive init 起的还是旧的对称 peergroup=peer、双方都写码互审),尚未改成本图里的 doer+validator。
  • 重写 /hive skill 成 AskUserQuestion(cell/crew) 选择器 + fetch + init。
diff --git a/plugins/hive/skills/hive/references/advanced-routing.md b/plugins/hive/skills/advanced-routing/SKILL.md similarity index 85% rename from plugins/hive/skills/hive/references/advanced-routing.md rename to plugins/hive/skills/advanced-routing/SKILL.md index 1f6ba5c..1ee4db3 100644 --- a/plugins/hive/skills/hive/references/advanced-routing.md +++ b/plugins/hive/skills/advanced-routing/SKILL.md @@ -1,3 +1,8 @@ +--- +name: advanced-routing +description: Hive 高级路由:active-turn fork、handoff 接管、复杂 thread routing。仅在 hive 协作中按需加载。 +--- + # advanced routing — thread takeover hive 路由的低频场景。常规 `hive send` / `hive reply` 流程不需要读这份,core「消息机制」已经覆盖。 diff --git a/plugins/hive/skills/hive/references/core.md b/plugins/hive/skills/core/SKILL.md similarity index 93% rename from plugins/hive/skills/hive/references/core.md rename to plugins/hive/skills/core/SKILL.md index 1d47550..34221ce 100644 --- a/plugins/hive/skills/hive/references/core.md +++ b/plugins/hive/skills/core/SKILL.md @@ -1,3 +1,8 @@ +--- +name: core +description: Hive 无角色 pane 的通信协议。仅当你被拉进 hive team 而没有分配角色时加载。 +--- + # core — 无角色 pane 的通信协议 你在一个 Hive team 里,但没有固定角色。先拿到通信底座,再按 team 里的任务做事。 @@ -34,7 +39,7 @@ hive reply dodo "ack, looking" - 标签里的 `body` 是短摘要。 - `artifact=` 是正文;需要细节时直接打开这个文件。 -- 以 `` block 为准。`hive thread` 只用于排障;需要时读 hive skill 的 `references/debug.md`。 +- 以 `` block 为准。`hive thread` 只用于排障;需要时调用 hive:debug skill。 ### 发消息:send 还是 reply @@ -42,7 +47,7 @@ hive reply dodo "ack, looking" - 新话题用 `hive send "<短摘要>"`,例如派任务、提新问题、发新汇报。`send` 不接 `--reply-to`。 - 回应入站消息用 `hive reply "<回复>"`。不传 `--reply-to` 时,它会锚到最近一条来自该 agent 且你还没回过的入站消息。 -- 有 anchor msgId 但当前 pane 没有那条入站消息时,显式 `hive reply --reply-to "<回复>"`。接管 thread 的细节需要时读 hive skill 的 `references/advanced-routing.md`。 +- 有 anchor msgId 但当前 pane 没有那条入站消息时,显式 `hive reply --reply-to "<回复>"`。接管 thread 的细节需要时调用 hive:advanced-routing skill。 不要因为“刚收到过对方消息”就用 `reply`。如果现在说的是新任务或新汇报,用 `send` 开新 thread。 @@ -113,5 +118,5 @@ source: ... 日常收发只用上面的通信底座。 -- 排障命令、delivery、thread、capture、inject、interrupt、kill:hive skill 的 `references/debug.md` -- active-turn fork、handoff 接管、复杂 thread routing:hive skill 的 `references/advanced-routing.md` +- 排障命令、delivery、thread、capture、inject、interrupt、kill:hive:debug skill +- active-turn fork、handoff 接管、复杂 thread routing:hive:advanced-routing skill diff --git a/plugins/hive/skills/hive/references/debug.md b/plugins/hive/skills/debug/SKILL.md similarity index 89% rename from plugins/hive/skills/hive/references/debug.md rename to plugins/hive/skills/debug/SKILL.md index 35ff215..f834f50 100644 --- a/plugins/hive/skills/hive/references/debug.md +++ b/plugins/hive/skills/debug/SKILL.md @@ -1,3 +1,8 @@ +--- +name: debug +description: Hive 排障深潜:delivery、thread、capture、inject、interrupt、kill。仅在 hive 协作排障时按需加载。 +--- + # debug + 协议边界 排障命令清单和 hive kernel 的协议硬约束。主通道见 core「消息机制」;日常收发消息不读这份。 diff --git a/plugins/hive/skills/hive/references/duo-validator.md b/plugins/hive/skills/duo-validator/SKILL.md similarity index 95% rename from plugins/hive/skills/hive/references/duo-validator.md rename to plugins/hive/skills/duo-validator/SKILL.md index 1d486a0..cb644f5 100644 --- a/plugins/hive/skills/hive/references/duo-validator.md +++ b/plugins/hive/skills/duo-validator/SKILL.md @@ -1,3 +1,8 @@ +--- +name: duo-validator +description: Hive duo 的 validator 角色协议。仅当 hive 出生提示或 init 输出指定你为 duo-validator 时加载;其他场景不要触发。 +--- + # duo-validator — 自包含协议 你是 standalone duo 的 **validator**。worker 写代码;你先审 worker 的 plan 并主笔 VAL,再审 handoff。 @@ -21,7 +26,7 @@ - 标签里的 `body` 是短摘要。 - `artifact=` 是正文;需要细节时直接打开这个文件。 -- 以 `` block 为准。`hive thread` 只用于排障;需要时读 hive skill 的 `references/debug.md`。 +- 以 `` block 为准。`hive thread` 只用于排障;需要时调用 hive:debug skill。 ### 发消息:send 还是 reply @@ -29,7 +34,7 @@ - 新话题用 `hive send "<短摘要>"`,例如派任务、提新问题、发新汇报。`send` 不接 `--reply-to`。 - 回应入站消息用 `hive reply "<回复>"`。不传 `--reply-to` 时,它会锚到最近一条来自该 agent 且你还没回过的入站消息。 -- 有 anchor msgId 但当前 pane 没有那条入站消息时,显式 `hive reply --reply-to "<回复>"`。接管 thread 的细节需要时读 hive skill 的 `references/advanced-routing.md`。 +- 有 anchor msgId 但当前 pane 没有那条入站消息时,显式 `hive reply --reply-to "<回复>"`。接管 thread 的细节需要时调用 hive:advanced-routing skill。 不要因为“刚收到过对方消息”就用 `reply`。如果现在说的是新任务或新汇报,用 `send` 开新 thread。 diff --git a/plugins/hive/skills/hive/references/duo-worker.md b/plugins/hive/skills/duo-worker/SKILL.md similarity index 96% rename from plugins/hive/skills/hive/references/duo-worker.md rename to plugins/hive/skills/duo-worker/SKILL.md index 85b1f36..d1b3f7d 100644 --- a/plugins/hive/skills/hive/references/duo-worker.md +++ b/plugins/hive/skills/duo-worker/SKILL.md @@ -1,3 +1,8 @@ +--- +name: duo-worker +description: Hive duo 的 worker 角色协议。仅当 hive 出生提示或 init 输出指定你为 duo-worker 时加载;其他场景不要触发。 +--- + # duo-worker — 自包含协议 你是 standalone duo 的 **worker**。validator 审你的 plan、主笔 VAL、最后审 code。你是 duo 对 human 的唯一出口。 @@ -21,7 +26,7 @@ - 标签里的 `body` 是短摘要。 - `artifact=` 是正文;需要细节时直接打开这个文件。 -- 以 `` block 为准。`hive thread` 只用于排障;需要时读 hive skill 的 `references/debug.md`。 +- 以 `` block 为准。`hive thread` 只用于排障;需要时调用 hive:debug skill。 ### 发消息:send 还是 reply @@ -29,7 +34,7 @@ - 新话题用 `hive send "<短摘要>"`,例如派任务、提新问题、发新汇报。`send` 不接 `--reply-to`。 - 回应入站消息用 `hive reply "<回复>"`。不传 `--reply-to` 时,它会锚到最近一条来自该 agent 且你还没回过的入站消息。 -- 有 anchor msgId 但当前 pane 没有那条入站消息时,显式 `hive reply --reply-to "<回复>"`。接管 thread 的细节需要时读 hive skill 的 `references/advanced-routing.md`。 +- 有 anchor msgId 但当前 pane 没有那条入站消息时,显式 `hive reply --reply-to "<回复>"`。接管 thread 的细节需要时调用 hive:advanced-routing skill。 不要因为“刚收到过对方消息”就用 `reply`。如果现在说的是新任务或新汇报,用 `send` 开新 thread。 diff --git a/plugins/hive/skills/hive/SKILL.md b/plugins/hive/skills/hive/SKILL.md index f320563..56a6c29 100644 --- a/plugins/hive/skills/hive/SKILL.md +++ b/plugins/hive/skills/hive/SKILL.md @@ -5,7 +5,7 @@ description: Hive 是 tmux 里的多 agent 协作 runtime。当收到 HIVE 消 # Hive — agent 协作入口 -Hive 让多个 agent 在 tmux 里用 `` 消息协作。这个文件只是发现入口;真正协议在本 skill 目录的 `references/` 下,按当前角色读取。 +Hive 让多个 agent 在 tmux 里用 `` 消息协作。这个文件只是发现入口;真正协议是按角色命名的独立 skill,按名加载。 ## 已在 team 里 @@ -17,18 +17,18 @@ hive team 看 `self`、`members`、`group`、`peer` 和当前 pane 状态。 -如果出生 prompt 或 init 输出给了角色,就只读那一份(路径相对本 skill 目录): +如果出生 prompt 或 init 输出给了角色,就只加载那一个 skill: -- `references/duo-worker.md` -- `references/duo-validator.md` -- `references/squad-orch.md` -- `references/squad-challenger.md` -- `references/squad-worker.md` -- `references/squad-validator.md` +- `hive:duo-worker` +- `hive:duo-validator` +- `hive:squad-orch` +- `hive:squad-challenger` +- `hive:squad-worker` +- `hive:squad-validator` -这些角色 spec 都是自包含协议。读完一份、照它做;不要再拼别的 role spec。 +这些角色 skill 都是自包含协议。加载一个、照它做;不要再拼别的角色 skill。 -如果你只是被拉进已有 team、没有角色,读 `references/core.md`。 +如果你只是被拉进已有 team、没有角色,加载 `hive:core`。 没有待办时结束当前 turn,pane 保持打开等下一条 `` 注入。不要 `sleep` 轮询,不要自己翻库找活。 @@ -46,8 +46,8 @@ hive duo init hive squad init ``` -init 的 JSON 会给 `next`,例如 `hive skill: read references/duo-worker.md`。按 `next` 读本 skill 目录下对应文件,取当前 pane 的完整协议。 +init 的 JSON 会给 `next`,例如 `invoke skill hive:duo-worker`。照 `next` 加载对应角色 skill,取当前 pane 的完整协议。 ## 速查 -`references/debug.md` 和 `references/advanced-routing.md` 是按需逃生口;日常流程按当前角色 spec。 +`hive:debug` 和 `hive:advanced-routing` 是按需逃生口;日常流程按当前角色 skill。 diff --git a/plugins/hive/skills/hive/references/squad-challenger.md b/plugins/hive/skills/squad-challenger/SKILL.md similarity index 93% rename from plugins/hive/skills/hive/references/squad-challenger.md rename to plugins/hive/skills/squad-challenger/SKILL.md index aa9d749..8cacecd 100644 --- a/plugins/hive/skills/hive/references/squad-challenger.md +++ b/plugins/hive/skills/squad-challenger/SKILL.md @@ -1,3 +1,8 @@ +--- +name: squad-challenger +description: Hive squad 的 challenger 角色协议。仅当 hive 出生提示或 init 输出指定你为 squad-challenger 时加载;其他场景不要触发。 +--- + # squad-challenger — 自包含协议 你是 squad 的 **challenger**。orch 拆需求和派 duo;你审 orch 的 plan,并评估 worker 的终态交付是否能推进给 orch。 @@ -16,7 +21,7 @@ - 标签里的 `body` 是短摘要。 - `artifact=` 是正文;需要细节时直接打开这个文件。 -- 以 `` block 为准。`hive thread` 只用于排障;需要时读 hive skill 的 `references/debug.md`。 +- 以 `` block 为准。`hive thread` 只用于排障;需要时调用 hive:debug skill。 ### 发消息:send 还是 reply @@ -24,7 +29,7 @@ - 新话题用 `hive send "<短摘要>"`,例如派任务、提新问题、发新汇报。`send` 不接 `--reply-to`。 - 回应入站消息用 `hive reply "<回复>"`。不传 `--reply-to` 时,它会锚到最近一条来自该 agent 且你还没回过的入站消息。 -- 有 anchor msgId 但当前 pane 没有那条入站消息时,显式 `hive reply --reply-to "<回复>"`。接管 thread 的细节需要时读 hive skill 的 `references/advanced-routing.md`。 +- 有 anchor msgId 但当前 pane 没有那条入站消息时,显式 `hive reply --reply-to "<回复>"`。接管 thread 的细节需要时调用 hive:advanced-routing skill。 不要因为“刚收到过对方消息”就用 `reply`。如果现在说的是新任务或新汇报,用 `send` 开新 thread。 diff --git a/plugins/hive/skills/hive/references/squad-orch.md b/plugins/hive/skills/squad-orch/SKILL.md similarity index 96% rename from plugins/hive/skills/hive/references/squad-orch.md rename to plugins/hive/skills/squad-orch/SKILL.md index 519210b..88df9eb 100644 --- a/plugins/hive/skills/hive/references/squad-orch.md +++ b/plugins/hive/skills/squad-orch/SKILL.md @@ -1,3 +1,8 @@ +--- +name: squad-orch +description: Hive squad 的 orch 角色协议。仅当 hive 出生提示或 init 输出指定你为 squad-orch 时加载;其他场景不要触发。 +--- + # squad-orch — 自包含协议 你是 squad 的 **orch**。human 给你高层需求;你拆 feature、派 duo、收结论、跑集成验、向 human 汇报。你不写业务代码。 @@ -23,7 +28,7 @@ - 标签里的 `body` 是短摘要。 - `artifact=` 是正文;需要细节时直接打开这个文件。 -- 以 `` block 为准。`hive thread` 只用于排障;需要时读 hive skill 的 `references/debug.md`。 +- 以 `` block 为准。`hive thread` 只用于排障;需要时调用 hive:debug skill。 ### 发消息:send 还是 reply @@ -31,7 +36,7 @@ - 新话题用 `hive send "<短摘要>"`,例如派任务、提新问题、发新汇报。`send` 不接 `--reply-to`。 - 回应入站消息用 `hive reply "<回复>"`。不传 `--reply-to` 时,它会锚到最近一条来自该 agent 且你还没回过的入站消息。 -- 有 anchor msgId 但当前 pane 没有那条入站消息时,显式 `hive reply --reply-to "<回复>"`。接管 thread 的细节需要时读 hive skill 的 `references/advanced-routing.md`。 +- 有 anchor msgId 但当前 pane 没有那条入站消息时,显式 `hive reply --reply-to "<回复>"`。接管 thread 的细节需要时调用 hive:advanced-routing skill。 不要因为“刚收到过对方消息”就用 `reply`。如果现在说的是新任务或新汇报,用 `send` 开新 thread。 diff --git a/plugins/hive/skills/hive/references/squad-validator.md b/plugins/hive/skills/squad-validator/SKILL.md similarity index 96% rename from plugins/hive/skills/hive/references/squad-validator.md rename to plugins/hive/skills/squad-validator/SKILL.md index 91efa4e..2489983 100644 --- a/plugins/hive/skills/hive/references/squad-validator.md +++ b/plugins/hive/skills/squad-validator/SKILL.md @@ -1,3 +1,8 @@ +--- +name: squad-validator +description: Hive squad 的 validator 角色协议。仅当 hive 出生提示或 init 输出指定你为 squad-validator 时加载;其他场景不要触发。 +--- + # squad-validator — 自包含协议 你是 squad 派生 duo 里的 **validator**:`.validator-`。worker 是 `.worker-`。你对照 orch 发来的 VAL 审 worker 的 plan,再审 handoff。 @@ -21,7 +26,7 @@ - 标签里的 `body` 是短摘要。 - `artifact=` 是正文;需要细节时直接打开这个文件。 -- 以 `` block 为准。`hive thread` 只用于排障;需要时读 hive skill 的 `references/debug.md`。 +- 以 `` block 为准。`hive thread` 只用于排障;需要时调用 hive:debug skill。 ### 发消息:send 还是 reply @@ -29,7 +34,7 @@ - 新话题用 `hive send "<短摘要>"`,例如派任务、提新问题、发新汇报。`send` 不接 `--reply-to`。 - 回应入站消息用 `hive reply "<回复>"`。不传 `--reply-to` 时,它会锚到最近一条来自该 agent 且你还没回过的入站消息。 -- 有 anchor msgId 但当前 pane 没有那条入站消息时,显式 `hive reply --reply-to "<回复>"`。接管 thread 的细节需要时读 hive skill 的 `references/advanced-routing.md`。 +- 有 anchor msgId 但当前 pane 没有那条入站消息时,显式 `hive reply --reply-to "<回复>"`。接管 thread 的细节需要时调用 hive:advanced-routing skill。 不要因为“刚收到过对方消息”就用 `reply`。如果现在说的是新任务或新汇报,用 `send` 开新 thread。 diff --git a/plugins/hive/skills/hive/references/squad-worker.md b/plugins/hive/skills/squad-worker/SKILL.md similarity index 96% rename from plugins/hive/skills/hive/references/squad-worker.md rename to plugins/hive/skills/squad-worker/SKILL.md index 94146ae..c74214c 100644 --- a/plugins/hive/skills/hive/references/squad-worker.md +++ b/plugins/hive/skills/squad-worker/SKILL.md @@ -1,3 +1,8 @@ +--- +name: squad-worker +description: Hive squad 的 worker 角色协议。仅当 hive 出生提示或 init 输出指定你为 squad-worker 时加载;其他场景不要触发。 +--- + # squad-worker — 自包含协议 你是 squad 派生 duo 里的 **worker**:`.worker-`。validator 是 `.validator-`。orch 派 feature,challenger 收终态交付,orch 管集成。 @@ -21,7 +26,7 @@ - 标签里的 `body` 是短摘要。 - `artifact=` 是正文;需要细节时直接打开这个文件。 -- 以 `` block 为准。`hive thread` 只用于排障;需要时读 hive skill 的 `references/debug.md`。 +- 以 `` block 为准。`hive thread` 只用于排障;需要时调用 hive:debug skill。 ### 发消息:send 还是 reply @@ -29,7 +34,7 @@ - 新话题用 `hive send "<短摘要>"`,例如派任务、提新问题、发新汇报。`send` 不接 `--reply-to`。 - 回应入站消息用 `hive reply "<回复>"`。不传 `--reply-to` 时,它会锚到最近一条来自该 agent 且你还没回过的入站消息。 -- 有 anchor msgId 但当前 pane 没有那条入站消息时,显式 `hive reply --reply-to "<回复>"`。接管 thread 的细节需要时读 hive skill 的 `references/advanced-routing.md`。 +- 有 anchor msgId 但当前 pane 没有那条入站消息时,显式 `hive reply --reply-to "<回复>"`。接管 thread 的细节需要时调用 hive:advanced-routing skill。 不要因为“刚收到过对方消息”就用 `reply`。如果现在说的是新任务或新汇报,用 `send` 开新 thread。 diff --git a/src/hive/cli.py b/src/hive/cli.py index 4678579..cb34caa 100644 --- a/src/hive/cli.py +++ b/src/hive/cli.py @@ -931,8 +931,8 @@ def _classify_pane(pane: tmux.PaneInfo) -> tuple[str, str]: def _hive_join_message(agent_name: str, team_name: str) -> str: return ( f"You are '{agent_name}' in hive team '{team_name}'. " - "Context is pre-bound. Load the hive skill and read its " - "`references/core.md` first and follow that protocol. " + "Context is pre-bound. Invoke the `hive:core` skill first and " + "follow that protocol. " "Hive messages will arrive inline as " " ... blocks. " "Use `hive team` to inspect the team; reply on an existing thread with " @@ -2221,9 +2221,9 @@ def layout_cmd(preset: str): def _inject_role_bootstrap(pane: str, role: str) -> bool: """Deliver the role bootstrap prompt to *pane* over its native transport. - Same text a spawned pane gets as its launch prompt (identity + the hive - skill reference to read + idle discipline) — adoption only changes the - delivery channel, never the wording. Returns True when the pane's + A dispatched running pane is told to invoke its role skill by name — + the launch-path equivalent is skill activation at spawn plus the + skill_loaded variant of the same prompt. Returns True when the pane's transport accepted it; False when the pane runs no known agent CLI or the transport refused (delivery is native-only, no keystroke fallback). """ @@ -2240,17 +2240,24 @@ def _inject_role_bootstrap(pane: str, role: str) -> bool: return True -def _role_bootstrap_prompt(role: str) -> str: - """Spawn first-message for a no-human role pane: identity + the one skill - reference that loads the role. The spec ships inside the hive plugin — - the spawned pane reads ``references/.md`` from the hive skill - exactly like a dispatched pane does (`_inject_role_bootstrap`), so there - is no inlined spec snapshot to keep in sync and the prompt stays short - enough to inline into the launch command. +def _role_bootstrap_prompt(role: str, skill_loaded: bool = False) -> str: + """First-message for a no-human role pane: identity + the role skill. + + The protocol ships as the plugin skill ``hive:``. A spawned pane + gets that skill activated as line one of its launch turn + (``Agent.spawn(skill=...)``), so its prompt only carries identity + (*skill_loaded=True*). A dispatched running pane + (`_inject_role_bootstrap`) instead gets told to invoke the skill by + name. Either way no spec snapshot is inlined or kept in sync. """ + identity = f"你是这个 team 的 {role}。" + act = ( + "按已加载的角色协议行动。" + if skill_loaded + else f"调用 hive:{role} skill 取你的角色协议 —— 照它做。" + ) return ( - f"你是这个 team 的 {role}。用 hive skill 取你的角色协议:读该 skill 目录下的 " - f"`references/{role}.md` —— 照它做。没有待办时结束当前 turn,让 pane 开着接收第一条任务消息" + f"{identity}{act}没有待办时结束当前 turn,让 pane 开着接收第一条任务消息" f"(orch / peer 会发来);在那之前别自己找活、别翻库、别 `sleep` 轮询。" ) @@ -2469,8 +2476,8 @@ def _spawn_duo_validator( split_size="50%", cli=cli, model=model, - skill="none", - prompt=_role_bootstrap_prompt("duo-validator"), + skill="hive:duo-validator", + prompt=_role_bootstrap_prompt("duo-validator", skill_loaded=True), workspace=ws, ) t.agents["validator"] = validator_agent @@ -2610,7 +2617,7 @@ def _attach_duo_to_team(t: Team, *, placement: _DuoPlacement, ws: str) -> dict[s layout_mod.apply_adaptive(window) # Hand the validator its role: a spawned validator already got the - # duo-validator bootstrap prompt at launch; an adopted idle + # hive:duo-validator skill at launch; an adopted idle # neighbor gets it injected here. The worker pane is the agent running # this very command — its role load is returned as `next` for it to run # in-turn, never injected into its input box as a fake user message. @@ -2632,7 +2639,7 @@ def _attach_duo_to_team(t: Team, *, placement: _DuoPlacement, ws: str) -> dict[s "mode": mode, }, "dispatched": dispatched, - "next": "hive skill: read references/duo-worker.md", + "next": "invoke skill hive:duo-worker", } @@ -3201,7 +3208,10 @@ def _resume_members_into_live_team( tmux.display_value(live_worker.pane_id, "#{pane_current_path}") if live_worker else "" ) cwd = live_cwd or snap_worker_cwd - session_kwargs: dict[str, str] = {"prompt": _role_bootstrap_prompt("duo-validator")} + session_kwargs: dict[str, str] = { + "prompt": _role_bootstrap_prompt("duo-validator", skill_loaded=True), + "skill": "hive:duo-validator", + } else: _resume_progress( f"resuming {name} ({m['cli']}) — replaying its session, this can take a while…" @@ -3217,7 +3227,7 @@ def _resume_members_into_live_team( split_size="50%", cli=str(m["cli"]), model=str(m.get("model", "")), - skill="none", + skill=session_kwargs.pop("skill", "none"), workspace=ws, **session_kwargs, ) @@ -3304,7 +3314,10 @@ def _resume_full_team( _resume_progress( f"spawning fresh {name} ({m['cli']}) — no saved session, starting clean…" ) - session_kwargs: dict[str, str] = {"prompt": _role_bootstrap_prompt("duo-validator")} + session_kwargs: dict[str, str] = { + "prompt": _role_bootstrap_prompt("duo-validator", skill_loaded=True), + "skill": "hive:duo-validator", + } else: _resume_progress( f"resuming {name} ({m['cli']}) — replaying its session, this can take a while…" @@ -3320,7 +3333,7 @@ def _resume_full_team( split_size="50%", cli=str(m["cli"]), model=str(m.get("model", "")), - skill="none", + skill=session_kwargs.pop("skill", "none"), workspace=ws, **session_kwargs, ) @@ -3714,8 +3727,8 @@ def squad_init_cmd(peer_cli: str | None, squad_name: str | None, worker_cli: str cwd=orch_cwd, split_horizontal=layout_mod.split_horizontal(squad_window, 2), split_size="50%", - skill="none", - prompt=_role_bootstrap_prompt("squad-challenger"), + skill="hive:squad-challenger", + prompt=_role_bootstrap_prompt("squad-challenger", skill_loaded=True), cli=peer_cli_name, model=peer_model_id, ) @@ -3748,7 +3761,7 @@ def squad_init_cmd(peer_cli: str | None, squad_name: str | None, worker_cli: str "orch": {"pane": orch_pane, "name": orch_agent_name}, "challenger": {"pane": challenger_agent.pane_id, "name": challenger_agent_name}, "dispatched": dispatched, - "next": "hive skill: read references/squad-orch.md", + "next": "invoke skill hive:squad-orch", }, indent=2)) @@ -4041,8 +4054,8 @@ def squad_spawn_duo_cmd(feature_id: str, task_artifact: str, val_artifact: str, target_pane=shell_pane, cwd=cwd, split_window=False, - skill="none", - prompt=_role_bootstrap_prompt("squad-worker"), + skill="hive:squad-worker", + prompt=_role_bootstrap_prompt("squad-worker", skill_loaded=True), cli=worker_cli, model=worker_model, ) @@ -4059,8 +4072,8 @@ def squad_spawn_duo_cmd(feature_id: str, task_artifact: str, val_artifact: str, cwd=cwd, split_horizontal=layout_mod.split_horizontal(peer_window, validator_pane_count_after), split_size="50%", - skill="none", - prompt=_role_bootstrap_prompt("squad-validator"), + skill="hive:squad-validator", + prompt=_role_bootstrap_prompt("squad-validator", skill_loaded=True), cli=validator_cli, model=validator_model, ) diff --git a/tests/cli/test_current_init_use.py b/tests/cli/test_current_init_use.py index 934df37..9aec21f 100644 --- a/tests/cli/test_current_init_use.py +++ b/tests/cli/test_current_init_use.py @@ -356,7 +356,7 @@ def test_init_creates_team_and_forms_duo(runner, configure_hive_home, monkeypatc assert payload["worker"]["name"] == "worker" assert payload["validator"]["name"] == "validator" assert payload["dispatched"] == ["validator"] - assert payload["next"] == "hive skill: read references/duo-worker.md" + assert payload["next"] == "invoke skill hive:duo-worker" # The team is created and the current pane is remembered as the worker. from hive.team import Team diff --git a/tests/cli/test_duo_init.py b/tests/cli/test_duo_init.py index 38e53a4..cd22e81 100644 --- a/tests/cli/test_duo_init.py +++ b/tests/cli/test_duo_init.py @@ -93,23 +93,22 @@ def fake_spawn(**kwargs): assert len(spawned) == 1 assert spawned[0]["name"] == "validator" assert spawned[0]["cli"] == "claude" - assert spawned[0]["skill"] == "none" - # validator's role bootstrap is a thin pointer: the spawned pane reads - # references/duo-validator.md from the hive skill — the same - # plugin-shipped channel a dispatched pane uses — so no spec snapshot is - # inlined into the launch command or cached on disk. + assert spawned[0]["skill"] == "hive:duo-validator" + # the role protocol arrives as skill activation at launch (skill kwarg); + # the prompt only carries identity + idle discipline, so no spec snapshot + # is inlined into the launch command or cached on disk. bootstrap = spawned[0]["prompt"] - assert bootstrap == cli_mod._role_bootstrap_prompt("duo-validator") - assert "references/duo-validator.md" in bootstrap + assert bootstrap == cli_mod._role_bootstrap_prompt("duo-validator", skill_loaded=True) + assert "hive:duo-validator" not in bootstrap # launch path loads the skill itself assert "别 `sleep` 轮询" in bootstrap assert "别退出" not in bootstrap assert payload["dispatched"] == ["validator"] - assert payload["next"] == "hive skill: read references/duo-worker.md" + assert payload["next"] == "invoke skill hive:duo-worker" # The worker runs init itself: nothing may be injected into its pane. worker_pane = payload["worker"]["pane"] - assert not [c for c in sent if c[0] == worker_pane and "references/" in c[1]] + assert not [c for c in sent if c[0] == worker_pane and "hive:" in c[1]] # Spawned validator gets its role via the launch prompt, not injection. - assert not [c for c in sent if "references/duo-validator.md" in c[1]] + assert not [c for c in sent if "hive:duo-validator" in c[1]] def test_role_bootstrap_prompts_do_not_tell_idle_agents_to_not_exit(configure_hive_home): @@ -162,13 +161,13 @@ def test_duo_init_two_panes_adopts_idle_antifamily_neighbor( assert spawned == [] # adopted the neighbor, did not spawn assert breaks == [] # 2-pane pairable → no break-out assert payload["dispatched"] == ["validator"] # adopted validator got its role injected - assert payload["next"] == "hive skill: read references/duo-worker.md" + assert payload["next"] == "invoke skill hive:duo-worker" worker_pane = payload["worker"]["pane"] - assert not [c for c in sent if c[0] == worker_pane and "references/" in c[1]] + assert not [c for c in sent if c[0] == worker_pane and "hive:" in c[1]] # Positive control: the adopted idle neighbor gets the exact same # bootstrap prompt a spawned validator would get at launch. validator_pane = payload["validator"]["pane"] - injected = [c[1] for c in sent if c[0] == validator_pane and "references/" in c[1]] + injected = [c[1] for c in sent if c[0] == validator_pane and "hive:duo-validator" in c[1]] assert injected == [cli_mod._role_bootstrap_prompt("duo-validator")] @@ -219,12 +218,12 @@ def fake_spawn(**kwargs): assert payload["validator"]["mode"] == "spawned" assert len(spawned) == 1 assert payload["dispatched"] == ["validator"] - assert payload["next"] == "hive skill: read references/duo-worker.md" + assert payload["next"] == "invoke skill hive:duo-worker" # The worker runs init itself: nothing may be injected into its pane. worker_pane = payload["worker"]["pane"] - assert not [c for c in sent if c[0] == worker_pane and "references/" in c[1]] + assert not [c for c in sent if c[0] == worker_pane and "hive:" in c[1]] # Spawned validator gets its role via the launch prompt, not injection. - assert not [c for c in sent if "references/duo-validator.md" in c[1]] + assert not [c for c in sent if "hive:duo-validator" in c[1]] def test_duo_init_three_panes_breaks_out_then_spawns( @@ -261,12 +260,12 @@ def fake_spawn(**kwargs): assert payload["validator"]["mode"] == "spawned" assert len(spawned) == 1 assert payload["dispatched"] == ["validator"] - assert payload["next"] == "hive skill: read references/duo-worker.md" + assert payload["next"] == "invoke skill hive:duo-worker" # The worker runs init itself: nothing may be injected into its pane. worker_pane = payload["worker"]["pane"] - assert not [c for c in sent if c[0] == worker_pane and "references/" in c[1]] + assert not [c for c in sent if c[0] == worker_pane and "hive:" in c[1]] # Spawned validator gets its role via the launch prompt, not injection. - assert not [c for c in sent if "references/duo-validator.md" in c[1]] + assert not [c for c in sent if "hive:duo-validator" in c[1]] def test_duo_init_breakout_names_team_from_final_window_not_origin( @@ -684,8 +683,8 @@ def test_duo_init_revives_missing_validator(runner, configure_hive_home, monkeyp assert len(spawned) == 1 assert spawned[0]["name"] == "validator" assert spawned[0]["cli"] == "codex" # anti-family of the claude worker - assert spawned[0]["skill"] == "none" - assert spawned[0]["prompt"] == cli_mod._role_bootstrap_prompt("duo-validator") + assert spawned[0]["skill"] == "hive:duo-validator" + assert spawned[0]["prompt"] == cli_mod._role_bootstrap_prompt("duo-validator", skill_loaded=True) assert spawned[0]["workspace"] == ws assert spawned[0]["cwd"] == str(tmp_path / "repo") assert peered == [("worker", "validator")] diff --git a/tests/cli/test_resume_command.py b/tests/cli/test_resume_command.py index 1eb4d81..108af87 100644 --- a/tests/cli/test_resume_command.py +++ b/tests/cli/test_resume_command.py @@ -932,8 +932,8 @@ def test_resume_full_restore_spawns_fresh_validator_without_session( # snapshot cli/model assert validator["name"] == "validator" assert "session_id" not in validator and "session_mode" not in validator - assert validator["prompt"] == cli_mod._role_bootstrap_prompt("duo-validator") - assert validator["skill"] == "none" + assert validator["prompt"] == cli_mod._role_bootstrap_prompt("duo-validator", skill_loaded=True) + assert validator["skill"] == "hive:duo-validator" assert validator["cwd"] == good_cwd assert validator["cli"] == "codex" and validator["model"] == "m2" @@ -980,7 +980,8 @@ def test_resume_live_revive_spawns_fresh_validator_in_live_worker_cwd( (validator,) = rec.spawns assert validator["name"] == "validator" assert "session_id" not in validator and "session_mode" not in validator - assert validator["prompt"] == cli_mod._role_bootstrap_prompt("duo-validator") + assert validator["prompt"] == cli_mod._role_bootstrap_prompt("duo-validator", skill_loaded=True) + assert validator["skill"] == "hive:duo-validator" # live worker pane's current cwd (mocked display_value), not the snapshot's assert validator["cwd"] == "/live/repo-cwd" sessions = {m["name"]: m["session"] for m in payload["members"]} diff --git a/tests/cli/test_squad_init.py b/tests/cli/test_squad_init.py index 2be4304..8b02aaf 100644 --- a/tests/cli/test_squad_init.py +++ b/tests/cli/test_squad_init.py @@ -66,12 +66,12 @@ def fake_spawn(**kwargs): assert payload["challenger"]["name"] == "peaky.challenger" assert payload["challenger"]["pane"] == "%101" assert payload["dispatched"] == ["peaky.challenger"] - assert payload["next"] == "hive skill: read references/squad-orch.md" + assert payload["next"] == "invoke skill hive:squad-orch" # The orch runs init itself: nothing may be injected into its pane. orch_pane = payload["orch"]["pane"] - assert not [c for c in sent if c[0] == orch_pane and "references/" in c[1]] + assert not [c for c in sent if c[0] == orch_pane and "hive:" in c[1]] # Positive control: the spawned challenger gets its role via launch prompt. - assert spawned[0]["prompt"] == cli_mod._role_bootstrap_prompt("squad-challenger") + assert spawned[0]["prompt"] == cli_mod._role_bootstrap_prompt("squad-challenger", skill_loaded=True) assert "board" not in payload assert selected_windows == ["dev:0"] @@ -151,9 +151,9 @@ def fake_spawn(**kwargs): assert payload["challenger"]["name"] == "peaky.challenger" assert spawned[0]["team_name"] == "dev-w88" # challenger spawned under the final-window team assert payload["dispatched"] == ["peaky.challenger"] - assert payload["next"] == "hive skill: read references/squad-orch.md" + assert payload["next"] == "invoke skill hive:squad-orch" orch_pane = payload["orch"]["pane"] - assert not [c for c in sent if c[0] == orch_pane and "references/" in c[1]] + assert not [c for c in sent if c[0] == orch_pane and "hive:" in c[1]] assert sidecar_calls == [("/tmp/hive-dev-w88", "dev-w88", "dev:8", "@88")] diff --git a/tests/conftest.py b/tests/conftest.py index 1397a5b..21633b2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -238,7 +238,7 @@ def _guard_break_pane(*_args, **_kwargs): "worker": {"pane": "%self", "name": "worker", "cli": "claude"}, "validator": {"pane": "%peer", "name": "validator", "cli": "codex", "mode": "spawned"}, "dispatched": ["validator"], - "next": "hive skill: read references/duo-worker.md", + "next": "invoke skill hive:duo-worker", }, ) monkeypatch.delenv("TMUX_PANE", raising=False) From 0734fdbc501c0465ac1ab5af6cf513cacdc7599b Mon Sep 17 00:00:00 2001 From: dp Date: Wed, 15 Jul 2026 14:55:16 +0800 Subject: [PATCH 5/5] test: assert the squad spawn sites pass their role skill --- tests/cli/test_squad_init.py | 1 + tests/cli/test_squad_spawn_duo.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/tests/cli/test_squad_init.py b/tests/cli/test_squad_init.py index 8b02aaf..bd1023e 100644 --- a/tests/cli/test_squad_init.py +++ b/tests/cli/test_squad_init.py @@ -72,6 +72,7 @@ def fake_spawn(**kwargs): assert not [c for c in sent if c[0] == orch_pane and "hive:" in c[1]] # Positive control: the spawned challenger gets its role via launch prompt. assert spawned[0]["prompt"] == cli_mod._role_bootstrap_prompt("squad-challenger", skill_loaded=True) + assert spawned[0]["skill"] == "hive:squad-challenger" assert "board" not in payload assert selected_windows == ["dev:0"] diff --git a/tests/cli/test_squad_spawn_duo.py b/tests/cli/test_squad_spawn_duo.py index 44aeb54..2185bb5 100644 --- a/tests/cli/test_squad_spawn_duo.py +++ b/tests/cli/test_squad_spawn_duo.py @@ -457,12 +457,14 @@ def fake_spawn(**kwargs): assert worker_spawn["name"] == "peaky.worker-1000" assert worker_spawn["cli"] == "claude" assert worker_spawn["model"] == "opus" + assert worker_spawn["skill"] == "hive:squad-worker" # Validator spawn validator_spawn = spawned[1] assert validator_spawn["name"] == "peaky.validator-1000" assert validator_spawn["cli"] == "codex" assert validator_spawn["model"] == "o3" + assert validator_spawn["skill"] == "hive:squad-validator" def test_squad_spawn_duo_validator_fallback_when_no_role_cli(