pdlc-ship — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited pdlc-ship (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
串联发布一个版本所需的所有步骤:收评审通过的功能 → 跑测试 → 升级 VERSION → 更新 CHANGELOG → 写发布说明草稿 → 创建 tag。CI 配置默认不动(见 §1.6)。
<!-- @include templates/prompts/iron-law.md(已内联于下方,无需另读) --> ⛔ IRON LAW · 不可违反的硬门禁
以下规则为不可协商的执行约束:
docs/.pdlc-state/<feature-id>.json 追加 history,不可跳过。/pdlc-implement 前,对应测试必须存在且处于红灯状态。违反则中止。current_stage 必须变更。收尾时若发现 current_stage 未推进,视为失败并报错,不得静默返回(防止外层循环拿滞后的状态空转烧额度)。唯一例外:命中人工点主动 block 时,current_stage 保持不变但必须写 last_phase_result.ok=false + blocked_reason。违反任一条 = 立即中止当前命令,输出违规详情,等待人工介入。 <!-- @include-end templates/prompts/iron-law.md -->
⛔ 发布是破坏性·不可逆操作:打 tag / bump 版本 / 触发 CI/CD 属破坏性范畴。`--autonomous` 对本命令无效——即使带该参数,§1.1(未完成功能)与 §1.2(测试门)的人工确认仍必须真实由人应答。自主循环(/pdlc-loop-run)停在「评审通过、next_step为pdlc-ship」(循环文档里称review_done),永不进入本命令。
参数带 --tag-after-merge:只做「合并后补打」(见 1.5 末尾),跳过下面全部步骤。master / main(参考 CLAUDE.md §5)git status clean)docs/.pdlc-state/ 下的功能(跳过 _ 前缀的索引文件与 statusline.json),逐个归入四类:next_step 为 pdlc-ship,且 last_phase_result.ok 不是 false——评审(或功能编排)已通过、等着发布。这一类纳入本次发布current_stage 为 ship_done 或 deploy_done——上一次发布已纳入,本次不再收current_stage 以 _done 结尾但不是上面两个(如 feature_done / fix_done / review_done)——旧版本写入,分不清是「已发布」还是「评审通过、待发布」。逐个列出,请人确认归入可发布还是已发布;--autonomous 也不替人判last_phase_result.ok 为 false 的阻塞功能)docs/00_standards/quality-targets.yml):读 docs/07_reviews/quality/ 下最近一份 `.md` 报告(同名 .html 只是视图, 闸门一律以 .md 为准——两者若不一致,信 .md):
/pdlc-quality,询问是继续还是先出报告该理由需记入本次发布的 CHANGELOG 或发布说明(不允许无声跳过)
#### 过期判定:按「改动了什么」分两档
报告头部记着生成时的 <!-- 仓库版本: <commit SHA> -->(由 /pdlc-quality 落盘时写入)。 取该 SHA,跑 git diff --name-only <SHA>..HEAD 看这期间动了什么:
| 这期间改动命中 | 判定 | 理由 |
|---|---|---|
docs/01_requirements/prd/、docs/00_standards/quality-targets.yml、docs/00_standards/e2e-flow-map.yml、docs/00_standards/test-commands.yml | 硬闸·不放行 | 报告里的 PRD ↔ core_flows 对账与 E2E 覆盖矩阵的输入变了,结论不再成立。重跑单测补不回对账——必须重跑 /pdlc-quality |
| 只有其它代码 / 文档 | 提示已过期,建议重跑 | 对账仍成立,测量数字可能略旧 |
报告里没有 仓库版本 字段,或该 SHA 在本仓库解析不了 | 按「不可判」处理:明确告知无法核对新鲜度,建议重跑 | 与三态语义同一条纪律——「查不了」不等于「没问题」,不得静默当作未过期 |
⛔ 硬闸同样允许人显式 override 并写明理由(与「总判定未达标」同一条通道,理由须记入 CHANGELOG 或发布说明),但不得无声跳过。
#### quality-targets.yml 变动要单独看一眼「闸门是不是被调松了」
若上表命中的文件里包含 `quality-targets.yml`,除了要求重跑,还必须把 git diff <SHA>..HEAD -- docs/00_standards/quality-targets.yml 原样展示给人, 并逐条点出下列「变松」信号——命中任何一条都要人明确确认,不得只当作普通过期:
core_flows 条目减少(核心流被移出闸门视野)为什么单列:报告达标之后把目标调低,重跑一次照样是绿的——日期新、SHA 新、结论"达标", 但尺子本身变短了。这与 /pdlc-test-setup --refresh 的「严向自动、松向人确认」是同一条 原则换个位置:放松闸门永远是人的决定,不能由流程默默吸收。必须主动询问用户(不得默认跳过,也不得默认强制跑):
📦 发布前测试门检查:是否先跑全量单测 + E2E 测试?
[选项 A] 跑测试(推荐)
→ 后端:./gradlew test 或 mvn test
→ 前端:pnpm test 或 npm test
→ E2E:pnpm exec playwright test(如有)
[选项 B] 跳过测试 —— 仅适用于生产 hotfix
→ 理由:<请说明>
→ 自动记录到 commit 消息
请选择 A 或 B。若选 B:本命令的参数必须含 --skip-tests 且有理由说明,否则中止。
VERSION 文件--version x.y.z,用指定值;否则按语义化规则建议:VERSIONB 开头的 ID)→ "修复";history 里有 refactor 阶段的功能 → "重构";其余 → "新增"[未发布] 段(开发各阶段可能已按同一格式为该功能追加过条目——[未发布] 段里已有同一 ID 的,不重复写)[未发布] 改为 [<new-version>] - <今日日期>按 assets/release-notes-template.md 写 docs/05_deployment/releases/v<new-version>-release-notes.md,把这次发布散在各处的结论汇到一页, 开 PR 时直接 gh pr create --body-file <这个文件>,发 Release 时也能用。只写这个文件,不开 PR、不合 PR、不推送。
docs/07_reviews/code/<ID>-*-review.md 的评审总结与「需人工处理」项、状态文件里 last_phase_result.checks(读的是推进到 ship_done 之前的那份,即评审阶段的结果),以及 §1.1 的质量报告结论、§1.2 测试门的选择、§1.5 的 tag 去向
checks 的值原样搬,不重算、不美化;null 照写 null。某项产物不存在就写「无」,不要补写一份先推进状态机,再提交:对每个纳入的功能按段四的规则运行状态推进脚本(--stage ship --next pdlc-deploy --done),让 ship_done 随发布提交一起进库。先提交后推进的话,状态文件的改动留在发布提交之外——squash 合并时就丢了,主干上这些功能会一直显示「待发布」。
git add VERSION CHANGELOG.md docs/05_deployment/releases/v<new-version>-release-notes.md docs/.pdlc-state/<纳入的功能ID>.json
git commit -m "release: v<new-version>"若选项 B(跳过测试):commit 消息末尾追加 [skip-tests: <理由>]。
tag 必须指向最终留在主干上的那个提交。本命令跑在发布分支上,分支怎么合进主干决定了现在打的 tag 还算不算数:
| 合并方式 | 发布分支上的提交会不会原样进主干 | 本命令的做法 |
|---|---|---|
| 普通合并(merge commit)/ 快进 | 会 | 当场打 tag |
| squash 合并 / rebase 合并 | 不会——主干上是新提交 | 不在分支上打,标为「待合并后打」 |
| 判断不了 | — | 问人(本命令不接受 --autonomous,见上) |
判断合并方式,按顺序取,命中即停:
CLAUDE.md、CONTRIBUTING.md、docs/00_standards/ 中关于合并的约定)gh repo view --json mergeCommitAllowed,squashMergeAllowed,rebaseMergeAllowed:只开了一种就是它;开了多种不算命中(#<编号>) 结尾 → squash;有带两个父提交的合并提交 → 普通合并。两种迹象都有、或历史太短 → 不算命中当场打:
git tag -a "v<new-version>" -m "Release v<new-version>"待合并后打:不建 tag,交接里写明「tag 待合并后打」,并给出下面的补打命令。
#### 合并后补打:/pdlc-ship --tag-after-merge [v<x.y.z>]
分支合并后,在主干上运行本命令的这个模式。它只打 tag,不跑 1.1–1.4、不改状态机(ship_done 已在发布时写过)。 所以本模式下「分支不是 main/master」的检查与自检项不适用,IRON LAW 的状态写入与「状态必推进」两条也不适用——它不是一次阶段推进。 参数带 --tag-after-merge 时,直接从这里开始,做完按下面的交接收尾:
git switch <主干> && git pull --ff-only)VERSION 的值v<x.y.z> 已存在:指向主干上的提交 → 告知已打过,结束;指向主干之外 → 列出它指向的提交,问人是否删掉重打(删 tag 属破坏性操作,不自动做)VERSION 变成该版本号的提交。不要用 git log -S,它也会命中把版本号改走的那个提交: git log --reverse --format=%H <主干> -- VERSION | while read -r c; do
[ "$(git show "$c:VERSION" | tr -d '[:space:]')" = "<x.y.z>" ] && { echo "$c"; break; }
done找不到 → 问人(可能是版本号写在别的文件里,由人指定提交)
git merge-base --is-ancestor <提交> <主干> 退出码为 0 才继续git tag -a "v<x.y.z>" <提交> -m "Release v<x.y.z>",不推送(推送 tag 会触发按 tag 运行的 CI,由人决定)✅ 已补打 tag:v<x.y.z> → <提交短 SHA>(<提交标题>),已核对在 <主干> 上
👉 下一步:git push origin v<x.y.z>(有按 tag 触发的 CI 时会随之运行),再 /pdlc-deploy v<x.y.z>本命令默认不生成、不修改任何 CI 配置。日常检查(lint / test / build)在本地跑(§1.2 已跑过),CI 按次计费, 每次 push / PR 都触发会很快烧掉额度。项目已有 CI 配置时只读不改:检查它是否覆盖本次新增的测试路径,没覆盖就在交接里提示,由人决定是否修改。
仅当用户明确要求生成或修改 CI 配置时才动手,且先确认再写:
月预计用量 = 触发次数/月 × 单次时长(分钟)× runner 倍率(Linux 1× / Windows 2× / macOS 10×)GitHub Actions(.github/workflows/release.yml):
workflow_dispatch + push: tags: ['v*']ubuntu-latest;只有必须在 macOS / Windows 上构建或签名时才用对应 runnerGitLab CI(.gitlab-ci.yml):
rules 只放行 tag 与手动(when: manual)Jenkins(Jenkinsfile)/ 云效 / 其他:按项目已有约定生成,同样只用手动 + tag 触发。
生成后在 docs/05_deployment/ci-cd/ 下记录流水线使用说明。
<!-- @include templates/prompts/self-audit.md(已内联于下方,无需另读) -->
重新阅读本次产出物,按质量关卡清单逐项检查。勾选已通过,标注未通过原因。
注意:自检清单的具体内容由各命令自行定义,本片段只规定结构。
针对自检段标注为未通过的项:
⚠️ 单次修复原则:若一轮修复后仍有项未通过,不再递归修复,防止死循环。 <!-- @include-end templates/prompts/self-audit.md -->
发布自检清单:
git tag -l v<new-version> 返回非空;待合并后打的,没有在分支上建 tag,交接里给了补打命令[skip-tests] 标记ship_done;未纳入的功能没被改动<!-- @include templates/prompts/loop-prevention.md(已内联于下方,无需另读) -->
本命令所有的自检-修复循环均受以下约束:
这是为了防止 agent 在"修完再查、查完再修"的往返中陷入死循环。 <!-- @include-end templates/prompts/loop-prevention.md -->
针对未通过项:
git tag -d v<new-version>),改为待合并后打状态推进已在 §1.5 提交之前做过(下面是它要遵守的规则)。这里只核对:纳入的每个功能current_stage都是ship_done,且状态文件已在发布提交里(git show --stat HEAD能看到)。
<!-- pdlc:meta 由 frontmatter 生成(adapters/sync_skills.py),勿手改 -->
本命令的状态机取值:阶段短名ship(写进history[].stage与last_phase_result.stage);下一跳pdlc-deploy(写进next_step,交接时提示)。
<!-- pdlc:meta-end --> <!-- @include templates/prompts/state-update.md(已内联于下方,无需另读) -->
本命令完成主产出后,必须推进状态机文件 docs/.pdlc-state/<feature-id>.json。
bash scripts/pdlc-advance.sh <功能ID> --stage <本阶段短名> --next <下一跳命令名|null> \
[--produced <产物路径,逗号分隔>] [--autonomous] [--decision "<确认点>=<所选默认>"]... \
[--name <kebab-case>(状态文件还不存在时)] <项目根>scripts/pdlc-advance.sh。阶段短名与下一跳就是本命令正文里「本命令的状态机取值」那两个值;带了 --autonomous 就照传scripts/pdlc-checks.sh(tdd 验红灯;impl / fix / refactor 跑 unit、coverage、lint;review 再加 e2e;e2e 只跑 e2e;文档类阶段不跑),据此判 ok、整段写入 history 与 last_phase_result, 写完调用 scripts/pdlc-state-lint.sh 体检。不要自己先写 checks,也不要挑检查项
--produced 必须列出写的测试文件:脚本记下它们的指纹,之后 impl / refactor / review 推进时比对--blocked "<原因>":脚本照跑检查、如实记录,但不推进,原因写进 blocked_reason/pdlc-ship 写 ship_done、/pdlc-deploy 写 deploy_done → 加 --done(只有这两个命令能用)脚本的退出码决定怎么收尾:
| 退出码 | 含义 | 你要做的 |
|---|---|---|
0 | 已推进 | 交接里引用 stdout 那行 JSON(checks、current_stage、next_step) |
3 | 未推进:检查没过 / 红灯没验证 / 主动 block / tdd 写的测试被改 | 把 stdout 里的 blocked_reason 原样写进报告,按阻塞收尾。不要为了让它推进去改测试或改检查命令 |
2 | 脚本跑不了(参数错、缺 jq、状态文件损坏) | 参数错就改正重跑;缺 jq 才按下面的格式手写,并在报告里写明「状态机未经脚本写入」 |
退出码3且原因是「tdd 阶段写的测试被改动」:实现阶段不改测试。测试是否真的写错了(与设计 / PRD 矛盾),只有人能判断: - 交互模式:先把矛盾讲清楚、问人;人同意后再带--tests-changed "<理由>"重跑,理由记进auto_decisions---autonomous:没有人可问,不要改测试,带--blocked "<矛盾在哪>"收尾。自主模式下带--tests-changed脚本也不推进
>
项目开了 PDLC 护栏(docs/00_standards/pdlc-hooks.yml)时,用 Write / Edit 直接写状态文件会被拦下。{
"feature_id": "<F/B ID>",
"feature_name": "<kebab-case>",
"created_at": "<首次创建时间 ISO 8601>",
"current_stage": "<当前阶段名>",
"run_mode": "interactive | autonomous",
"history": [
{
"stage": "<阶段名>",
"done_at": "<ISO 8601>",
"produced": ["<相对路径 1>", "<相对路径 2>"],
"self_audit": { "passed": <N>, "failed": <N>, "manual": <N> },
"auto_decisions": [
{ "point": "<autonomous 下自动前进的确认点>", "chose": "<所选默认>", "at": "<ISO 8601>" }
]
}
],
"last_phase_result": {
"stage": "<本次阶段名>",
"ok": true,
"advanced_to": "<推进到的下一阶段 | null>",
"checks": {},
"self_audit": { "failed": 0 },
"blocked_reason": null,
"run_mode": "interactive | autonomous",
"at": "<ISO 8601>"
},
"relations": {
"extends": [],
"depends_on": [],
"supersedes": [],
"resolves": [],
"conflicts_with": [],
"relates_to": [],
"_updated_at": "<ISO 8601 | 省略>"
},
"next_step": "<下一跳命令名,如 pdlc-design;若流程结束则为 null>"
}⛔ 示例里的 "checks": {} 是「本阶段没有命令可跑」的样子,不是键名示范——键名与取值见下方 §1。`relations` 块(RFC#6,Phase 1 可选,Phase 2 推荐):6 个 key 对应 6 种关系类型,各为 ID 数组,存出边。其中conflicts_with/relates_to是对称类型,两端都要写;其余四种有向,只写在源 feature 上。拿不准时用/pdlc-relate set写入,它会按规则校验。旧状态文件无此块时视为全空,向后兼容。入边由/pdlc-relate rebuild派生到_relations.json,不在此块手维护。
⛔ 写状态机的四条硬约束——读侧(/pdlc-status、/pdlc-retro、/pdlc-relate)会逐条体检, 违反的每一处都会出现在它们输出的最前面:
>
1. 实例里不写 `terminal_state`。skill frontmatter 的terminal_state:是「这个命令走完后应到达的终态名」, 不是状态字段。判终态只看current_stage是否以_done结尾。 2. `history[].stage` 写本命令的阶段短名(见本命令正文里「本命令的状态机取值」)——pdlc-implement写impl, 不写implement/implementation;pdlc-prd写requirements,不写prd。 3. 时间戳必须带时刻:created_at/done_at/at一律写完整 ISO 8601(如2026-07-28T10:40:00+08:00)。 只写日期,同一天内的阶段耗时就全部算成 0——读侧只能记「不可测」。 4. `next_step` 只写命令名或 `null`,不附说明文字(如「pdlc-ship(等评审通过)」)。 要说明原因,阻塞时写进last_phase_result.blocked_reason。
⛔ `_done` 的含义是「已发布」,只由 `/pdlc-ship`(写 `ship_done`)与 `/pdlc-deploy`(写 `deploy_done`)写入。 其它命令的current_stage一律写本命令的阶段短名,走完整条链路的编排命令(/pdlc-feature)也一样—— 它收尾时current_stage是最后一个阶段的短名,next_step是pdlc-ship。
>
- 「评审通过、等待发布」就是current_stage为review且next_step为pdlc-ship(/pdlc-e2e在评审之前,它的下一跳是pdlc-review)。 循环相关文档里说的review_done指的就是这个状态,不是要写进current_stage的值。 - 为什么:读侧判「已抵达终态」只看current_stage是否以_done结尾。评审通过就写_done,/pdlc-ship就分不清哪些功能已经发布过,发布说明会重复或漏收。 - 旧版本写入的feature_done/fix_done/review_done分不清是否已发布,/pdlc-ship会列出来请人确认。
history 为含当前阶段的数组)history,更新 current_stage 和 next_stepjq 或等效工具保持格式化bash scripts/pdlc-state-lint.sh <项目根>,属于本功能状态文件的偏差当场改正。退出码 2(缺 jq 等)= 没体检,报告里写明「未体检」,不得当作通过。⚠️ 若更新失败(文件损坏/权限问题),必须中止命令并在最终报告中报错。状态机不可跳过。
last_phase_result(机器可读阶段结果;脚本按下面的规则写,手写回退时照做)顶层 last_phase_result 是循环判停的唯一真源,外层只需 jq '.last_phase_result.ok' 即可决定 继续 / 停止 / 交还人类。规则:
scripts/pdlc-checks.sh 的 stdout 原样),键名只能是 tests_pass / coverage_pass /lint_clean / e2e_pass(tdd 段 red_verified),值只能是布尔或 null(无法判定)。不照抄 test-commands.yml 的 unit / lint, 不写 "4 passed, 1 failed" 这类摘要。没有检查可跑的阶段(文档类阶段、项目无 test-commands.yml)→ checks: {}, 绝不因为「本阶段成功」就填 true——schema 示例里 checks 是空的,正是这个意思。
blocked_reason,且 tdd 段 red_verified 为 true、其它阶段没有任何检查为 false(null 不拦)→ true;否则 false。advanced_to = 下一阶段的短名,不是命令名、也不是本阶段的 `current_stage`。三者关系:stage=本阶段短名、current_stage=本阶段完成后的当前短名、advanced_to=下一阶段短名、next_step=下一跳命令名。⛔ 短名不是「命令名去掉 `pdlc-` 前缀」——pdlc-implement 的短名是 `impl`,不是 implement。别推导,查下表:
<!-- stage-map:start -->
next_step(下一跳命令名) | advanced_to(下一阶段短名) |
|---|---|
pdlc-tdd | tdd |
pdlc-implement | impl |
pdlc-review | review |
pdlc-design | design |
pdlc-ship | ship |
pdlc-deploy | deploy |
<!-- stage-map:end -->
next_step 为 null(终态或无后续)时 advanced_to 也是 null。
📌 本表是唯一真源,且是被断言钉住的:每行的短名必须等于该 skill 自己 frontmatter 里声明的stage:,且任何 skill 的非nullnext_step都必须在表里有行——两个方向 都由tests/frontmatter-check.sh检查,所以表不会和实现各自漂移。
>
写错短名的后果与键名写错同类:消费方按契约名匹配,认不出就当没这个阶段。
ok=true 时本阶段必须真的推进了 current_stage(与第 6 条 IRON LAW 呼应);到达终态或无后续时 advanced_to=null。ok=false(含 blocked)时 current_stage 不变、advanced_to=null、blocked_reason 写明原因。--autonomous(带了写 autonomous,没带写 interactive)。<!-- @include-end templates/prompts/state-update.md -->
本阶段状态机更新:对本次纳入发布的每个功能(§1.1 的可发布 + 人工确认归入的),各运行一次 bash scripts/pdlc-advance.sh <功能ID> --stage ship --next pdlc-deploy --done --produced VERSION,CHANGELOG.md <项目根>:脚本追加 ship 到 history,写 current_stage: ship_done、next_step: pdlc-deploy(发布阶段不跑检查,测试门已在 §1.2 由人决定)。_done 的含义是「已发布」,只由本命令与 /pdlc-deploy 写入。
<!-- @include templates/prompts/handoff.md(已内联于下方,无需另读) -->
命令完成后必须输出以下格式的最终消息:
✅ <阶段名> 完成:<主要产出物路径>
📊 自检:<通过数>/<总数> 通过(若有未通过,附要点)
📦 状态快照:docs/.pdlc-state/<feature-id>.json
👉 下一步:/pdlc-<next_step>
(如果有分叉)或 /pdlc-<alt>(条件:<选择依据>)规则:
next_step,此时输出 👉 下一步:(本次流程结束,无后续)/pdlc-tdd;若测试已齐 → /pdlc-review"<!-- @include-end templates/prompts/handoff.md -->
本命令的 handoff 输出:
✅ 发布准备完成:v<new-version>
- VERSION:已更新
- CHANGELOG:已追加
- 发布说明草稿:docs/05_deployment/releases/v<new-version>-release-notes.md(开 PR 时 gh pr create --body-file 这个文件)
- Tag:<v<new-version> 已创建 / 待合并后打(合并方式:squash 或 rebase)>
- 测试:<通过 / [skip-tests: 理由]>
📦 状态机:<N> 个功能已推进到 ship_done(<ID 列表>)
👉 下一步:/pdlc-deploy v<new-version>(部署本次发布;项目有 tag 触发的 CI 时,也可手动 git push origin v<new-version>)tag 待合并后打时,下一步改为:
👉 下一步:开 PR 合并发布分支;合并后在主干上运行 /pdlc-ship --tag-after-merge v<new-version>,再 /pdlc-deploy v<new-version>参数:$ARGUMENTS
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.