同步 latest skill releases

This commit is contained in:
2026-09-07 13:38:29 +00:00
parent 3da100f300
commit 4d9fa1134c
8 changed files with 566 additions and 79 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
"baimiao-table-ocr": "v1", "baimiao-table-ocr": "v1",
"disk-master": "v1", "disk-master": "v1",
"everything-file-search": "v1", "everything-file-search": "v1",
"init-workspace": "v1", "init-workspace": "v2",
"teach-me": "v2", "teach-me": "v2",
"think-twice": "v3.1", "think-twice": "v3.1",
"why-here": "v1" "why-here": "v1"
+74 -45
View File
@@ -1,73 +1,102 @@
--- ---
name: init-workspace name: init-workspace
description: 初始化或补全个人执行工作区,搭目录脚手架、生成 AGENTS.md 与可选 CODINGRULES.md。仅在用户明确要求初始化或重构工作区时使用。 description: 检查当前开发环境,按需初始化或补全个人执行工作区,生成目录脚手架、AGENTS.md 与可选 CODINGRULES.md。
disable-model-invocation: true disable-model-invocation: true
--- ---
# Init Workspace # Init Workspace
为个人执行工作区搭脚手架并生成规则文件。工作区模式:一个目录同时容纳源码仓库、本地文档库(`docs/`)和参考库(`pkgs/`),由 `AGENTS.md` 承载工作区规则。生成的文件假定目标机器装有 `$think-twice`。 先检查环境,再根据用户要求决定只报告、初始化新目录或补全已有工作区。一个工作区可以容纳多个源码仓、`docs/` 本地文档和 `pkgs/` 参考库。环境检查不是初始化授权,也不是启动 worker 的授权。
## Step 1 — 访谈 ## Step 1 — 检查 Herdr 与 skills
一轮问完,每题带默认值;已存在的工作区只问缺口: 先检查 `HERDR_ENV` 和 PATH 上的 `herdr`,再查找当前宿主列出或用户指定的 `herdr`、`think-twice`、`why-here` skill,并实际读取文件。记录来源和能否读取;目录存在不等于宿主可用,同名副本以宿主当前选择为准。
1. 目标目录:在哪个路径创建新工作区,或补全哪个已有目录 - `HERDR_ENV` 不为 `1`:不查询或控制 Herdr 会话。可以记录 CLI 存在,并按用户要求为以后生成条件式规则。
2. 工作区名/项目名(决定目录名与 `<project>.code-workspace` 文件名) - 在 Herdr 内:读取可访问的 Herdr skill 后,按其要求查询 CLI 帮助与当前会话,核验可用性。宿主没有列出 Herdr skill 时,可运行 `herdr --skill` 获取内置文本并读取;记录其来源为 CLI,不声称已安装到所有宿主。
3. 源码仓库:已有路径还是待克隆(待克隆的要拿远端 URL),一个还是多个 - 缺少 skill:报告缺口,不自动安装,也不生成无法执行的 skill 引用。用户明确允许安装后,重新检查。
4. Quick Context 内容:用户负责的模块、维护的上游库、需要防止误判的事项 - 主控可读不代表 worker 可读;协作规则要求启动 worker 后重新核验必需 skill。
5. 是否生成 `CODINGRULES.md`(默认否)
6. 工作区是否需要 git 跟踪(默认否,有些工作区不需要)
## Step 2 — 探测现状 **完成条件:** Herdr 环境和三个 skill 的状态明确;“未查”“不可读”“当前可读”分开记录。
检查:`AGENTS.md`、`CLAUDE.md`(是否已是可用的链接或 `@AGENTS.md` 桥)、`CODINGRULES.md`、`.agents/skills` 与 `.claude/skills` 的连接、`*.code-workspace`、PATH 上是否有 `just`、`think-twice` skill 是否可用(生成的文件引用它,不可用时提醒用户先安装)。 ## Step 2 — 检查工具、模型与目标目录
已有文件一律不覆盖:逐节给出保留/替换/新增提案,模板不认识的章节原样保留,用户确认后才写入。 读取 [references/environment-checks.md](references/environment-checks.md),按其中的命令和边界运行检查。脚本路径相对本 skill,不相对目标工作区。
## Step 3 — 目录脚手架 - 有 Python 3.9+ 时使用 `scripts/inspect_environment.py`。默认只检查 PATH、受支持 CLI 的版本/帮助、目标文件存在性;输出 JSON,不写工作区。
- 将宿主找到的 skill 路径用 `--skill NAME=PATH` 传给脚本。脚本只核验文件可读性,Step 1 的宿主判断仍由 Agent 负责。
- 包含 `pi`、`claude`、`opencode`、`grok`;`codex`、`gemini` 只查 PATH。用户要求列表时,按参考说明显式开启 Pi/Grok/OpenCode 模型查询和 OpenCode agent 定义查询。其他宿主接口按需补充,没有接口或无法确认副作用时标记未查。
- 区分 agent 定义与运行实例,模型配置与 CLI 列表;这些都不证明真实调用成功。不推断额度,不发测试 prompt、不登录、不启动 worker。
- 检查目标的 `AGENTS.md`、`CLAUDE.md` 桥、`CODINGRULES.md`、skills 连接、`*.code-workspace` 和 PATH 上的 `just`。新目录尚不存在时先扫描其现有父目录,并注明这不是目标目录现状。
- 无 Python 时按同一清单人工检查并报告,不自动安装,不执行无法核验的命令。
只创建缺失的部分: 只输出允许的字段,不展示配置全文、密钥或原始命令错误日志。脚本输出统一屏蔽 `sk-[0-9a-zA-Z-]+`;人工报告同样检查。匹配串可能不是密钥,其他格式也可能漏检;不据此宣称报告完全无敏感数据,不将脱敏后的路径或 ID 自动用于执行。
``` **完成条件:** 提供简短能力/缺口报告及检查范围。用户只要求检查时,到此结束;报告默认不持久化,需要保留时先确认位置。
<project>-workspace/
## Step 3 — 补充需求与选择章节
根据已有要求一轮询问缺口,不重复询问探测能回答的事实:
1. 目标目录、工作区名、维护者与用途。
2. 源码仓库:已有路径还是待克隆、一个还是多个;待克隆需远端 URL。
3. Quick Context:模块归属、维护的上游库、需要防止误判的事项。
4. 是否采用 Herdr 并行协作(当前可用时建议采用,但不自动启用)。只读调研和评审不额外建 worktree,并行修改源码的 worker 用独立 worktree。
5. 是否生成 `CODINGRULES.md`(默认否),工作区是否 git 跟踪(默认否)。
6. 是否引用用户指定的模型策略文件(默认不生成)。策略表达工具/模型/额度偏好,不把本次扫描清单复制进去。
已有文件逐节提出保留/替换/新增方案,模板不认识的章节原样保留。确认后才写入。用户已明确授权的全新空目录无需再问一次。
**完成条件:** 明确本次修改范围、源码仓位置与章节选择,已获相应写入确认。
## Step 4 — 生成候选规则与脚手架
按 [references/environment-checks.md](references/environment-checks.md) 的 decisions 格式整理明确选择,用 `scripts/render_agents.py` 输出候选 `AGENTS.md` 到系统临时目录。它只做模板渲染,不读取扫描结果自行决策。
- `AGENTS.md`:使用 [assets/AGENTS.template.md](assets/AGENTS.template.md)。新文件核验后写入;已有文件逐节修改,不将完整候选重定向覆盖。重复补全先对照现有章节,避免重复规则。
- 可选 Herdr 章节来自 [assets/sections/herdr.md](assets/sections/herdr.md),只有采用 Herdr 且可读其 skill 时加入。`think-twice` 用于正式任务书/交付;`why-here` 只在废弃尝试、纠正误解、完成迁移或替换实现后使用。
- 模型策略引用必须指向目标工作区实际可读的文件;在本次明确授权创建该文件时可以先生成候选,交付前检查引用。没有文件就省略,不生成空策略文件。采用个人外部策略时,在工作区文件中写清实际来源、读取条件和失效处理。
- `CLAUDE.md`:先尝试符号链接到 `AGENTS.md`;Windows 需要管理员权限或开发者模式。失败时写一行 `@AGENTS.md`。已有可用桥不动。
- `CODINGRULES.md`:用户选择生成时使用 [assets/CODINGRULES.template.md](assets/CODINGRULES.template.md),同时保留 AGENTS 指针;未选择时不删已有文件。
- Python 不可用时,Agent 根据同一模板与章节选择生成候选;仍执行占位符、指针、脱敏及行数检查。
只创建缺失的部分:
```text
<workspace>/
|- .agents/skills/ 真身目录 |- .agents/skills/ 真身目录
|- .claude/skills Junction 指向 .agents/skills |- .claude/skills 指向 .agents/skills
|- docs/ 本地文档库(prd/ plans/ guides/ sop/ 按需) |- docs/ 按需建立 prd/ plans/ guides/ sop/
|- <repo>/ 源码仓库,可多个 |- <repo>/ 一个或多个源码仓
|- pkgs/ 参考库 |- pkgs/ 参考库
|- scripts/ 工作区脚本 |- scripts/ 工作区脚本
|- .gitignore 至少含 nul(Git Bash 下 > nul 会产生真实文件) |- .gitignore 至少含 nul
|- <project>.code-workspace 从 assets/project.code-workspace 填充 |- <project>.code-workspace
|- justfile 仅当 PATH 上有 just 时创建空骨架 |- justfile 仅当 PATH 有 just
|- AGENTS.md, CLAUDE.md |- AGENTS.md, CLAUDE.md
``` ```
- Windows 目录 Junction 不需要管理员权限:`New-Item -ItemType Junction -Path .claude\skills -Target <绝对路径>\.agents\skills` - Windows Junction:`New-Item -ItemType Junction -Path .claude\skills -Target <绝对路径>\.agents\skills`,无需管理员权限。POSIX:`ln -s ../.agents/skills .claude/skills`。已有目录/连接先检查,不覆盖。
- POSIX:`ln -s ../.agents/skills .claude/skills` - 从 [assets/project.code-workspace](assets/project.code-workspace) 填充:Workspace(`.`)、每个源码仓、Docs 各一个入口;有独立入口的目录在 Workspace 视图的 `files.exclude` 隐藏。
- `code-workspace` 的 folders:Workspace(`.`)+ 每个源码仓一个入口 + Docs;有独立入口的目录在 `files.exclude` 里对 Workspace 视图隐藏,从 [assets/project.code-workspace](assets/project.code-workspace) 填充 - 有 URL 的待克隆仓按授权克隆到对应目录;没有 URL 的只建空目录并记入 `docs/TODO.md`。
- 访谈给到远端 URL 的待克隆仓库,克隆到对应目录;没有 URL 的只建空目录并记入 `docs/TODO.md` - 写最小 `docs/README.md` 目录地图,与生成规则中引用的目录保持一致。
- `docs/` 的子目录与生成的 `AGENTS.md` Workspace Docs 一节保持一致,并写一个最小的 `docs/README.md` 目录地图,不留悬空指针 - `justfile` 空骨架:`default:` 下接 `@just --list`。
- `justfile` 骨架只需一条默认任务:`default:` 下接 `@just --list`
## Step 4 — 生成规则文件 **完成条件:** 仅授权且缺失的资源被创建,既有内容按确认方案修改,候选中的可选指针已有实际目标。
- `AGENTS.md`:复制 [assets/AGENTS.template.md](assets/AGENTS.template.md),填访谈槽位。未生成 `CODINGRULES.md` 时,整节删除 Coding Rules。 ## Step 5 — git 跟踪(用户选择才执行)
- `CLAUDE.md` 桥:先尝试符号链接指向 `AGENTS.md`(Windows 需要管理员权限或开发者模式);失败则写只有一行 `@AGENTS.md` 的文件。已有可用桥不动。
- `CODINGRULES.md`(访谈答是才生成):复制 [assets/CODINGRULES.template.md](assets/CODINGRULES.template.md),并保留 `AGENTS.md` 的 Coding Rules 指针节。
## Step 5 — git 跟踪(访谈答是才做) 1. `git init`。
2. `pkgs/.gitignore` 屏蔽全部,只放行 `.gitignore` 与 `README.md`;`pkgs/README.md` 维护参考仓名单。
1. `git init` 3. 对已有源码仓确认是否登记为 submodule;确认后用 `git submodule add <url> <dir>` 登记。拿不到远端 URL 的记入 `docs/TODO.md`,不强行处理。
2. `pkgs/.gitignore`:屏蔽全部,只放行 `.gitignore` 与 `README.md`;`pkgs/README.md` 维护参考仓名单
3. 已存在的源码仓用 `git submodule add <url> <dir>` 登记;拿不到远端 URL 的仓记入 `docs/TODO.md`,不强行处理。`pkgs/README.md` 只维护参考仓名单
## Done when ## Done when
- [ ] 写入已有文件前得到过用户确认;全新空目录且用户明确说过不必再问时可直接执行。任何已有内容都没有被覆盖 - [ ] 只检查模式没有创建工作区资源、启动 Agent、登录或发起模型推理
- [ ] 每条规则只存在于一个文件,`AGENTS.md` 与 `CODINGRULES.md` 无重复 - [ ] 报告区分宿主可读/文件存在、agent 定义/运行实例、模型列出/调用验证
- [ ] `AGENTS.md` 里的指针都带触发条件和动作,不是裸文件名 - [ ] 写入已有文件前得到确认;未知章节和无关用户内容保留,重复补全无重复章节
- [ ] commit 尾注规则在 `AGENTS.md`,不在 `CODINGRULES.md` - [ ] 每条规则有唯一来源;`AGENTS.md` 与 `CODINGRULES.md` 无重复,commit 尾注规则只在前者
- [ ] `.claude/skills` Junction 解析成功;`code-workspace` 是合法 JSON - [ ] skill、文档与策略指针带触发条件和动作,且目标实际可读;Herdr 执行前会重新核验环境
- [ ] 生成的文件不含 `<占位符>`、模板注释或未替换的示例行 - [ ] skills 连接解析成功,CLAUDE 桥有效,code-workspace 是合法 JSON
- [ ] `AGENTS.md` 不超过 200 行 - [ ] 无占位符、模板注释、未替换示例;`AGENTS.md` 不超过 200 行
- [ ] 输出做字段筛选及密钥模式屏蔽,说明未验证能力与未完成事项
+23 -33
View File
@@ -1,52 +1,42 @@
# <工作区名> Workspace Guide # {{name}} Workspace Guide
这个工作区是 <谁> 进行 <项目> 工作时使用的个人执行目录。 这个工作区是 {{owner}} 进行 {{purpose}} 工作时使用的个人执行目录。
## Quick Context ## Quick Context
<!-- 访谈填写:模块归属、维护的上游库、需要防止误判的事项。示例: --> {{context}}
- <用户> 主要负责 <模块> 相关内容。
- 任务模块不明确时先确认或结合最近上下文判断,不要直接假设。
- <上游库> 也是 <用户> 维护的库。问题疑似来自这些库时,先说明判断并询问是否去上游排查,不在业务仓库堆绕行补丁。
## Communication ## Communication
- 默认中文;路径、命令、代码标识符、技术术语保留原文。 - 默认中文;路径、命令、代码标识符、技术术语保留原文。
- 描述工作时直接说动作和结果:做了什么、改了哪个文件、行为变化是什么;每个说法都能对应到具体的文件、命令或数据。 - 描述实际动作、文件与行为变化,结论先行;未验证的判断明确标注。
- 结论先行;有取舍、风险或需要证据链时再补原因。 - 如无特殊说明,“文档”指工作区文档(`docs/` 与 `AGENTS.md`)。
- 如无特殊说明或指定,"文档"指工作区文档(`docs/` 与 `AGENTS.md`)。
## Workspace Docs ## Workspace Docs
<!-- 访谈填写:docs/ 目录地图。示例: --> - 处理需求、方案、review 或恢复上下文时,先读 `docs/README.md`,再按目录地图查找材料。
- 处理需求、方案、review 或恢复上下文时,优先查看 `docs/README.md`、`docs/prd/`、`docs/plans/`、`docs/guides/`、`docs/sop/`。 {{docs}}
- 目录边界不用卡得太死,重点是快速找到信息并继续推进。
## Shell on Windows ## Shell
- 临时文件放系统临时目录下的任务独立子目录,不在仓库里散落。 - 临时文件放系统临时目录下的任务独立子目录,不在仓库里散落。
- 路径含空格或中文时加引号。 - 路径含空格或中文时加引号。
- Git Bash/MSYS2 下抑制输出用 `/dev/null`;`> nul` 会创建名为 `nul` 的真实文件,发现即删。 - Git Bash/MSYS2 下抑制输出用 `/dev/null`;`> nul` 会创建名为 `nul` 的真实文件,发现即删。
## Commits ## Commits
- commit message 不加 `Co-Authored-By` 等 AI 署名 trailer,不加会话链接(如 claude.ai/code 的 session URL)。 - commit message 不加 `Co-Authored-By` 等 AI 署名 trailer,不加会话链接。
## Design Check ## Design Check
- 实现或 review 中发现修复在叠加条件、状态同步、调用方绕行或特殊分支(patch-on-patch)时,回到模块边界、数据流、调用关系重新看问题,说明更干净的方向。 - 实现或 review 中发现修复在叠加条件、状态同步、调用方绕行或特殊分支时,重新检查模块边界、数据流和调用关系,说明更干净的方向。
## Code Review ## Code Review
- findings 按 P0 到 Px 排序,P0.1 式细分可用。 - findings 按 P0 到 Px 排序,可用 P0.1 式细分。
- 定级前 double-check;缺复现证据的低概率问题降级并标注不确定性。 - 定级前核验;缺复现证据的低概率问题降级并标注不确定性。
- 根据 diff、代码、测试和工作区文档反推修改意图或对应 PRD;推测标注为推测。 - 根据 diff、代码、测试和工作区文档核对修改意图;推测标注为推测。
- 提到路径、函数名时顺带说明这块是做什么的。 - 提到路径、函数名时,说明它负责什么。
- review 结论输出前用 `$think-twice` 做自包含、证据和表达检查。
## Output Quality ## Output Quality
- 交付需要独立成立的文本(handoff、PR/issue 正文、文档)前,用 `$think-twice`。 - 交付写清结论、范围、实际验证和未解决事项,使接收者只靠正文及可访问的文件就能继续工作。
{{quality}}
## Coding Rules
<!-- 仅在生成 CODINGRULES.md 时保留本节,否则整节删除 -->
- 实现、修复或 review 代码前,先完整读取 @CODINGRULES.md 并遵循;宿主已自动加载其内容时不必重复读取。纯文档改动不适用。
{{collaboration}}
{{coding}}
{{models}}
## Structure ## Structure
<!-- 访谈填写:目录结构。示例: --> {{structure}}
- `<repo>/`: 源码仓库
- `pkgs/`: 参考库,仅作补充上下文
- `docs/`: 工作区文档、PRD、计划、SOP 与临时记录
- `scripts/`: 工作区脚本
@@ -0,0 +1,14 @@
## Herdr Collaboration
- 本工作区采用 Herdr 管理并行 worker。操作前检查 `HERDR_ENV=1`,读取宿主可访问的 `$herdr`;宿主未列出时,在 Herdr 内运行 `herdr --skill` 并读取返回指令。核验 CLI 与当前会话,以当前 skill 和 CLI 帮助为准。不可用时串行推进或请用户进入 Herdr,不从外部操控会话或绕用其他后台方式。
- 主控负责拆分、共同接口、任务负责人、跟踪、评审、集成、验收与清理;worker 只完成明确授权的任务。主控自身的子任务可以串行委派。
- 只读调研和评审不额外创建 worktree。并行修改源码的 worker 通过 Herdr 的 worktree 功能管理独立工作树与 workspace;建在实际源码仓,不在外层资料目录建树。
- 每项任务有唯一负责人;同一文件同时只有一个写入负责人。共享契约、依赖、锁文件和公共配置由主控指定修改者。共同接口未定时先设计、核验,不为填满并发提前实现。
- 发现上游错误或需要跨范围修改时,回报成因与所需改动,不在调用方吞错或堆兼容分支。
- 任务书写明目标、仓库/分支/基线提交、共同接口、允许修改的文件、必读材料、依赖、验收命令及失败情形、停下报告的条件、提交授权和交付接收方。材料必须在 worker 的 cwd 或 worktree 中可读。
- 后台创建保持用户焦点;使用返回的 workspace/pane ID 和唯一 Agent 名称。启动后核验身份、模型、cwd、分支、必需 skill 的可访问性和可接收任务状态。只读任务也要明确使用的代码基线。
- 每个任务独立使用测试数据、临时目录和端口;共享依赖明确使用约束。worktree 不隔离凭据或外部文件,不是安全沙箱。
- 双向消息以 `[主控]` 或 `[任务名]` 开头。完成或阻塞时按本节开头的方式获取并读取 Herdr 指令,通过 Herdr 向实际接收方主动回传一次:完成情况、提交(或未提交原因/不适用)、改动、实际检查与结果、限制、需要主控的动作。只在自己的终端打印不算交付。
- 回传不等待主控当前任务结束;发送失败时保留消息并报告,不连续重发或操纵对方按键。`idle` / `done` 只是运行状态,不表示验收通过。
- worker 只在授权分支提交;主控按范围检查、独立评审、依赖集成、集成验证、验收的顺序推进。push、安装、发布另按项目授权执行。
- 检查命令取自项目指令和实际配置;局部/全量测试、类型检查、真实模型、浏览器和宿主验证分别报告,不能相互替代。测试失败、实现不完整或关键条件未验证时保持未完成或阻塞,不用 skip 消除问题。
- 关闭前确认交付已回传、提交已保全、未提交文件已处理、剩余事项及下一步已登记。只清理本轮创建的资源;移除共享依赖链接与目录前先核验,不关闭他人的终端。恢复时重新查询状态和 ID。
@@ -0,0 +1,87 @@
# Environment Checks
## 检查入口与结果边界
命令中的 `"${SKILL_DIR}"` 是当前已加载 `SKILL.md` 所在目录的绝对路径,由 Agent 根据实际位置设置;不要假定 cwd 在源码仓或安装目录。先由宿主读取 Herdr 环境与可访问的 skill,再使用脚本;`--skill` 只是候选路径,不验证宿主发现顺序或 worker 访问权限。
```bash
python -B "${SKILL_DIR}/scripts/inspect_environment.py" --target /existing/workspace
python -B "${SKILL_DIR}/scripts/inspect_environment.py" --target /existing/workspace --skill herdr=/accessible/herdr/SKILL.md
```
默认不保存文件。需要留档时,将 stdout 重定向到系统临时目录下本轮专用目录中的新文件;不要写进 skill 安装目录或直接覆盖工作区文件。所有脚本使用 Python 3.9+ 标准库,不安装依赖。Windows 上使用可用的 `python` / `py -3`,参数路径需加引号。
JSON 顶层字段:`schema_version`、`checked_at`、`target`、`herdr`、`skills`、`tools`、`workspace`、`limits`。
- 工具 `found` 只证明 PATH 可定位;`version.status=ok` 证明版本格式识别成功,不证明认证可用。
- Herdr `candidate` 只证明环境变量与 CLI help 通过。Agent 按 Herdr skill 查询当前会话后,才能另行报告会话可用。
- skill `readable_candidate` 只证明所传文件可读。Agent 实际读取、确认身份和适用范围后,才能将它写入生成决策。
- 模型 `listed` / `evidence=cli_listed` 不等于调用成功;`call_verified` 始终为 false。
- `not_checked` 不是空列表或不支持;`unrecognized_output` 不是“没有模型”。`timeout`、`command_failed`、`execution_failed` 和 `output_too_large` 分别保留。
单项失败仍输出其余检查,扫描进程返回 0;输入或脚本执行错误返回 2。成功退出不表示每个工具可用。每条命令默认超时 10 秒,可用 `--timeout` 调整到最多 60 秒。失败命令的输出丢弃;成功 help 的 stderr 也参与解析(OpenCode 将帮助写到该流),但原始文本不入报告,仅提取允许字段。CLI 子进程不是沙箱,工具自身可能写缓存或执行启动逻辑,不承诺系统零副作用。
## CLI 与模型、agent 的检查
默认适配 `pi`、`claude`、`opencode`、`grok` 的版本查询;后三者读取 help 并提取少量参数名。`codex`、`gemini` 仅查 PATH。扫描器不根据用户输入拼接 shell 命令。
| 工具 | 模型检查 | Agent 定义检查 |
|---|---|---|
| Pi | 显式 `--pi-models` 使用受限 `--list-models` | 用当前宿主扩展提供的只读 catalog/list;不假设统一内置接口 |
| Claude Code | 当前 help 未证明安全的模型枚举接口,默认未查 | `claude agents` 的含义依版本变化;当前帮助将其描述为后台实例管理,不能当定义清单 |
| OpenCode | 显式 `--opencode-models` 查询 `opencode models --pure`,不传 `--refresh` | 显式 `--opencode-agents` 查询 `opencode agent list --pure`,只提取名称和类型 |
| Grok Build | 显式 `--grok-models` 查询 `grok models` | `--agent` 是选择参数,不是列表接口;不读取配置全文猜定义 |
按用户的列表查询要求显式开启对应选项;默认不查列表。Grok/OpenCode 查询可能访问目录服务或缓存,`--pure` 只排除 OpenCode 外部插件,不保证离线。命令不发推理 prompt、不登录、不启动 worker。已验证输出格式:Grok 1.0.13、OpenCode 1.15.13;格式无法识别时报告未识别,不猜列表。查询报错或超时就报告,不自动改成带认证、刷新或启动功能的命令。
```bash
python -B "${SKILL_DIR}/scripts/inspect_environment.py" --target /existing/workspace --grok-models --opencode-models --opencode-agents --timeout 30
```
新 CLI 或其他列表接口,先检查本机帮助、文档或源码。涉及额外动作时先确认;没有可用接口就标记未查。
Pi 的受限查询:
```bash
python -B "${SKILL_DIR}/scripts/inspect_environment.py" --target /existing/workspace --pi-models
```
内部使用 `--offline --no-extensions --no-skills --no-prompt-templates --no-themes --no-context-files --no-approve --no-session --list-models`,并设置 `PI_OFFLINE=1`、`PI_TELEMETRY=0`。这不是当前会话模型清单:扩展 provider 与项目资源被排除,仍会进入 Pi 运行时,可能涉及本地缓存或 provider 自定义行为。旧版本不支持参数时报告失败,不降级成裸命令。Pi 当前源码的 `dist/main.js` 与 `dist/cli/list-models.js` 可用于核验实现;安装位置随机器变化,不硬编码。
宿主能提供更完整的模型/agent 列表时,报告来源与查询范围,不混入脚本结果冒充统一扫描完成。仅输出 agent 名称、来源、启用状态(接口提供时)和配置模型;不复制 prompt、凭据、会话内容。运行实例的 pane ID 属于本轮,不写进长期规则。
## 生成决策与 stdout
扫描结果不能直接输入渲染器。Agent 结合检查和用户授权,准备以下 JSON 到本轮临时目录:
```json
{
"schema_version": 1,
"name": "Example",
"owner": "维护者",
"purpose": "维护应用",
"context": ["app/ 是实际源码仓。"],
"docs": ["docs/sop/ 保存可重复执行的步骤。"],
"structure": ["app/:源码仓", "docs/:工作区文档", "pkgs/:只读参考库", "scripts/:工作区脚本"],
"skills": {"herdr": true, "think-twice": true, "why-here": false},
"herdr": true,
"coding_rules": false,
"model_policy": null
}
```
`skills` 中的 true 表示 Agent 已实际读取并确认可访问,不是照抄文件存在性。`herdr` 表示用户选择采用该协作方式,独立于当前是否在 Herdr 中;启用时必须有可读 Herdr skill。`coding_rules` 表示候选需要该指针,同时确认目标文件会存在。
`model_policy` 为 null 或省略时不生成选型章节。非空时是工作区相对 `.md` 路径,例如 `docs/model-policy.md`;不接受绝对路径或 `..`。渲染器不验证目标存在,Agent 在交付前负责核验。策略内容必须来自用户,不由扫描推断额度、偏好或默认 provider。
```bash
python -B "${SKILL_DIR}/scripts/render_agents.py" --decisions /task-temp/decisions.json > /task-temp/AGENTS.candidate.md
```
stdout 是 UTF-8 Markdown,错误走 stderr。输出前校验字段、布尔值、占位符和 200 行上限。新工作区验证后保存;已有工作区对照候选逐节修改,保留未知章节,不用此命令覆盖原文件。无 Python 时,Agent 读取基础模板与可选章节,按同一决策填充并人工执行这些检查。
## 脱敏
`scripts/output_safety.py` 统一处理脚本输出,移除 ANSI 转义后将 `sk-[0-9a-zA-Z-]+` 匹配串替换为 `[REDACTED]`。JSON 值及键均处理,键发生碰撞时拒绝输出,避免静默丢字段。参数解析错误也经过这一边界;文件/JSON 错误只报告类型,不回显内容。
这是保守的最后一道检查:短字符串也可能误伤,非 `sk-` 凭据、分段或编码后的密钥不在保证范围内。因此主要防护是字段白名单和丢弃原始错误,不收集认证文件、环境变量全集或完整配置。Agent 自己写的报告同样执行字段筛选与匹配检查。若合法路径或模型 ID 被屏蔽,提示用户核验,不把 `[REDACTED]` 当执行参数。
@@ -0,0 +1,218 @@
"""Inspect installed tools without prompts, authentication or workspace writes."""
import os
from pathlib import Path
import re
import shutil
import subprocess
import sys
from datetime import datetime, timezone
sys.dont_write_bytecode = True
from output_safety import Parser, emit, entrypoint, redact
TOOLS = ("pi", "claude", "opencode", "grok", "codex", "gemini")
SKILLS = ("herdr", "think-twice", "why-here")
VERSION = re.compile(r"(?<![\w.-])\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?(?![\w.-])")
def run_query(executable, arguments, timeout, cwd, env=None):
"""Only callers' fixed argument lists reach this boundary; stdin stays closed."""
try:
result = subprocess.run(
[executable, *arguments], cwd=cwd, env=env,
stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
stderr=subprocess.PIPE if arguments == ["--help"] else subprocess.DEVNULL, timeout=timeout, check=False,
encoding="utf-8", errors="replace",
)
except subprocess.TimeoutExpired:
return {"status": "timeout"}, ""
except OSError:
return {"status": "execution_failed"}, ""
if result.returncode:
return {"status": "command_failed", "exit_code": result.returncode}, ""
text = result.stdout + (result.stderr or "") if arguments == ["--help"] else result.stdout
if len(text) > 256_000:
return {"status": "output_too_large"}, ""
return {"status": "ok"}, redact(text)
def inspect_cli(name, cwd, timeout, *, which=shutil.which, runner=run_query):
executable = which(name)
row = {"name": name, "status": "found" if executable else "missing",
"models": {"status": "not_checked", "items": []},
"agent_definitions": {"status": "not_checked", "items": []}}
if not executable:
return row
row["path"] = executable
# Unknown CLI adapters are discovery-only. pi help can load extensions.
if name not in ("pi", "claude", "opencode", "grok"):
row["version"] = {"status": "not_checked"}
return row
env = {**os.environ, "PI_OFFLINE": "1", "PI_TELEMETRY": "0", "NO_COLOR": "1"}
check, text = runner(executable, ["--version"], timeout, cwd, env)
match = VERSION.search(text)
row["version"] = {**check}
if check["status"] == "ok":
row["version"] = {"status": "ok", "value": match.group()} if match else {"status": "unrecognized_output"}
if name != "pi":
check, help_text = runner(executable, ["--help"], timeout, cwd, env)
row["help"] = check if check["status"] != "ok" or help_text.strip() else {"status": "unrecognized_output"}
row["options"] = [flag for flag in ("--model", "--agent", "--effort", "--reasoning-effort")
if re.search(r"(?<![\w-])" + re.escape(flag) + r"(?![\w-])", help_text)]
if name in ("grok", "opencode") and re.search(r"(?m)^\s*(?:opencode\s+)?models\b", help_text):
row["models"]["query_hint"] = name + " models"
if name == "claude":
row["agent_definitions"]["note"] = "claude agents may list background instances, not definitions; inspect the current interface."
else:
row["models"]["query_hint"] = "Explicit --pi-models scan (extensions disabled; incomplete catalog)."
row["agent_definitions"]["note"] = "Use the host extension's read-only catalog; pi has no universal built-in agent registry."
return row
def parse_pi_models(text):
lines = [line.strip() for line in text.splitlines() if line.strip()]
header = "provider model context max-out thinking images".split()
if not lines or lines[0].split() != header:
return {"status": "unrecognized_output", "items": []}
items = []
for line in lines[1:]:
fields = line.split()
if (len(fields) != 6 or fields[4] not in ("yes", "no")
or fields[5] not in ("yes", "no")
or not all(re.fullmatch(r"[0-9]+(?:\.[0-9]+)?[KM]?", v) for v in fields[2:4])):
return {"status": "unrecognized_output", "items": []}
items.append({"provider": fields[0], "id": fields[1], "evidence": "cli_listed",
"call_verified": False})
return {"status": "listed", "items": items}
def parse_grok_models(text):
lines = [line.strip() for line in text.splitlines() if line.strip()]
if "Available models:" not in lines:
return {"status": "unrecognized_output", "items": []}
items = []
for line in lines[lines.index("Available models:") + 1:]:
match = re.fullmatch(r"[*-]\s+([A-Za-z0-9][A-Za-z0-9_.:/-]*)(?:\s+\(default\))?", line)
if not match:
return {"status": "unrecognized_output", "items": []}
items.append({"id": match[1], "evidence": "cli_listed", "call_verified": False})
return {"status": "listed", "items": items}
def parse_opencode_models(text):
items = []
for line in text.splitlines():
if not line.strip():
continue
match = re.fullmatch(r"([A-Za-z0-9_.:-]+)/([A-Za-z0-9_.:/-]+)", line.strip())
if not match:
return {"status": "unrecognized_output", "items": []}
items.append({"provider": match[1], "id": match[2], "evidence": "cli_listed", "call_verified": False})
return {"status": "listed", "items": items} if items else {"status": "unrecognized_output", "items": []}
def parse_opencode_agents(text):
# This command also prints large permission objects. Only definition headings leave here.
headings = re.findall(r"(?m)^([A-Za-z0-9_-]+)\s+\((primary|subagent|all)\)\s*$", text)
return {"status": "listed" if headings else "unrecognized_output",
"items": [{"name": name, "mode": mode, "evidence": "cli_listed"} for name, mode in headings]}
def query_catalog(row, field, arguments, parser, scope, cwd, timeout):
if row["status"] != "found":
row[field] = {"status": "missing_cli", "items": [], "scope": scope}
return
check, text = run_query(row["path"], arguments, timeout, cwd,
{**os.environ, "PI_OFFLINE": "1", "PI_TELEMETRY": "0", "NO_COLOR": "1"})
row[field] = parser(text) if check["status"] == "ok" else {**check, "items": []}
row[field]["scope"] = scope
def inspect_herdr(cwd, timeout, *, environ=None, which=shutil.which, runner=run_query):
environ = os.environ if environ is None else environ
inside = environ.get("HERDR_ENV") == "1"
executable = which("herdr")
row = {"in_session": inside, "cli_found": bool(executable), "status": "not_in_herdr"}
if not inside:
return row # No Herdr CLI calls outside its managed environment.
if not executable:
row["status"] = "missing_cli"
return row
check, _ = runner(executable, ["--help"], timeout, cwd)
row["help"] = check
row["status"] = "candidate" if check["status"] == "ok" else "unavailable"
row["session_verified"] = False
return row
def inspect_skills(paths):
result = {name: {"status": "not_provided"} for name in SKILLS}
for value in paths:
name, separator, filename = value.partition("=")
if not separator or name not in SKILLS or result[name]["status"] != "not_provided":
raise ValueError("expected one NAME=PATH per supported skill")
try:
path = Path(filename).expanduser().resolve()
with path.open(encoding="utf-8-sig") as source:
text = source.read(256_001)
status = "readable_candidate" if text.strip() and len(text) <= 256_000 else "invalid_candidate"
except (OSError, UnicodeError, RuntimeError):
result[name] = {"status": "unreadable", "host_access_verified": False}
continue
result[name] = {"status": status, "path": str(path), "host_access_verified": False}
return result
def inspect(args):
cwd = str(Path(args.target).expanduser().resolve())
if not Path(cwd).is_dir():
raise ValueError("target must be an existing directory; use its parent for a new workspace")
# Deliberately preserve the requested inspection order.
herdr = inspect_herdr(cwd, args.timeout)
skills = inspect_skills(args.skill)
tools = [inspect_cli(name, cwd, args.timeout) for name in TOOLS]
by_name = {row["name"]: row for row in tools}
if args.pi_models:
flags = ["--offline", "--no-extensions", "--no-skills", "--no-prompt-templates",
"--no-themes", "--no-context-files", "--no-approve", "--no-session", "--list-models"]
query_catalog(by_name["pi"], "models", flags, parse_pi_models,
"User/built-in catalog only; extensions and project resources disabled; not a live-session catalog.",
cwd, args.timeout)
if args.grok_models:
query_catalog(by_name["grok"], "models", ["models"], parse_grok_models,
"Grok CLI catalog; may access catalog services/cache; no inference or quota verification.", cwd, args.timeout)
if args.opencode_models:
query_catalog(by_name["opencode"], "models", ["models", "--pure"], parse_opencode_models,
"OpenCode catalog with external plugins disabled; no --refresh; may access services/cache; no inference.", cwd, args.timeout)
if args.opencode_agents:
query_catalog(by_name["opencode"], "agent_definitions", ["agent", "list", "--pure"], parse_opencode_agents,
"OpenCode definitions with external plugins disabled; names/modes only, not running instances or launch verification.", cwd, args.timeout)
return {"schema_version": 1, "checked_at": datetime.now(timezone.utc).isoformat(),
"target": cwd, "herdr": herdr, "skills": skills, "tools": tools,
"workspace": {name: os.path.lexists(Path(cwd) / name) for name in
("AGENTS.md", "CLAUDE.md", "CODINGRULES.md", ".agents/skills", ".claude/skills")},
"limits": ["No model inference or credential validation performed.",
"External CLI help/version may have tool-specific side effects; this is not a sandbox.",
"Failed command output is discarded; successful help stderr is parsed, never forwarded raw.",
"No skill discovery precedence is inferred from filesystem presence."]}
def main():
parser = Parser(description=__doc__)
parser.add_argument("--target", default=".", help="Existing directory to inspect (new workspace: inspect parent).")
parser.add_argument("--skill", action="append", default=[], metavar="NAME=PATH",
help="Host-supplied skill candidate, not proof of host accessibility.")
parser.add_argument("--timeout", type=float, default=10)
parser.add_argument("--pi-models", action="store_true", help="Opt into a restricted pi catalog query; not inference.")
parser.add_argument("--grok-models", action="store_true", help="Opt into grok models; may access catalog services/cache, no inference.")
parser.add_argument("--opencode-models", action="store_true", help="Opt into opencode models --pure; may access services/cache, no refresh/inference.")
parser.add_argument("--opencode-agents", action="store_true", help="Opt into opencode agent list --pure; definitions only, no worker launch.")
args = parser.parse_args()
if not 0 < args.timeout <= 60:
parser.error("timeout must be in (0, 60]")
emit(inspect(args), as_json=True)
return 0
if __name__ == "__main__":
sys.exit(entrypoint(main))
@@ -0,0 +1,64 @@
"""Shared output boundary. Never forward raw command errors or credentials."""
import argparse
import json
import re
import sys
SECRET = re.compile(r"sk-[0-9a-zA-Z-]+")
ANSI = re.compile(r"\x1b\[[0-?]*[ -/]*[@-~]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)")
def redact(text):
# Remove terminal formatting first so escape codes cannot split a key.
return SECRET.sub("[REDACTED]", ANSI.sub("", str(text)))
def sanitize(value):
if isinstance(value, str):
return redact(value)
if isinstance(value, list):
return [sanitize(item) for item in value]
if isinstance(value, dict):
result = {}
for key, item in value.items():
cleaned = redact(key)
if cleaned in result:
raise ValueError("redaction produced duplicate object keys")
result[cleaned] = sanitize(item)
return result
return value
def emit(value, *, as_json=False, stream=None):
stream = stream or sys.stdout
if hasattr(stream, "reconfigure"):
stream.reconfigure(encoding="utf-8")
text = json.dumps(sanitize(value), ensure_ascii=False, indent=2) if as_json else redact(value)
stream.write(text + "\n")
class Parser(argparse.ArgumentParser):
def _print_message(self, message, file=None):
if message:
emit(message.rstrip("\n"), stream=file or sys.stderr)
def read_json(path):
# Input sizes are bounded; errors are reported by type, not file contents.
with open(path, encoding="utf-8-sig") as source:
text = source.read(1_000_001)
if len(text) > 1_000_000:
raise ValueError("JSON input exceeds size limit")
return json.loads(text)
def entrypoint(main):
try:
return main()
except BrokenPipeError:
return 0
except Exception as error:
# Last-resort CLI boundary: no exception message/path may bypass redaction.
# SystemExit and KeyboardInterrupt intentionally retain normal CLI behavior.
emit("Input or execution error: " + type(error).__name__, stream=sys.stderr)
return 2
@@ -0,0 +1,85 @@
"""Render a candidate AGENTS.md from explicit, host-confirmed decisions."""
from pathlib import Path
import re
import sys
sys.dont_write_bytecode = True
from output_safety import Parser, emit, entrypoint, read_json, redact
ASSETS = Path(__file__).resolve().parent.parent / "assets"
FIELDS = {"schema_version", "name", "owner", "purpose", "context", "docs", "structure",
"skills", "herdr", "coding_rules", "model_policy"}
def line(value):
if not isinstance(value, str) or not value.strip() or "\n" in value or "\r" in value:
raise ValueError("expected a nonempty single line")
return value.strip()
def bullets(value):
if not isinstance(value, list) or not value:
raise ValueError("expected a nonempty list")
return "\n".join("- " + line(item) for item in value)
def validate_policy(value):
value = line(value)
# Only portable workspace-relative paths, never user-specific absolute paths.
if (value.startswith(("/", "\\")) or any(c in value for c in ("\\", ":", "`", "<", ">"))
or any(part in ("", ".", "..") for part in value.split("/"))
or not value.endswith(".md")):
raise ValueError("model_policy must be a workspace-relative Markdown path")
return value
def render(config):
if not isinstance(config, dict) or set(config) - FIELDS or config.get("schema_version") != 1:
raise ValueError("unsupported decisions object")
skills = config.get("skills", {})
if not isinstance(skills, dict) or set(skills) - {"herdr", "think-twice", "why-here"}:
raise ValueError("unsupported skill decisions")
if any(type(value) is not bool for value in skills.values()):
raise ValueError("skill decisions must be boolean")
for name in ("herdr", "coding_rules"):
if type(config.get(name, False)) is not bool:
raise ValueError("section decisions must be boolean")
if config.get("herdr") and not skills.get("herdr"):
raise ValueError("Herdr rules require a host-confirmed readable Herdr skill")
slots = {name: line(config[name]) for name in ("name", "owner", "purpose")}
slots.update({name: bullets(config[name]) for name in ("context", "docs", "structure")})
quality = []
if skills.get("think-twice"):
quality.append("- 正式任务书、review 结论及独立交付文本(handoff、PR/issue 正文、文档)输出前,读取并使用 `$think-twice` 检查自包含、事实和表达。")
if skills.get("why-here"):
quality.append("- 工作涉及废弃尝试、纠正误解、完成迁移或替换实现时,交付前读取并使用 `$why-here` 检查遗留内容;检查本身不授权清理代码。")
slots["quality"] = "\n".join(quality)
slots["collaboration"] = (ASSETS / "sections/herdr.md").read_text(encoding="utf-8") if config.get("herdr") else ""
slots["coding"] = ("## Coding Rules\n- 实现、修复或 review 代码前,完整读取 `CODINGRULES.md` 并遵循;宿主已加载时不重复读取。纯文档改动不适用。\n"
if config.get("coding_rules") else "")
slots["models"] = ""
if config.get("model_policy") is not None:
policy = validate_policy(config["model_policy"])
slots["models"] = ("## Agent and Model Selection\n"
f"- 分派任务前读取 `{policy}`,取得用户的工具、模型和额度偏好;启动前核验准确的 CLI、provider、model 与 thinking 参数。\n"
"- 文件表达偏好,不证明当前可用。模型列表不等于调用成功;策略缺失或与实际能力冲突时报告缺口并确认选择,不自行推断额度或替换模型。\n")
text = (ASSETS / "AGENTS.template.md").read_text(encoding="utf-8")
text = re.sub(r"\{\{([a-z_]+)\}\}", lambda match: slots[match[1]], text)
text = re.sub(r"\n{3,}", "\n\n", text).strip()
if "{{" in text or "}}" in text or "<!--" in text or re.search(r"<[^>\n]+>", text):
raise ValueError("unresolved template marker")
if len(text.splitlines()) > 200:
raise ValueError("AGENTS.md exceeds 200 lines")
return redact(text)
def main():
parser = Parser(description=__doc__)
parser.add_argument("--decisions", required=True, help="JSON decisions prepared after host checks and user direction.")
args = parser.parse_args()
emit(render(read_json(args.decisions)))
return 0
if __name__ == "__main__":
sys.exit(entrypoint(main))