Compare commits

..
15 Commits
Author SHA1 Message Date
agent-skills-bot 92361eed22 同步 latest skill releases 2026-10-12 07:57:06 +00:00
agent-skills-bot c69d7b9ce0 同步 latest skill releases 2026-09-07 17:42:50 +00:00
agent-skills-bot 4d9fa1134c 同步 latest skill releases 2026-09-07 13:38:29 +00:00
agent-skills-bot 3da100f300 同步 latest skill releases 2026-09-04 07:46:37 +00:00
im0o c7e0a421f9 接入 init-workspace Release 同步 2026-09-04 15:39:59 +08:00
im0o 3c2c530c65 接入 why-here Release 同步 2026-08-31 15:46:13 +08:00
agent-skills-bot 7a909a4cd6 同步 latest skill releases 2026-08-21 13:01:40 +00:00
im0o 9946233ffb 接入 disk-master Release 同步
将 disk-master 加入同步白名单,首次运行不预填版本记录。
2026-08-21 21:01:14 +08:00
agent-skills-bot 030cda3454 同步 latest skill releases 2026-08-21 12:56:01 +00:00
im0o e73636caea 接入两个工具 skill 的 Release 同步
将 everything-file-search 和 baimiao-table-ocr 加入同步白名单,首次运行不预填版本记录。
2026-08-21 20:55:33 +08:00
agent-skills-bot 180fff22d9 同步 latest skill releases 2026-08-21 12:31:13 +00:00
im0o f02872b256 接入 teach-me Release 同步
将 teach-me 加入分发仓同步白名单,首次运行不预填版本记录。
2026-08-21 20:30:49 +08:00
im0o cf34e7358d Clarify repository access check 2026-08-21 18:21:08 +08:00
im0o 2694b2c081 Rewrite README for skill users 2026-08-21 18:09:18 +08:00
agent-skills-bot b1d8dd0439 同步 latest skill releases 2026-08-21 09:49:35 +00:00
17 changed files with 1275 additions and 83 deletions
+14
View File
@@ -26,6 +26,20 @@
"skills": [
"./skills/baimiao-table-ocr"
]
},
{
"name": "delivery-review",
"description": "Review a change before delivery: keep what the finished result needs, drop what only the making of it needed.",
"skills": [
"./skills/why-here"
]
},
{
"name": "workspace-setup",
"description": "Scaffold a personal execution workspace: directories, code-workspace file, AGENTS.md and optional CODINGRULES.md.",
"skills": [
"./skills/init-workspace"
]
}
]
}
+30
View File
@@ -4,6 +4,36 @@
"name": "think-twice",
"repository": "agent-skills/think-twice",
"asset": "think-twice.zip"
},
{
"name": "teach-me",
"repository": "agent-skills/teach-me",
"asset": "teach-me.zip"
},
{
"name": "everything-file-search",
"repository": "agent-skills/everything-skill",
"asset": "everything-file-search.zip"
},
{
"name": "baimiao-table-ocr",
"repository": "agent-skills/img2excel-skill",
"asset": "baimiao-table-ocr.zip"
},
{
"name": "disk-master",
"repository": "agent-skills/disk-master-skill",
"asset": "disk-master.zip"
},
{
"name": "why-here",
"repository": "agent-skills/why-here",
"asset": "why-here.zip"
},
{
"name": "init-workspace",
"repository": "agent-skills/init-workspace",
"asset": "init-workspace.zip"
}
]
}
+9 -1
View File
@@ -1 +1,9 @@
{}
{
"baimiao-table-ocr": "v1",
"disk-master": "v1",
"everything-file-search": "v1",
"init-workspace": "v4",
"teach-me": "v2",
"think-twice": "v3.1",
"why-here": "v1"
}
+2
View File
@@ -50,8 +50,10 @@ jobs:
"baimiao-table-ocr",
"disk-master",
"everything-file-search",
"init-workspace",
"teach-me",
"think-twice",
"why-here",
}
output = Path("/tmp/skills-list.txt").read_text(encoding="utf-8")
lines = {line.strip(" │└├─") for line in output.splitlines()}
+88 -36
View File
@@ -1,50 +1,102 @@
# agent-skills
# Agent Skills
从一个入口按用途选择并安装 im0o 的 agent skill。
这是我日常使用的一组 agent skills。
我把反复遇到、又适合交给 agent 稳定处理的任务整理成了独立 skills。每个 skill 只处理一类问题:需要哪个就安装哪个,也可以按任务组合使用。
## 安装
运行:
```bash
npx skills add ssh://gitea/agent-skills/skills.git
npx skills add https://git.im0o.top/agent-skills/skills.git
```
会出现一个按用途分组的多选列表。可以选择整组,也可以只勾选其中几个 skill。仓库是
私有的,`skills add` 使用系统 Git 克隆;本机的 SSH key 能访问该仓库即可,不需要另外
配置安装凭据。
安装程序会让你选择需要的 skills,以及要安装到哪些 agent。获取仓库前,可以先运行下面的命令检查本机是否有访问权限:
## 目录里有什么
```bash
git ls-remote https://git.im0o.top/agent-skills/skills.git HEAD
```
| 组 | skill | 做什么 | 平台 |
|---|---|---|---|
| Writing Clarity | `think-twice` | 重写一段输出,让它脱离上下文也能读懂,每条声明都经过验证,不留只在写作过程中有意义的内容 | 任意 |
| Writing Clarity | `teach-me` | 对接上下文:长任务跑完、隔久回到旧项目、面对陌生代码时,先自己探索,再一轮讲一层 | 任意 |
| Windows Tools | `disk-master` | 只读扫描磁盘占用,解释盘为什么满了,生成可交互 HTML 报告 | Windows |
| Windows Tools | `everything-file-search` | 借 Everything 的 IPC 按文件名和路径极快定位文件 | Windows,需要 Everything 在跑 |
| Image To Data | `baimiao-table-ocr` | 表格截图转 CSV / JSON / Markdown / XLSX | 任意,需要网络 |
命令返回 commit hash 就表示可以继续安装。
`think-twice` 和 `teach-me` 各自完整,单独装任何一个都能用,不存在跨 skill 的文件
依赖。放同一组是因为它们互相交接:`think-twice` 重写一次仍然没打中的时候转给
`teach-me`,`teach-me` 遇到会活得比对话久的内容时交给 `think-twice` 过一遍。两个都
装上,这两条路才通。
以后需要拉取新版本时运行:
`baimiao-table-ocr` 会把图片上传到第三方服务。隐私敏感的图不要用它。
```bash
npx skills update
```
## 这个仓和源码仓的关系
> [!TIP]
> 安装后直接描述任务即可。agent 会在任务匹配时使用对应的 skill;你也可以明确说“用 `think-twice` 重写这份文档”。
`skills/` 下的五个目录是真副本,不是子模块也不是软链接。原因在
`vercel-labs/skills` 的两处代码:`git.ts:241` 的克隆没有 `--recurse-submodules`,
子模块目录克隆出来是空的;`plugin-manifest.ts:89` 会跳过 marketplace.json 里的
远端来源,只认仓内的本地路径。
## Skills
各自的源码仓在同一个 org 下:
### 写作与上下文
| skill | 源码仓 | 仓内路径 |
|---|---|---|
| `think-twice` | `agent-skills/think-twice` | `skill/` |
| `teach-me` | `agent-skills/teach-me` | 仓根 |
| `disk-master` | `agent-skills/disk-master-skill` | `skill/` |
| `everything-file-search` | `agent-skills/everything-skill` | `everything-file-search/` |
| `baimiao-table-ocr` | `agent-skills/img2excel-skill` | `skills/baimiao-table-ocr/` |
#### [`think-twice`](./skills/think-twice/SKILL.md)
改动在源码仓完成,分发仓只承载可安装副本。源码仓发布正式 `v*` tag 并生成 Release
ZIP 后,分发仓按 `.dist-sync/sources.json` 白名单同步 latest Release。当前只自动同步
`think-twice`;其余四个 skill 具备标准 Release 后再逐项加入。白名单来源尚无 latest
Release 时会跳过并保留现有副本。
重写准备交付的文字,让没有参与过当前对话的人也能读懂并采取行动。它会先补齐和核实事实,再从结论开始重写,而不是只润色原稿。
适合文档、交接说明、PR 或 issue 正文,以及任何需要脱离当前对话独立成立的文字。
#### [`teach-me`](./skills/teach-me/SKILL.md)
当你面对陌生代码、隔了很久回到旧项目,或者想弄清 agent 刚完成的长任务时,让 agent 先自行查清事实,再一次讲明白一层内容。
它不会一次塞给你一篇长讲义。讲解会根据你的回复继续,直到你确认已经理解。
`think-twice` 负责把一份文字写得独立、准确;`teach-me` 负责通过来回交流把一件事真正讲懂。两者可以单独安装,也可以一起使用。
### 交付前审查
#### [`why-here`](./skills/why-here/SKILL.md)
在交付前对改动里每个可疑的部分问同一个问题:它现在为什么还在这里。完成后的结果仍然需要的留下,只因为做的过程走过某条路才存在的删掉,例如回应旧误解的说明、放弃方案留下的分支、迁移完成后仍在的临时字段。
它默认只审查和报告。文字按删掉后读者的理解会不会改变判断;代码、接口和数据结构要先查当前调用方、契约、已有数据和外部约束。公开接口、持久化数据和仍在使用的兼容路径需要你明确授权才会动。
`think-twice` 负责把文字写得独立可读;`why-here` 负责判断哪些内容还属于完成后的结果,范围同时包括文字和代码。
### Windows 工具
#### [`disk-master`](./skills/disk-master/SKILL.md)
只读分析 Windows 磁盘占用,找出最大的目录、文件和常见空间热点,并生成可交互的 HTML 报告。
它只负责扫描、解释和给出建议,不会删除文件、卸载软件或自动清理系统。
#### [`everything-file-search`](./skills/everything-file-search/SKILL.md)
通过 [Everything](https://www.voidtools.com/) 快速按文件名、扩展名或路径定位 Windows 上的文件和目录,适合搜索范围很大或目标位置不明确的情况。
使用前需要运行 Everything 桌面程序。这个 skill 负责查找文件位置,不负责搜索文件内容或理解代码语义。
### 图片转结构化数据
#### [`baimiao-table-ocr`](./skills/baimiao-table-ocr/SKILL.md)
把表格截图或扫描图片转换成 ASCII、Markdown、CSV、JSON 或 XLSX,适合提取账单、清单、报表和课程表等图片里的行列数据。
> [!WARNING]
> 图片会上传到白描的第三方服务进行识别。不要用它处理隐私敏感或不能离开本机的图片。
### 工作区初始化
#### [`init-workspace`](./skills/init-workspace/SKILL.md)
为个人执行工作区搭脚手架:创建目录骨架和 `.claude/skills` Junction,生成 `<project>.code-workspace`、`AGENTS.md` 与可选的 `CODINGRULES.md`。工作区模式是一个目录同时容纳源码仓库、本地文档库和参考库。
它只在你明确要求初始化或重构工作区时触发。已有文件不会被覆盖,改动会先逐节给出提案,确认后才写入。
## 常见用法
你可以直接对 agent 说:
- “用 `think-twice` 过一下这份交接文档,让没看过对话的人也能接手。”
- “我半年没碰这个项目了,先读代码,再用 `teach-me` 给我讲明白它现在怎么工作。”
- “分析一下 `C:` 盘为什么满了,只做只读扫描,并生成 HTML 报告。”
- “帮我找出这台 Windows 电脑上所有叫 `settings.json` 的文件。”
- “把这张表格截图转成 Markdown 表格。”
- “在 `E:\Projects\foo` 用 `init-workspace` 建一个工作区,源码仓是这个地址。”
oo` 用 `init-workspace` 建一个工作区,源码仓是这个地址。”
- “这次改动中途换过方案,提交前用 `why-here` 看看还有没有只属于旧方案的内容。”
+76 -46
View File
@@ -1,63 +1,93 @@
---
name: disk-master
description: Diagnose Windows disk usage with disk-master.exe, explain what is filling the drive, and generate an interactive SpaceSniffer-style HTML report.
description: >
Diagnose Windows disk usage with the bundled read-only disk-master.exe. Use
when a user asks why C: or another Windows drive is full, wants the largest
files or folders, needs AppData or installed app/game space analysis, or asks
for a SpaceSniffer-style HTML disk report. The scan stays local and does not
delete, uninstall, move, or clean anything. Do not use for non-Windows or
network-drive analysis, or as an automatic cleanup tool.
---
# Disk Master
Use this skill when the user asks why a Windows drive is full, wants to find large files or folders, wants to inspect C: drive software/AppData usage, or wants an interactive disk report.
Disk Master collects Windows drive data as JSON, explains what is consuming
space, and can turn the same JSON into an interactive HTML report.
## Workflow
## When to use
1. If the user does not specify a drive, first inspect available local drives, then ask which drive to analyze. Also ask whether they approve a UAC prompt for faster NTFS/MFT scanning. Explain that the scan is local and read-only.
2. Run `disk-master.exe scan --drive C: --scan quick --out result.json` for a fast estimate, or `disk-master.exe scan --drive C: --scan full --out result.json` when the user asks for a complete scan.
3. If the user explicitly agrees to a UAC prompt for privileged NTFS/MFT scanning, add `--elevate --mode mft` and keep `--out <path>`.
4. After scanning, use the CLI status output to report the JSON path and a short human-readable summary. Do not run an extra file-existence check unless the command fails or the path is missing.
5. Read `result.json` and explain the disk state: largest directories, largest files, file types, user folders, AppData hotspots, installed apps/games, and advice items.
6. Generate an interactive static HTML report from `template.html` by replacing `%%DATA%%` with the JSON text.
7. Present the summary first, then link the JSON path and HTML report path. Make clear that all suggestions are review-only and no cleanup was performed.
Use it for:
## Clarifying Questions
- Diagnosing a full Windows drive or low free space.
- Finding the largest directories, files, and file types.
- Inspecting AppData, user folders, installed applications, or games.
- Producing a local SpaceSniffer-style disk report.
- Getting an application inventory or a quick text-only disk summary.
When the request is ambiguous, ask concise interactive questions before scanning:
Use another approach for non-Windows systems, UNC/network drives, or requests to
automatically delete, uninstall, move, or clean data. Disk Master may identify
candidates for review; it does not prove that a file is safe to remove.
- Drive: choose from detected local drives, for example `C:` or `D:`.
- Scan depth: quick estimate or full scan.
- UAC/MFT: whether to allow an administrator prompt for the faster NTFS engine.
- Output: whether to generate only JSON or also an HTML report.
## Safety boundary
Recommended wording: "我可以全程本地只读扫描。允许 UAC 的话,NTFS 盘会优先用更快的 MFT 引擎;不允许也可以用普通模式,只是更慢。你想分析哪个分区?"
The executable does not modify scanned user data. This workflow writes the
requested JSON and, when requested, an HTML report generated from that JSON;
elevation may also use temporary status/output files for process handoff. Treat
every recommendation as review-only and state that no cleanup was performed.
## SOP
## Workflow (SOP)
1. Stop and ask before scanning if any required detail is not confirmed. Use the available interactive question tool when possible instead of guessing.
2. Confirm the target drive. If the user only says "scan my disk" or similar, list local fixed drives first, then ask which one to analyze.
3. Confirm whether to use fast index mode. Explain it as: "快速索引需要管理员权限,会优先使用 NTFS/MFT 引擎,速度更快;不授权也可以用普通遍历模式,只是更慢。"
4. Confirm scan scope when unclear: quick scan for a fast estimate, full scan for the final report.
5. Confirm output expectation: JSON only, natural-language analysis, HTML report, or all of them. Default to JSON + HTML report when the user asks for a report.
6. If fast index mode is enabled, tell the user before running the command: "接下来会出现一个 Windows UAC/授权弹窗,请选择允许;扫描只读、仅在本机运行。"
7. Run the scan with an explicit output path. Prefer timestamped names such as `result-<timestamp>.json` and `report-<timestamp>.html` to avoid overwriting prior reports.
8. Expect the report JSON to be large. Be prepared to read it in chunks or use targeted extraction instead of assuming one read will return the whole file.
9. Generate the HTML report by injecting the JSON into `template.html`. If the environment can display or open the HTML directly for the user, do so; otherwise provide the absolute report path.
10. Present results in this order: short human summary, JSON path, HTML path, top findings, safety note. Never imply that cleanup was performed.
1. Choose the operation from the user's goal:
- Full diagnosis or final report: `scan --scan full`.
- Fast, incomplete estimate: `scan --scan quick`.
- Installed applications only: `apps`.
- Short text-only overview: `summary`.
2. Resolve the target drive. If the user did not name one, list available
Windows drive letters and ask which to scan. Do not silently assume `C:`.
3. Before every `scan` or `summary`, ask once whether the user allows a Windows
UAC prompt. Explain that approval lets `--mode auto` use the faster NTFS/MFT
path when eligible; declining keeps the local read-only scan in normal mode.
Combine this with the drive question when both are needed. `apps` does not
require this scan confirmation.
4. Infer scope and output from the request instead of asking a confirmation
checklist. Words such as “quick”, “rough”, or “estimate” select `quick`;
“complete”, “full”, “final”, or “report” select `full`. Otherwise use `full`.
Generate HTML only when the user asks for a report, visualization, treemap,
or SpaceSniffer-style view.
5. For each JSON-producing operation, create an explicit absolute output path.
A drive scan and its optional HTML report share one timestamped basename:
`disk-master-<drive>-<YYYYMMDD-HHmmss>.json` and `.html`. Remove the colon
from the drive token, for example `disk-master-C-20260519-170000.json` and
`disk-master-C-20260519-170000.html`. Name an app-only inventory
`disk-master-apps-<YYYYMMDD-HHmmss>.json`.
6. Run the bundled executable from the skill directory. Use `--mode auto` for
normal operation and add `--elevate` only after the user approves UAC:
Minimum confirmations before execution:
```text
disk-master.exe scan --drive C: --scan full --mode auto --out <absolute-json-path>
disk-master.exe scan --drive C: --scan quick --mode auto --out <absolute-json-path>
disk-master.exe scan --drive C: --scan full --mode auto --elevate --out <absolute-json-path>
disk-master.exe apps --out <absolute-apps-json-path>
disk-master.exe summary --drive C: --mode auto
```
- Target drive, for example `C:` or `D:`.
- Fast index / administrator permission: yes or no.
- Scan scope: quick or full.
- Output format: JSON, HTML, or both.
Optional public flags are `--depth <N>`, `--top <N>`, `--no-apps`, and
`--mode <auto|mft|walk>`. `--depth` limits quick-scan depth and JSON tree
depth. Prefer `auto`; force `mft` or `walk` only when the user requests it or
when diagnosing scan behavior.
7. On a successful `scan`, use the command's status output as the authoritative
JSON path. For `apps`, use the explicit output path after a successful exit.
Read large JSON in chunks or extract relevant fields. Cover the disk state,
scan completeness and errors, largest directories/files, extension totals,
user folders, AppData hotspots, applications, and advice that materially
affects the answer. Label a quick scan as incomplete.
8. If HTML was requested, read `template.html`, replace the single `%%DATA%%`
marker with the raw JSON text, and write the matching `.html` path from step
5. The CLI has no `report` command.
9. Present the result in this order: concise diagnosis, highest-impact findings,
uncertainty or scan errors, JSON/HTML absolute paths, then the safety note
that all actions remain suggestions and no cleanup occurred.
## Report Sections
- Summary cards: used/free space, largest directory, number of high-priority suggestions.
- SpaceSniffer section: interactive treemap with drill-down, breadcrumbs, search, and detail panel.
- Diagnostic details: large files, user paths, AppData, C: drive apps/games, file extensions.
- Advice tutorials: each recommendation expands into step-by-step safe handling guidance.
- Scan metadata: mode, permissions, MFT state, errors, and raw JSON summary.
## Safety
The tool is read-only for user data. Do not delete files, uninstall applications, clean the registry, or modify user data as part of this skill.
Never present a recommendation as an automatic cleanup action. Use language like "建议检查", "可以考虑", and "请先确认" for uncertain items.
If a command fails or UAC is declined/cancelled, report the error plainly and
offer the non-elevated `--mode auto` scan when applicable. Do not claim an
output file exists unless the command reported success.
+116
View File
@@ -0,0 +1,116 @@
---
name: init-workspace
description: 检查开发环境;首次初始化个人执行工作区,重复使用时按更新策略补全规则、目录和文档。
disable-model-invocation: true
---
# Init Workspace
先检查环境,再根据用户要求决定只报告、初始化新目录或补全已有工作区。一个工作区可以容纳多个源码仓、`docs/` 本地文档和 `pkgs/` 参考库。环境检查不是初始化授权,也不是启动 worker 的授权。
## Step 1 — 确认 Herdr 环境与 skills
先检查 `HERDR_ENV` 和 PATH 上的 `herdr`,再查找当前宿主列出或用户指定的 `herdr`、`think-twice`、`why-here` skill,并实际读取文件。记录来源和能否读取;目录存在不等于宿主可用,同名副本以宿主当前选择为准。
- `HERDR_ENV` 不为 `1`:不查询或控制 Herdr 会话。可以记录 CLI 存在,并按用户要求为以后生成条件式规则。
- 在 Herdr 内:读取可访问的 Herdr skill。宿主没有列出时,可运行 `herdr --skill` 获取内置文本并读取;记录其来源为 CLI,不声称已安装到所有宿主。
- 缺少 skill:报告缺口,不自动安装,也不生成无法执行的 skill 引用。用户明确允许安装后,重新检查。
**完成条件:** 是否处于 Herdr 环境及三个 skill 的可读性明确。
## Step 2 — 检查协作环境、工具与目标目录
读取 [references/environment-checks.md](references/environment-checks.md),按其中的命令和边界运行检查。脚本路径相对本 skill,不相对目标工作区。
- 有 Python 3.9+ 时使用 `scripts/inspect_environment.py`。默认检查 PATH、受支持 CLI 的版本/帮助、Herdr 内的 integration 状态、受限 Pi package 列表和目标文件存在性;输出 JSON,不写工作区。扫描做过的检查直接引用结果,不重复执行。
- 将宿主找到的 skill 路径用 `--skill NAME=PATH` 传给脚本;Shepherdr 的 git/local、项目级或显式加载来源,可用 `--shepherdr-path PACKAGE_DIR` 提供确认的包根目录。脚本只检查文件,不加载扩展。
- 在 Herdr 内按其 skill 核验当前会话;结合宿主提供的工具来源,报告 Shepherdr 的安装线索与当前加载状态。不为检查启动 worker。
- 包含 `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 自动用于执行。
**完成条件:** 提供简短能力/缺口报告及检查范围。用户只要求检查时,到此结束;报告默认不持久化,需要保留时先确认位置。
## Step 3 — 选择初始化或更新
先按目标目录现状和用户要求选择路径:
- 只要求检查环境:在 Step 2 结束,不进入初始化或更新。
- 用户说明已用过本 skill,或目标已有 `AGENTS.md`、`CLAUDE.md` 等工作区规则:读取 [references/update-workspace.md](references/update-workspace.md),走更新策略,不重跑完整初始化。无需证明旧文件出自本 skill;用户手写的规则也按更新方式保护。
- 目标不存在或尚无工作区规则:继续 Step 4;已有目录内容仍先检查,只补缺失部分。
更新路径完成后直接执行适用的 Done when 检查。只有更新提案明确需要某项脚手架操作时,才引用 Step 5/6 的对应步骤,不自动重建连接、克隆仓库或改变 git 跟踪方式。
## Step 4 — 补充需求与选择章节
根据已有要求一轮询问缺口,不重复询问探测能回答的事实:
1. 目标目录、工作区名、维护者与用途。
2. 源码仓库:已有路径还是待克隆、一个还是多个;待克隆需远端 URL。
3. Quick Context:模块归属、维护的上游库、需要防止误判的事项。
4. 是否采用 Herdr 并行协作(当前可用时建议采用,但不自动启用)。Pi 任务优先使用已配置的 Shepherdr,其他任务按 `$herdr` 交互;用户的工具选择优先。只读任务不额外建 worktree,并行修改用独立 worktree。
5. 是否生成 `CODINGRULES.md`(默认否),工作区是否 git 跟踪(默认否)。
6. 是否引用用户指定的模型策略文件(默认不生成)。策略表达工具/模型/额度偏好,不把本次扫描清单复制进去。
已有文件逐节提出保留/替换/新增方案,模板不认识的章节原样保留。确认后才写入。用户已明确授权的全新空目录无需再问一次。
**完成条件:** 明确本次修改范围、源码仓位置与章节选择,已获相应写入确认。
## Step 5 — 生成候选规则与脚手架
按 [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 时加入,保留用户的工具选择。环境故障参考链接指向本次使用的 `init-workspace/SKILL.md`;渲染器填入其实际文件 URI,手工生成时同样填写可读地址。`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/ 真身目录
|- .claude/skills 指向 .agents/skills
|- docs/ 按需建立 prd/ plans/ guides/ sop/
|- <repo>/ 一个或多个源码仓
|- pkgs/ 参考库
|- scripts/ 工作区脚本
|- .gitignore 至少含 nul
|- <project>.code-workspace
|- justfile 仅当 PATH 有 just
|- AGENTS.md, CLAUDE.md
```
- Windows Junction:`New-Item -ItemType Junction -Path .claude\skills -Target <绝对路径>\.agents\skills`,无需管理员权限。POSIX:`ln -s ../.agents/skills .claude/skills`。已有目录/连接先检查,不覆盖。
- 从 [assets/project.code-workspace](assets/project.code-workspace) 填充:Workspace(`.`)、每个源码仓、Docs 各一个入口;有独立入口的目录在 Workspace 视图的 `files.exclude` 隐藏。
- 有 URL 的待克隆仓按授权克隆到对应目录;没有 URL 的只建空目录并记入 `docs/TODO.md`。
- 写最小 `docs/README.md` 目录地图,与生成规则中引用的目录保持一致。
- `justfile` 空骨架:`default:` 下接 `@just --list`。
**完成条件:** 仅授权且缺失的资源被创建,既有内容按确认方案修改,候选中的可选指针已有实际目标。
## Step 6 — git 跟踪(用户选择才执行)
1. `git init`。
2. `pkgs/.gitignore` 屏蔽全部,只放行 `.gitignore` 与 `README.md`;`pkgs/README.md` 维护参考仓名单。
3. 对已有源码仓确认是否登记为 submodule;确认后用 `git submodule add <url> <dir>` 登记。拿不到远端 URL 的记入 `docs/TODO.md`,不强行处理。
## Done when
先确定验收范围:只检查时只验收检查报告;新建时验证本次全部生成资源;更新时验证本次修改及其必要依赖,不将范围外的既有问题变成本轮修复要求。以下文件检查只适用于对应范围。既有问题按更新策略登记;本轮必要依赖未验证时明确标为未验证或阻塞。
- [ ] 只检查模式没有创建工作区资源、启动 Agent、登录或发起模型推理
- [ ] 报告区分宿主可读/文件存在、agent 定义/运行实例、模型列出/调用验证;integration、扩展声明/文件/宿主加载/worker 可用分开,受限列表未发现不等于全机未安装
- [ ] 重复使用或已有规则时进入更新策略,仅询问缺口;环境暂时缺失不导致删除既有规则
- [ ] 写入已有文件前得到确认;未知章节和无关用户内容保留,重复补全无重复章节;没有差异时不写文件
- [ ] 本次新增或修改的规则不重复,`AGENTS.md` 与 `CODINGRULES.md` 职责分明,commit 尾注规则放在前者
- [ ] 本轮涉及的 skill、文档与策略指针带触发条件和动作,且必要目标可读,包括协作章节的环境故障参考链接
- [ ] 协作规则按任务选择回传方式,没有同时要求插件自动交付和手工重复回报;下发状态不明时不降级重发,迁移不重启或重发活动任务
- [ ] 本轮创建、修改或依赖的 skills 连接和 CLAUDE 桥有效,涉及的 code-workspace 是合法 JSON
- [ ] 新生成内容无占位符、模板注释或未替换示例,候选 `AGENTS.md` 不超过 200 行;更新不为清除既有超限而删除用户内容
- [ ] 输出做字段筛选及密钥模式屏蔽,说明未验证能力与未完成事项
@@ -0,0 +1,42 @@
# {{name}} Workspace Guide
这个工作区是 {{owner}} 进行 {{purpose}} 工作时使用的个人执行目录。
## Quick Context
{{context}}
## Communication
- 默认中文;路径、命令、代码标识符、技术术语保留原文。
- 描述实际动作、文件与行为变化,结论先行;未验证的判断明确标注。
- 如无特殊说明,“文档”指工作区文档(`docs/` 与 `AGENTS.md`)。
## Workspace Docs
- 处理需求、方案、review 或恢复上下文时,先读 `docs/README.md`,再按目录地图查找材料。
{{docs}}
## Shell
- 临时文件放系统临时目录下的任务独立子目录,不在仓库里散落。
- 路径含空格或中文时加引号。
- Git Bash/MSYS2 下抑制输出用 `/dev/null`;`> nul` 会创建名为 `nul` 的真实文件,发现即删。
## Commits
- commit message 不加 `Co-Authored-By` 等 AI 署名 trailer,不加会话链接。
## Design Check
- 实现或 review 中发现修复在叠加条件、状态同步、调用方绕行或特殊分支时,重新检查模块边界、数据流和调用关系,说明更干净的方向。
## Code Review
- findings 按 P0 到 Px 排序,可用 P0.1 式细分。
- 定级前核验;缺复现证据的低概率问题降级并标注不确定性。
- 根据 diff、代码、测试和工作区文档核对修改意图;推测标注为推测。
- 提到路径、函数名时,说明它负责什么。
## Output Quality
- 交付写清结论、范围、实际验证和未解决事项,使接收者只靠正文及可访问的文件就能继续工作。
{{quality}}
{{collaboration}}
{{coding}}
{{models}}
## Structure
{{structure}}
@@ -0,0 +1,34 @@
# Coding Rules
Bias toward caution over speed. For trivial tasks, use judgment.
## Before coding
- State assumptions explicitly. When interpretations genuinely diverge in
outcome, list them and let the user pick; otherwise state your assumption
and proceed.
- If you cannot form a workable assumption at all, stop and ask, naming
exactly what is confusing. Do not invent one to keep moving.
- If a simpler approach exists, say so.
## Scope of change
- Every changed line traces to the request, or to low-risk cleanup in the
same context; call out anything beyond that instead of doing it.
- Match existing style. Mention unrelated dead code; leave it in place.
- Remove imports/variables/functions that your change orphaned.
## Simplicity
- Minimum code that solves the problem. No speculative features,
abstractions, or configurability.
- Defensive handling only at untrusted boundaries: external input, network,
permissions, migration.
## Verification
- Define a verifiable success check before starting. Prefer a failing test
when reproduction is cheap; otherwise state how the change was verified.
- For multi-step tasks, keep a brief plan with a check per step.
- Documentation-only changes: read/diff self-check, and say tests were not
run.
@@ -0,0 +1,35 @@
{
"folders": [
{
"name": "Workspace",
"path": "."
},
{
"name": "<Repo>",
"path": "<repo-dir>"
},
{
"name": "Docs",
"path": "docs"
}
],
"settings": {
"explorer.compactFolders": false,
"workbench.startupEditor": "none",
"files.exclude": {
"<repo-dir>": true,
"docs": true,
"**/.DS_Store": true,
"**/node_modules": true,
"**/dist": true,
"**/build": true,
"CLAUDE.md": true,
".claude/": true
},
"search.exclude": {
"**/node_modules": true,
"**/dist": true,
"**/build": true
}
}
}
@@ -0,0 +1,26 @@
## Herdr Collaboration
### 共同协作要求
- 本工作区采用 Herdr 管理并行 worker,具体操作遵循 `$herdr`。遇到环境问题时,参考 [init-workspace 的环境检查]({{init_workspace}});需要重新扫描时,请用户以“只检查环境”调用该 skill。
- 主控负责拆分、共同接口、唯一任务负责人、跟踪、评审、集成、验收与清理;worker 只完成明确授权的任务。遵守用户的工具选择和宿主委派限制,冲突时报告;主控不重复实现已委派范围。
- 只读调研和评审不额外创建 worktree。并行修改源码的 worker 通过 Herdr 的 worktree 功能管理独立工作树与 workspace;建在实际源码仓,不在外层资料目录建树。同一文件同时只有一个写入负责人,共享契约、依赖、锁文件和公共配置由主控指定修改者。
- 共同接口未定时先设计、核验,不为填满并发提前实现。发现上游错误或需要跨范围修改时,回报成因与所需改动,不在调用方吞错或堆兼容分支。
- 任务书写明目标、仓库/分支/基线提交、共同接口、允许修改的文件、必读材料、依赖、验收命令及失败情形、停下报告的条件、提交授权和交付接收方。共同要求通过可读引用传递;worker 的 cwd/worktree 中不可见的工作区规则提供可读副本或必要摘录。
- 每个任务独立使用测试数据、临时目录和端口,共享依赖明确使用约束。worktree 不隔离凭据或外部文件,不是安全沙箱。
- 交付报告包括完成情况、提交(或未提交原因/不适用)、改动、实际检查与结果、限制和需要主控的动作。`idle` / `done` 或自动完成通知只说明运行状态,不表示验收通过。
- worker 交付后,没有补充任务、修改要求或待澄清事项时,主控不发送“收到”等确认性消息,直接评审或收尾。worker 等待明确指令,不主动续聊;修改要求、阻塞解除和任务取消仍须通知相关 Agent,主控仍向用户交付最终结果。
### 任务交互
- Pi 任务优先使用已配置的 Shepherdr `agents`,按工具帮助下发任务、发送消息和读取结果。Shepherdr 任务由 worker 的最终答复交付,主控等待插件通知。
- 未使用 Shepherdr 的任务按 `$herdr` 交互;worker 完成或阻塞时,通过 Herdr 向实际主控主动回传一次交付报告。
- 任务是否接收不明时,先查原任务状态,不立即降级重发。
- worktree、布局、原始终端及其他 CLI agent 由 Herdr 管理。
### 验收与收尾
- worker 只在授权分支提交;主控按范围检查、独立评审、依赖集成、集成验证、验收的顺序推进。push、安装、发布另按项目授权执行。
- 检查命令取自项目指令和实际配置;局部/全量测试、类型检查、真实模型、浏览器和宿主验证分别报告,不能相互替代。测试失败、实现不完整或关键条件未验证时保持未完成或阻塞,不用 skip 消除问题。
- 收尾包括源码集成、Herdr 资源和工作区文档三部分,不以关闭 worker 代替。主控按授权合并分支或应用提交,执行集成验证;Herdr workspace 是终端组织,不代替 Git 合并。
- worker 后续不再需要,且交付已送达、提交已保全、未提交文件已处理后,及时按当前工具契约关闭本轮创建的 Agent/pane,不为等待确认回复保留进程。仍需返工或澄清时可暂留并记录原因。
- 核验成果已集成或另有可恢复副本、没有活动进程和待处理文件后,按授权移除本轮无用的 worktree、分支和空 Herdr workspace。仍需使用的终端按当前 CLI 支持的方式整理到保留布局,移动后使用返回的新 ID,不搬动他人资源。关闭进程不授权删除未集成成果,无法证明可清理时保留并报告。
- 同步本轮相关的工作区文档:写入有效结论、验证和后续动作,更新任务状态与目录地图,按项目约定及授权归档或删除失效临时计划。保留仍有效的决定、风险和阻塞,不借收尾清理无关材料。
- 只清理本轮创建且已无用途的资源;移除共享依赖链接与目录前先核验,不关闭他人的终端。剩余任务和暂留资源登记负责人、保留原因及下一步,恢复时重新查询状态和 ID。
@@ -0,0 +1,112 @@
# 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`、`shepherdr`、`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 秒。失败命令的输出丢弃;帮助的 stderr 也参与解析(OpenCode 和 Herdr 命令组会写到该流)。Herdr 的裸 `integration` 在退出码为 2 且输出以已知帮助标题开头时按帮助处理,其余非零退出仍是失败。原始文本不入报告,仅提取允许字段。CLI 子进程不是沙箱,工具自身可能写缓存或执行启动逻辑,不承诺系统零副作用。
## Herdr integration 与 Shepherdr
默认执行以下检查,不安装、不 reload、不启动 worker:
- `HERDR_ENV` 不为 `1` 时不运行任何 Herdr CLI;`herdr.pi_integration.status=not_checked`。在 Herdr 内检查 `--help`、`--version`,帮助包含 integration 时查询命令组,确认支持 status 后运行 `herdr integration status`,仅提取 Pi 状态,不输出其他 integration 的路径。
- `pi_integration` 区分 `installed`、`not_installed`、`outdated`、`unrecognized_output`、`unsupported_interface` 和命令失败;未识别的输出不推断安装状态。安装侧状态不证明当前会话已加载 integration。
- Pi 在 PATH 时运行 `pi list --no-approve`,设置 `PI_OFFLINE=1`、`PI_TELEMETRY=0`、`NO_COLOR=1`,关闭 stdin 并限制超时。只提取用户级 `npm:@howaboua/pi-shepherdr`(含版本)的声明、filtered 标记及安装路径;`filtered` 只说明存在资源筛选,不证明扩展已禁用。
- `shepherdr.packages.status` 为 `listed`、`not_found_in_scope` 或解析/命令错误;`items[].files` 另报候选文件状态。没有返回安装路径为 `path_not_reported`,不能当作文件已安装。`not_found_in_scope` 只说明本次用户级 npm 范围未发现声明,不排除项目、git/local、显式加载或未登记的副本。
- 候选包只读 `package.json`,核对准确 name、version 和 `pi.extensions` 中精确入口文件存在性;结果为 `files_present`、`missing_manifest`、`missing_entry`、`identity_mismatch`、`invalid_manifest`、`unreadable_or_invalid` 等。含通配符、越界或不支持的路径为 `unsupported_manifest`;精确入口不是文件(包括目录)为 `missing_entry`,交给宿主核验。脱敏路径为 `redacted_path`,不会拿来读文件。包存在不等于已加载。
- `shepherdr.node` 查询 Node 版本;结合 `tools` 的 Pi 版本、`herdr.version` 和实际插件版本说明核对前提,不内置永久版本白名单。Shepherdr 0.2.18 要求 Pi ≥ 1.1.0、Node ≥ 22.18、Herdr ≥ 0.9 及 Pi integration;其他版本重新查证。
宿主提供或用户指定的 git/local、项目级安装可传包根目录,重复参数检查多个明确候选,不扫描全盘:
```bash
python -B "${SKILL_DIR}/scripts/inspect_environment.py" --target /existing/workspace --shepherdr-path /confirmed/package-root
```
这些文件结果在 `shepherdr.candidates`,不代表来源已配置或获得项目信任。脚本不 import 扩展、不执行其代码、不读取认证文件,也不自行实现 Pi 资源解析规则。
脚本的 `shepherdr.host_loaded` 与 `worker_loaded` 为 `not_checked`。Agent 根据当前宿主提供的工具来源补充加载证据;没有接口时标为未验证,不另开会话探测。worker 不在此次扫描范围内。Shepherdr profile 的模型和 thinking 独立配置,不继承主控。
Pi 1.1.0 的裸 `pi list` 可能在项目信任流程加载扩展;`--no-approve` 排除该路径和项目资源。旧版不接受参数或输出不同就报告失败,不自动改成裸命令或 `--approve`。首次加载 Shepherdr 会创建默认 profiles,因此不能为检查而新开加载插件的 Pi。受限查询仍进入 Pi CLI,不保证没有宿主缓存副作用。
缺少 integration 或扩展时报告缺口,只有用户另行授权才能安装。直接 Herdr 协作不要求安装 Shepherdr。
## 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` 是选择参数,不是列表接口;不读取配置全文猜定义 |
按用户的模型或 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。用户明确的工具偏好由 Agent 在候选中保留。`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。Herdr 章节的环境故障链接自动指向本次执行的 skill 文件 URI;迁移到其他机器时,按目标机器可读的 skill 位置更新。输出前校验字段、布尔值、占位符和 200 行上限。新工作区验证后保存;已有工作区对照候选逐节修改,保留未知章节,不用此命令覆盖原文件。无 Python 时,Agent 读取基础模板与可选章节,按同一决策填充并人工执行这些检查。
## 脱敏
`scripts/output_safety.py` 统一处理脚本输出,移除 ANSI 转义后将 `sk-[0-9a-zA-Z-]+` 匹配串替换为 `[REDACTED]`。JSON 值及键均处理,键发生碰撞时拒绝输出,避免静默丢字段。参数解析错误也经过这一边界;文件/JSON 错误只报告类型,不回显内容。
这是保守的最后一道检查:短字符串也可能误伤,非 `sk-` 凭据、分段或编码后的密钥不在保证范围内。因此主要防护是字段白名单和丢弃原始错误,不收集认证文件、环境变量全集或完整配置。Agent 自己写的报告同样执行字段筛选与匹配检查。若合法路径或模型 ID 被屏蔽,提示用户核验,不把 `[REDACTED]` 当执行参数。
@@ -0,0 +1,46 @@
# Update Workspace
已有工作区再次使用 `init-workspace` 时,更新当前规则和缺失资源,不重新初始化。此流程更新工作区文件,不安装或升级 skill 包,也不自动执行工作区里登记的业务任务。
## 1. 读取当前约定
读取已有 `AGENTS.md`、`CLAUDE.md`(含链接目标)、`docs/README.md`、引用的 coding/model 策略及本次相关文档。结合刚完成的环境检查,列出文件现状、用户当前要求和缺失信息。
- 保留现有目录布局、语言偏好、源码仓关系、自定义章节和用户明确关闭的可选功能。
- 无法证明某条规则由旧模板生成时,视为用户内容;不根据相似标题认领整节。
- 当次不在 Herdr 中或某个 skill 暂时不可读,只报告可用性变化;不据此删除工作区长期规则。需要调整时说明理由并询问。
- 既有文档足够回答的问题不重复访谈。只有矛盾、缺口或新的可选功能才询问用户。
**完成条件:** 能说明本轮要更新哪些内容,以及哪些既有选择原样保留。
## 2. 提出逐项差异
提出差异前,读取本次范围对应的当前规则:基础约定见 [AGENTS.template.md](../assets/AGENTS.template.md),Herdr 协作见 [herdr.md](../assets/sections/herdr.md),coding 正文见 [CODINGRULES.template.md](../assets/CODINGRULES.template.md)。涉及质量检查、coding 或模型策略指针时,核对 [render_agents.py](../scripts/render_agents.py) 中相应生成逻辑,或按 [生成决策说明](environment-checks.md#生成决策与-stdout) 渲染候选来比较。用户要求整体更新时覆盖当前采用的规则;局部修改只读取相关材料,不强制生成完整候选。
对本次范围给出“现状 → 建议变化 → 原因”,并明确受影响的文件。新版规则只用于提出建议,用户明确的语言、布局及关闭选项优先;不因新版默认值变化重新开启功能或覆盖既有选择。
- 新增规则、修正文档指针、补缺失目录分别列项;删除或替换既有规则单独说明证据。
- 避免在旧节旁边再附一节同义规则;能定位具体句子就修改具体句子。
- 模型列表是本轮事实,不自动写成永久白名单或额度策略。
- 整理工作区文档时,只处理本轮涉及的状态、结论、计划和目录地图;仍有用的决策与验证证据保留。归档/删除按工作区约定和用户授权处理,不扫描全仓顺手清理。
### 更新 Herdr 协作规则时
对照当前协作模板逐项处理:
- 保留任务负责人、代码基线、独立 worktree、评审、验收和成果保全要求。用户的模型、布局、工具选择和禁用功能优先于模板的条件式“优先使用 Shepherdr”。
- 将全局“worker 经 Herdr 主动回传”限定到直接 Herdr 任务;Shepherdr 任务改为最终答复由插件交付,不再另发完成报告。交付内容要求不变,运行结束仍不等于验收通过。
- 删除被工具帮助覆盖的操作说明和例行环境自检要求;遇到环境问题时,通过模板中的链接参考 `init-workspace`。保留用户仍需的自定义约定。
- 优先让活动任务按原方式收尾。更新规则不授权重启、关闭或重发任务;确需接管时另行确认原任务状态、责任方和回传方式。
已有文件修改取得用户确认后再执行。没有差异时报告无需更新,到此结束,不写时间戳、空报告或状态文件来制造变化。
## 3. 应用并验证
按确认的差异局部修改。新增脚手架时只执行 `SKILL.md` 对应操作;更改 git 跟踪、submodule、已有链接或仓库位置需明确授权。
验收本次修改及其必要依赖:规则不重复,文档与 skill 指针可读,链接目标正确,涉及的 JSON 有效。这不等同于整个工作区合规,也不取消初始化流程 Step 1/2 的环境检查。
范围外的既有超 200 行、失效引用或连接问题只登记,不自动扩修或强制开启精简访谈;需要处理时另提修改范围。若不可读文件是本次修改依赖的策略,或相关连接无法验证,将受影响部分标为未验证或阻塞,不能登记后仍宣称通过。新生成的候选仍遵守 200 行上限,不为满足模板限制删除既有用户内容。
再次按相同要求比较,应该没有需要重复添加的内容。涉及协作迁移时,检查同一任务不会同时受到“插件自动交付”和“额外手工回传”的要求;用户自定义章节未变,活动任务没有因规则更新被重新下发。交付实际改动、执行的检查和仍未完成的事项,不声称本轮未检查的整个工作区已健康。
@@ -0,0 +1,334 @@
"""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, read_json, 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.-])")
SHEPHERDR = "@howaboua/pi-shepherdr"
def run_query(executable, arguments, timeout, cwd, env=None):
"""Only callers' fixed argument lists reach this boundary; stdin stays closed."""
help_query = arguments in (["--help"], ["integration"])
try:
result = subprocess.run(
[executable, *arguments], cwd=cwd, env=env,
stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
stderr=subprocess.PIPE if help_query else subprocess.DEVNULL, timeout=timeout, check=False,
encoding="utf-8", errors="replace",
)
except subprocess.TimeoutExpired:
return {"status": "timeout"}, ""
except OSError:
return {"status": "execution_failed"}, ""
text = result.stdout + (result.stderr or "") if help_query else result.stdout
# Herdr 0.9 prints bare group help on stderr with syntax exit 2.
group_help = (arguments == ["integration"] and result.returncode == 2
and redact(text).startswith("herdr integration commands:\n"))
if result.returncode and not group_help:
return {"status": "command_failed", "exit_code": result.returncode}, ""
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 query_version(executable, cwd, timeout, runner=run_query):
check, text = runner(executable, ["--version"], timeout, cwd)
if check["status"] != "ok":
return check
match = VERSION.search(re.sub(r"^v(?=\d)", "", text.strip()))
return {"status": "ok", "value": match.group()} if match else {"status": "unrecognized_output"}
def parse_pi_integration(text):
lines = [line.strip() for line in text.splitlines() if line.startswith("pi:")]
if len(lines) != 1:
return {"status": "unrecognized_output"}
match = re.fullmatch(r"pi: (not installed|installed|outdated)(?: \([^\r\n]+\))?", lines[0])
if not match:
return {"status": "unrecognized_output"}
return {"status": {"not installed": "not_installed", "installed": "installed", "outdated": "outdated"}[match[1]]}
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",
"version": {"status": "not_checked"}, "pi_integration": {"status": "not_checked"},
"session_verified": False}
if not inside:
return row # No Herdr CLI calls outside its managed environment.
if not executable:
row["status"] = "missing_cli"
return row
check, help_text = runner(executable, ["--help"], timeout, cwd)
row["help"] = check
row["status"] = "candidate" if check["status"] == "ok" else "unavailable"
row["version"] = query_version(executable, cwd, timeout, runner)
if check["status"] == "ok" and re.search(r"\bherdr integration\b", help_text):
# A bare command group is discovery; never probe install with missing arguments.
check, help_text = runner(executable, ["integration"], timeout, cwd)
if check["status"] != "ok":
row["pi_integration"] = check
elif re.search(r"\bherdr integration status\b", help_text):
check, text = runner(executable, ["integration", "status"], timeout, cwd)
row["pi_integration"] = parse_pi_integration(text) if check["status"] == "ok" else check
else:
row["pi_integration"] = {"status": "unsupported_interface"}
return row
def parse_shepherdr_packages(text):
"""Parse the restricted user list, not Pi's complete resource resolution policy."""
if text.strip() == "No packages installed.":
return {"status": "not_found_in_scope", "items": []}
lines = [line.rstrip() for line in text.splitlines() if line.strip()]
if not lines or lines.pop(0) != "User packages:":
return {"status": "unrecognized_output", "items": []}
items, current = [], None
for line in lines:
if re.fullmatch(r" \S.*", line):
source = line[2:]
filtered = source.endswith(" (filtered)")
if filtered:
source = source[:-11]
current = {"path_seen": False}
if re.fullmatch(r"npm:" + re.escape(SHEPHERDR) + r"(?:@[^\s]+)?", source):
current = {"source": source, "filtered": filtered, "path_seen": False}
items.append(current)
elif re.fullmatch(r" \S.*", line) and current is not None and not current["path_seen"]:
current["path_seen"] = True
current["path"] = line[4:]
else:
return {"status": "unrecognized_output", "items": []}
for item in items:
item.pop("path_seen")
return {"status": "listed" if items else "not_found_in_scope", "items": items}
def inspect_shepherdr_files(filename):
"""Read one candidate package only. Never load its code or follow manifest escapes."""
if "[REDACTED]" in str(filename):
return {"status": "redacted_path"}
try:
root = Path(filename).expanduser().resolve()
manifest = read_json(root / "package.json")
if not isinstance(manifest, dict) or manifest.get("name") != SHEPHERDR:
return {"status": "identity_mismatch"}
version = manifest.get("version")
if not isinstance(version, str) or not VERSION.fullmatch(version):
return {"status": "invalid_manifest"}
pi = manifest.get("pi")
entries = pi.get("extensions") if isinstance(pi, dict) else None
row = {"status": "files_present", "path": str(root), "version": version}
if not isinstance(entries, list) or not entries:
return {**row, "status": "unsupported_manifest"}
for entry in entries:
if not isinstance(entry, str) or not entry or any(c in entry for c in "*?![]{}\\:"):
return {**row, "status": "unsupported_manifest"}
target = (root / entry).resolve()
if root not in target.parents:
return {**row, "status": "unsupported_manifest"}
if not target.is_file():
return {**row, "status": "missing_entry"}
return row
except FileNotFoundError:
return {"status": "missing_manifest"}
except (OSError, ValueError, RuntimeError):
return {"status": "unreadable_or_invalid"}
def inspect_shepherdr(pi, paths, cwd, timeout, *, which=shutil.which, runner=run_query):
row = {"packages": {"status": "missing_cli", "items": []},
"scope": "User npm declaration only; project/git/local/explicit loads need host-supplied paths.",
"candidates": [], "host_loaded": "not_checked", "worker_loaded": "not_checked",
"node": {"status": "missing_cli"}}
node = which("node")
if node:
row["node"] = query_version(node, cwd, timeout, runner)
if pi["status"] == "found":
check, text = runner(pi["path"], ["list", "--no-approve"], timeout, cwd,
{**os.environ, "PI_OFFLINE": "1", "PI_TELEMETRY": "0", "NO_COLOR": "1"})
row["packages"] = parse_shepherdr_packages(text) if check["status"] == "ok" else {**check, "items": []}
for item in row["packages"]["items"]:
item["files"] = inspect_shepherdr_files(item["path"]) if "path" in item else {"status": "path_not_reported"}
row["candidates"] = [inspect_shepherdr_files(path) for path in paths]
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}
shepherdr = inspect_shepherdr(by_name["pi"], args.shepherdr_path, cwd, args.timeout)
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, "shepherdr": shepherdr,
"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("--shepherdr-path", action="append", default=[], metavar="PACKAGE_DIR",
help="Host-supplied package root; reads manifest/entry presence without loading code.")
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,88 @@
"""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"] = ""
if config.get("herdr"):
slots["collaboration"] = (ASSETS / "sections/herdr.md").read_text(encoding="utf-8").replace(
"{{init_workspace}}", (ASSETS.parent / "SKILL.md").resolve().as_uri())
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))
+159
View File
@@ -0,0 +1,159 @@
---
name: why-here
description: >
Review a change before delivery after the work involved an abandoned attempt,
corrected misunderstanding, completed transition, or superseded implementation,
or when the user asks what no longer belongs in the finished result. Keep
current requirements and unproven cases unchanged. Do not use for ordinary
prose editing, general refactoring, or a repository-wide simplification review.
---
# Why Here
Review what is about to be delivered by asking of each questionable part:
**Why is this here now?**
Keep it when the finished result still needs it. Remove it when it exists only
because of an abandoned attempt, an earlier misunderstanding, a completed
transition, or an old implementation that no current requirement preserves.
This is not a general simplification pass and less is not automatically better.
## Scope
Start with the current diff, user-named files, and any text or artifact being
delivered with this change. Treat the task as review-only unless the user
explicitly asks to edit, clean up, remove, or otherwise change the artifact.
Loading or naming this skill alone does not grant permission to modify files. Do
not turn the review into repository-wide cleanup. Note an adjacent problem only
when it would materially affect this delivery.
Preserve unrelated user work. Inspect first; do not assume that an unfamiliar
field, branch, comment, test, or document is unnecessary.
## Process
### 1. Establish the finished result
Derive the required behavior, structure, contract, and documentation from the
user's request, acceptance criteria, owning documentation, current contracts, and
real consumers. Identify any public interface, stored data, external system, or
compatibility promise in scope. When the sources are missing or conflict, mark the
corresponding target as unproven instead of inferring it from the current
implementation. Candidates whose required result is unproven remain **Unproven**;
do not classify them as **Keep**, **Confine**, or **Remove**.
**Complete when:** the required result and the boundaries that can constrain
cleanup are explicit.
### 2. Find parts whose reason may have expired
Inspect the scoped change for parts that appear to exist only because work took
a particular route:
- explanations, denials, or comments that answer an earlier misunderstanding or
abandoned implementation rather than a likely question or current constraint
raised by the finished result;
- narration of attempts, reversals, or investigation instead of the conclusion
and its evidence;
- temporary fields, states, flags, branches, adapters, or documentation left by
a completed internal transition;
- tests for behavior that was intentionally removed or whose coverage has been
replaced;
- old and new representations both retained after the old one has lost its
purpose.
Use the counterfactual only to find candidates: if the work had begun from the
finished design, would this part still have been introduced? A “no” is a reason
to investigate, not permission to delete.
**Complete when:** every candidate has a location and a concrete reason it may
no longer belong.
### 3. Prove what still earns a place
For prose and comments, judge from the finished artifact. The reader is the
person who will receive, use, or maintain that artifact after this conversation,
without access to the conversation. Would removing the candidate create a
reasonable misunderstanding or change that reader's current understanding,
decision, or action? Check any source or requirement the statement claims to
preserve.
For code, interfaces, configuration, tests, and data structures, check current
callers, clients, configuration, stored data, documented contracts, and external
constraints. History may explain why a candidate appeared, but current evidence
decides whether it stays.
Classify the result:
- **Keep:** current understanding or action, behavior, a real consumer, a
contract, stored data, or an external constraint still requires it.
- **Confine:** a temporary compatibility need remains, but keeping it at the
conversion, migration, or version boundary would prevent it from spreading
through the maintained model. This classification does not by itself authorize
the move.
- **Remove prose:** removing it changes no intended reader's understanding,
decision, or action, and no current requirement calls for it.
- **Remove structure:** no current behavior or obligation depends on it, and
available validation can show that removal is safe.
- **Unproven:** evidence is insufficient. Keep it unchanged and state what would
settle the question.
Do not treat an awkward external requirement as residue merely because a clean
redesign would prefer not to have it.
**Complete when:** no removal rests only on taste, age, or the counterfactual.
### 4. Clean only within the proven boundary
If the current request authorizes edits, remove prose and comments when their
absence changes no current understanding, decision, or action. Otherwise,
classify and report them without editing. Keep a reason, constraint, or
invalidation condition that a future maintainer needs and cannot recover from
the artifact itself. Treat safety, security, privacy, recovery, legal, generated,
and tool-read notices as behavior or constraints; remove them only after proving
they no longer apply and checking the system or tool that consumes them.
If the current request authorizes edits, remove a test only when its protected
behavior was intentionally removed or its purpose is covered elsewhere. Otherwise,
classify and report it without editing. Read the assertions and confirm the
remaining coverage; absence of a runtime caller is not evidence that a test is
obsolete.
If the current request authorizes edits, remove internal code or structure only
after consumer and contract checks support it and relevant validation is
available. A **Confine** change may be made under that authorization only when it
is internal, preserves supported behavior, and does not affect a public
interface, durable data, wire format, migration history, or a still-used
compatibility path.
Before changing a public interface, durable data, wire format, migration history,
or still-used compatibility path, present the evidence and proposed finished
state, then obtain explicit authorization.
**Complete when:** every authorized edit is supported by the classification
above, every review-only candidate remains unchanged, and no unrelated cleanup
entered the diff.
### 5. Validate the result
If edits were made, run the focused checks that can detect a changed behavior,
broken consumer, or invalid contract. Repeat relevant searches after removal and
inspect the final diff for newly orphaned code, tests, configuration, or
documentation. For a review-only run, report the evidence checks performed and
which validation would be required before an edit.
**Complete when:** edited results are supported by concrete checks; review-only
findings name the evidence still required; and every unresolved boundary remains
unchanged and is reported.
## Delivery
Report the finished state, material constraints that remain, checks actually
run, and any candidate left unchanged for lack of evidence or authorization.
Do not recreate the discarded path as a removal diary unless the user explicitly
asks for an audit record.
Finish by accounting for every reviewed candidate: a retained candidate has a
current reason to stay; an unproven candidate remains unchanged with the missing
evidence or authorization named; and a removed candidate has supporting evidence
and validation.