Files
HomeAgent/internal/plugins/skillmgr/builtin/kernel-terminal/SKILL.md
JianFeeeee 387ae945df fix(agent): 子 agent 继承技能索引 —— 修 agent 把终端当命令执行器用
## 缺陷(生产实测)

`SetSkillIndexProvider` 只在 cmd/homed/bootstrap.go 里对**根 agent** 调了一次。
rtchild / 驻留子各自 `New(AgentConfig{...})` 构造载体,都没传这个字段 ⇒
子的 system prompt 里**完全没有 skill 索引**。

症状(日志实录):A2A 会话由「临时分诊助手」(驻留子)接手,它手上没有
`cmd_run`(cmd 插件 `auto_start=false`),而 `cmd-tools` skill 正文里写着
「先 plugin_start cmd」。它看不到这条 skill,于是把 `terminal_create` 当成
命令执行器:每条 `cd … && echo … && grep …` 开一个新终端,终端随命令结束退出,
`terminal_read` 立刻撞 `read /dev/ptmx: input/output error`,然后为下一条命令
再开一个 —— 终端列表被填满,每一条都死在半路。

与本仓 Security 那次同型:父有的能力,子没继承。

## 修法

rtchild 与驻留子的 AgentConfig 各补一行 `SkillIndexProvider: a.skillIndex`,
与 `SystemPrompt` / `StageHost` / `Security` 同一组「必须继承父」的字段。

## 顺带两件事

### 1. 内置 skill 的更新永远到不了已部署机器

原落盘规则是「目标目录已存在就跳过」,本意是保住用户改动。但它把两件事混成一件:

    用户改过的版本     ⇒ 应该保留
    内核自己写过的旧版 ⇒ 应该随内核更新

两者都表现为「目录已存在」,于是 b850fea 把 `memory_recall` 改成
`stablemem_recall` 后,生产机的 kernel-memory 一直停在旧名,模型照着
旧名调工具必然失败,而日志里一个字都没有。

改为用 `.builtin-sha256` 摘要区分:
  当前内容 == stamp  ⇒ 没人动过 ⇒ 可安全覆盖成新版
  当前内容 != stamp  ⇒ 用户改过 ⇒ 保留
  没有 stamp(老目录)⇒ 保守视为用户版本,不覆盖

### 2. 新增 kernel-terminal skill

工具描述只说「适用于交互式程序」,没说何时**不该**用;`command` 参数又鼓励
传整条命令。补一条内置 skill 讲清 cmd_run 与 terminal_create 的边界、
以及终端的 input/key/notify 用法。描述控制在 80 字符内(进索引不截断)。

## 判据(均经变异验证)

  TestRTChild_InheritsSkillIndex        删掉接线 ⇒ 红(实测确认)
  TestResident_InheritsSkillIndex       同上(生产实际中招的那一类)
  TestRootAgent_SkillIndexStillInjected 确认没改坏根 agent 路径
  TestChildWithoutSkillIndexIsNotBroken 父无索引时子不 panic
  TestMaterializeBuiltinSkills_*        5 条:覆盖语义四条成组断言
                                        (改回旧行为 ⇒ OverwritesUnmodified 红,实测确认)

判据测的是**生成的 prompt**,不是 `skillIndex != nil`:
后者接上了但注入那段被删照样绿。

## 验证

  go build ./...                       PASS
  go vet ./...                         PASS
  gofmt -l                             干净
  go test ./... -count=1               62 包全过,0 FAIL
  干净检出(git worktree)单独验证      PASS
2026-11-04 10:17:45 +08:00

3.0 KiB

name, description, version
name description version
kernel-terminal Terminal waits for input; it is NOT cmd_run. Read before using vim/ssh/top. 1.0.0

Terminals vs one-shot commands

Two different tools. Picking the wrong one looks like it works, then fails in a way that points somewhere else.

Tool Use for Lifetime
cmd_run One command, then done: queries, scripts, cd && … && grep …, pipes, redirects, heredocs Process exits when the command exits
terminal_create A program that keeps running and keeps asking for input: vim, ssh, top, less, an interactive REPL Stays open until you terminal_close or its timeout expires

The failure this prevents

Running terminal_create with a compound one-shot command (cd /x && echo … && ls … && grep …) creates a terminal that exits the moment the command finishes. Reading it then fails with read /dev/ptmx: input/output error. The next instinct is to create another terminal for the next command — and now every "command" costs a terminal, fills the terminal list, and each one dies mid-read.

If the command runs to completion on its own, it belongs in cmd_run. A terminal is only justified when the thing you are talking to is still running and waiting for you.

Driving a terminal

terminal_create returns an ID. From then on the terminal is silent by default — it does not push output at you.

  1. terminal_read(id) — read what is on screen now. Do this after acting, not on a timer.
  2. terminal_write(id, input=..., key=...) — send text and/or a key (Enter, Escape, C-c) in one call. input without key does not submit; the program just sits there.
  3. terminal_close(id) — release it. Terminals default to a 5-minute timeout; leaving them open wastes a slot.

notify is opt-in. Without it you get nothing. Add it only when you genuinely want a wake-up (exit for "tell me when it finished", buffer=8192 for "tell me when there is a lot of unread output"). Watching a fast-scrolling program with interval produces noise, not signal.

Sending keystrokes

key is for control, input is for text:

Want Send
Submit a command line input="ls -la", key="Enter"
Interrupt a running program key="C-c"
Leave a pager or editor without saving key="Escape", then key="q" for pagers
Quit vim without saving key="Escape", then input=":q!", key="Enter"

Reading output that scrolled past

terminal_read returns what is currently visible plus recent scrollback. If the output you need scrolled out of range, do not blindly re-read in a loop — either grow rows at create time, or for one-shot inspection prefer cmd_run, whose result is returned whole.

Checklist before reaching for a terminal

  • Does the command finish by itself? → cmd_run.
  • Does it need me to type at it more than once? → terminal_create.
  • Am I about to create a second terminal for "the next command"? → stop; the first one was the wrong tool.