Compare commits
15
Commits
1e5b2d0b99
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
92361eed22 | ||
|
|
c69d7b9ce0 | ||
|
|
4d9fa1134c | ||
|
|
3da100f300 | ||
|
|
c7e0a421f9 | ||
|
|
3c2c530c65 | ||
|
|
7a909a4cd6 | ||
|
|
9946233ffb | ||
|
|
030cda3454 | ||
|
|
e73636caea | ||
|
|
180fff22d9 | ||
|
|
f02872b256 | ||
|
|
cf34e7358d | ||
|
|
2694b2c081 | ||
|
|
b1d8dd0439 |
@@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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()}
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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))
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user