@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.
@@ -8,17 +8,19 @@ import {
8
8
  mkdirSync,
9
9
  openSync,
10
10
  readFileSync,
11
+ realpathSync,
11
12
  renameSync,
12
13
  rmSync,
13
14
  statSync,
14
15
  writeFileSync,
15
16
  } from 'node:fs'
16
17
  import { tmpdir } from 'node:os'
17
- import { basename, join, relative } from 'node:path'
18
+ import { basename, isAbsolute, join, relative, resolve, sep } from 'node:path'
18
19
  import { loadEnvPolicy, resolveTargetRequiredVars } from './env-policy.js'
19
20
 
20
21
  const PROFILE_CONFIG_FILE = 'env-profiles.json'
21
22
  const PROFILE_NAME_PATTERN = /^[a-z0-9][a-z0-9-]*$/
23
+ const ENV_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/
22
24
  const SIGNAL_EXIT_CODES = { SIGINT: 130, SIGTERM: 143, SIGHUP: 129 }
23
25
 
24
26
  function assert(condition, message) {
@@ -69,7 +71,29 @@ export function loadEnvProfileConfig(configDir) {
69
71
  assert(config.environments.includes(environment), `requiredLocalKeys 包含未声明环境: ${environment}`)
70
72
  assert(Array.isArray(keys), `requiredLocalKeys.${environment} 必须为数组`)
71
73
  for (const key of keys) {
72
- assert(/^[A-Za-z_][A-Za-z0-9_]*$/.test(key), `requiredLocalKeys.${environment} 包含非法键: ${key}`)
74
+ assert(ENV_KEY_PATTERN.test(key), `requiredLocalKeys.${environment} 包含非法键: ${key}`)
75
+ }
76
+ }
77
+
78
+ if (config.committedRuntime !== undefined) {
79
+ const committedRuntime = config.committedRuntime
80
+ assert(
81
+ committedRuntime && typeof committedRuntime === 'object' && !Array.isArray(committedRuntime),
82
+ `${PROFILE_CONFIG_FILE}.committedRuntime 必须为对象`,
83
+ )
84
+ assert(
85
+ typeof committedRuntime.target === 'string' && committedRuntime.target.trim(),
86
+ `${PROFILE_CONFIG_FILE}.committedRuntime.target 必须为非空字符串`,
87
+ )
88
+ assert(Array.isArray(committedRuntime.keys), `${PROFILE_CONFIG_FILE}.committedRuntime.keys 必须为数组`)
89
+ const seenKeys = new Set()
90
+ for (const key of committedRuntime.keys) {
91
+ assert(
92
+ typeof key === 'string' && ENV_KEY_PATTERN.test(key),
93
+ `${PROFILE_CONFIG_FILE}.committedRuntime.keys 包含非法键: ${String(key)}`,
94
+ )
95
+ assert(!seenKeys.has(key), `${PROFILE_CONFIG_FILE}.committedRuntime.keys 包含重复键: ${key}`)
96
+ seenKeys.add(key)
73
97
  }
74
98
  }
75
99
 
@@ -163,6 +187,70 @@ function mergeEntries(...maps) {
163
187
  return merged
164
188
  }
165
189
 
190
+ function resolveCommittedRuntimeEnv({ projectRoot, config, policy, environment }) {
191
+ const committedRuntime = config.committedRuntime
192
+ if (!committedRuntime || committedRuntime.keys.length === 0) return {}
193
+
194
+ const targetId = committedRuntime.target
195
+ const target = policy.targets?.[targetId]
196
+ assert(target, `${PROFILE_CONFIG_FILE}.committedRuntime.target 指向不存在的 policy target: ${targetId}`)
197
+ assert(
198
+ policy.environments.includes(environment),
199
+ `${PROFILE_CONFIG_FILE}.committedRuntime 不支持 policy 未声明的环境: ${environment}`,
200
+ )
201
+
202
+ const committedTemplate = target.files.committed
203
+ assert(
204
+ committedTemplate.includes('{env}'),
205
+ `env-policy.jsonc.targets.${targetId}.files.committed 必须包含 {env} 才能用于 committed runtime`,
206
+ )
207
+ const projectBoundary = resolve(projectRoot)
208
+ const committedPath = resolve(projectBoundary, committedTemplate.replace(/\{env\}/g, environment))
209
+ const pathWithinProject = relative(projectBoundary, committedPath)
210
+ assert(
211
+ pathWithinProject &&
212
+ pathWithinProject !== '..' &&
213
+ !pathWithinProject.startsWith(`..${sep}`) &&
214
+ !isAbsolute(pathWithinProject),
215
+ `committed runtime 文件必须位于项目根目录内: ${committedTemplate}`,
216
+ )
217
+
218
+ const secretKeys = new Set(policy.keys?.secret || [])
219
+ const localOnlyKeys = new Set(policy.keys?.localOnly || [])
220
+ for (const key of committedRuntime.keys) {
221
+ assert(!secretKeys.has(key), `committed runtime 不允许导出机密键: ${key}`)
222
+ assert(!localOnlyKeys.has(key), `committed runtime 不允许导出 localOnly 键: ${key}`)
223
+ }
224
+
225
+ assert(existsSync(committedPath), `committed runtime 文件不存在: ${pathWithinProject}`)
226
+ const linkStat = lstatSync(committedPath)
227
+ assert(!linkStat.isSymbolicLink(), `committed runtime 文件不允许使用符号链接: ${pathWithinProject}`)
228
+ assert(statSync(committedPath).isFile(), `committed runtime 路径必须是普通文件: ${pathWithinProject}`)
229
+ const realProjectBoundary = realpathSync(projectBoundary)
230
+ const realCommittedPath = realpathSync(committedPath)
231
+ const realPathWithinProject = relative(realProjectBoundary, realCommittedPath)
232
+ assert(
233
+ realPathWithinProject &&
234
+ realPathWithinProject !== '..' &&
235
+ !realPathWithinProject.startsWith(`..${sep}`) &&
236
+ !isAbsolute(realPathWithinProject),
237
+ `committed runtime 文件解析后必须位于项目根目录内: ${pathWithinProject}`,
238
+ )
239
+
240
+ const placeholder = normalizedValue(policy.secretPlaceholder)
241
+ const committedEntries = parseEnvContent(readFileSync(committedPath, 'utf8'), committedPath)
242
+ const committedEnv = {}
243
+
244
+ for (const key of committedRuntime.keys) {
245
+ assert(committedEntries.has(key), `${pathWithinProject}: 缺少 committed runtime 键 ${key}`)
246
+ const value = normalizedValue(committedEntries.get(key))
247
+ assert(value && value !== placeholder, `${pathWithinProject}: committed runtime 键 ${key} 不能是空值或占位符`)
248
+ committedEnv[key] = value
249
+ }
250
+
251
+ return committedEnv
252
+ }
253
+
166
254
  export function validateEnvProfile({ projectRoot, configDir, profile, environment }) {
167
255
  const config = loadEnvProfileConfig(configDir)
168
256
  assert(config.profiles[profile], `未声明 env profile: ${profile}`)
@@ -177,6 +265,12 @@ export function validateEnvProfile({ projectRoot, configDir, profile, environmen
177
265
  assertGitIgnored(projectRoot, paths.source)
178
266
 
179
267
  const policy = loadEnvPolicy(configDir)
268
+ const committedRuntimeEnv = resolveCommittedRuntimeEnv({
269
+ projectRoot,
270
+ config,
271
+ policy,
272
+ environment,
273
+ })
180
274
  const placeholder = normalizedValue(policy.secretPlaceholder)
181
275
  const secretKeys = new Set(policy.keys?.secret || [])
182
276
  const allowedLocalKeys = new Set([
@@ -240,6 +334,7 @@ export function validateEnvProfile({ projectRoot, configDir, profile, environmen
240
334
  source: paths.source,
241
335
  target: paths.target,
242
336
  keyCount: profileEntries.size,
337
+ committedRuntimeEnv,
243
338
  }
244
339
  }
245
340
 
@@ -421,6 +516,7 @@ export async function executeWithEnvProfile({
421
516
  env: {
422
517
  ...process.env,
423
518
  ...profileEnv,
519
+ ...validated.committedRuntimeEnv,
424
520
  DX_ENV_PROFILE: profile,
425
521
  DX_ENV_PROFILE_ENVIRONMENT: environment,
426
522
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weotro/dx",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -19,6 +19,7 @@
19
19
  "bin/",
20
20
  "lib/",
21
21
  "skills/",
22
+ "agent-references/",
22
23
  "LICENSE",
23
24
  "README.md",
24
25
  "package.json"
@@ -0,0 +1,40 @@
1
+ ---
2
+ name: cc-ship-issue-pr
3
+ description: 仅在用户显式调用 $cc-ship-issue-pr、/cc-ship-issue-pr 或明确要求使用 cc-ship-issue-pr 技能时使用,派发 follow-up 交付的 subagent 也从这里进入;不要通过关键词、任务类型或上下文自动触发。
4
+ ---
5
+
6
+ # CC Ship Issue PR
7
+
8
+ Claude Code 入口:主进程守门并执行轻改动,Codex 承接重改动的方案讨论、实现与独立审查。
9
+
10
+ ## 步骤一:Claude Code 门禁
11
+
12
+ 任何文件检查、Git/GitHub 操作或共享流程加载之前执行:
13
+
14
+ ```bash
15
+ if [[ -z "${CLAUDECODE:-}" ]]; then
16
+ echo "cc-ship-issue-pr 仅允许 Claude Code 调用;当前运行时不是 Claude Code,已退出。"
17
+ exit 64
18
+ fi
19
+ ```
20
+
21
+ 门禁失败后立即结束。门禁通过的完成标准是 `CLAUDECODE` 非空。
22
+
23
+ ## 步骤二:加载共享流程
24
+
25
+ ```bash
26
+ AGENTS_ROOT="${AGENTS_HOME:-$HOME/.agents}"
27
+ SHIP_CORE="$AGENTS_ROOT/references/ship-issue-pr-core.md"
28
+ test -f "$SHIP_CORE"
29
+ ```
30
+
31
+ 完整读取 `SHIP_CORE` 后执行全部阶段。运行时映射如下:
32
+
33
+ - 主进程:当前 Claude Code。
34
+ - 外部专家:Codex。
35
+ - 入口显示名:`CC Ship Issue PR`。
36
+ - Follow-up:Claude Code `Agent(subagent_type: "general-purpose", run_in_background: true)`。
37
+
38
+ 第一次进入外部专家或 follow-up 分支前,完整读取 [references/runtime.md](references/runtime.md)。该文件是本入口的平台调用契约;共享流程仍是交付语义的唯一真源。
39
+
40
+ 只有共享流程全部完成且 `mergedAt` 非空、Issue 回访完成,才输出 `CC Ship Issue PR 完成`。
@@ -0,0 +1,2 @@
1
+ policy:
2
+ allow_implicit_invocation: false
@@ -0,0 +1,96 @@
1
+ # Claude Code Runtime Contract
2
+
3
+ 仅在共享流程进入外部专家或 follow-up 分支时读取。
4
+
5
+ ## Codex 通道
6
+
7
+ Codex 永远不碰 Git 历史和 GitHub。它把改动落在工作树,由 Claude Code 主进程核验、验证和分组提交。
8
+
9
+ - **方案讨论 / 实现 / 重修复**:`Agent(subagent_type: "codex:codex-rescue")`。
10
+ - **普通 review**:Codex companion 的 `review` 子命令。
11
+ - **adversarial review**:Codex companion 的 `adversarial-review` 子命令。
12
+ - **定制上下文审查**:只读 `Agent(subagent_type: "codex:codex-rescue")`,prompt 明确禁止写操作和递归委派。
13
+
14
+ companion 每次调用都动态解析插件路径:
15
+
16
+ ```bash
17
+ CODEX_ROOT=$(/bin/ls -d ~/.claude/plugins/marketplaces/openai-codex/plugins/codex 2>/dev/null \
18
+ || /bin/ls -d ~/.claude/plugins/cache/openai-codex/codex/*/ 2>/dev/null | sort -V | tail -1) \
19
+ && node "$CODEX_ROOT/scripts/codex-companion.mjs" --help
20
+ ```
21
+
22
+ 插件升级会换版本目录,不把解析结果硬编码回技能。
23
+
24
+ 普通 review:
25
+
26
+ ```bash
27
+ CODEX_ROOT=$(/bin/ls -d ~/.claude/plugins/marketplaces/openai-codex/plugins/codex 2>/dev/null \
28
+ || /bin/ls -d ~/.claude/plugins/cache/openai-codex/codex/*/ 2>/dev/null | sort -V | tail -1) \
29
+ && node "$CODEX_ROOT/scripts/codex-companion.mjs" review --wait --base main
30
+ ```
31
+
32
+ 需要挑战方案时把 `review` 换成 `adversarial-review`。需要定制上下文时改派只读 Codex agent。
33
+
34
+ ## 补跑清单
35
+
36
+ Codex 沙箱不能绑本地端口、不能起 PostgreSQL 共享内存、不能写主仓 `.git` index、无网络下载。派发 prompt 原文附上以下主进程补跑项:
37
+
38
+ `dx db migrate`(实 apply)、后端 E2E、`dx build front --dev`、`flutter test`、Dart OpenAPI 生成(要下 Maven)、`git commit`。
39
+
40
+ ## 取结果
41
+
42
+ `codex:codex-rescue` 可能转后台。按 `workspaceRoot` 精确匹配当前仓库:
43
+
44
+ ```bash
45
+ python3 - <<'PY'
46
+ import glob, json, os
47
+ root = os.path.realpath(os.getcwd())
48
+ rows = []
49
+ for f in glob.glob(os.path.expanduser('~/.claude/plugins/data/codex-openai-codex/state/*/state.json')):
50
+ for j in json.load(open(f)).get('jobs', []):
51
+ if os.path.realpath(j.get('workspaceRoot', '')) == root:
52
+ rows.append((j.get('createdAt'), j.get('id'), j.get('kind'), j.get('status'), j.get('phase'), f))
53
+ for r in sorted(rows)[-5:]:
54
+ print(r)
55
+ PY
56
+ ```
57
+
58
+ 定位到 job 后读取同目录 `jobs/<id>.json`。只有 `status=completed` 且 `phase=done` 才算返回,报告正文取 `result.rawOutput`。
59
+
60
+ ## 等待与假死
61
+
62
+ 等待期间工作树由 Codex 独占,主进程只做不写工作树的并行事项。没别的工作时每 2 分钟取一次活性指纹:
63
+
64
+ ```bash
65
+ python3 - <<'LIVE'
66
+ import glob, json, os, subprocess
67
+ root = os.path.realpath(os.getcwd())
68
+ rows = []
69
+ for f in glob.glob(os.path.expanduser('~/.claude/plugins/data/codex-openai-codex/state/*/state.json')):
70
+ for j in json.load(open(f)).get('jobs', []):
71
+ if os.path.realpath(j.get('workspaceRoot', '')) == root:
72
+ rows.append((j.get('createdAt'), j.get('id'), j.get('status'), j.get('phase'), f))
73
+ if not rows:
74
+ raise SystemExit('no job for this workspace')
75
+ created, jid, status, phase, statefile = sorted(rows)[-1]
76
+ jf = os.path.join(os.path.dirname(statefile), 'jobs', jid + '.json')
77
+ size, mtime = (os.path.getsize(jf), int(os.path.getmtime(jf))) if os.path.exists(jf) else (0, 0)
78
+ dirty = len([l for l in subprocess.run(['git', 'status', '--porcelain'], capture_output=True, text=True).stdout.splitlines() if l.strip()])
79
+ print(f'{jid} status={status} phase={phase} out={size}/{mtime} dirty={dirty}')
80
+ LIVE
81
+ ```
82
+
83
+ 连续 3 次(约 6 分钟)三项全不变且仍为 running,按假死阻塞,报告 job id 与 phase。companion 的 `review --wait` / `adversarial-review --wait` 是前台阻塞调用,不走此轮询。
84
+
85
+ ## 阶段与报告映射
86
+
87
+ - 阶段四方案讨论:只读 Codex agent;主进程先写初步方案,最终拍板权在主进程。
88
+ - 阶段四实现与阶段 7.4 重修复:可写 Codex agent,prompt 要求不 commit、不 push、不碰 GitHub、不递归委派。
89
+ - 阶段 7.3:复杂度门禁命中且本 PR 尚未用过 Codex 审查时执行 review;需要挑战方案时执行 adversarial review。
90
+ - reviewer:`Claude Code 主进程自审 / codex native review / codex adversarial review / codex 只读 subagent`。
91
+
92
+ Codex 返回后主进程必须直读源码和 diff 核验,不采信转述。重改动实现通道不可用时阻塞;条件式独立审查不可用时由主进程完成本轮自审并在报告标明降级。
93
+
94
+ ## Follow-up
95
+
96
+ 使用 `Agent(subagent_type: "general-purpose", run_in_background: true)`,prompt 指定 worktree 绝对路径并要求调用 `cc-ship-issue-pr`。派发深度只允许一层;子 agent 发现的新 follow-up 只建 Issue。派不动 Codex 时子 agent 自己实现,并在 PR body 记录主进程直改。
@@ -7,13 +7,14 @@ description: 仅在用户显式调用 $git-release 或明确要求使用 git-rel
7
7
 
8
8
  ## 目标
9
9
 
10
- 在 `release/vX.Y.Z` 或 `release/vX.Y.Z-<prerelease>.N` 分支上,完成从发布前检查到 GitHub Release 创建的全流程;若当前不在 release 分支,则先从最新 `main` 自动创建目标 release 分支。
10
+ 在 `release/vX.Y.Z` 或 `release/vX.Y.Z-<prerelease>.N` 分支上,完成从发布前检查、GitHub Release 创建、生产服务器验证到发布后回访 Issue 建立的全流程;若当前不在 release 分支,则先从最新 `main` 自动创建目标 release 分支。
11
11
 
12
12
  ## 执行原则
13
13
 
14
14
  - 全程使用中文输出。
15
15
  - 严格执行前置校验,任何硬性条件不满足时立即终止。
16
16
  - 发行说明必须结构化、可读、可追溯。
17
+ - 发布完成必须留下一个可追踪的部署后回访 Issue;终端里临时打印 checklist 不算完成。
17
18
  - 命令默认在仓库根目录执行。
18
19
  - 若能从当前 release 分支或自动建分支流程唯一推断出合法版本号,直接使用该版本继续发布,不要询问用户确认。
19
20
 
@@ -156,7 +157,19 @@ gh release create v<VERSION> \
156
157
  EOF
157
158
  ```
158
159
 
159
- 4. 输出发布 URL 与发布后检查清单。
160
+ 4. 读回 Release,确认 tag、标题、正文和 URL 正确:`gh release view v<VERSION> --json tagName,name,body,url,isDraft,isPrerelease`。
161
+
162
+ ### 六、生产环境验证与发布后回访
163
+
164
+ 1. 完整读取 [发布后回访契约](references/post-release-follow-up.md)。
165
+ 2. 从仓库实际部署配置和运维文档确定生产服务器、连接方式、服务、品牌、target、版本标记、数据库和只读验证入口;禁止猜测主机或使用历史地址。
166
+ 3. 直接登录每台相关生产服务器,核对实际运行版本/commit、进程或容器、health/readiness、关键日志、worker/cron 和资源状态,确认部署事实而不是依赖流水线状态。
167
+ 4. 根据发行说明、PR、迁移和运维脚本识别本次需要人工执行的脚本,使用迁移表、任务记录、审计日志、marker、状态命令和数据结果验证是否已经执行且结果符合预期。检查默认只读;未经当前用户明确授权,不执行生产写脚本、迁移、回填、重启或重新部署。
168
+ 5. 自动创建一个发布后回访 Issue。Issue 必须覆盖:发布与服务器版本一致性、服务健康、运维脚本执行状态、数据结果、配置、可观测性、本次变更特有验收、回滚准备和延迟观察窗口。
169
+ 6. 立即执行当前可验证的检查;只有附有脱敏命令、查询结果摘要、日志位置或可观察结果的项目才能勾选。缺生产权限或需要观察窗口的项目保持未勾选,并写清所需权限或最早检查时间。
170
+ 7. 使用 `gh issue view` 读回 Issue,确认没有占位符、每个生产目标和本次发布影响面都有对应检查项,并输出 Issue URL。
171
+
172
+ GitHub Release 已创建但无法登录生产服务器、实际版本不匹配、必需运维脚本未执行或数据不符合预期时,仍然创建回访 Issue,把状态标为阻塞并立即向用户报告证据、影响、建议动作和重新验证项;不得静默修复或用“Release 已创建”掩盖生产问题。
160
173
 
161
174
  ## 终止条件
162
175
 
@@ -166,6 +179,7 @@ EOF
166
179
  - 当前分支不符合 release 分支命名规则,且无法从 `main` 自动创建 release 分支。
167
180
  - 版本号格式非法或与现有 tag 冲突。
168
181
  - 自上次发布以来无新提交。
182
+ - 无法创建或读回发布后回访 Issue。
169
183
 
170
184
  ## 输出模板
171
185
 
@@ -191,4 +205,6 @@ EOF
191
205
  - 分支名
192
206
  - tag 推送状态
193
207
  - Release URL
194
- - 发布后清单
208
+ - 生产服务器、实际运行版本与验证状态
209
+ - 发布后回访 Issue URL
210
+ - 已验证、阻塞、待授权、待观察项目数量
@@ -1,7 +1,7 @@
1
1
  interface:
2
2
  display_name: "Git Release"
3
- short_description: "在 release 分支上自动完成版本发布与发行说明生成"
4
- default_prompt: "使用 $git-release 按规范执行一次发布并生成高质量发行说明。"
3
+ short_description: "完成版本发布、生产服务器核验与发布后回访"
4
+ default_prompt: "使用 $git-release 执行发布,登录生产服务器核验版本、运维脚本和数据,并创建带证据的回访 Issue checklist。"
5
5
 
6
6
  policy:
7
7
  allow_implicit_invocation: false
@@ -0,0 +1,158 @@
1
+ # 发布后回访契约
2
+
3
+ 本文件只在 GitHub Release 创建成功后读取。目标是把部署后的未知风险变成一个有证据、可关闭的 Issue,而不是生成一张固定且无法证明完成的清单。
4
+
5
+ ## 完成边界
6
+
7
+ `git-release` 本次调用完成的最低条件是:
8
+
9
+ - Release 已读回确认;
10
+ - 已从实际配置确定或明确无法确定生产服务器与验证入口;
11
+ - 已登录所有相关生产服务器核对实际运行状态,或记录无法访问的证据;
12
+ - 发布后回访 Issue 已创建并读回;
13
+ - 本次发布涉及的每个环境、品牌、target、迁移、配置和用户可观察变化都有检查项;
14
+ - 当前能自动验证的项目已执行,只有带证据的项目被勾选。
15
+
16
+ 回访 Issue 不要求在当前调用中关闭。需要观察窗口、人工账号或业务确认的项目保持未勾选,避免为了“流程完成”伪造通过状态。
17
+
18
+ ## 生产服务器核验
19
+
20
+ 先检查仓库真实部署配置和运维文档,包括部署脚本、inventory、`dx/config/commands.json`、`dx/release/`、systemd/container 配置和 runbook。确定:
21
+
22
+ - 生产主机、SSH 入口、区域、品牌、服务与前端 target;
23
+ - 服务由 systemd、Docker、Kubernetes、PM2、serverless 或其他方式运行;
24
+ - 服务器上可验证版本/commit 的文件、环境变量、镜像标签、健康接口或运行参数;
25
+ - 数据库、队列、worker、cron、缓存和第三方连接的只读检查入口;
26
+ - 本次发布要求人工执行的迁移、回填、缓存刷新、配置生成或运维脚本。
27
+
28
+ 逐台登录本次涉及的生产服务器,使用仓库已有状态命令或等价的只读命令检查实际状态。按运行方式核对进程、容器、镜像、release 目录、符号链接、启动时间、health/readiness 和最近日志;命令必须收敛到明确服务,避免无边界扫描整台服务器。
29
+
30
+ 服务器验证默认只读。不要执行重启、重新部署、迁移、回填、数据修复、缓存清空或其他生产写操作;发现需要执行时,在 Issue 和当前回复中写明命令、原因、影响和验证方式,等待用户对该具体动作授权。
31
+
32
+ 如果缺少主机信息、SSH 权限或数据库只读权限,在 Issue 中明确写“生产验证受阻”,附已检查的配置位置和失败命令。此状态属于阻塞,不得写成部署成功。
33
+
34
+ ## 动态生成检查项
35
+
36
+ 先从发行说明、提交、PR、Issue、运维提醒和部署配置建立“发布影响面”,再为每个影响面生成至少一个可验证检查。只保留本次适用的分支,但以下类别必须逐项判定是否适用,不能静默跳过。
37
+
38
+ ### 发布与制品一致性
39
+
40
+ - tag、Release、版本文件和服务器实际运行 commit 指向同一版本;
41
+ - 服务器上的制品、镜像、release 目录或构建产物使用正确版本与 commit;
42
+ - 所有预期生产主机、品牌和 target 都运行目标版本,没有漏发、错发或部分节点仍运行旧版;
43
+ - Release 不是意外的 draft/prerelease,除非版本本身要求如此。
44
+
45
+ ### 服务器与服务状态
46
+
47
+ - 目标进程、容器、pod 或 function 正常运行,启动时间与本次发布一致;
48
+ - health/readiness、端口、反向代理和服务间连接正常;
49
+ - worker、cron、队列消费者和后台作业运行在目标版本;
50
+ - 最近日志没有启动失败、error、panic、timeout、权限错误或重复重启;
51
+ - CPU、内存、磁盘、连接数等没有因本次发布出现明显异常。
52
+
53
+ ### 运行时健康与可观测性
54
+
55
+ - health/readiness、关键进程、容器或 serverless function 健康;
56
+ - 发布后错误率、延迟、5xx、告警、日志异常没有明显回归;
57
+ - 队列积压、定时任务、事件消费、webhook 或后台作业正常;
58
+ - 域名、TLS、静态资源、CDN、缓存版本和 source map 符合预期。
59
+
60
+ ### 数据、配置与兼容性
61
+
62
+ - 本次迁移已应用到正确数据库,schema 与应用版本兼容;
63
+ - 新增或变更的环境变量、secret、feature flag 和第三方配置已生效;
64
+ - 公共 API、DTO、事件、数据库和客户端兼容性按发行说明验证;
65
+ - 使用有边界的只读查询核对关键总数、不变量、状态分布和抽样结果;不得在输出中泄露密钥、令牌、个人信息或业务敏感原始数据;
66
+ - 涉及数据修复或回填时,记录预期数量、实际数量、失败项和幂等/重跑状态。
67
+
68
+ ### 运维脚本执行状态
69
+
70
+ - 从本次 PR、迁移、release note 和 runbook 列出所有需要人工执行的脚本,不能只检查熟悉的脚本;
71
+ - 对每个脚本记录名称/版本、目标主机或数据库、预期效果和可验证完成标记;
72
+ - 通过迁移表、任务表、审计日志、marker、脚本状态子命令或数据结果判断 `已执行 / 未执行 / 无法确认 / 不适用`;
73
+ - “文件存在”或“有人说跑过”不算执行证据;
74
+ - 未经授权不代替运维执行写脚本。未执行或结果异常时立即报告,并给出建议命令和执行后的复核查询。
75
+
76
+ ### 用户与业务路径
77
+
78
+ - 为发行说明中的每个用户可观察变化生成对应 smoke check;
79
+ - 覆盖登录/鉴权、核心读写路径、支付或其他高风险业务路径中本次受影响的部分;
80
+ - 前端、管理端、移动端或多品牌变更分别验证对应入口,不能用一个端的通过代表全部端;
81
+ - 修复项要复现旧失败条件并验证新行为,新增项要验证成功路径和关键错误路径。
82
+
83
+ ### 回滚与延迟观察
84
+
85
+ - 记录上一稳定版本、回滚命令或恢复入口,以及迁移是否可逆;
86
+ - 对需要时间才能暴露的问题设置观察窗口和最早复查时间,例如错误率、队列积压、定时任务或缓存过期;
87
+ - 明确 Issue 关闭条件:所有必需项完成,阻塞项有结论,发现的问题已修复或拆成关联 Issue。
88
+
89
+ ## Issue 结构
90
+
91
+ 创建前先读取仓库 Issue 模板和现有 labels,沿用其结构与可用标签。标题使用:
92
+
93
+ ```text
94
+ chore(release): 回访 v<VERSION> 部署结果
95
+ ```
96
+
97
+ 正文至少包含:
98
+
99
+ ```markdown
100
+ ## 发布信息
101
+
102
+ - 版本:v<VERSION>
103
+ - Release:<URL>
104
+ - Tag commit:<SHA>
105
+ - 部署环境/品牌/target:<实际范围>
106
+ - 生产服务器与服务:<脱敏主机标识和实际范围>
107
+ - 实际运行版本/commit:<逐目标结果>
108
+ - 生产验证时间:<时间与时区>
109
+ - 验证状态:通过 / 阻塞 / 发现异常
110
+
111
+ ## 部署后检查清单
112
+
113
+ ### 发布与制品一致性
114
+
115
+ - [ ] <检查对象、预期结果、验证命令或证据位置>
116
+
117
+ ### 生产服务器与服务
118
+
119
+ - [ ] <检查项>
120
+
121
+ ### 运行时与可观测性
122
+
123
+ - [ ] <检查项>
124
+
125
+ ### 数据、配置与兼容性
126
+
127
+ - [ ] <检查项或“不适用 + 证据”>
128
+
129
+ ### 运维脚本
130
+
131
+ - [ ] <脚本、目标、预期结果、执行状态与证据>
132
+
133
+ ### 本次变更验收
134
+
135
+ - [ ] <逐项映射发行说明/PR/Issue 的用户可观察变化>
136
+
137
+ ### 回滚与观察窗口
138
+
139
+ - [ ] <回滚入口已确认>
140
+ - [ ] <观察指标、窗口、最早复查时间、负责人或所需权限>
141
+
142
+ ## 发现与后续
143
+
144
+ - 当前阻塞:无 / <失败证据与恢复动作>
145
+ - 关联问题:无 / #<issue>
146
+ - 关闭条件:<所有必需检查完成且发现的问题有结论>
147
+ ```
148
+
149
+ 正文通过 heredoc 或临时文件传给 `gh issue create --body-file`,禁止用字面量 `\n` 拼接。创建后用 `gh issue view <id> --json title,body,url,state` 读回。
150
+
151
+ ## 勾选与问题处理
152
+
153
+ - 勾选项必须同时写入证据:脱敏命令与结果摘要、日志位置、只读查询汇总、监控链接或具体可观察结果。
154
+ - “服务器能登录”“进程在运行”“页面能开”不能替代版本、脚本和数据的专项验证。
155
+ - 检查失败时保留未勾选状态,在“发现与后续”记录严重级、时间、主机/服务、证据、影响和建议动作,并立即在当前回复中报告。
156
+ - 发现问题后默认只报告和保全证据,不静默修改生产环境。修复需要生产写操作时等待用户对具体动作授权。
157
+ - 新发现的问题超出本次回访可直接修复的范围时创建关联 Issue;回访 Issue 保持打开,直到该问题有明确处置结果。
158
+ - 不适用项必须写出判定依据;不要为了模板完整保留空占位项。
@@ -0,0 +1,44 @@
1
+ ---
2
+ name: oo-ship-issue-pr
3
+ description: 仅在用户显式调用 $oo-ship-issue-pr、/oo-ship-issue-pr 或明确要求使用 oo-ship-issue-pr 技能时使用;不要通过关键词、任务类型或上下文自动触发。
4
+ ---
5
+
6
+ # OO Ship Issue PR
7
+
8
+ Codex 入口:主进程守门并执行轻改动,Claude Fable 5 优先承接重改动的方案讨论、实现与独立审查;Fable 额度不足时回退 Claude Opus 5,Claude 通道不可用时改派 fresh Codex subagent,不中断交付链。
9
+
10
+ ## 步骤一:Codex 门禁
11
+
12
+ 任何文件检查、Git/GitHub 操作或共享流程加载之前执行:
13
+
14
+ ```bash
15
+ if [[ -n "${CLAUDECODE:-}" ]]; then
16
+ echo "oo-ship-issue-pr 仅允许 Codex 调用;检测到 Claude Code 运行时,已退出。"
17
+ exit 64
18
+ fi
19
+ if [[ -z "${CODEX_THREAD_ID:-}" && -z "${CODEX_SESSION_ID:-}" ]]; then
20
+ echo "oo-ship-issue-pr 仅允许 Codex 调用;当前运行时不是 Codex,已退出。"
21
+ exit 64
22
+ fi
23
+ ```
24
+
25
+ `CLAUDECODE` 存在时优先拒绝,避免 Codex 启动的 Claude 子进程继承 `CODEX_*` 后误过门禁。门禁通过的完成标准是 `CLAUDECODE` 为空,且至少一个 Codex 身份变量非空。
26
+
27
+ ## 步骤二:加载共享流程
28
+
29
+ ```bash
30
+ AGENTS_ROOT="${AGENTS_HOME:-$HOME/.agents}"
31
+ SHIP_CORE="$AGENTS_ROOT/references/ship-issue-pr-core.md"
32
+ test -f "$SHIP_CORE"
33
+ ```
34
+
35
+ 完整读取 `SHIP_CORE` 后执行全部阶段。运行时映射如下:
36
+
37
+ - 主进程:当前 Codex。
38
+ - 外部专家:优先 Claude Fable 5;仅在 Fable 额度不足时回退 Claude Opus 5;Claude 通道不可用时改派 fresh Codex subagent。
39
+ - 入口显示名:`OO Ship Issue PR`。
40
+ - Follow-up:Codex `spawn_agent` 的 `worker` 角色。
41
+
42
+ 第一次进入外部专家或 follow-up 分支前,完整读取 [references/runtime.md](references/runtime.md)。该文件是本入口的平台调用契约;共享流程仍是交付语义的唯一真源。
43
+
44
+ 只有共享流程全部完成且 `mergedAt` 非空、Issue 回访完成,才输出 `OO Ship Issue PR 完成`。
@@ -0,0 +1,2 @@
1
+ policy:
2
+ allow_implicit_invocation: false