diff --git a/AGENTS.md b/AGENTS.md
index 3350cca..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`, `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`) 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 1a1f783..21d6124 100644
--- a/hive-topology.html
+++ b/hive-topology.html
@@ -138,12 +138,12 @@
入口交互 · 单一选择
你打 /hive (任何 pane 都行)
│
- ▼ 自动加载通信底座 (hive skills get core: send/reply/team)
+ ▼ 自动加载通信底座 (hive:core skill: send/reply/team)
[问] AskUserQuestion —— 唯一一次交互:"起哪种拓扑?"
├─ cell worker + validator,你自己编排 (2 panes)
└─ crew orch + skeptic + 按需 worker-cell (orch = 当前 pane)
│
- ▼ hive skills get <选的> → 拉该拓扑的角色说明
+ ▼ 加载 hive:<选的> skill → 拉该拓扑的角色说明
▼ 跑对应 init:hive cell init / hive crew init → 自动布 tmux + spawn 角色
▼ 开干
@@ -164,7 +164,7 @@ ✓ 已在 main(via #5,非本会话)
⧗ 待建(设计已定,未写码)
- hive skills get <core|cell|crew> —— 新命令,目前不存在。
+ hive:<core|cell|crew> —— role spec 分发面,已改为按 role 独立的插件 skill,按名加载。
hive cell init —— 把 cell 落成 worker+validator(固定角色)。注意:当前 hive init 起的还是旧的对称 peer(group=peer、双方都写码互审),尚未改成本图里的 doer+validator。
- 重写
/hive skill 成 AskUserQuestion(cell/crew) 选择器 + fetch + init。
diff --git a/src/hive/core_assets/specs/advanced-routing.md b/plugins/hive/skills/advanced-routing/SKILL.md
similarity index 85%
rename from src/hive/core_assets/specs/advanced-routing.md
rename to plugins/hive/skills/advanced-routing/SKILL.md
index 1f6ba5c..1ee4db3 100644
--- a/src/hive/core_assets/specs/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/src/hive/core_assets/specs/core.md b/plugins/hive/skills/core/SKILL.md
similarity index 92%
rename from src/hive/core_assets/specs/core.md
rename to plugins/hive/skills/core/SKILL.md
index 975d35d..34221ce 100644
--- a/src/hive/core_assets/specs/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 skills get debug`。
+- 以 `` 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 skills get advanced-routing`。
+- 有 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 skills get debug`
-- active-turn fork、handoff 接管、复杂 thread routing:`hive skills get advanced-routing`
+- 排障命令、delivery、thread、capture、inject、interrupt、kill:hive:debug skill
+- active-turn fork、handoff 接管、复杂 thread routing:hive:advanced-routing skill
diff --git a/src/hive/core_assets/specs/debug.md b/plugins/hive/skills/debug/SKILL.md
similarity index 89%
rename from src/hive/core_assets/specs/debug.md
rename to plugins/hive/skills/debug/SKILL.md
index 35ff215..f834f50 100644
--- a/src/hive/core_assets/specs/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/src/hive/core_assets/specs/duo-validator.md b/plugins/hive/skills/duo-validator/SKILL.md
similarity index 95%
rename from src/hive/core_assets/specs/duo-validator.md
rename to plugins/hive/skills/duo-validator/SKILL.md
index e70117b..cb644f5 100644
--- a/src/hive/core_assets/specs/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 skills get debug`。
+- 以 `` 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 skills get advanced-routing`。
+- 有 anchor msgId 但当前 pane 没有那条入站消息时,显式 `hive reply --reply-to "<回复>"`。接管 thread 的细节需要时调用 hive:advanced-routing skill。
不要因为“刚收到过对方消息”就用 `reply`。如果现在说的是新任务或新汇报,用 `send` 开新 thread。
diff --git a/src/hive/core_assets/specs/duo-worker.md b/plugins/hive/skills/duo-worker/SKILL.md
similarity index 96%
rename from src/hive/core_assets/specs/duo-worker.md
rename to plugins/hive/skills/duo-worker/SKILL.md
index df5bb3e..d1b3f7d 100644
--- a/src/hive/core_assets/specs/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 skills get debug`。
+- 以 `` 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 skills get advanced-routing`。
+- 有 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 c7440c2..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 里用 `` 消息协作。这个文件只是发现入口;真正协议由 CLI 按当前角色取回。
+Hive 让多个 agent 在 tmux 里用 `` 消息协作。这个文件只是发现入口;真正协议是按角色命名的独立 skill,按名加载。
## 已在 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
-```
+- `hive:duo-worker`
+- `hive:duo-validator`
+- `hive:squad-orch`
+- `hive:squad-challenger`
+- `hive:squad-worker`
+- `hive:squad-validator`
-这些角色 spec 都是自包含协议。取回一份、读完、照它做;不要再拼别的 role spec。
+这些角色 skill 都是自包含协议。加载一个、照它做;不要再拼别的角色 skill。
-如果你只是被拉进已有 team、没有角色,取:
-
-```bash
-hive skills get core
-```
+如果你只是被拉进已有 team、没有角色,加载 `hive:core`。
没有待办时结束当前 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`,例如 `invoke skill hive:duo-worker`。照 `next` 加载对应角色 skill,取当前 pane 的完整协议。
## 速查
-```bash
-hive skills list
-hive skills get debug
-hive skills get advanced-routing
-```
-
-`debug` 和 `advanced-routing` 是按需逃生口;日常流程按当前角色 spec。
+`hive:debug` 和 `hive:advanced-routing` 是按需逃生口;日常流程按当前角色 skill。
diff --git a/src/hive/core_assets/specs/squad-challenger.md b/plugins/hive/skills/squad-challenger/SKILL.md
similarity index 93%
rename from src/hive/core_assets/specs/squad-challenger.md
rename to plugins/hive/skills/squad-challenger/SKILL.md
index c0b6813..8cacecd 100644
--- a/src/hive/core_assets/specs/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 skills get debug`。
+- 以 `` 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 skills get advanced-routing`。
+- 有 anchor msgId 但当前 pane 没有那条入站消息时,显式 `hive reply --reply-to "<回复>"`。接管 thread 的细节需要时调用 hive:advanced-routing skill。
不要因为“刚收到过对方消息”就用 `reply`。如果现在说的是新任务或新汇报,用 `send` 开新 thread。
diff --git a/src/hive/core_assets/specs/squad-orch.md b/plugins/hive/skills/squad-orch/SKILL.md
similarity index 96%
rename from src/hive/core_assets/specs/squad-orch.md
rename to plugins/hive/skills/squad-orch/SKILL.md
index e55f312..88df9eb 100644
--- a/src/hive/core_assets/specs/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 skills get debug`。
+- 以 `` 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 skills get advanced-routing`。
+- 有 anchor msgId 但当前 pane 没有那条入站消息时,显式 `hive reply --reply-to "<回复>"`。接管 thread 的细节需要时调用 hive:advanced-routing skill。
不要因为“刚收到过对方消息”就用 `reply`。如果现在说的是新任务或新汇报,用 `send` 开新 thread。
diff --git a/src/hive/core_assets/specs/squad-validator.md b/plugins/hive/skills/squad-validator/SKILL.md
similarity index 96%
rename from src/hive/core_assets/specs/squad-validator.md
rename to plugins/hive/skills/squad-validator/SKILL.md
index b085882..2489983 100644
--- a/src/hive/core_assets/specs/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 skills get debug`。
+- 以 `` 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 skills get advanced-routing`。
+- 有 anchor msgId 但当前 pane 没有那条入站消息时,显式 `hive reply --reply-to "<回复>"`。接管 thread 的细节需要时调用 hive:advanced-routing skill。
不要因为“刚收到过对方消息”就用 `reply`。如果现在说的是新任务或新汇报,用 `send` 开新 thread。
diff --git a/src/hive/core_assets/specs/squad-worker.md b/plugins/hive/skills/squad-worker/SKILL.md
similarity index 96%
rename from src/hive/core_assets/specs/squad-worker.md
rename to plugins/hive/skills/squad-worker/SKILL.md
index b4f9a56..c74214c 100644
--- a/src/hive/core_assets/specs/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 skills get debug`。
+- 以 `` 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 skills get advanced-routing`。
+- 有 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 743717a..cb34caa 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. 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 "
"`hive reply \"...\"`; open a new thread with "
@@ -2217,86 +2218,12 @@ 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
- 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).
"""
@@ -2313,16 +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 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.
+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 skills get {role}` 取你的角色协议 "
- f"—— 照它做。没有待办时结束当前 turn,让 pane 开着接收第一条任务消息"
+ f"{identity}{act}没有待办时结束当前 turn,让 pane 开着接收第一条任务消息"
f"(orch / peer 会发来);在那之前别自己找活、别翻库、别 `sleep` 轮询。"
)
@@ -2541,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
@@ -2681,8 +2616,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
+ # 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.
@@ -2704,7 +2639,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": "invoke skill hive:duo-worker",
}
@@ -3273,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…"
@@ -3289,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,
)
@@ -3376,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…"
@@ -3392,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,
)
@@ -3786,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,
)
@@ -3820,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 skills get squad-orch",
+ "next": "invoke skill hive:squad-orch",
}, indent=2))
@@ -4113,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,
)
@@ -4131,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 4808c80..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 skills get duo-worker"
+ 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 6343026..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 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.
+ 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 "hive skills get duo-validator" 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 skills get duo-worker"
+ 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 "skills get" 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 "skills get duo-validator" 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 skills get duo-worker"
+ 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 "skills get" 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 "skills get" 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 skills get duo-worker"
+ 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 "skills get" 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 "skills get duo-validator" 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 skills get duo-worker"
+ 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 "skills get" 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 "skills get duo-validator" 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_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_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_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..bd1023e 100644
--- a/tests/cli/test_squad_init.py
+++ b/tests/cli/test_squad_init.py
@@ -66,12 +66,13 @@ 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"] == "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 "skills get" 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 spawned[0]["skill"] == "hive:squad-challenger"
assert "board" not in payload
assert selected_windows == ["dev:0"]
@@ -151,9 +152,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"] == "invoke skill hive:squad-orch"
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 "hive:" in c[1]]
assert sidecar_calls == [("/tmp/hive-dev-w88", "dev-w88", "dev:8", "@88")]
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(
diff --git a/tests/conftest.py b/tests/conftest.py
index 925bb3c..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 skills get duo-worker",
+ "next": "invoke skill hive:duo-worker",
},
)
monkeypatch.delenv("TMUX_PANE", raising=False)