Skip to content

Commit c5319bb

Browse files
authored
[build] 产物能力声明与分发一致性门(requiresCommands + check:requires) (#36)
* build(distribution): 产物能力声明与一致性门——requiresCommands + check:requires,pin 升 v1.5.0 并由门互校 * docs(shadow): brief 最终态——published, PR #36 * docs(shadow): brief 最终态——published, PR #36
1 parent b7e335e commit c5319bb

12 files changed

Lines changed: 273 additions & 24 deletions

File tree

‎README.md‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ propose → apply → review → release → archive
7878

7979
CLI 独立分发于 [stack-wuh/shadow-dev-cli](https://github.com/stack-wuh/shadow-dev-cli)(纯脚手架实现:brief、INDEX、Git 与 GitHub 写操作的确定性执行层)。插件不再内置或 vendored CLI,而是通过 **SessionStart hook 自动安装锁版本 CLI**:
8080

81-
- `package.json` 的 `cliVersion` 字段锁定 CLI 版本(当前 `v1.1.0`),与插件版本配对发布,兼容配对由 manifest 机检。
81+
- `package.json` 的 `cliVersion` 字段锁定 CLI 版本(当前 `v1.5.0`),与插件版本配对发布,兼容配对由 manifest 机检;此外产物用 `requiresCommands` 自声明所需命令键,CLI 在物化/直通落盘前断言(缺失即 `ARTIFACT_INCOMPATIBLE`,指针不动)。
8282
- 首次会话自动安装到 `~/.local/share/shadow-dev-cli/shadow-dev-cli-<ver>/`(版本化目录 + `CURRENT`/`PREVIOUS` 指针),并在 `~/.local/bin/shadow-dev` 生成托管 shim;此后每次会话幂等秒退(不触网)。
8383
- hook 永不阻塞会话:安装失败仅 stderr 警告;CLI 未就位时 skills 中 `shadow-dev` 命令不可用。
8484
- `SHADOW_CLI_HOOK_DISABLE=1` 跳过自举(双仓开发时保护手动 `--channel main` / `--from` 安装)。
@@ -87,15 +87,15 @@ CLI 独立分发于 [stack-wuh/shadow-dev-cli](https://github.com/stack-wuh/shad
8787

8888
```bash
8989
bash scripts/install-cli.sh install # 装锁版本(同 hook 行为)
90-
bash scripts/install-cli.sh install --version v1.1.0 # 显式锁版本
90+
bash scripts/install-cli.sh install --version v1.5.0 # 显式锁版本
9191
bash scripts/install-cli.sh rollback # 切回上一版(离线)
9292
bash scripts/install-cli.sh status # 查看当前/上一版本指针
9393
```
9494

9595
排障:
9696

9797
- `shadow-dev: command not found`:确认 `~/.local/bin` 在 PATH(`export PATH="$HOME/.local/bin:$PATH"`)。
98-
- 升级 CLI 与更新 pin:CLI 仓库发新版 → 验证 → 本仓库改 `cliVersion` 并同步 `scripts/install-cli.sh`,随插件发版。
98+
- 升级 CLI 与更新 pin:CLI 仓库发新版 → 验证 → 本仓库改 `cliVersion` 并同步 `scripts/install-cli.sh`,随插件发版;**改完必须跑 `npm run check:requires`**——它判 `skills 引用 ⊆ requiresCommands ⊆ 已装 CLI 命令目录`,pin 与能力声明不再靠人记忆同步。
9999

100100
示例:
101101

@@ -112,7 +112,7 @@ brief、INDEX、Git 和 GitHub 写操作由 CLI 统一管理。写操作需要 `
112112

113113
原生宿主是 **Claude Code**(本插件为其开发),Codex 以 `.codex-plugin/` 清单 + `hooks/hooks.codex.json` 原生消费,zcode 等兼容宿主可直接消费同一产物。安装入口正从「插件引导 CLI」反转为「CLI 驱动分发」:
114114

115-
当前形态(v6.3.0):作为 Claude Code 插件安装,SessionStart hook 自动就位锁版本 CLI,无需手动初始化。
115+
当前形态(v6.5.0):作为 Claude Code 插件安装,SessionStart hook 自动就位锁版本 CLI,无需手动初始化。
116116

117117
### Codex 宿主
118118

@@ -133,11 +133,11 @@ codex plugin add shadow-dev-workflow@shadow-dev-workflow-local # 装完重启
133133

134134
Windows 自举:Codex 走 `hooks.codex.json` 里的 `commandWindows` 覆盖,优先 `%ProgramFiles%\Git\bin\bash.exe`,找不到再退回 PATH 上的 `bash`。若 PATH 上只有 WSL 的 `System32\bash.exe`,自举会把 CLI 装进 WSL 文件系统、Windows 侧看不到 shim——此时手动 `bash scripts/install-cli.sh install`,或设 `SHADOW_CLI_HOOK_DISABLE=1` 关掉自举。
135135

136-
目标形态(随 shadow-dev-cli 的 workflow/bind 域发布启用):
136+
CLI 轨(shadow-dev-cli 的 workflow/bind 域,v1.4.0 起可用,已实测):
137137

138138
```bash
139-
shadow-dev workflow install # 拉取本仓 release tarball,物化到 ~/.local/share/shadow-dev-workflow/
140-
shadow-dev workflow bind # 按 adapters/<host>.json 把 skills 绑入宿主发现目录
139+
shadow-dev workflow plan && shadow-dev workflow execute --plan-hash <哈希> --confirm # 物化本仓 release tarball 到 ~/.local/share/shadow-dev-workflow/
140+
shadow-dev bind plan --host auto && shadow-dev bind execute --host <名> --plan-hash <哈希> --confirm # 按 adapters/<host>.json 把 skills 绑入宿主发现目录
141141
```
142142

143143
产物与 adapters 契约:

‎docs/cli-guide.md‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# shadow-dev CLI 使用指南
22

3-
`shadow-dev` 是 shadow-dev-workflow 的确定性执行层:brief、INDEX、Git 和 GitHub 的全部写操作都由它完成,技能(skills)只负责编排与判断,不直接执行写命令。CLI 独立分发于 [stack-wuh/shadow-dev-cli](https://github.com/stack-wuh/shadow-dev-cli)(CLI v1.1.0,对应插件 `package.json` 的 `cliVersion` pin),本文档是完整命令参考。
3+
`shadow-dev` 是 shadow-dev-workflow 的确定性执行层:brief、INDEX、Git 和 GitHub 的全部写操作都由它完成,技能(skills)只负责编排与判断,不直接执行写命令。CLI 独立分发于 [stack-wuh/shadow-dev-cli](https://github.com/stack-wuh/shadow-dev-cli)(命令面以**已安装 CLI** 为准:产物用 `requiresCommands` 声明所需命令键,CLI 落盘前断言;`cliVersion` pin 只决定装哪个版本,两者由 `npm run check:requires` 互校),本文档是完整命令参考。
44

55
## 安装(SessionStart 自动自举)
66

@@ -14,10 +14,10 @@
1414

1515
```bash
1616
bash scripts/install-cli.sh install # 装锁版本(同 hook 行为)
17-
bash scripts/install-cli.sh install --version v1.1.0 # 显式锁版本
17+
bash scripts/install-cli.sh install --version v1.5.0 # 显式锁版本
1818
bash scripts/install-cli.sh rollback # 切回上一版(离线,不动 shim)
1919
bash scripts/install-cli.sh status # 查看 CURRENT/PREVIOUS
20-
bash scripts/install-cli.sh install --from dist/shadow-dev-cli-v1.1.0.tar.gz # 离线安装
20+
bash scripts/install-cli.sh install --from dist/shadow-dev-cli-v1.5.0.tar.gz # 离线安装
2121
```
2222

2323
排障:
@@ -70,9 +70,9 @@ bash scripts/install-cli.sh install --from dist/shadow-dev-cli-v1.1.0.tar.gz #
7070
| `pr inspect --name <n>` | 读取 brief 记录的 PR(GET /pulls/{n}) |
7171
| `conflict inspect --name <n>` | 报告与该 change 文件列表重叠的其他活动 change |
7272
| `task list --name <n>` | 列出 brief 正文的 checkbox 任务(task-1、task-2…) |
73-
| `worktree inspect --name <n>` | 列出 git worktree(分支/脏净/归属),按 brief 复杂度评级给 `recommendation`(create/reuse/inline)(CLI ≥ v1.5.0) |
73+
| `worktree inspect --name <n>` | 列出 git worktree(分支/脏净/归属),按 brief 复杂度评级给 `recommendation`(create/reuse/inline);能力由产物 `requiresCommands` 声明保证 |
7474

75-
### 并行工作区(worktree,CLI ≥ v1.5.0)
75+
### 并行工作区(worktree)
7676

7777
| 命令 | 说明 |
7878
|------|------|
@@ -194,4 +194,4 @@ npm test # wrapper 契约 + 安装器 --from fixture 测
194194
bash scripts/install-cli.sh install # 从 release 真实安装(同 hook)
195195
```
196196

197-
`.shadow-dev/` 项目配置目录约定为另案提案,当前未实现;项目级事实仍以 `shadow-docs/` 为准。
197+
`.shadow-dev/config.json` **已实现**(双层:项目级自 cwd 向上第一个命中 + 用户级 `~/.shadow-dev/config.json`;优先级 flag > env > 项目 config > 用户 config > 默认;键 `lang`/`quiet`/`json`/`github.*`/`blog.*` 逐键校验,损坏报 `CONFIG_INVALID`)。token 只走环境变量;项目级稳定事实仍以 `shadow-docs/` 为准。
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
---
2+
title: 产物能力声明与分发一致性门
3+
domain: distribution
4+
keywords: [分发, pin, cliVersion, requiresCommands, 能力声明, 一致性门, check:requires, ARTIFACT_INCOMPATIBLE, missingCommands, 静默降级, 响亮阻塞, 版本错位]
5+
scope: [cross-project]
6+
status: active
7+
source:
8+
- changes/20261009-build-artifact-capability-gate/brief.md
9+
verified: 2026-10-09
10+
verified-depth: runtime
11+
verified-scope: `npm run check:requires` 两级差集为空;把声明里删掉一键的门负例(exit 1 且点名该键);越权 fixture 被 v1.5.0 以 `ARTIFACT_INCOMPATIBLE` 拒绝且指针不动
12+
---
13+
14+
# 产物能力声明与分发一致性门
15+
16+
## 当前结论
17+
18+
内容包与执行层之间有两件事必须**分开**,混淆就会漂:
19+
20+
1. **装哪个 CLI** —— 由内容包的版本 pin 决定(本项目 `package.json.cliVersion`)。它是静态声明,必须有人或有机检维持。
21+
2. **这份内容能不能装进这个 CLI** —— 由内容包自声明所需命令键(`requiresCommands`)、执行层在**任何落盘动作之前**拿自身命令目录断言决定;缺失即响亮失败(`ARTIFACT_INCOMPATIBLE`),指针与既有安装态不变。未声明视为兼容,旧包零破坏。
22+
23+
命令键是唯一会随两侧自然同步的货币;版本号需要人工维护,一旦漏维护就把错位伪装成正常使用(实证:README 写 v1.1.0、pin 写 v1.4.0、skills 已指挥 v1.5.0 才发布的 worktree 域,三处同时漂)。
24+
25+
## 执行约束
26+
27+
- pin 与能力声明**必须共存且互相校验**:改 pin 之后必须跑一致性门(本项目 `npm run check:requires`:`skills 引用 ⊆ requiresCommands ⊆ 已装 CLI 命令目录`),两级差集非空即判红。禁止只靠人记忆同步。
28+
- 声明集必须**恰好覆盖内容包真正会跑的命令**:内容里出现即要声明,包括恢复动作(如「升级 CLI」用的 `workflow plan/execute`)——它们是同一份指令要执行的命令。
29+
- 内容层遇到命令缺失**只能阻塞,不能降级**:禁止「worktree 不可用就改走 branch」这类静默兜底,它把版本错位转成更难发现的长期偏差。
30+
- 观测面恒不抛:`workflow status` 回 `artifactVersion / cliVersion / missingCommands`,引导页/巡检读一个字段即可判断「内容比工具新」。
31+
- 跨仓演进顺序:消费方先行兼容(执行层发布支持断言的版本),生产方随后声明;反序会留下无人兜底的窗口。
32+
- 宿主差异只进描述符(`adapters/<host>.json`),插件/hook 轨与 bind 轨可并存(见 [多宿主插件分发](multi-host-plugin-distribution.md)),但两条轨都不得自行安装或选择 CLI 版本以外的东西。
33+
34+
## 适用边界
35+
36+
适用于「工具链 + 内容/规范包」的双仓结构与多宿主分发。不适用:单一可执行体自更新;无安装态的纯文档产物。
37+
38+
## 验证方式
39+
40+
1. `npm run check:requires` → `undeclared=[] missing-in-cli=[]` 且退出码 0。
41+
2. **门负例**:把声明里任意一键删掉后重跑,必须退出码 1 并点名该键(证明门真的在判,不是摆设)。
42+
3. `workflow plan --from <本仓>` → `ok:true` 且 `missingCommands=[]`;构造声明了不存在命令键的 fixture → `workflow execute`/`link` 报 `ARTIFACT_INCOMPATIBLE` 且 `CURRENT`/`PREVIOUS`/`LINK` 不变。
43+
4. 缺省兼容:无 `requiresCommands` 的历史包仍能物化切指针。
44+
45+
## 关联知识
46+
47+
- [多宿主插件分发契约](multi-host-plugin-distribution.md)
48+
- [Bug 调查保持单一上下文](bug-investigation.md)
49+
- CLI 仓 `shadow-docs/knowledge/install-distribution.md`、`distribution-capability-contract`(断言落点与指针语义)

‎menu.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
| 数据库变更 | 数据库 MongoDB Mongoose Schema Model 字段 索引 迁移 回填 同步 seed | norms/code-style-database.md, 项目数据模型 active Knowledge |
1717
| 性能优化 | 首屏 加载 缓存 LCP CLS SSR ISR | norms/code-style.md, 项目性能 active Knowledge |
1818
| 插件多宿主分发 | Codex Claude Code zcode 宿主 插件清单 marketplace hooks adapters 打包 分发 技能加载 命令 not found | knowledge/multi-host-plugin-distribution.md |
19+
| 分发一致性与能力声明 | pin cliVersion requiresCommands 能力声明 一致性门 check:requires ARTIFACT_INCOMPATIBLE missingCommands 静默降级 响亮阻塞 版本错位 命令 not found | knowledge/distribution-capability-contract.md |
1920
| Bug 修复 | 报错 崩溃 异常 不对 显示不正常 | norms/tdd-verification.md, knowledge/bug-investigation.md, 项目相关 active Knowledge |
2021
| 交互/动画 | 动效 过渡 动画 手势 滚动 | norms/interaction.md, 项目动效 active Knowledge |
2122
| 无障碍 | a11y aria 对比度 屏幕阅读 焦点 键盘 | norms/interaction.md, norms/ui-patterns.md |

‎norms/tdd-verification.md‎

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,27 @@
2424
| 级 | 测试要求 |
2525
|----|----------|
2626
| **S** | 不创建测试;验证 = diff 走查 + 结构/引用扫描 |
27-
| **M** | 绿灯测试(写测试,不强制先红);unit + 走查 |
28-
| **L** | **完整 TDD**:先写能复现失败的测试 → 确认失败 → 最小实现 → 确认通过;无失败测试记录视为未开始 |
27+
| **M** | 默认**不新建测试文件**;冒烟 + 既有测试保持绿 + 走查(见「验证方式裁决」) |
28+
| **L** | 原「完整 TDD(先红后绿)」,2026-10-09 用户裁决后改为**可追溯 runtime 观察点**:拒绝路径与状态不变量必须真实跑过,或 CI 全绿链接 |
2929

3030
- 评级是 AI 提案、人批准;review 阶段检查「验证强度与评级匹配」:S 级过度测试同样指出(浪费),L 级验证不足直接阻塞。
3131
- 实现期发现风险升/降级(如纯文档 change 需要改运行时代码)时,先修订 brief 评级再继续,不得静默跨级。
3232

33+
## 验证方式裁决(2026-10-09 用户裁决)
34+
35+
> 裁决原文:「不要写 TDD 了,太浪费时间了。」据此,**先红仪式与「为新 change 新建测试文件」不再是默认要求**;不可议的是另一条——**验证强度与复杂度评级匹配**。达成强度的手段改为:
36+
37+
| 级 | 现行达成方式 |
38+
|----|--------------|
39+
| S | diff 走查 + 结构/引用/残留扫描 |
40+
| M | 冒烟命令 + 既有测试保持绿 + 走查;触及既有契约时**更新既有断言**(属维护,不算新增测试) |
41+
| L | 可追溯 runtime 观察点:真实命令 + 真实输出,必须覆盖**拒绝路径**与**状态不变量**(指针不动、无半成品残留),或 CI 全绿链接 |
42+
43+
- **既有测试必须保持绿色**——「不写 TDD」不等于「不跑测试」。
44+
- 本机取不到证据时(runner 挂死、环境 139 风暴等)必须写明证据缺口与替代观察点(CI run 链接),禁止用「应该没问题」占位。
45+
- 「无验证输出的完成声明一律无效」「强度与评级不匹配一律退回」两条阻断条款不变;过度测试(S 级写测试)同样 review 阻塞。
46+
- 进度可见性条款(报数、逐完成一行、卡点先报、收口对账)不变——它治的是静默长跑,与写不写测试无关。
47+
3348
## 进度可见性(执行纪律)
3449

3550
> 动机:静默执行无法察觉卡死——进程空转、外部命令挂起、反复重试都表现为「无输出」,用户无从判断是慢还是死。以下条款非协商,适用于一切 TDD / 绿灯测试与 Phase 任务执行。

‎package.json‎

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,44 @@
11
{
22
"name": "shadow-dev-workflow",
3-
"version": "6.4.0",
3+
"version": "6.5.0",
44
"type": "module",
55
"engines": {
66
"node": ">=20"
77
},
88
"scripts": {
9-
"test": "node --test test/install-cli.test.mjs"
9+
"test": "node --test test/",
10+
"check:requires": "node scripts/check-requires.mjs"
1011
},
11-
"cliVersion": "v1.4.0"
12+
"cliVersion": "v1.5.0",
13+
"requiresCommands": [
14+
"archive.execute",
15+
"archive.plan",
16+
"branch.execute",
17+
"branch.plan",
18+
"change.approve",
19+
"change.create",
20+
"conflict.inspect",
21+
"issue.execute",
22+
"issue.plan",
23+
"release.execute",
24+
"release.plan",
25+
"repo.inspect",
26+
"review.execute",
27+
"review.plan",
28+
"sync.execute",
29+
"sync.plan",
30+
"task.list",
31+
"task.set",
32+
"worktree.execute",
33+
"worktree.inspect",
34+
"worktree.plan",
35+
"worktree.remove.execute",
36+
"worktree.remove.plan",
37+
"workflow.plan",
38+
"workflow.execute",
39+
"workflow.status",
40+
"bind.plan",
41+
"bind.execute",
42+
"bind.status"
43+
]
1244
}

‎rules/iron-laws.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
- **防什么**:实现偏离需求、回归不可检测
88
- **触发**:任何代码/行为变更
9-
- **做什么**:按 brief 复杂度评级的验证强度执行(`norms/tdd-verification.md` 分级制):**L**——完整 TDD,先写能复现失败的测试确认红,再最小实现确认绿;**M**——写测试确认绿灯,不强制先红;**S**——不创建测试文件,执行结构、引用、残留扫描。评级在 propose 确认时批准;实现期风险升/降级先修订 brief 再继续。
9+
- **做什么**:按 brief 复杂度评级的验证强度执行(`norms/tdd-verification.md` 分级制):**L**——可追溯 runtime 观察点(真实命令 + 真实输出,覆盖拒绝路径与状态不变量),或 CI 全绿;**M**——冒烟 + 既有测试保持绿 + 走查,默认不新建测试文件;**S**——不创建测试文件,执行结构、引用、残留扫描。(2026-10-09 用户裁决:不写 TDD、不强制先红;强度与评级匹配仍不可议。)评级在 propose 确认时批准;实现期风险升/降级先修订 brief 再继续。
1010
- **阻断**:验证强度与评级不符视为未开始;对 S/M 级需求一刀切强制测试同样违规(过度测试 review 阻塞)
1111

1212
## 2. 不要声称完成而未验证

‎scripts/check-requires.mjs‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
#!/usr/bin/env node
2+
// 分发一致性门:skills 引用的命令 ⊆ 产物 requiresCommands ⊆ 已安装 CLI 的命令目录。
3+
// 分工:cliVersion pin 只决定「装哪个 CLI」,requiresCommands 决定「这份内容能不能装进这个 CLI」,
4+
// 本门负责让两者不互相冒充。用法:npm run check:requires(CI 或本机皆可,SHADOW_DEV_CLI 可指定 cli.mjs)。
5+
import { execFileSync } from 'node:child_process'
6+
import { readFileSync, readdirSync, existsSync } from 'node:fs'
7+
import { dirname, join } from 'node:path'
8+
import { fileURLToPath } from 'node:url'
9+
10+
const root = dirname(dirname(fileURLToPath(import.meta.url)))
11+
const declared = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')).requiresCommands || []
12+
const set = new Set(declared)
13+
const refs = new Map()
14+
for (const d of readdirSync(join(root, 'skills'))) {
15+
const f = join(root, 'skills', d, 'SKILL.md')
16+
if (!existsSync(f)) continue
17+
for (const m of readFileSync(f, 'utf8').matchAll(/shadow-dev\s+([a-z][\w-]*(?:\s+[a-z][\w-]*){0,2})/g)) {
18+
const words = m[1].replace(/\//g, ' ').split(/\s+/).filter(Boolean)
19+
const cand = [words.slice(0, 3).join('.'), words.slice(0, 2).join('.'), words[0]]
20+
const key = cand.find(k => set.has(k)) || words.slice(0, 2).join('.') + ' (未声明)'
21+
refs.set(key, (refs.get(key) || 0) + 1)
22+
}
23+
}
24+
const undeclared = [...refs.keys()].filter(k => k.endsWith('(未声明)'))
25+
let catalog = null, missingInCli = []
26+
const cli = process.env.SHADOW_DEV_CLI
27+
const argv = cli ? [process.execPath, [cli, 'help', '--full', '--json']] : ['shadow-dev', ['help', '--full', '--json']]
28+
try { catalog = Object.keys(JSON.parse(execFileSync(argv[0], argv[1], { encoding: 'utf8', stdio: ['ignore','pipe','ignore'] })).data.commands || {}) } catch { catalog = null }
29+
if (catalog) missingInCli = declared.filter(k => !catalog.includes(k))
30+
console.log(`declared=${declared.length} skills-referenced=${refs.size} undeclared=[${undeclared.join(', ')}] cli-catalog=${catalog ? catalog.length : 'unresolved'} missing-in-cli=[${missingInCli.join(', ')}]`)
31+
if (undeclared.length) console.error('✗ skills 有命令未进 requiresCommands 声明:', undeclared.join(', '))
32+
if (missingInCli.length) console.error('✗ requiresCommands 声明的命令在已装 CLI 里不存在(pin 或 CLI 版本过旧):', missingInCli.join(', '))
33+
if (!undeclared.length && !missingInCli.length) console.log('✓ skills ⊆ requiresCommands' + (catalog ? ' ⊆ CLI 命令目录' : '(CLI 目录未参与断言)'))
34+
process.exitCode = (undeclared.length || missingInCli.length) ? 1 : 0

0 commit comments

Comments
 (0)