@bossmissing/agent-meta 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -2
- package/assets/skills/deploy-ansible/deploy-ansible/SKILL.md +71 -0
- package/assets/skills/deploy-cloudflare/deploy-cloudflare/SKILL.md +51 -0
- package/assets/skills/deploy-github-actions/deploy-github-actions/SKILL.md +50 -0
- package/assets/skills/dev-env/dev-env/SKILL.md +52 -0
- package/assets/skills/ironbank/ironbank-citations/SKILL.md +2 -0
- package/assets/skills/ironbank/ironbank-docs-research/SKILL.md +1 -1
- package/assets/skills/ironbank/ironbank-mcp-reference/SKILL.md +1 -0
- package/assets/skills/ironbank/ironbank-note-links/SKILL.md +87 -0
- package/dist/cli.js +2 -1
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +26 -8
- package/dist/commands/new-adr.js +2 -1
- package/dist/commands/sync.js +11 -2
- package/dist/core/deploy-packs.d.ts +11 -0
- package/dist/core/deploy-packs.js +42 -0
- package/dist/core/filesystem.js +9 -1
- package/dist/core/frontmatter.js +10 -9
- package/dist/core/mcp-packs.js +11 -0
- package/dist/generators/adr-generator.js +3 -1
- package/dist/generators/index-generator.d.ts +2 -2
- package/dist/generators/index-generator.js +15 -8
- package/dist/schemas/config.schema.d.ts +0 -10
- package/dist/schemas/config.schema.js +0 -2
- package/dist/validators/adr-validator.js +4 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ pnpm test # Vitest
|
|
|
24
24
|
|
|
25
25
|
| 命令 | 用途 |
|
|
26
26
|
| --- | --- |
|
|
27
|
-
| `meta init` | 初始化 Agent Meta 基础结构(`--yes` 非交互,`--type`,`--with-mcp <pack>`,`--dry-run`) |
|
|
27
|
+
| `meta init` | 初始化 Agent Meta 基础结构(`--yes` 非交互,`--type`,`--with-mcp <pack>`,`--with-deploy <target>`,`--dry-run`) |
|
|
28
28
|
| `meta new-domain <name>` | 创建领域文档目录与局部 `AGENTS.md`(`--title` `--owner` `--with-state-machine` `--with-glossary` `--no-local-agents` `--force` `--dry-run`) |
|
|
29
29
|
| `meta new-adr <slug>` | 自动编号创建 ADR(`--title` `--owner` `--status` `--path` `--dry-run`) |
|
|
30
30
|
| `meta check` | 确定性元数据校验(`--strict` `--docs` `--adr` `--agents` `--openspec` `--json`) |
|
|
@@ -87,8 +87,9 @@ meta check --strict
|
|
|
87
87
|
|
|
88
88
|
| Pack | 安装内容 | 附带 |
|
|
89
89
|
| --- | --- | --- |
|
|
90
|
-
| `ironbank` | MCP:`https://test-ib-mcp.hillinsight.tech/mcp` | skills:ironbank-mcp-reference / ironbank-citations / ironbank-docs-research |
|
|
90
|
+
| `ironbank` | MCP:`https://test-ib-mcp.hillinsight.tech/mcp` | skills:ironbank-mcp-reference / ironbank-citations / ironbank-docs-research / ironbank-note-links;AGENTS.md 规则:文档与 bug 记录在 IronBank,遇到 `hillinsight://ib_note?...` 深链用 MCP 解析,引用笔记时输出同格式链接 |
|
|
91
91
|
| `playwright` | MCP:`npx @playwright/mcp@latest`([microsoft/playwright-mcp](https://github.com/microsoft/playwright-mcp),浏览器自动化) | — |
|
|
92
|
+
| `dev-env` | skills:dev-env(本地环境约定:项目进程本机启动,redis/mysql/mq 等中间件用 docker compose 启动,compose 只声明中间件) | AGENTS.md 规则:中间件依赖走 Docker,绝不本机全局安装 |
|
|
92
93
|
| `superpowers` | Claude Code 插件([obra/superpowers](https://github.com/obra/superpowers),planning/TDD/debugging skills) | AGENTS.md 规则:默认不使用 brainstorming,除非人类明确要求 |
|
|
93
94
|
|
|
94
95
|
```bash
|
|
@@ -97,6 +98,21 @@ meta init --yes --with-mcp ironbank,playwright,superpowers
|
|
|
97
98
|
|
|
98
99
|
安装后请提交 `.mcp.json`、`.claude/settings.json` 和 `.claude/skills/`,团队成员即可获得相同的工具配置。
|
|
99
100
|
|
|
101
|
+
## 部署 skill
|
|
102
|
+
|
|
103
|
+
`meta init` 时可选择部署方式(交互式单选,或 `--with-deploy <target>` 非交互指定),安装对应的部署 skill 到 `.claude/skills/`,并在新生成的 AGENTS.md 里注入部署规则:
|
|
104
|
+
|
|
105
|
+
| Target | 部署方式 | Skill 要点 |
|
|
106
|
+
| --- | --- | --- |
|
|
107
|
+
| `cloudflare` | 本地 wrangler CLI 部署 Workers / Pages | 部署前跑 build+test;secrets 用 `wrangler secret put`;部署后 curl 验证 + `wrangler tail` 排错 |
|
|
108
|
+
| `ansible` | Ansible playbook 部署云主机(ECS/EC2/VPS) | `deploy/ansible/` 目录约定;Windows 或无 ansible 时用 Docker 运行 ansible-playbook;先 `--check` 干跑再实跑 |
|
|
109
|
+
| `github-actions` | GitHub Actions workflow CI/CD | build+test 通过才部署;`gh secret set` 管理 secrets;`gh run watch` 监控 |
|
|
110
|
+
| `none` | 暂时不集成部署(默认) | — |
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
meta init --yes --with-deploy cloudflare
|
|
114
|
+
```
|
|
115
|
+
|
|
100
116
|
## 模板覆盖
|
|
101
117
|
|
|
102
118
|
在项目内创建 `templates/agent-meta/<name>.tmpl` 可覆盖内置模板(仅支持 `{{variable}}` 替换):
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: deploy-ansible
|
|
3
|
+
description: |
|
|
4
|
+
使用 Ansible playbook 将项目部署到云主机(ECS / EC2 / VPS 等 Linux 服务器)。
|
|
5
|
+
Windows 上没有原生 Ansible,必须用 Docker 容器运行 ansible-playbook。
|
|
6
|
+
触发条件:用户要求"部署到服务器 / 云主机 / ECS / VPS"、"ansible 部署"、"上线到主机",
|
|
7
|
+
或要求编写、修改部署 playbook / inventory。执行任何主机部署操作前必须先读取本 skill。
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Deploy to Cloud Hosts(Ansible)
|
|
11
|
+
|
|
12
|
+
## 目录约定
|
|
13
|
+
|
|
14
|
+
```text
|
|
15
|
+
deploy/ansible/
|
|
16
|
+
inventory.ini # 主机清单:IP / 域名、ansible_user
|
|
17
|
+
playbook.yml # 部署 playbook
|
|
18
|
+
group_vars/all.yml # 变量;敏感值用 ansible-vault 加密
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
首次部署且该目录不存在时,按此结构生成模板,并和用户确认:目标主机地址、
|
|
22
|
+
SSH 用户、服务的运行方式(systemd 还是 docker compose)、部署目录。
|
|
23
|
+
|
|
24
|
+
## 运行方式(按平台选择)
|
|
25
|
+
|
|
26
|
+
- **macOS / Linux 且本地已装 ansible**:
|
|
27
|
+
`ansible-playbook -i deploy/ansible/inventory.ini deploy/ansible/playbook.yml`
|
|
28
|
+
- **Windows,或本地没有 ansible**:用 Docker 运行(Windows 下这是唯一方式,
|
|
29
|
+
先用 `docker info` 确认 Docker 正在运行):
|
|
30
|
+
|
|
31
|
+
POSIX shell:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
docker run --rm -it \
|
|
35
|
+
-v "$PWD:/work" -v "$HOME/.ssh:/root/.ssh:ro" -w /work \
|
|
36
|
+
willhallonline/ansible:2.16-alpine \
|
|
37
|
+
ansible-playbook -i deploy/ansible/inventory.ini deploy/ansible/playbook.yml
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
PowerShell(Windows):
|
|
41
|
+
|
|
42
|
+
```powershell
|
|
43
|
+
docker run --rm -it `
|
|
44
|
+
-v "${PWD}:/work" -v "$env:USERPROFILE\.ssh:/root/.ssh:ro" -w /work `
|
|
45
|
+
willhallonline/ansible:2.16-alpine `
|
|
46
|
+
ansible-playbook -i deploy/ansible/inventory.ini deploy/ansible/playbook.yml
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
平台判定:shell 里看 `uname`(无 uname 即 Windows),Node 环境看 `process.platform === "win32"`。
|
|
50
|
+
|
|
51
|
+
## 部署流程
|
|
52
|
+
|
|
53
|
+
1. **连通性**:`ansible all -i deploy/ansible/inventory.ini -m ping`。
|
|
54
|
+
2. **干跑**:`ansible-playbook --check --diff ...`——playbook 有改动时必做,
|
|
55
|
+
把 diff 摘要给用户看。
|
|
56
|
+
3. **实跑**:干跑无误后执行真实部署。
|
|
57
|
+
4. **验证**:curl 健康检查 URL,或 `ansible all -i ... -m shell -a "systemctl status <svc>"`。
|
|
58
|
+
|
|
59
|
+
## Playbook 编写规则
|
|
60
|
+
|
|
61
|
+
- 幂等优先:用 `copy` / `template` / `git` / `systemd` / `docker_compose_v2`
|
|
62
|
+
等模块,不用裸 `shell`/`command`(确需 shell 时加 `creates`/`changed_when`)。
|
|
63
|
+
- 敏感值(数据库密码、token)用 `ansible-vault encrypt` 加密进 group_vars,
|
|
64
|
+
vault 密码不进仓库(用 `--ask-vault-pass` 或本地密码文件)。
|
|
65
|
+
- 多主机滚动发布用 `serial`,避免全量同时重启。
|
|
66
|
+
|
|
67
|
+
## 硬性规则
|
|
68
|
+
|
|
69
|
+
- `--check` 干跑未通过(或未执行)时绝不实跑。
|
|
70
|
+
- 对 production inventory 执行 playbook 前必须得到用户明确确认。
|
|
71
|
+
- 绝不把 SSH 私钥、vault 密码或明文 secret 写入仓库;私钥只读挂载进容器。
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: deploy-cloudflare
|
|
3
|
+
description: |
|
|
4
|
+
使用本地 wrangler CLI 将项目部署到 Cloudflare(Workers / Pages)。
|
|
5
|
+
触发条件:用户要求"部署"、"deploy"、"上线"、"发布到 Cloudflare"、"部署到 CF"、
|
|
6
|
+
"wrangler deploy",或要求配置 Cloudflare 部署环境、管理 Workers secrets、
|
|
7
|
+
创建 D1/KV/R2 资源、查看部署日志。执行任何 Cloudflare 部署相关操作前必须先读取本 skill。
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Deploy to Cloudflare(wrangler CLI)
|
|
11
|
+
|
|
12
|
+
所有部署都通过项目本地的 wrangler CLI 完成,不依赖 CI。
|
|
13
|
+
|
|
14
|
+
## 前置检查(每次部署前依次执行)
|
|
15
|
+
|
|
16
|
+
1. **wrangler 可用**:`wrangler --version` 或 `pnpm exec wrangler --version`。
|
|
17
|
+
不存在时优先项目级安装:`pnpm add -D wrangler`(按项目包管理器换成 npm/yarn)。
|
|
18
|
+
2. **已登录**:`wrangler whoami`。未登录时不要代替用户登录——提示用户自己运行
|
|
19
|
+
`wrangler login`(浏览器 OAuth),或配置 `CLOUDFLARE_API_TOKEN` 环境变量。
|
|
20
|
+
3. **配置存在**:项目根应有 `wrangler.toml` / `wrangler.jsonc`。缺失时先和用户确认
|
|
21
|
+
项目形态(Worker 还是 Pages、入口文件、兼容日期),再生成最小配置。
|
|
22
|
+
4. **构建与测试通过**:先运行项目的 build 和 test 命令(见 AGENTS.md 项目命令),
|
|
23
|
+
任何一步失败立即中止部署。
|
|
24
|
+
|
|
25
|
+
## 部署
|
|
26
|
+
|
|
27
|
+
- Workers:`wrangler deploy`;先验证产物可用 `wrangler deploy --dry-run`。
|
|
28
|
+
- Pages:`wrangler pages deploy <构建输出目录> --project-name <name>`。
|
|
29
|
+
- 多环境:在 wrangler.toml 里定义 `[env.staging]` / `[env.production]`,
|
|
30
|
+
部署用 `wrangler deploy --env staging`。默认先部署 staging 再部署 production。
|
|
31
|
+
|
|
32
|
+
## Secrets 与资源
|
|
33
|
+
|
|
34
|
+
- Secret:`wrangler secret put <NAME>`(值由用户交互输入;绝不写入仓库文件、
|
|
35
|
+
命令行参数或 shell 历史)。多环境时加 `--env`。
|
|
36
|
+
- 普通变量:写在 wrangler.toml 的 `[vars]`。
|
|
37
|
+
- 资源绑定:先创建(`wrangler d1 create` / `wrangler kv namespace create` /
|
|
38
|
+
`wrangler r2 bucket create`),再把返回的 binding/id 写入 wrangler.toml。
|
|
39
|
+
|
|
40
|
+
## 部署后验证(必做)
|
|
41
|
+
|
|
42
|
+
1. 记录部署输出中的 URL 和 version id,反馈给用户。
|
|
43
|
+
2. `curl -sf <部署 URL>` 或项目健康检查端点,确认返回 200。
|
|
44
|
+
3. 行为异常时用 `wrangler tail` 看实时日志定位,再决定是否回滚
|
|
45
|
+
(`wrangler rollback` 或重新部署上一个可用版本)。
|
|
46
|
+
|
|
47
|
+
## 硬性规则
|
|
48
|
+
|
|
49
|
+
- 绝不在 build 或 test 未通过时部署。
|
|
50
|
+
- 绝不把 API token / secret 写入仓库中的任何文件。
|
|
51
|
+
- 部署 production 环境前必须得到用户明确确认。
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: deploy-github-actions
|
|
3
|
+
description: |
|
|
4
|
+
使用 GitHub Actions workflow 部署项目(CI/CD 自动部署)。
|
|
5
|
+
触发条件:用户要求"用 GitHub Actions 部署"、"CI 自动部署"、"push 后自动上线"、
|
|
6
|
+
"配置 deploy workflow",或要求排查部署 workflow 失败。
|
|
7
|
+
执行任何 GitHub Actions 部署相关操作前必须先读取本 skill。
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Deploy via GitHub Actions
|
|
11
|
+
|
|
12
|
+
部署由仓库内的 workflow 完成;本地只负责编写 workflow、配置 secrets、触发和监控,
|
|
13
|
+
统一使用 `gh` CLI。
|
|
14
|
+
|
|
15
|
+
## 前置检查
|
|
16
|
+
|
|
17
|
+
1. `gh auth status` 已登录;`git remote -v` 的 origin 指向 GitHub 仓库。
|
|
18
|
+
2. workflow 文件:`.github/workflows/deploy.yml`。
|
|
19
|
+
|
|
20
|
+
## Workflow 约定
|
|
21
|
+
|
|
22
|
+
`deploy.yml` 不存在时生成模板,结构固定为两个 job:
|
|
23
|
+
|
|
24
|
+
- 触发:`push` 到 main + `workflow_dispatch`(保留手动触发入口)。
|
|
25
|
+
- `build` job:checkout → 安装依赖 → build → test。
|
|
26
|
+
- `deploy` job:`needs: build`,部署步骤由目标平台决定(wrangler action、
|
|
27
|
+
scp/ssh、云厂商 action 等),生成前先和用户确认目标平台。
|
|
28
|
+
- production 部署加 `environment: production`,便于用户在 GitHub 配置
|
|
29
|
+
环境保护规则(人工审批、分支限制)。
|
|
30
|
+
|
|
31
|
+
## Secrets
|
|
32
|
+
|
|
33
|
+
- 写入:`gh secret set <NAME>`(值走交互输入或 stdin 管道,不出现在命令行参数里)。
|
|
34
|
+
- 使用:workflow 中引用 `${{ secrets.NAME }}`。
|
|
35
|
+
- 列出核对:`gh secret list`。
|
|
36
|
+
|
|
37
|
+
## 触发与监控
|
|
38
|
+
|
|
39
|
+
- 手动触发:`gh workflow run deploy.yml`(分支参数 `--ref <branch>`)。
|
|
40
|
+
- push 触发后查看:`gh run list --workflow=deploy.yml -L 3`。
|
|
41
|
+
- 实时跟踪:`gh run watch <run-id>`;结束后向用户报告结论。
|
|
42
|
+
- 失败排查:`gh run view <run-id> --log-failed`,定位失败 step 后修复再触发。
|
|
43
|
+
|
|
44
|
+
## 硬性规则
|
|
45
|
+
|
|
46
|
+
- deploy job 必须 `needs` 通过 build+test 的 job;测试不过不部署。
|
|
47
|
+
- secrets 只存放在 GitHub Actions secrets(或环境 secrets),绝不写入
|
|
48
|
+
workflow 文件或仓库。
|
|
49
|
+
- 修改 workflow 后先用 `workflow_dispatch` 在非 production 环境验证,
|
|
50
|
+
再合入 main 启用自动部署。
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: dev-env
|
|
3
|
+
description: |
|
|
4
|
+
本地开发 / 测试环境启动约定:项目进程直接本机启动,依赖的中间件
|
|
5
|
+
(Redis / MySQL / PostgreSQL / Kafka / RabbitMQ 等)通过 Docker 启动。
|
|
6
|
+
触发条件:用户要求"起本地环境"、"启动开发环境"、"跑集成测试"、
|
|
7
|
+
"起 redis / mysql / mq",或应用报中间件连接失败(Connection refused /
|
|
8
|
+
ECONNREFUSED)需要排查本地依赖时。执行本地环境相关操作前必须先读取本 skill。
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Local Dev Environment(进程本机跑,中间件 Docker 跑)
|
|
12
|
+
|
|
13
|
+
## 原则
|
|
14
|
+
|
|
15
|
+
- 项目进程**永远直接在本机启动**(`pnpm dev` 等,见 AGENTS.md 项目命令):
|
|
16
|
+
热重载、debugger、IDE 集成都保持可用。
|
|
17
|
+
- 中间件依赖一律用 docker compose 启动,**不在本机全局安装**
|
|
18
|
+
(不 `brew install mysql`、不 `apt install redis`)。
|
|
19
|
+
- compose 文件只声明中间件,**绝不包含应用本身**。
|
|
20
|
+
|
|
21
|
+
## 文件约定
|
|
22
|
+
|
|
23
|
+
- 项目根的 `docker-compose.dev.yml`:每个中间件一个 service,数据挂 named
|
|
24
|
+
volume,端口映射到 localhost。
|
|
25
|
+
- 项目具体依赖哪些中间件、端口、应用启动命令:以 AGENTS.md 项目命令段和
|
|
26
|
+
`docs/engineering/testing.md` 为准,本 skill 只定义通用流程。
|
|
27
|
+
- 文件不存在时:先从 AGENTS.md / README / `.env.example` / 配置文件识别项目
|
|
28
|
+
依赖哪些中间件,生成 compose 模板,并和用户确认版本与端口后再落盘。
|
|
29
|
+
|
|
30
|
+
## 启动流程
|
|
31
|
+
|
|
32
|
+
1. `docker info` 确认 Docker 正在运行。
|
|
33
|
+
2. `docker compose -f docker-compose.dev.yml up -d`
|
|
34
|
+
3. 等依赖就绪再起应用:`docker compose ps` 看 `healthy`;没有 healthcheck 的
|
|
35
|
+
service 用端口探测(`nc -z localhost <port>`)。MySQL/PostgreSQL 首次启动
|
|
36
|
+
有初始化耗时,连接被拒时先等再重试,不要急着改配置。
|
|
37
|
+
4. 本机启动应用(AGENTS.md 项目命令)。
|
|
38
|
+
5. 跑测试。
|
|
39
|
+
|
|
40
|
+
## 日常操作
|
|
41
|
+
|
|
42
|
+
- 状态:`docker compose -f docker-compose.dev.yml ps`
|
|
43
|
+
- 单个中间件日志:`docker compose -f docker-compose.dev.yml logs -f redis`
|
|
44
|
+
- 停止:`docker compose -f docker-compose.dev.yml down`(保留数据卷)
|
|
45
|
+
- 重置数据:`down -v`(会删数据,执行前必须用户确认)
|
|
46
|
+
- 端口冲突:`lsof -i :<port>` 找到占用方,和用户确认后再调整端口映射。
|
|
47
|
+
|
|
48
|
+
## 硬性规则
|
|
49
|
+
|
|
50
|
+
- 绝不在本机全局安装中间件;缺什么就加进 compose。
|
|
51
|
+
- `down -v` 或删除数据卷前必须得到用户确认。
|
|
52
|
+
- compose 里的账号密码只用于本地开发,绝不复用任何真实环境的凭据。
|
|
@@ -113,6 +113,8 @@ SOURCE_2: { id: "doc_id_yyy", title: "另一个文档", snippet: "相关内容..
|
|
|
113
113
|
> **必须使用 Markdown 列表格式(每条前加 `- `)**,确保渲染时正确换行。
|
|
114
114
|
> 每条摘要**控制在 20 字以内**。
|
|
115
115
|
> 每次回答的引用来源**最多 8 条**,优先保留最相关的来源。
|
|
116
|
+
> 来源是 IronBank **笔记**且 portal / task_id / note_id 已知时,标题写成深链形式(见 `ironbank-note-links` skill):
|
|
117
|
+
> `- ¹ [笔记标题](hillinsight://ib_note?portal=...&task=...&id=...&view=detail) — "摘要"`,让来源可点击打开。
|
|
116
118
|
|
|
117
119
|
### 标准回答格式
|
|
118
120
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
name: ironbank-docs-
|
|
2
|
+
name: ironbank-docs-research
|
|
3
3
|
description: 在 IronBank Docs(网盘 / 知识库文件)或 IronBank Notes(笔记 + 笔记附件)中做深度检索与证据收集时必须使用此 skill。触发场景包括:用户要求"去文档里找""搜一下 docs / notes""看看某份文档讲了什么""帮我搜笔记内容""确认文档 / 笔记里有没有某个细节""提取某个表格/段落/原文"。本 skill 覆盖两组并列工具——docs 四件套(全局搜索、文档摘要、单文档检索、chunk 精读)与 notes 三件套(全局搜索、单文档检索、chunk 精读,没有 summary),并给出它们之间的协作策略与选用规则。
|
|
4
4
|
---
|
|
5
5
|
|
|
@@ -9,6 +9,7 @@ description: IronBank MCP 强制参考指南。只要请求涉及 IronBank,或
|
|
|
9
9
|
|
|
10
10
|
- 只要将要调用任意 `ironbank_*` 工具,必须先使用本 skill,不允许跳过。
|
|
11
11
|
- 只要用户提到 IronBank / ironbank mcp,或提到看板(board)、任务(task)、最近任务(recent task)、任务关系(relation/mention)、频道(channel)、动态(activity)、笔记(note)、评论(comment)、附件(attachment)、`portal`、`fileId` 中任一关键词,默认触发本 skill。
|
|
12
|
+
- 上下文中出现 `hillinsight://ib_note?...` 深链时,先加载 `ironbank-note-links` skill 解析出 `portal` / `task_id` / `note_id`,再按本 skill 的参数约定调用工具(链接已带齐三个 ID,可直接调 `ironbank_get_note_detail`,无需逐级定位)。
|
|
12
13
|
- 下载附件时,必须先调用 `ironbank_get_file_download_info` 获取 `download_url`、`file_key`、`digest`、`size`,再调用 `decrypt-attachment` skill 解密。
|
|
13
14
|
- 如果不确定是否属于 IronBank 场景,按“属于”处理,先触发本 skill。
|
|
14
15
|
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ironbank-note-links
|
|
3
|
+
description: |
|
|
4
|
+
解析与生成 IronBank 笔记深链(`hillinsight://ib_note?...`)。本项目的文档和 bug 都记录在 IronBank 中,仓库内文档(docs/、ADR、openspec、issue、commit message)以及与 LLM 的对话里,统一用这种链接引用 IronBank 笔记。触发条件(任一命中即必须使用本 skill):
|
|
5
|
+
- 上下文中出现 `hillinsight://` 开头的链接(用户消息、文件内容、搜索结果中均算)
|
|
6
|
+
- 用户要求"打开/看看/读一下这个链接"且链接是 hillinsight 深链
|
|
7
|
+
- 需要在项目文档或回答中引用某条 IronBank 笔记(生成链接)
|
|
8
|
+
- 用户提到"把这个 bug/文档记到 IronBank 并留个链接"
|
|
9
|
+
解析链接需要 IronBank MCP(ironbank_* 工具),调用前先加载 ironbank-mcp-reference skill。
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# IronBank 笔记深链(hillinsight://)解析与生成
|
|
13
|
+
|
|
14
|
+
## 背景
|
|
15
|
+
|
|
16
|
+
本项目的项目文档和 bug 记录都放在 IronBank。仓库内引用这些内容时,不复制正文,而是留一条深链:
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
hillinsight://ib_note?portal=<portal>&task=<task_id>&id=<note_id>&view=detail
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
示例:
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
hillinsight://ib_note?portal=v9owxx8o&task=83a5a825-d9a7-4114-ad63-08c26fb1a3b8&id=5a14a6da-210a-4d1c-8033-785d93412b47&view=detail
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
人类在 HLink / hillinsight 客户端里点击即可打开;Agent 遇到时必须通过 IronBank MCP 把正文读出来,**不允许**因为链接协议陌生而跳过、也不允许猜测其内容。
|
|
29
|
+
|
|
30
|
+
## 链接结构 → MCP 参数映射
|
|
31
|
+
|
|
32
|
+
| Query 参数 | 含义 | 对应 MCP 参数 |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| `portal` | 工作空间(多租户) | `portal` |
|
|
35
|
+
| `task` | 笔记所属任务 ID | `task_id` |
|
|
36
|
+
| `id` | 笔记 ID | `note_id` |
|
|
37
|
+
| `view` | 客户端视图(通常 `detail`) | 无需传给 MCP,忽略 |
|
|
38
|
+
|
|
39
|
+
三个 ID 一条链接全带齐了,**不需要**再走 board → task → channel 的逐级定位流程。
|
|
40
|
+
|
|
41
|
+
## 流程一:解析(读)一条链接
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
Step 1 从链接中提取 portal / task / id 三个参数
|
|
45
|
+
Step 2 ironbank_get_note_detail → note_id=id, task_id=task, portal=portal
|
|
46
|
+
(返回 Markdown 正文 + 附件 + 元数据)
|
|
47
|
+
Step 3 如需讨论上下文:ironbank_get_note_comments → note_id, portal
|
|
48
|
+
Step 4 如正文含 `fileId:` 附件链接且用户需要附件内容:
|
|
49
|
+
ironbank_get_file_download_info → 再交给 decrypt-attachment skill 解密
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
注意:
|
|
53
|
+
|
|
54
|
+
- 一次出现多条链接时,可**并行**调用 `ironbank_get_note_detail`
|
|
55
|
+
- 返回 `coverage: "partial"` 时要说明部分内容因权限被隐藏,不能当完整事实
|
|
56
|
+
- 403 / 404 时如实告知链接不可访问或已删除,不要编造内容
|
|
57
|
+
|
|
58
|
+
## 流程二:生成(写)一条链接
|
|
59
|
+
|
|
60
|
+
当需要在项目文档、ADR、openspec change、bug 记录或回答中引用某条 IronBank 笔记时:
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
Step 1 确认该笔记的 portal、task_id、note_id
|
|
64
|
+
(来自 ironbank_get_notes / ironbank_list_recent_notes / ironbank_get_note_detail
|
|
65
|
+
/ ironbank_create_note 的返回,或用户提供)
|
|
66
|
+
Step 2 按固定格式拼接:
|
|
67
|
+
hillinsight://ib_note?portal=<portal>&task=<task_id>&id=<note_id>&view=detail
|
|
68
|
+
Step 3 在 Markdown 中带上笔记标题作为可读文本:
|
|
69
|
+
[笔记标题](hillinsight://ib_note?portal=...&task=...&id=...&view=detail)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
规则:
|
|
73
|
+
|
|
74
|
+
- **只对真实存在的笔记生成链接**——三个 ID 必须来自工具返回或用户输入,禁止拼造
|
|
75
|
+
- 参数顺序固定为 `portal` → `task` → `id` → `view`,`view` 固定填 `detail`
|
|
76
|
+
- 在 docs/、ADR、openspec 里引用 IronBank 内容时,优先留链接而不是复制大段正文;需要摘录时附上链接作为出处
|
|
77
|
+
- 用 `ironbank_create_note` 新建了 bug / 文档笔记后,把生成的链接回填到相关文档或回复里
|
|
78
|
+
|
|
79
|
+
## 与其他 skill 的配合
|
|
80
|
+
|
|
81
|
+
- 调用任何 `ironbank_*` 工具前,先加载 **ironbank-mcp-reference**(工具参数与层级模型)
|
|
82
|
+
- 基于链接解析出的内容作答时,按 **ironbank-citations** 的角标格式标注来源;来源为笔记时,来源列表中的标题直接使用上面的深链 Markdown 链接
|
|
83
|
+
- 需要在笔记正文里做语义定位时,转 **ironbank-docs-research** 的 notes 链路(`ironbank_notes_search_single` 等)
|
|
84
|
+
|
|
85
|
+
## MCP 不可用时
|
|
86
|
+
|
|
87
|
+
如果 `ironbank_*` 工具不存在或未连接,不要假装解析:明确告知用户 IronBank MCP 未安装 / 未连接(处理方式见 ironbank-mcp-reference 的「MCP 连接检查」),并原样保留链接。
|
package/dist/cli.js
CHANGED
|
@@ -51,6 +51,7 @@ program
|
|
|
51
51
|
.option("-y, --yes", "non-interactive mode, accept all defaults")
|
|
52
52
|
.option("--type <type>", "project type: node | monorepo | nextjs | cocos | backend | other")
|
|
53
53
|
.option("--with-mcp <pack>", "install an MCP pack (.mcp.json + .claude/skills/), e.g. ironbank")
|
|
54
|
+
.option("--with-deploy <target>", "install a deploy skill: cloudflare | ansible | github-actions | none")
|
|
54
55
|
.option("--dry-run", "show what would be created without writing files")
|
|
55
56
|
.action(async (opts) => {
|
|
56
57
|
try {
|
|
@@ -222,7 +223,7 @@ configCmd
|
|
|
222
223
|
configCmd
|
|
223
224
|
.command("set")
|
|
224
225
|
.description("Set a configuration value, e.g. meta config set defaults.agents_max_lines 220")
|
|
225
|
-
.argument("<key>", "dot-separated key, e.g.
|
|
226
|
+
.argument("<key>", "dot-separated key, e.g. validation.stale_review_days.active")
|
|
226
227
|
.argument("<value>", "value")
|
|
227
228
|
.action((key, value) => {
|
|
228
229
|
try {
|
package/dist/commands/init.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ export interface InitOptions {
|
|
|
3
3
|
type?: string;
|
|
4
4
|
/** Comma-separated MCP pack names to install, e.g. "ironbank,playwright". */
|
|
5
5
|
withMcp?: string;
|
|
6
|
+
/** Deployment integration: cloudflare | ansible | github-actions | none. */
|
|
7
|
+
withDeploy?: string;
|
|
6
8
|
dryRun?: boolean;
|
|
7
9
|
}
|
|
8
10
|
export declare function detectProjectType(root: string): string;
|
package/dist/commands/init.js
CHANGED
|
@@ -11,6 +11,7 @@ import { CONFIG_FILENAME, configExists } from "../core/config.js";
|
|
|
11
11
|
import { padAdrNumber } from "../generators/adr-generator.js";
|
|
12
12
|
import { installMcpPack } from "../generators/mcp-generator.js";
|
|
13
13
|
import { getMcpPack, packServerSummary, MCP_PACKS } from "../core/mcp-packs.js";
|
|
14
|
+
import { getDeployPack, DEPLOY_PACKS } from "../core/deploy-packs.js";
|
|
14
15
|
import { EXIT_CODES, MetaError } from "../core/errors.js";
|
|
15
16
|
const PROJECT_TYPES = ["node", "monorepo", "nextjs", "cocos", "backend", "other"];
|
|
16
17
|
export function detectProjectType(root) {
|
|
@@ -89,7 +90,24 @@ export async function runInit(root, opts) {
|
|
|
89
90
|
});
|
|
90
91
|
mcpPacks = picks.map(getMcpPack);
|
|
91
92
|
}
|
|
92
|
-
|
|
93
|
+
// Deployment integration: installs a deploy skill into .claude/skills/.
|
|
94
|
+
let deployTarget = opts.withDeploy?.trim();
|
|
95
|
+
if (deployTarget && deployTarget !== "none") {
|
|
96
|
+
getDeployPack(deployTarget); // fail fast on unknown targets, before any write
|
|
97
|
+
}
|
|
98
|
+
if (!deployTarget && !opts.yes) {
|
|
99
|
+
deployTarget = await select({
|
|
100
|
+
message: "部署方式(安装对应的部署 skill,可稍后再加):",
|
|
101
|
+
choices: [
|
|
102
|
+
...Object.entries(DEPLOY_PACKS).map(([value, p]) => ({ name: p.title, value })),
|
|
103
|
+
{ name: "暂时不集成部署", value: "none" },
|
|
104
|
+
],
|
|
105
|
+
default: "none",
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
const deployPack = deployTarget && deployTarget !== "none" ? getDeployPack(deployTarget) : null;
|
|
109
|
+
const allPacks = deployPack ? [...mcpPacks, deployPack] : mcpPacks;
|
|
110
|
+
const packRules = allPacks.flatMap((p) => p.agentRules ?? []);
|
|
93
111
|
// Existing AGENTS.md handling.
|
|
94
112
|
const agentsRel = DEFAULT_CONFIG.paths.root_agents;
|
|
95
113
|
let agentsAction = writer.exists(agentsRel) ? "keep" : "create";
|
|
@@ -155,8 +173,8 @@ export async function runInit(root, opts) {
|
|
|
155
173
|
// 4. First ADR
|
|
156
174
|
const adrNumber = padAdrNumber(1, DEFAULT_CONFIG.defaults.adr_padding);
|
|
157
175
|
writer.write(`docs/adr/${adrNumber}-repository-conventions.md`, renderNamedTemplate("adr-0001.md", root, templatesDir, { adr_number: adrNumber, owner, date }));
|
|
158
|
-
// 5. Optional MCP packs
|
|
159
|
-
for (const pack of
|
|
176
|
+
// 5. Optional MCP packs and deploy skill
|
|
177
|
+
for (const pack of allPacks)
|
|
160
178
|
installMcpPack(writer, root, pack);
|
|
161
179
|
// Report
|
|
162
180
|
if (dryRun) {
|
|
@@ -193,14 +211,14 @@ export async function runInit(root, opts) {
|
|
|
193
211
|
if (fs.existsSync(path.join(root, DEFAULT_CONFIG.paths.openspec))) {
|
|
194
212
|
logger.ok(`OpenSpec detected at ${DEFAULT_CONFIG.paths.openspec}/`);
|
|
195
213
|
}
|
|
196
|
-
for (const pack of
|
|
214
|
+
for (const pack of allPacks) {
|
|
197
215
|
logger.ok(`Pack '${pack.name}' installed: ${packServerSummary(pack)}`);
|
|
198
216
|
}
|
|
199
|
-
if (
|
|
217
|
+
if (allPacks.length > 0) {
|
|
200
218
|
const artifacts = [
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
219
|
+
allPacks.some((p) => p.server) ? ".mcp.json" : "",
|
|
220
|
+
allPacks.some((p) => p.settings) ? ".claude/settings.json" : "",
|
|
221
|
+
allPacks.some((p) => p.hasSkills) ? ".claude/skills/" : "",
|
|
204
222
|
].filter(Boolean);
|
|
205
223
|
logger.info(` Commit ${artifacts.join(", ")} so the whole team gets the same tools.`);
|
|
206
224
|
}
|
package/dist/commands/new-adr.js
CHANGED
|
@@ -46,7 +46,8 @@ export function runNewAdr(root, name, opts) {
|
|
|
46
46
|
throw new MetaError(e.message, EXIT_CODES.CHECK_FAILED);
|
|
47
47
|
}
|
|
48
48
|
if (config.features.generate_indexes && !opts.dryRun) {
|
|
49
|
-
|
|
49
|
+
// Keep the index next to the ADRs, including when --path overrides the directory.
|
|
50
|
+
updateAdrIndex(writer, root, config, relDir);
|
|
50
51
|
}
|
|
51
52
|
if (opts.dryRun) {
|
|
52
53
|
logger.info("Would create:");
|
package/dist/commands/sync.js
CHANGED
|
@@ -5,6 +5,7 @@ import { FileWriter } from "../core/filesystem.js";
|
|
|
5
5
|
import { logger } from "../core/logger.js";
|
|
6
6
|
import { loadConfig } from "../core/config.js";
|
|
7
7
|
import { updateAdrIndex, updateDomainIndex } from "../generators/index-generator.js";
|
|
8
|
+
import { EXIT_CODES, MetaError } from "../core/errors.js";
|
|
8
9
|
export function runSync(root, opts) {
|
|
9
10
|
const { config } = loadConfig(root);
|
|
10
11
|
const all = !opts.indexes && !opts.openspec && !opts.templates;
|
|
@@ -35,8 +36,16 @@ export function runSync(root, opts) {
|
|
|
35
36
|
execSync("openspec update", { cwd: root, stdio: "inherit" });
|
|
36
37
|
logger.ok("OpenSpec updated");
|
|
37
38
|
}
|
|
38
|
-
catch {
|
|
39
|
-
|
|
39
|
+
catch (e) {
|
|
40
|
+
const err = e;
|
|
41
|
+
// ENOENT / exit 127: the binary is missing. Anything else is a real
|
|
42
|
+
// failure of an installed CLI and must fail the sync.
|
|
43
|
+
if (err.code === "ENOENT" || err.status === 127) {
|
|
44
|
+
logger.warn("Could not run `openspec update` (is the OpenSpec CLI installed?). Skipped.");
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
throw new MetaError(`\`openspec update\` failed${typeof err.status === "number" ? ` with exit code ${err.status}` : ""}. Fix the OpenSpec errors above and re-run \`meta sync\`.`, EXIT_CODES.CHECK_FAILED);
|
|
48
|
+
}
|
|
40
49
|
}
|
|
41
50
|
}
|
|
42
51
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { McpPack } from "./mcp-packs.js";
|
|
2
|
+
/**
|
|
3
|
+
* Deployment packs are skills-only packs (no MCP server, no plugin settings):
|
|
4
|
+
* installing one copies a deploy skill into `.claude/skills/` and appends its
|
|
5
|
+
* working rules to a freshly generated AGENTS.md. They reuse the McpPack
|
|
6
|
+
* shape so `installMcpPack` / `listPackSkillFiles` work unchanged.
|
|
7
|
+
*/
|
|
8
|
+
export declare const DEPLOY_PACKS: Record<string, McpPack>;
|
|
9
|
+
/** Valid values for `--with-deploy`; 'none' means skip deployment integration. */
|
|
10
|
+
export declare const DEPLOY_TARGETS: string[];
|
|
11
|
+
export declare function getDeployPack(target: string): McpPack;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { EXIT_CODES, MetaError } from "./errors.js";
|
|
2
|
+
/**
|
|
3
|
+
* Deployment packs are skills-only packs (no MCP server, no plugin settings):
|
|
4
|
+
* installing one copies a deploy skill into `.claude/skills/` and appends its
|
|
5
|
+
* working rules to a freshly generated AGENTS.md. They reuse the McpPack
|
|
6
|
+
* shape so `installMcpPack` / `listPackSkillFiles` work unchanged.
|
|
7
|
+
*/
|
|
8
|
+
export const DEPLOY_PACKS = {
|
|
9
|
+
cloudflare: {
|
|
10
|
+
name: "deploy-cloudflare",
|
|
11
|
+
title: "Cloudflare(本地 wrangler CLI)",
|
|
12
|
+
hasSkills: true,
|
|
13
|
+
agentRules: [
|
|
14
|
+
"Deploy via the deploy-cloudflare skill (local wrangler CLI). Run the project's build and tests before any deploy; manage secrets with `wrangler secret put`, never commit them.",
|
|
15
|
+
],
|
|
16
|
+
},
|
|
17
|
+
ansible: {
|
|
18
|
+
name: "deploy-ansible",
|
|
19
|
+
title: "云主机(Ansible playbook;Windows 下用 Docker 运行 Ansible)",
|
|
20
|
+
hasSkills: true,
|
|
21
|
+
agentRules: [
|
|
22
|
+
"Deploy to cloud hosts via the deploy-ansible skill (ansible-playbook; on Windows run Ansible through Docker). Always dry-run with `--check` first; keep secrets in ansible-vault.",
|
|
23
|
+
],
|
|
24
|
+
},
|
|
25
|
+
"github-actions": {
|
|
26
|
+
name: "deploy-github-actions",
|
|
27
|
+
title: "GitHub Workflow(GitHub Actions CI/CD)",
|
|
28
|
+
hasSkills: true,
|
|
29
|
+
agentRules: [
|
|
30
|
+
"Deploy via the deploy-github-actions skill (GitHub Actions). The deploy job must depend on passing build and tests; repository secrets belong in GitHub Actions secrets, never in workflow files.",
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
/** Valid values for `--with-deploy`; 'none' means skip deployment integration. */
|
|
35
|
+
export const DEPLOY_TARGETS = [...Object.keys(DEPLOY_PACKS), "none"];
|
|
36
|
+
export function getDeployPack(target) {
|
|
37
|
+
const pack = DEPLOY_PACKS[target];
|
|
38
|
+
if (!pack) {
|
|
39
|
+
throw new MetaError(`Unknown deploy target '${target}'. Valid targets: ${DEPLOY_TARGETS.join(", ")}`, EXIT_CODES.BAD_ARGS);
|
|
40
|
+
}
|
|
41
|
+
return pack;
|
|
42
|
+
}
|
package/dist/core/filesystem.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { EXIT_CODES, MetaError } from "./errors.js";
|
|
3
4
|
/**
|
|
4
5
|
* All file writes go through this class so that --dry-run can report the
|
|
5
6
|
* exact set of files that would be created or updated without touching disk.
|
|
@@ -16,7 +17,14 @@ export class FileWriter {
|
|
|
16
17
|
return path.isAbsolute(p) ? path.relative(this.root, p) : p;
|
|
17
18
|
}
|
|
18
19
|
abs(p) {
|
|
19
|
-
|
|
20
|
+
const abs = path.resolve(this.root, p);
|
|
21
|
+
const root = path.resolve(this.root);
|
|
22
|
+
// Paths come from user config (paths.*) and CLI flags; never let a
|
|
23
|
+
// `../`-style value escape the project root.
|
|
24
|
+
if (abs !== root && !abs.startsWith(root + path.sep)) {
|
|
25
|
+
throw new MetaError(`Path '${p}' resolves outside the project root. Check paths.* in agent-meta.config.yaml.`, EXIT_CODES.BAD_ARGS);
|
|
26
|
+
}
|
|
27
|
+
return abs;
|
|
20
28
|
}
|
|
21
29
|
exists(p) {
|
|
22
30
|
return fs.existsSync(this.abs(p));
|
package/dist/core/frontmatter.js
CHANGED
|
@@ -91,6 +91,7 @@ export function sectionHasContent(content, names) {
|
|
|
91
91
|
let capturing = false;
|
|
92
92
|
let sectionLevel = 0;
|
|
93
93
|
let inFence = false;
|
|
94
|
+
const captured = [];
|
|
94
95
|
for (const line of lines) {
|
|
95
96
|
if (/^\s*(```|~~~)/.test(line))
|
|
96
97
|
inFence = !inFence;
|
|
@@ -106,14 +107,14 @@ export function sectionHasContent(content, names) {
|
|
|
106
107
|
}
|
|
107
108
|
continue;
|
|
108
109
|
}
|
|
109
|
-
if (
|
|
110
|
-
|
|
111
|
-
const text = line
|
|
112
|
-
.replace(/<!--[\s\S]*?-->/g, "")
|
|
113
|
-
.replace(/^[-*+]\s*/, "")
|
|
114
|
-
.trim();
|
|
115
|
-
if (text.length > 0 && !/^<!--/.test(line.trim()))
|
|
116
|
-
return true;
|
|
110
|
+
if (capturing)
|
|
111
|
+
captured.push(line);
|
|
117
112
|
}
|
|
118
|
-
|
|
113
|
+
// Strip comments across line boundaries (like extractSection does), so a
|
|
114
|
+
// comment spanning multiple lines never counts as content.
|
|
115
|
+
return captured
|
|
116
|
+
.join("\n")
|
|
117
|
+
.replace(/<!--[\s\S]*?-->/g, "")
|
|
118
|
+
.split(/\r?\n/)
|
|
119
|
+
.some((line) => line.replace(/^[-*+]\s*/, "").trim().length > 0);
|
|
119
120
|
}
|
package/dist/core/mcp-packs.js
CHANGED
|
@@ -8,6 +8,9 @@ export const MCP_PACKS = {
|
|
|
8
8
|
title: "IronBank MCP (hillinsight)",
|
|
9
9
|
server: { type: "http", url: "https://test-ib-mcp.hillinsight.tech/mcp" },
|
|
10
10
|
hasSkills: true,
|
|
11
|
+
agentRules: [
|
|
12
|
+
"Project docs and bug reports live in IronBank and are referenced by `hillinsight://ib_note?...` deep links. When you encounter one, resolve it with the IronBank MCP (see the ironbank-note-links skill) instead of skipping it; when citing IronBank notes in docs or replies, emit links in the same format.",
|
|
13
|
+
],
|
|
11
14
|
},
|
|
12
15
|
playwright: {
|
|
13
16
|
name: "playwright",
|
|
@@ -15,6 +18,14 @@ export const MCP_PACKS = {
|
|
|
15
18
|
server: { command: "npx", args: ["@playwright/mcp@latest"] },
|
|
16
19
|
hasSkills: false,
|
|
17
20
|
},
|
|
21
|
+
"dev-env": {
|
|
22
|
+
name: "dev-env",
|
|
23
|
+
title: "本地开发环境约定(进程本机跑,redis/mysql/mq 用 Docker)",
|
|
24
|
+
hasSkills: true,
|
|
25
|
+
agentRules: [
|
|
26
|
+
"Local dev/test environment: run the project process directly on the host; start middleware dependencies (redis/mysql/mq) with Docker via the dev-env skill. Never install middleware globally on the host.",
|
|
27
|
+
],
|
|
28
|
+
},
|
|
18
29
|
superpowers: {
|
|
19
30
|
name: "superpowers",
|
|
20
31
|
title: "Superpowers (obra/superpowers, Claude Code plugin: planning/TDD/debugging skills)",
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
|
|
3
|
+
// Any digit count, so files generated under a custom defaults.adr_padding
|
|
4
|
+
// (schema minimum: 1) are recognized by numbering, indexing and validation.
|
|
5
|
+
export const ADR_FILE_RE = /^(\d+)-(.+)\.md$/;
|
|
4
6
|
export function listAdrFiles(adrDirAbs) {
|
|
5
7
|
if (!fs.existsSync(adrDirAbs))
|
|
6
8
|
return [];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { MetaConfig } from "../schemas/config.schema.js";
|
|
2
2
|
import type { FileWriter } from "../core/filesystem.js";
|
|
3
3
|
export declare function buildDomainIndexTable(root: string, config: MetaConfig): string;
|
|
4
|
-
export declare function buildAdrIndexTable(root: string, config: MetaConfig): string;
|
|
4
|
+
export declare function buildAdrIndexTable(root: string, config: MetaConfig, relDir?: string): string;
|
|
5
5
|
export declare function updateDomainIndex(writer: FileWriter, root: string, config: MetaConfig): void;
|
|
6
|
-
export declare function updateAdrIndex(writer: FileWriter, root: string, config: MetaConfig): void;
|
|
6
|
+
export declare function updateAdrIndex(writer: FileWriter, root: string, config: MetaConfig, relDir?: string): void;
|
|
@@ -5,15 +5,22 @@ import { dateValueToString } from "../core/paths.js";
|
|
|
5
5
|
import { listAdrFiles } from "./adr-generator.js";
|
|
6
6
|
import { renderNamedTemplate } from "../core/templates.js";
|
|
7
7
|
import { todayISO } from "../core/paths.js";
|
|
8
|
+
import { EXIT_CODES, MetaError } from "../core/errors.js";
|
|
8
9
|
const DOMAIN_MARKERS = ["<!-- meta:domain-index:start -->", "<!-- meta:domain-index:end -->"];
|
|
9
10
|
const ADR_MARKERS = ["<!-- meta:adr-index:start -->", "<!-- meta:adr-index:end -->"];
|
|
10
|
-
function replaceBetweenMarkers(content, [start, end], body) {
|
|
11
|
+
function replaceBetweenMarkers(content, [start, end], body, relPath) {
|
|
11
12
|
const startIdx = content.indexOf(start);
|
|
12
13
|
const endIdx = content.indexOf(end);
|
|
13
|
-
if (startIdx === -1
|
|
14
|
+
if (startIdx === -1 && endIdx === -1) {
|
|
14
15
|
// No markers: append a managed section at the end.
|
|
15
16
|
return `${content.trimEnd()}\n\n${start}\n${body}\n${end}\n`;
|
|
16
17
|
}
|
|
18
|
+
if (startIdx === -1 || endIdx === -1 || endIdx < startIdx) {
|
|
19
|
+
// Exactly one marker (or reversed order): regenerating here could swallow
|
|
20
|
+
// user content between the orphan marker and a freshly appended section.
|
|
21
|
+
throw new MetaError(`Index markers in ${relPath} are malformed: expected '${start}' followed by '${end}'. ` +
|
|
22
|
+
`Restore both markers (in order), or remove both to let the index be re-appended.`, EXIT_CODES.CHECK_FAILED);
|
|
23
|
+
}
|
|
17
24
|
return (content.slice(0, startIdx + start.length) + "\n" + body + "\n" + content.slice(endIdx));
|
|
18
25
|
}
|
|
19
26
|
function frontmatterStr(data, key) {
|
|
@@ -52,8 +59,8 @@ export function buildDomainIndexTable(root, config) {
|
|
|
52
59
|
...rows,
|
|
53
60
|
].join("\n");
|
|
54
61
|
}
|
|
55
|
-
export function buildAdrIndexTable(root, config) {
|
|
56
|
-
const adrDir = path.join(root,
|
|
62
|
+
export function buildAdrIndexTable(root, config, relDir = config.paths.adr_docs) {
|
|
63
|
+
const adrDir = path.join(root, relDir);
|
|
57
64
|
const rows = listAdrFiles(adrDir).map((info) => {
|
|
58
65
|
const doc = parseDocFile(info.file);
|
|
59
66
|
const title = frontmatterStr(doc.data, "title") || info.slug;
|
|
@@ -74,14 +81,14 @@ function updateIndexFile(writer, relPath, markers, table, templateName, root, co
|
|
|
74
81
|
date: todayISO(),
|
|
75
82
|
});
|
|
76
83
|
}
|
|
77
|
-
const updated = replaceBetweenMarkers(content, markers, table);
|
|
84
|
+
const updated = replaceBetweenMarkers(content, markers, table, relPath);
|
|
78
85
|
writer.write(relPath, updated, { overwrite: true });
|
|
79
86
|
}
|
|
80
87
|
export function updateDomainIndex(writer, root, config) {
|
|
81
88
|
const rel = path.join(config.paths.domain_docs, "README.md");
|
|
82
89
|
updateIndexFile(writer, rel, DOMAIN_MARKERS, buildDomainIndexTable(root, config), "domain-index-readme.md", root, config);
|
|
83
90
|
}
|
|
84
|
-
export function updateAdrIndex(writer, root, config) {
|
|
85
|
-
const rel = path.join(
|
|
86
|
-
updateIndexFile(writer, rel, ADR_MARKERS, buildAdrIndexTable(root, config), "adr-index-readme.md", root, config);
|
|
91
|
+
export function updateAdrIndex(writer, root, config, relDir = config.paths.adr_docs) {
|
|
92
|
+
const rel = path.join(relDir, "README.md");
|
|
93
|
+
updateIndexFile(writer, rel, ADR_MARKERS, buildAdrIndexTable(root, config, relDir), "adr-index-readme.md", root, config);
|
|
87
94
|
}
|
|
@@ -38,20 +38,14 @@ export declare const configSchema: z.ZodObject<{
|
|
|
38
38
|
templates?: string | undefined;
|
|
39
39
|
}>>;
|
|
40
40
|
defaults: z.ZodDefault<z.ZodObject<{
|
|
41
|
-
doc_status: z.ZodDefault<z.ZodString>;
|
|
42
|
-
review_interval_days: z.ZodDefault<z.ZodNumber>;
|
|
43
41
|
adr_padding: z.ZodDefault<z.ZodNumber>;
|
|
44
42
|
agents_max_lines: z.ZodDefault<z.ZodNumber>;
|
|
45
43
|
local_agents_max_lines: z.ZodDefault<z.ZodNumber>;
|
|
46
44
|
}, "strip", z.ZodTypeAny, {
|
|
47
|
-
doc_status: string;
|
|
48
|
-
review_interval_days: number;
|
|
49
45
|
adr_padding: number;
|
|
50
46
|
agents_max_lines: number;
|
|
51
47
|
local_agents_max_lines: number;
|
|
52
48
|
}, {
|
|
53
|
-
doc_status?: string | undefined;
|
|
54
|
-
review_interval_days?: number | undefined;
|
|
55
49
|
adr_padding?: number | undefined;
|
|
56
50
|
agents_max_lines?: number | undefined;
|
|
57
51
|
local_agents_max_lines?: number | undefined;
|
|
@@ -141,8 +135,6 @@ export declare const configSchema: z.ZodObject<{
|
|
|
141
135
|
templates: string;
|
|
142
136
|
};
|
|
143
137
|
defaults: {
|
|
144
|
-
doc_status: string;
|
|
145
|
-
review_interval_days: number;
|
|
146
138
|
adr_padding: number;
|
|
147
139
|
agents_max_lines: number;
|
|
148
140
|
local_agents_max_lines: number;
|
|
@@ -184,8 +176,6 @@ export declare const configSchema: z.ZodObject<{
|
|
|
184
176
|
templates?: string | undefined;
|
|
185
177
|
} | undefined;
|
|
186
178
|
defaults?: {
|
|
187
|
-
doc_status?: string | undefined;
|
|
188
|
-
review_interval_days?: number | undefined;
|
|
189
179
|
adr_padding?: number | undefined;
|
|
190
180
|
agents_max_lines?: number | undefined;
|
|
191
181
|
local_agents_max_lines?: number | undefined;
|
|
@@ -33,8 +33,6 @@ export const configSchema = z.object({
|
|
|
33
33
|
.default({}),
|
|
34
34
|
defaults: z
|
|
35
35
|
.object({
|
|
36
|
-
doc_status: z.string().default("active"),
|
|
37
|
-
review_interval_days: z.number().default(180),
|
|
38
36
|
adr_padding: z.number().int().min(1).default(4),
|
|
39
37
|
agents_max_lines: z.number().default(180),
|
|
40
38
|
local_agents_max_lines: z.number().default(120),
|
|
@@ -27,8 +27,8 @@ export function validateAdrs(ctx) {
|
|
|
27
27
|
code: "ADR_FILENAME_INVALID",
|
|
28
28
|
severity: "error",
|
|
29
29
|
file: path.join(relDir, f),
|
|
30
|
-
message:
|
|
31
|
-
suggestion:
|
|
30
|
+
message: "ADR filename must match <number>-slug.md",
|
|
31
|
+
suggestion: `Rename the file, e.g. ${"2".padStart(config.defaults.adr_padding, "0")}-my-decision.md.`,
|
|
32
32
|
});
|
|
33
33
|
}
|
|
34
34
|
}
|
|
@@ -87,8 +87,8 @@ export function validateAdrs(ctx) {
|
|
|
87
87
|
});
|
|
88
88
|
}
|
|
89
89
|
else {
|
|
90
|
-
const
|
|
91
|
-
if (!
|
|
90
|
+
const titleNumbers = title.match(/\d+/g) ?? [];
|
|
91
|
+
if (!titleNumbers.some((s) => parseInt(s, 10) === info.number)) {
|
|
92
92
|
issues.push({
|
|
93
93
|
code: "ADR_TITLE_NUMBER_MISMATCH",
|
|
94
94
|
severity: "error",
|