@weotro/dx 0.1.4 → 0.1.6

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 CHANGED
@@ -202,6 +202,10 @@ target(端)不写死,由 `env-policy.jsonc.targets` 定义;`commands.jso
202
202
  "primary": { "label": "Primary brand" },
203
203
  "secondary": { "label": "Secondary brand" }
204
204
  },
205
+ "committedRuntime": {
206
+ "target": "backend",
207
+ "keys": ["SENTRY_ORG"]
208
+ },
205
209
  "requiredLocalKeys": {
206
210
  "staging": ["DATABASE_URL"],
207
211
  "production": ["DATABASE_URL"]
@@ -218,7 +222,13 @@ dx env validate secondary --staging
218
222
  dx env exec secondary --staging -- dx deploy backend
219
223
  ```
220
224
 
221
- `dx env exec` 会加锁、原子装配 `.env.<environment>.local`、把同一份值注入子进程,并在成功、
225
+ `committedRuntime` 是可选的公开 runtime allowlist。`target` 必须指向 `env-policy.jsonc.targets`
226
+ 中的 target;`keys` 只会从该 target 当前环境的 committed 文件读取。未配置时默认空 allowlist,
227
+ 行为与旧版本一致。allowlist 不接受 secret/localOnly 键、空值或机密占位符,也不允许 committed
228
+ 文件越出项目根目录。合并优先级为调用进程环境 < private profile < committed allowlist,因此仓库
229
+ 已审查的 committed 公共值最终生效,未列入 allowlist 的 committed 字段不会被导出。
230
+
231
+ `dx env exec` 会加锁、原子装配 `.env.<environment>.local`、把合并后的值注入直接子进程,并在成功、
222
232
  失败或中断后删除临时文件。根目录不允许持久保存 staging/production `.local`;发现旧文件时命令
223
233
  会直接报错,必须先迁移到品牌 profile。内部 `dx` 命令未指定环境时会自动补齐;指定冲突环境时
224
234
  直接拒绝。该命令只操作本机文件和子进程,不包含上传、同步或修改 GitHub Environment 的能力。
@@ -292,8 +302,10 @@ dx cache clear -Y
292
302
  关于 `dx initial`:
293
303
 
294
304
  - `dx initial` 会把 npm 包内置的 `skills/` 覆盖同步到 `~/.agents/skills`。
305
+ - 包内共享 agent reference 会覆盖同步到 `~/.agents/references`,供多个 skill 共用同一真源。
295
306
  - `~/.claude/skills` 中包内管理的同名非软链接 skill 会先删除,再创建指向 `~/.agents/skills` 的软链接。
296
307
  - `~/.codex/skills` 中包内管理的同名非软链接 skill 会被清理;已有软链接不会按旧副本删除。
308
+ - 从包内移除的历史 skill 会在 `~/.agents/skills`、`~/.claude/skills`、`~/.codex/skills` 中一并清理。
297
309
  - 不属于包内管理的其他用户自有 skill 目录会保留。
298
310
 
299
311
  关于 `help`:
@@ -0,0 +1,502 @@
1
+ # Ship Issue PR Core
2
+
3
+ 这是 `cc-ship-issue-pr` 与 `oo-ship-issue-pr` 共用的外部流程真源,不独立调用。
4
+
5
+ 入口技能必须先完成自身运行时门禁,再完整读取本文件。入口技能只负责平台差异;本文件负责交付阶段、复杂度门禁、Git/GitHub 约束、验证矩阵、审查循环、合并和回访。两者冲突时,平台调用方式服从入口技能,交付语义服从本文件。
6
+
7
+ ## 入口契约
8
+
9
+ 入口技能必须在执行本流程前明确:
10
+
11
+ - **主进程**:当前宿主 agent,负责全部授权判断、轻改动、Git/GitHub 写操作、权威验证和事实核验。
12
+ - **外部专家**:重改动的方案讨论、实现、独立 review 或 adversarial review 承接者。
13
+ - 只读讨论、可写实现、普通 review、adversarial review 四种调用方式。
14
+ - 外部专家的补跑清单、结果取得、等待/超时与不可用时的处理。
15
+ - follow-up subagent 的派发工具、当前入口技能名和后台运行方式。
16
+ - 审查报告里的 reviewer 名称,以及成功/阻塞输出的入口显示名。
17
+
18
+ 外部专家永远不写 Git 历史和 GitHub。它只在工作树落改动或返回报告,最终结论由主进程核验。
19
+
20
+ ## 复杂度门禁
21
+
22
+ 每次派发外部专家前先过这道门禁。命中任一信号即**重改动**,走外部专家;一个都不命中即**轻改动**,主进程直接做。派发往返成本高于轻改动直改,门禁结果和命中/未命中的信号编号必须记入审查报告。
23
+
24
+ 1. 改公共 API、DTO/OpenAPI 契约、数据库 schema 或迁移、权限/安全边界、计费/积分结算。
25
+ 2. 改并发、事务、缓存、重试、幂等、状态机或流式管线的语义。
26
+ 3. 跨模块或跨端(backend/front/admin/mobile/packages 之间),且需要协调契约、共享状态,或与数据库迁移/数据回填联动。
27
+ 4. 实现文件超过 5 个,或有效可执行改动超过约 100 行(测试、快照、生成产物不计)。
28
+ 5. 仅审查站点适用:主进程初审发现未确认的 Critical/Major,需要独立证据裁决。
29
+
30
+ 三个站点共用这一份信号清单,输入不同:
31
+
32
+ - **方案与实现**(阶段四):按 Issue 验收标准预估改动面。预估命中信号时,先走方案讨论定案,再派外部专家实现;拿不准按轻改动直改,实际 diff 若命中信号,审查站点按重改动兜底。
33
+ - **审查**(阶段 7.3):按 `gh pr diff` 的实际 diff 判定。
34
+ - **修复**(阶段 7.4):按单条修复自身的改动面判定,与原问题的严重级无关。
35
+
36
+ ## 硬门禁
37
+
38
+ | 门禁 | 要求 |
39
+ |---|---|
40
+ | 中文输出 | 过程说明、报告、阻塞原因用中文 |
41
+ | Issue | 提交/发 PR 前必须有 Issue ID;没有就先创建 |
42
+ | Follow-up | 发现不属于本次范围的问题即建 follow-up Issue(当前入口技能被调用即为授权),正文质量同阶段二;建完按 **Follow-up 即建即派** 判闸门,并在 PR body 与审查报告登记编号 |
43
+ | 分支 | 禁止在 `main` 直接提交;分支必须含 Issue ID |
44
+ | 命令位置 | 从仓库根目录执行;Flutter 命令在 `apps/mobile` |
45
+ | 模板 | Issue/PR 正文结构一律取自 `.github/`,禁止自造结构 |
46
+ | Heredoc | 多行 Issue body、commit message、PR body、PR comment 必须用 heredoc |
47
+ | 暂存 | 按明确路径 `git add <path>`,禁止 `git add -A` / `git add .` |
48
+ | 验证 | 发 PR 前必须运行并记录相关验证;改代码后必须重跑 |
49
+ | 正文质量 | 模板占位注释、`TODO`、`TBD`、「稍后补充」残留一律阻塞 |
50
+ | 合并 | 只能 `gh pr merge --squash --auto`,禁止 `--admin` |
51
+ | 真完成 | 必须轮询到 `mergedAt` 非空,并回访关联 Issue 状态 |
52
+
53
+ ## Follow-up 即建即派
54
+
55
+ Follow-up Issue 建完就派出独立 subagent 在自己的 worktree 里跑完整条链路,本次交付继续往下走。攒成清单等用户下次再派,等于把交付责任退回给用户。
56
+
57
+ ### 派发闸门
58
+
59
+ 按顺序判,命中即停:
60
+
61
+ 1. **需要用户授权**——新依赖、生产数据操作或回填、部署发布、真实外部资源(域名、密钥、素材、第三方配置)、产品或设计决策未定、验收标准写不出来。→ 只建 Issue **不派**,在成功输出里写明编号和缺哪一项授权。
62
+ 2. **依赖本次改动**——follow-up 要动本 PR 尚未合并的代码,或改动文件与本 PR 重叠。→ 记账,**阶段九本 PR 合并后再派**,那时 `origin/main` 已带上本次改动。
63
+ 3. **其余**——建完 Issue 立即派,不等本次交付结束。
64
+
65
+ ### 派发步骤
66
+
67
+ worktree 从 `origin/main` 起:从当前分支起会把本 PR 未合并的提交带进 follow-up PR,PR diff 和审查全部失真。分支 `<type>` 按 follow-up 自身性质取值,取值范围同阶段三,不要一律写 `fix`。
68
+
69
+ ```bash
70
+ WT=~/.ship-worktrees/ai-monorepo-<issue-id>
71
+ mkdir -p ~/.ship-worktrees
72
+ git fetch origin main
73
+ git worktree add -b <type>/<issue-id>-<slug> "$WT" origin/main
74
+ ```
75
+
76
+ 新 worktree 只有入库文件,`node_modules` 和 `.env*.local` 都被 gitignore。不用手工补:`dx` 每条命令的启动检查都会从主检出根目录同步 `.env.*.local`,并在 `node_modules` 缺失时跑 `pnpm install --frozen-lockfile`。一条命令 bootstrap 到位,和 `paseo.json` 的 `worktree.setup` 同款:
77
+
78
+ ```bash
79
+ (cd "$WT" && dx build all)
80
+ ```
81
+
82
+ 不要用 `dx worktree make`:它建的分支叫 `issue-<n>`,不符合当前流程的 `<type>/<id>-<slug>` 格式,还会 `git fetch origin main:main` 动本地 `main`。
83
+
84
+ 然后使用入口技能声明的 follow-up 派发通道,prompt 必须包含:
85
+
86
+ - 「工作目录是 `<WT 绝对路径>`,每条命令都从这里执行,不要进入主检出或别的 worktree。」
87
+ - 「调用当前入口技能交付 Issue #<id>,直到 `mergedAt` 非空且回访完成;技能清单里没有它就读取入口技能文件全文。」
88
+ - 「Issue 和分支都已建好,从阶段二起走:阶段二绑定已有 Issue #<id>,不要新建 Issue,不要新建分支。」
89
+ - 「你自己发现的 follow-up 只建 Issue 并写进 PR body,不要继续派发。」——派发深度只有一层,否则一次交付会无限扇出。
90
+ - 「外部专家通道不可用时按入口技能的降级规则处理,并在 PR body 记录。」
91
+ - 下方**并发边界**的后端 E2E 抢锁写法原文。
92
+
93
+ 入口技能必须把自身名称、subagent 工具和后台运行方式写进派发 prompt;共享流程不猜测宿主工具。
94
+
95
+ ### 并发边界
96
+
97
+ 本机只有一套本地 PostgreSQL 和一份 `.env.e2e.local`,两条交付线同时跑后端 E2E 会互相清库,失败看起来像代码回归。要跑后端 E2E 的一律先抢锁:
98
+
99
+ ```bash
100
+ until mkdir /tmp/ship-issue-pr-e2e.lock 2>/dev/null; do sleep 30; done
101
+ dx test e2e backend <file-or-dir>; rc=$?
102
+ rmdir /tmp/ship-issue-pr-e2e.lock; exit $rc
103
+ ```
104
+
105
+ 整段使用入口技能声明的后台执行方式运行。锁目录 mtime 超过 30 分钟即认定上一条线已死,`rmdir` 后重抢。
106
+
107
+ 同时在跑的 follow-up subagent 最多 3 个。超出的由主进程记在待派清单里,等某条线的完成通知到达后再派下一个——排队没有别的执行者,主进程不记就等于丢了。
108
+
109
+ ### 登记与回收
110
+
111
+ 派发后主进程不等它,继续本次交付。每个派出的 follow-up 在 PR body、审查报告和成功输出里都要有一行:Issue 编号、worktree 路径、subagent 名。
112
+
113
+ 完成通知到达时向用户汇报它是合并了还是阻塞了。确认合并后回收 worktree 和本地分支——PR 走 `--squash` 合并,git 不认为分支已合并,`git branch -d` 会被拒,必须 `-D`:
114
+
115
+ ```bash
116
+ git worktree remove ~/.ship-worktrees/ai-monorepo-<issue-id>
117
+ git branch -D <type>/<issue-id>-<slug>
118
+ ```
119
+
120
+ ## 阶段一:状态检测
121
+
122
+ ```bash
123
+ git status --short
124
+ git branch --show-current
125
+ git log origin/main..HEAD --oneline
126
+ git diff --stat origin/main...HEAD
127
+ gh pr status
128
+ ```
129
+
130
+ | 现状 | 入口 | 跳到该阶段前必须先补齐 |
131
+ |---|---|---|
132
+ | 有需求但工作树干净 | 阶段二起完整链路 | — |
133
+ | 工作树改动经确认属于本次任务 | 阶段二起完整链路,**跳过阶段四** | 先按下方要求区分相关与无关改动 |
134
+ | 工作树只有无关改动 | 阶段二起完整链路,**照常走阶段四** | 同上;无关改动原样留在工作树,不暂存、不提交 |
135
+ | 工作树干净且分支有未发 PR 提交 | 阶段六 | 阶段二的 Issue 绑定、阶段三的分支合规、阶段五的验证 |
136
+ | 当前分支已有 OPEN PR | 阶段七 | 上一行全部,外加阶段六的 PR body 读回校验与依赖冲突检查 |
137
+ | 工作树干净、无分支差异、无 OPEN PR | 输出「无需交付」并结束 | — |
138
+
139
+ 快捷入口只省掉已经做过的动作,不豁免任何门禁。补齐前禁止进入目标阶段:没有 Issue ID 就先建 Issue,分支不合规就先改分支,验证没跑过就先跑,依赖 PR 未真合并就停下。
140
+
141
+ 阻塞条件:不在 Git 仓库;`gh auth status` 不可用;当前分支为 `main` 且无法创建新分支。
142
+
143
+ 保护工作区内一切未提交改动。发现脏工作树时先逐个文件区分本次任务相关与无关修改,不覆盖、不回退、不顺手整理无关文件;禁止 `git checkout <ref> -- <path>` 一类静默覆盖命令。
144
+
145
+ 脏工作树不等于「本次实现已完成」。只有确认改动确实实现了本次需求才允许跳过阶段四;改动与本次任务无关时照常走阶段四,并在阶段五只暂存本次任务的文件。判断不了归属就直接问用户,不要猜。
146
+
147
+ ## 阶段二:Issue 创建或绑定
148
+
149
+ 先从用户输入、分支名、commit、已有 PR body 提取 Issue ID。提取不到就创建。
150
+
151
+ 创建前先开工查 `main` 有没有并行的重复交付,撞车了先拆聚焦范围再动手。
152
+
153
+ **正文结构取自仓库模板**,`gh issue create` 不会自动套模板,必须自己读出来再填:
154
+
155
+ ```bash
156
+ sed '1,/^---$/d' .github/ISSUE_TEMPLATE/issue.md > /tmp/ship-issue-pr-issue-body.md
157
+ ```
158
+
159
+ 按模板段落逐段填写真实内容,再创建:
160
+
161
+ ```bash
162
+ gh issue create \
163
+ --title "fix(scope): 问题摘要" \
164
+ --label enhancement \
165
+ --body-file /tmp/ship-issue-pr-issue-body.md
166
+ ```
167
+
168
+ 标题格式 `<type>(<scope>): 简洁目标` 或 `[模块] 简洁目标`。标签从 `bug`、`enhancement`、`documentation`、`performance`、`refactor`、`backend`、`frontend`、`infrastructure`、`test` 中选。
169
+
170
+ 填写质量要求,模板本身不管这些:
171
+
172
+ - 「验收标准」每条必须能在 diff、命令输出或手测步骤中找到证据。写不出可验证的标准就先停下回到目标澄清,禁止用「代码更优雅」凑数。
173
+ - 「背景」要让 reviewer 理解为什么现在要做,附失败证据、相关文件/PR/Issue。
174
+ - 「目标」写完成后可观察的状态,不写操作清单。
175
+ - 依赖上游配置、外部服务或后续 PR 时,必须写进正文。
176
+
177
+ 创建后读回校验:
178
+
179
+ ```bash
180
+ gh issue view <ISSUE_ID> --json title,body,labels,url
181
+ ```
182
+
183
+ ```bash
184
+ gh issue view <ISSUE_ID> --json body -q .body > /tmp/ship-issue-pr-issue-readback.md
185
+ grep -oE '<!--.*-->' .github/ISSUE_TEMPLATE/issue.md | grep -qFf - /tmp/ship-issue-pr-issue-readback.md \
186
+ && echo "阻塞:模板提示注释未替换" || echo "占位检查通过"
187
+ grep -c '^- \[ \]' /tmp/ship-issue-pr-issue-readback.md
188
+ ```
189
+
190
+ 校验失败条件:缺模板任一段落;验收标准少于 1 条;验收标准是动作清单而非可验证结果;模板提示注释未替换;`- [ ]` 空条目仍留在正文。
191
+
192
+ ## 阶段三:分支
193
+
194
+ 格式 `<type>/<issue-id>-<slug>`,`<type>` 只能是 `feat`、`fix`、`refactor`、`docs`、`chore`、`test`。
195
+
196
+ ```bash
197
+ git switch -c fix/1234-short-topic
198
+ ```
199
+
200
+ 当前分支是 `main` 或不含 Issue ID 时必须新建;已合规则继续使用。
201
+
202
+ ## 阶段四:实现
203
+
204
+ 工作树已有改动时跳过本阶段。
205
+
206
+ 先按**复杂度门禁**判定:
207
+
208
+ - **轻改动**:主进程直接实现。先读涉及目录的 `AGENTS.md`,改动收敛在 Issue 验收标准范围内,完成后直接进阶段五。
209
+ - **重改动**:先走方案讨论定案,再派外部专家实现。
210
+
211
+ ### 方案讨论(仅重改动)
212
+
213
+ 主进程先读涉及目录的 `AGENTS.md` 和相关源码,写出自己的初步方案:实现路径、涉及文件、关键取舍、已想到的备选。有了初步方案再讨论,问题清单越具体,讨论产出越有用。
214
+
215
+ 然后使用入口技能声明的只读方案讨论通道,prompt 写死「只讨论方案,禁止任何写操作,不要递归委派」,并附 Issue 原文与验收标准、初步方案、备选与取舍、希望裁决的具体问题。同一任务的相关取舍合并成一次讨论,不反复往返。
216
+
217
+ 外部专家返回后主进程逐条核验它引用的事实(直读源码验证,不采信转述),采纳与拒绝各记一句理由,最终方案由主进程拍板。
218
+
219
+ ### 派外部专家实现(仅重改动)
220
+
221
+ 使用入口技能声明的实现通道,按入口技能声明的结果取得与等待规则推进。prompt 必须包含:
222
+
223
+ - Issue 编号、原文目标与逐条验收标准。
224
+ - 方案讨论定案的最终方案原文。
225
+ - 涉及的目录,以及该目录 `AGENTS.md` 要求先读。
226
+ - 「改动留在工作树,不要 `git commit`、不要 `git push`、不要碰 GitHub」。
227
+ - 入口技能声明的补跑清单,并要求把无法运行的验证列出来交回。
228
+ - 「不要递归委派给别的 agent」。
229
+
230
+ 外部专家返回后,主进程逐条核验它的结论,不照单全收:
231
+
232
+ ```bash
233
+ git status --short
234
+ git diff --stat
235
+ ```
236
+
237
+ 改动范围超出 Issue 目标、或引入未经说明的依赖时,主进程收敛回范围内,不带进提交。
238
+
239
+ ## 阶段五:验证与提交
240
+
241
+ 按改动范围执行最低充分验证:
242
+
243
+ | 改动范围 | 必跑命令 |
244
+ |---|---|
245
+ | 任意 JS/TS | `dx lint` |
246
+ | mobile(哪怕只加一行 import) | `dx lint`(含 `design:lint`,覆盖 Dart) |
247
+ | 后端 | `dx build backend --dev`,相关 `dx test unit backend [path]` |
248
+ | 后端 E2E 相关 | `dx test e2e backend <file-or-dir>`,禁止无参全量 |
249
+ | 前端用户端 | `dx build front --dev`,相关 `dx test unit front [path]` |
250
+ | 管理端 | `dx build admin --dev`,相关 `dx test unit admin [path]` |
251
+ | DTO/API 契约 | `dx export openapi --dev` 后再 `dx build api-contracts` |
252
+ | 共享包 | `dx build shared` 或相关 shared 测试 |
253
+ | Flutter | `cd apps/mobile && flutter analyze`,相关 `flutter test <path>` |
254
+ | 范围不确定 | `dx lint` + `dx build affected --dev` + 相关测试 |
255
+
256
+ 失败先查 `docs/agents/known-test-failures.md`:清单里有就是既有失败,清单里没有就按本次改动引入处理。修复或新发现既有失败时同步更新该清单。
257
+
258
+ 无法在本 PR 内修复的既有失败,创建 follow-up Issue,并在 PR body 与审查报告同时登记编号。
259
+
260
+ 提交由主进程做,每个逻辑变更一个 commit。
261
+
262
+ 按明确路径暂存,禁止 `git add -A` / `git add .`:工作树可能带着用户的无关未提交改动,全量暂存会把草稿甚至密钥一起提交推送,也让 PR 范围失控。暂存后必须核对 `--cached` 清单,出现本次任务之外的文件就撤出该文件重来。
263
+
264
+ ```bash
265
+ git add <本次改动的明确路径>
266
+ git diff --cached --stat
267
+ git commit -F - <<'MSG'
268
+ fix: 简洁摘要
269
+
270
+ 变更说明:
271
+ - 说明关键改动
272
+ - 说明影响范围
273
+
274
+ Refs: #123
275
+ MSG
276
+ ```
277
+
278
+ 标题必须是 Conventional Commit。既有问题的顺手修复单独用 `chore(precheck):` 提交,与本次功能改动分开。
279
+
280
+ ## 阶段六:PR 创建或更新
281
+
282
+ ```bash
283
+ git push -u origin HEAD
284
+ git log origin/main..HEAD --oneline
285
+ git diff origin/main...HEAD --stat
286
+ ```
287
+
288
+ **正文结构取自仓库模板**:
289
+
290
+ ```bash
291
+ cp .github/pull_request_template.md /tmp/ship-issue-pr-pr-body.md
292
+ ```
293
+
294
+ 按模板段落填真实内容后自检再创建。并行交付多个 PR 时用带 PR 号的文件名,不要共用同一个 `/tmp` 路径:
295
+
296
+ ```bash
297
+ # 模板自带的提示注释必须已被替换;按模板原文匹配,不要按字面 `<!--` 匹配
298
+ grep -oE '<!--.*-->' .github/pull_request_template.md | grep -qFf - /tmp/ship-issue-pr-pr-body.md \
299
+ && echo "阻塞:模板提示注释未替换" || echo "占位检查通过"
300
+ ! grep -qE '稍后补充|TODO|TBD' /tmp/ship-issue-pr-pr-body.md
301
+ grep -q 'Closes: #[0-9]' /tmp/ship-issue-pr-pr-body.md
302
+
303
+ gh pr create --base main --title "fix: 简洁摘要" --body-file /tmp/ship-issue-pr-pr-body.md
304
+ ```
305
+
306
+ 按字面 `<!--` 匹配会误伤讨论模板机制本身的 PR,正文里合法引用 HTML 注释会被判成占位残留。
307
+
308
+ 填写质量要求:
309
+
310
+ - 「已做的验证」必须列实际命令、涉及测试文件、关键结果。写「已测试」「本地通过」等于没写。命令没跑就不许创建 PR。
311
+ - 有新增/修改测试时必须列测试文件;没有测试时说明为什么没有。
312
+ - Issue 有未覆盖的验收标准时,「遗留的问题」不得写「无」,必须补做或建 follow-up Issue 并引用。
313
+ - `#123` 只用于真实 Issue/PR 引用;普通序号写 `[1]`、`问题 1`,避免被 GitHub 误链接。
314
+ - 更新既有 PR 时同样要过这套检查,不合格立刻 `gh pr edit <PR_NUMBER> --body-file` 修正。
315
+
316
+ 创建或更新后读回:
317
+
318
+ ```bash
319
+ gh pr view <PR_NUMBER> --json title,body,url
320
+ ```
321
+
322
+ body 为空、缺模板段落、模板提示注释未替换、留有占位符、「已做的验证」没有实际命令、缺 `Closes: #<issue-id>`,任一命中即修正后重读。
323
+
324
+ ### 依赖与冲突
325
+
326
+ PR body 标了 `PR Train`、`依赖 #<PR_NUMBER>` 时,先确认依赖 PR 的 `mergedAt` 非空,否则停止,禁止继续审查或合并。
327
+
328
+ ```bash
329
+ git fetch origin main
330
+ git merge --no-commit --no-ff origin/main
331
+ ```
332
+
333
+ 无冲突则 `git merge --abort` 继续。有冲突则 `git merge --abort` 后真实合并、解冲突、重跑验证、单独提交并推送,commit 说明保留哪一侧语义及原因。
334
+
335
+ ## 阶段七:审查修复循环
336
+
337
+ 最多 3 轮。每轮四件事:Issue 验收、验证流水线、代码审查、逐项修复。
338
+
339
+ ### 7.1 Issue 验收(主进程)
340
+
341
+ ```bash
342
+ gh issue view <ISSUE_ID> --json title,body,state
343
+ gh pr diff <PR_NUMBER>
344
+ ```
345
+
346
+ 逐条核对验收标准:
347
+
348
+ | 状态 | 后续 |
349
+ |---|---|
350
+ | 通过 | 记录文件/行号或模块 |
351
+ | 部分 | 必须修复 |
352
+ | 未实现 | 必须补做或拆 follow-up Issue |
353
+ | 超出范围 | PR body 补释或拆 Issue |
354
+
355
+ ### 7.2 验证流水线(主进程)
356
+
357
+ 重跑阶段五中与本轮改动相关的命令。失败必须保留完整错误:文件、行号、命令、失败摘要。
358
+
359
+ ### 7.3 代码审查
360
+
361
+ 每轮由主进程自审:通读 `gh pr diff` 全部改动,顺手核查 diff 涉及文件内的真实 bug,不主动扩大到无关模块。
362
+
363
+ 外部专家独立审查是条件式补充,同时满足两条才派发:
364
+
365
+ 1. 实际 diff 命中**复杂度门禁**任一信号(重改动)。
366
+ 2. 本 PR 尚未用过外部专家审查——同一 PR 最多一次,覆盖整个 PR 生命周期;派发前查本 PR 已有审查报告评论的 reviewer 字段,出现过外部专家即只由主进程复审后续修复。轻改动 PR 的这次额度可以留到后续 diff 扩大首次命中信号时再用。
367
+
368
+ 使用入口技能声明的 review 通道。需要挑战实现方案、状态机、不变量或安全边界时改用 adversarial review;需要对照 Issue 验收标准或排除已确认问题时,把定制上下文一并传入。
369
+
370
+ 外部专家返回后主进程逐条核验,直读源码复核,不采信转述。合并重复项,记录拒绝依据。
371
+
372
+ 无论 reviewer 是谁,每条进入报告的问题必须有四要素:严重级、文件:行号、具体问题、具体改法。缺四要素或只写「建议优化」的条目不进报告。功能未实现类问题归入 7.1,不在这里重复。
373
+
374
+ | 严重级 | 例子 | 默认处理 |
375
+ |---|---|---|
376
+ | Critical | 构建失败、测试失败、安全漏洞、数据丢失 | 必修 |
377
+ | Major | 逻辑缺陷、错误处理缺失、性能问题、验收部分缺失 | 修复或拆 follow-up |
378
+ | Minor | 命名、局部风格、文档、小范围清理 | 小于 5 行优先修,否则写拒绝理由 |
379
+
380
+ 发布审查报告评论:
381
+
382
+ ```bash
383
+ gh pr comment <PR_NUMBER> --body-file - <<'MSG'
384
+ ## 代码审查报告(第 N 轮)
385
+
386
+ ### 概要
387
+
388
+ - Critical:0 / Major:0 / Minor:0
389
+ - reviewer:主进程自审 / 外部专家 review / 外部专家 adversarial review
390
+ - 复杂度门禁:轻改动(未命中信号)/ 重改动(命中信号 N);外部专家额度:未用 / 本轮已用 / 此前已用
391
+
392
+ ### 验证流水线
393
+
394
+ - Lint:通过/失败
395
+ - 构建:通过/失败(实际命令)
396
+ - 测试:通过/失败/跳过(实际命令)
397
+
398
+ ### 问题列表
399
+
400
+ | 严重级 | 文件:行号 | 描述 | 建议改法 | 处理 |
401
+ |---|---|---|---|---|
402
+ | Major | path/file.ts:42 | 具体描述 | 具体改法 | 修复/拒绝/follow-up |
403
+
404
+ ### 拒绝依据
405
+
406
+ - 逐条写具体理由
407
+ MSG
408
+ ```
409
+
410
+ ### 7.4 逐项修复
411
+
412
+ 按 `Critical -> Major -> Minor` 处理,每个问题一个 commit,禁止攒到最后。
413
+
414
+ 修复默认主进程直接改;只有单条修复自身命中**复杂度门禁**信号(判定的是这次修复的改动面,不是原问题的严重级)才派外部专家,规则同阶段四。
415
+
416
+ 修复后重跑相关验证,并发布修复报告评论,列出已修复项及其 commit、拒绝项及理由、follow-up 项及 Issue 编号。
417
+
418
+ ### 循环终止
419
+
420
+ 结束条件:验收全部完成;验证全部通过或不可修项已登记 follow-up;无未处理的 Critical/Major。
421
+
422
+ 已达 3 轮仍未满足时,禁止合并,走阻塞输出。
423
+
424
+ ## 阶段八:合并
425
+
426
+ 合并前最后一次运行相关验证并发布验证总结评论,列出各步骤状态与实际命令、审查轮数与问题统计。验证失败时禁止写「可合并」。
427
+
428
+ ```bash
429
+ gh pr merge <PR_NUMBER> --squash --auto
430
+ gh pr view <PR_NUMBER> --json state,mergedAt,url
431
+ ```
432
+
433
+ 本仓库的 GitHub Actions 都不在 `pull_request` 上触发,PR 没有 CI 门禁——质量门禁全靠上面的本地验证。不要等待或监控 `gh pr checks`,`--auto` 设置后通常立即合并。
434
+
435
+ 终止条件:`mergedAt` 非空即成功;`state=CLOSED` 且 `mergedAt=null` 为阻塞;轮询数分钟仍未合并则输出 stuck 状态,禁止 `--admin` 越权。
436
+
437
+ ## 阶段九:回访
438
+
439
+ ```bash
440
+ gh pr view <PR_NUMBER> --json mergedAt,mergeCommit,url
441
+ gh issue view <ISSUE_ID> --json state,title,url
442
+ ```
443
+
444
+ 确认 Issue 因 `Closes:` 正确关闭、验收标准真的全部覆盖、PR body 的遗留项都有 follow-up Issue。Issue 被误关但仍有未完成验收标准时,重开或拆 follow-up,并在最终输出写明编号。
445
+
446
+ 用一组 commit 带入 main 的集成分支会让各 PR 的 `Closes:` 全部失效,此时必须手工逐个回访关闭。
447
+
448
+ 本 PR 已进 main,把**派发闸门**第 2 挡记账的 follow-up 现在派出去,`origin/main` 已经带上本次改动。
449
+
450
+ ## 成功输出
451
+
452
+ 只在确认真合并和回访完成后输出:
453
+
454
+ ```text
455
+ <入口显示名> 完成
456
+
457
+ - Issue:#123 标题
458
+ - PR:#456 链接
459
+ - 分支:fix/123-short-topic
460
+ - 实现:重改动(方案讨论 + 外部专家)/ 轻改动(主进程直改);Commit:首个提交摘要;修复提交 X 个
461
+ - 验证:Lint 通过;构建通过;测试通过/跳过原因
462
+ - 审查:N 轮;发现 X 个;修复 Y 个;拒绝 Z 个
463
+ - 合并:已合并到 main,merged_at=<timestamp>
464
+ - 回访:Issue 状态已确认
465
+ - Follow-up:#789 已派发(worktree ~/.ship-worktrees/ai-monorepo-789,subagent <名字>);#790 未派(缺生产数据操作授权)
466
+ ```
467
+
468
+ ## 阻塞输出
469
+
470
+ ```text
471
+ <入口显示名> 阻塞
472
+
473
+ - 停止阶段:阶段名
474
+ - 已完成:已完成的可审计动作
475
+ - 阻塞原因:具体错误、缺权限、验证失败、依赖未合并
476
+ - 不应做的事:禁止绕过的门禁
477
+ - 下一步建议:可执行命令或需要用户提供的信息
478
+ ```
479
+
480
+ ## 红旗
481
+
482
+ 出现这些想法时停止,回到对应阶段:
483
+
484
+ - 「这个改动很小,不需要 Issue。」
485
+ - 「PR body 之后再补。」/「先写『已通过本地测试』。」
486
+ - 「模板注释留着不影响阅读。」
487
+ - 「既有测试失败不是我引入的,跳过。」——查 `known-test-failures.md`,没有就是本次引入。
488
+ - 「这个改动很简单,但派外部专家更保险。」——过复杂度门禁,轻改动主进程直改。
489
+ - 「方案直接问外部专家,我照它说的做。」——先写出自己的初步方案再讨论,避免被锚定;拍板权在主进程。
490
+ - 「方案讨论太啰嗦,重改动直接开写。」——预估命中信号就必须先讨论定案。
491
+ - 「改动命中信号了,不过我直接改更快。」——重改动派外部专家,直改省下的时间会在审查修复里还回去。
492
+ - 「修了一轮,再让外部专家复查一次。」——外部专家审查每 PR 最多一次,后续由主进程复审。
493
+ - 「派给外部专家了,我先空等。」——按入口技能声明的等待与超时规则推进;可并行时只做不写工作树的事。
494
+ - 「外部专家说改好了,直接提交。」——主进程必须核验 diff 和验证结果。
495
+ - 「外部专家跑验证失败了,让它反复重试。」——查入口技能的补跑清单,由主进程执行权威验证。
496
+ - 「让外部专家顺手 commit 一下。」——Git 历史只由主进程写。
497
+ - 「follow-up 先记着,等用户下次再派。」——过**派发闸门**,只有第 1 挡才留给用户。
498
+ - 「follow-up 的 worktree 从当前分支开更省事。」——从 `origin/main` 起,否则本 PR 未合并的提交会混进 follow-up PR。
499
+ - 「派出去的 subagent 也让它自己往下派 follow-up。」——派发深度只有一层,它只建 Issue。
500
+ - 「GitHub 显示 mergeable,不用逐条核对验收标准。」
501
+ - 「auto-merge 已设置,所以算完成。」
502
+ - 「先 `--admin` 合了再说。」
@@ -5,6 +5,7 @@ import os from 'node:os'
5
5
  import { logger } from './logger.js'
6
6
 
7
7
  const TEMP_DIR_PATTERN = /^\..+\.(tmp|backup)-\d+-\d+$/
8
+ const MANAGED_AGENT_REFERENCES = ['ship-issue-pr-core.md']
8
9
 
9
10
  // 历史上曾在 skills/ 目录托管、但后续已删除的 skill 名称。
10
11
  // 来源:git log --all --diff-filter=A --name-only -- 'skills/*' | sed -n 's#^skills/\([^/]*\)/.*#\1#p' | sort -u
@@ -27,6 +28,9 @@ const DELETED_SKILLS = [
27
28
  'pagination-dto-audit-fixer',
28
29
  'pr-ship',
29
30
  'pr-train-ship',
31
+ 'create-issue',
32
+ 'ship-issue-pr',
33
+ 'third-party-review',
30
34
  ]
31
35
 
32
36
  async function collectSkillNames(dir) {
@@ -147,6 +151,43 @@ async function copyDirMerge({ srcDir, dstDir }) {
147
151
  return { fileCount }
148
152
  }
149
153
 
154
+ async function copyManagedFiles({ srcDir, dstDir, fileNames }) {
155
+ let fileCount = 0
156
+
157
+ for (const fileName of fileNames) {
158
+ const source = join(srcDir, fileName)
159
+ const target = join(dstDir, fileName)
160
+ const token = `${process.pid}-${Date.now()}`
161
+ const temporary = join(dstDir, `.${fileName}.tmp-${token}`)
162
+ const backup = join(dstDir, `.${fileName}.backup-${token}`)
163
+ let hasBackup = false
164
+
165
+ try {
166
+ const sourceStat = await fs.stat(source)
167
+ if (!sourceStat.isFile()) throw new Error(`agent reference 不是文件: ${source}`)
168
+
169
+ await fs.copyFile(source, temporary)
170
+ if (await lstatIfExists(target)) {
171
+ await fs.rename(target, backup)
172
+ hasBackup = true
173
+ }
174
+ await fs.rename(temporary, target)
175
+ if (hasBackup) await fs.rm(backup, { recursive: true, force: true })
176
+ fileCount++
177
+ } catch (error) {
178
+ await fs.rm(temporary, { recursive: true, force: true })
179
+ if (hasBackup && !(await lstatIfExists(target))) {
180
+ await fs.rename(backup, target)
181
+ hasBackup = false
182
+ }
183
+ if (hasBackup) await fs.rm(backup, { recursive: true, force: true })
184
+ throw error
185
+ }
186
+ }
187
+
188
+ return { fileCount }
189
+ }
190
+
150
191
  async function removeManagedNonSymlinkSkills(skillsDir, skillNames) {
151
192
  for (const skillName of skillNames) {
152
193
  const target = join(skillsDir, skillName)
@@ -195,6 +236,17 @@ async function removeStaleTempDirs(skillsDir) {
195
236
  }
196
237
  }
197
238
 
239
+ async function removeStaleManagedFileArtifacts(dir, fileNames) {
240
+ const entries = await fs.readdir(dir, { withFileTypes: true })
241
+ const prefixes = fileNames.flatMap((fileName) => [`.${fileName}.tmp-`, `.${fileName}.backup-`])
242
+
243
+ for (const entry of entries) {
244
+ const prefix = prefixes.find((candidate) => entry.name.startsWith(candidate))
245
+ if (!prefix || !/^\d+-\d+$/.test(entry.name.slice(prefix.length))) continue
246
+ await fs.rm(join(dir, entry.name), { recursive: true, force: true })
247
+ }
248
+ }
249
+
198
250
  async function linkSkillsToClaude({ skillNames, agentsSkillsDir, claudeSkillsDir }) {
199
251
  for (const skillName of skillNames) {
200
252
  const srcSkillDir = join(agentsSkillsDir, skillName)
@@ -225,11 +277,14 @@ export async function runCodexInitial(options = {}) {
225
277
 
226
278
  const homeDir = options.homeDir || os.homedir()
227
279
  const srcSkillsDir = join(packageRoot, 'skills')
280
+ const srcAgentReferencesDir = join(packageRoot, 'agent-references')
228
281
  const agentsSkillsDir = join(homeDir, '.agents', 'skills')
282
+ const agentsReferencesDir = join(homeDir, '.agents', 'references')
229
283
  const claudeSkillsDir = join(homeDir, '.claude', 'skills')
230
284
  const codexSkillsDir = join(homeDir, '.codex', 'skills')
231
285
 
232
286
  await assertDirExists(srcSkillsDir, '模板目录 skills')
287
+ await assertDirExists(srcAgentReferencesDir, '模板目录 agent-references')
233
288
  const skillNames = await collectSkillNames(srcSkillsDir)
234
289
 
235
290
  // 护栏:已删除名单不得与现存 skill 重叠,否则会把刚同步的 skill 又清掉(自相矛盾)。
@@ -241,6 +296,7 @@ export async function runCodexInitial(options = {}) {
241
296
  await ensureDir(codexSkillsDir)
242
297
  await ensureDir(claudeSkillsDir)
243
298
  await ensureDir(agentsSkillsDir)
299
+ await ensureDir(agentsReferencesDir)
244
300
 
245
301
  await removeManagedNonSymlinkSkills(codexSkillsDir, skillNames)
246
302
  await removeManagedNonSymlinkSkills(claudeSkillsDir, skillNames)
@@ -254,11 +310,19 @@ export async function runCodexInitial(options = {}) {
254
310
 
255
311
  await removeStaleTempDirs(agentsSkillsDir)
256
312
  const copyStats = await copyDirMerge({ srcDir: srcSkillsDir, dstDir: agentsSkillsDir })
313
+ await removeStaleManagedFileArtifacts(agentsReferencesDir, MANAGED_AGENT_REFERENCES)
314
+ const referenceStats = await copyManagedFiles({
315
+ srcDir: srcAgentReferencesDir,
316
+ dstDir: agentsReferencesDir,
317
+ fileNames: MANAGED_AGENT_REFERENCES,
318
+ })
319
+ await removeStaleManagedFileArtifacts(agentsReferencesDir, MANAGED_AGENT_REFERENCES)
257
320
  await removeStaleTempDirs(agentsSkillsDir)
258
321
  await linkSkillsToClaude({ skillNames, agentsSkillsDir, claudeSkillsDir })
259
322
 
260
323
  logger.success('已初始化 skills 模板')
261
324
  logger.info(`agents skills: 覆盖复制 ${copyStats.fileCount} 个文件 -> ${agentsSkillsDir}`)
325
+ logger.info(`agents references: 覆盖复制 ${referenceStats.fileCount} 个文件 -> ${agentsReferencesDir}`)
262
326
  logger.info(`claude skills: 已创建 ${skillNames.length} 个软链接 -> ${claudeSkillsDir}`)
263
327
  logger.info(`codex skills: 已清理 ${skillNames.length} 个包内托管 skill 的旧副本 -> ${codexSkillsDir}`)
264
328
  logger.info(