jt-push-main
git push origin main 之前的前置门禁. 全部通过才允许 push origin main.
声明: "正在使用 jt-push-main skill 执行 push origin main 前置检查."
适用范围
只管 push origin main. 用户 push 其它分支时本 skill 不介入 (直接正常 push). 全局规则里 "push" 默认指当前分支 — 所以只有当前在 main 且要 push 到 origin 时, 本门禁才生效.
设计理念: main 只应该装 "已完成并已归档" 的 OpenSpec change. 进行中的 change 工作应在 worktree (分支) 里做, 完成 + 归档后再合回 main. 本门禁就是把这个理念落到 push 这一步.
流程
依次执行三个 gate, 任一硬失败则报告并退出, 不要 push.
Gate 1: 场景确认 (是否 push origin main)
current_branch=$(git branch --show-current)
if [ "$current_branch" != "main" ]; then
echo "INFO: 当前分支是 '$current_branch', 不是 main."
echo "jt-push-main 只管 push origin main, 其它分支不介入 — 请直接 git push."
exit 0 # 退出门禁, 让用户正常 push 自己的分支
fi
git fetch origin --quiet
不在 main → 本 skill 不适用, 退出 (exit 0, 非失败), 用户照常 push 该分支.
Gate 2: OpenSpec change 门禁 (聚焦本次 push 引入的内容)
只检查 本次 push 会带上去的 commit (origin/main..HEAD) 所触及的 active change.
对已经存在于 origin/main 的历史遗留 active change (本次未触及) 只 warn 不拦 —— 拦了也没用,
已是既成事实, 否则每次 push 都会被 main 上长期存在的 proposal 反复拦截.
change 的"是否 active(未归档)"和"完成度"直接问 openspec list --json (官方权威源, 只列 active
change, 输出无 ANSI), 不解析 tasks.md 勾选 —— 摆脱对其 markdown 格式的依赖.
阶段按 completedTasks / totalTasks 判: 相等且 >0 = 完成未归档; 0<完成<总数 = 进行中; 完成=0 = proposal.
for cmd in openspec jq; do
command -v "$cmd" >/dev/null 2>&1 || { echo "ERROR: 未找到 $cmd, 无法做 change 门禁 (需在 openspec 项目根目录, 且装有 openspec/jq)."; exit 1; }
done
blocked=0
has_remote_main=0
git rev-parse --verify origin/main >/dev/null 2>&1 && has_remote_main=1
# 本次 push 触及的 change 名字 (git diff 输出无色; --no-color 双保险防 color.ui=always 注入)
touched=""
if [ "$has_remote_main" -eq 1 ]; then
touched=$(git diff --no-color --name-only origin/main..HEAD -- openspec/changes/ 2>/dev/null \
| sed -n 's#^openspec/changes/\([^/]*\)/.*#\1#p' | grep -v '^archive$' | sort -u)
fi
# active(未归档) change + 完成度: 直接问 openspec CLI — 官方权威源, 输出无 ANSI, 不依赖
# tasks.md 的 markdown 勾选格式; 且只列 active change (自动排除 archive/, 本次 push 把某
# change 归档后它从 list 消失 -> 天然不拦, 无需 [ -d ] 特判).
# 注意: 用进程替换 < <(...) 而非管道 | while, 否则 blocked=1 落在子 shell 会丢失.
while IFS=$'\t' read -r name comp total; do
[ -n "$name" ] || continue
# 是否本次 push 引入? 远端无 main (首次 push) 时全部算引入; 否则只 git diff 命中的算.
if [ "$has_remote_main" -eq 1 ]; then
printf '%s\n' "$touched" | grep -Fqx "$name" || {
echo "WARN [$name] main 上历史遗留的 active change (非本次 push 引入) -> 不阻止, 建议另行归档清理"
continue
}
fi
if [ "$total" -gt 0 ] && [ "$comp" -eq "$total" ]; then
echo "BLOCK [$name] 已完成但未归档 -> 先归档: openspec archive $name -y"
elif [ "$comp" -gt 0 ]; then
echo "BLOCK [$name] 进行中 (已完成 $comp / 共 $total) -> 先完成并归档, 或把改动挪到 worktree 再 push"
else
echo "BLOCK [$name] 仍在 proposal/未开始 ($comp / $total) -> 先完成实现并归档, 或把改动挪到 worktree 再 push"
fi
blocked=1
done < <(openspec list --json | jq -r '.changes[] | [.name, .completedTasks, .totalTasks] | @tsv')
if [ "$blocked" -eq 1 ]; then
echo "RESULT: openspec-gate FAILED — 不允许 push origin main"
exit 1
fi
echo "RESULT: openspec-gate PASSED"
命中 BLOCK 时的处理 (任一命中即不许 push):
已完成但未归档: 引导用户openspec archive <name> -y归档后重新 push.进行中/proposal: 引导用户先完成实现并归档.- 如果用户确实就是想 push (不想先处理 change): 引导用 jt-worktree skill 把这些改动挪到一个 worktree (分支) 里去 push, 不要 push 到 main.
Gate 3: Lint 门禁 (本项目 lint = bun run check, 底层 ultracite)
对当前工作树做硬检查. 红了先 bun run fix; 修不了就停下报告, 不要硬 push.
ultracite 已忽略 .gitignore 里的 .worktrees/, 常驻 worktree 不影响全仓 lint (实测 170 files / 0 errors),
所以不做 "看到 worktree 就降级" 的预判 —— 那会让 lint gate 在有常驻 worktree 时永远不真跑. 直接真跑:
if [ -f package.json ] && grep -q '"check"' package.json; then
echo "运行 lint: bun run check ..."
if ! bun run check; then
echo "lint 红了, 尝试自动修复: bun run fix ..."
bun run fix || true
echo "重新检查: bun run check ..."
if ! bun run check; then
echo "ERROR: lint 仍未通过, 自动修复无法解决. 停下报告, 不要 push."
exit 1
fi
echo "RESULT: lint-gate PASSED (经 bun run fix 修复)"
echo "NOTE: bun run fix 可能改动了文件, 见下方处理"
else
echo "RESULT: lint-gate PASSED"
fi
fi
如果 bun run fix 改动了文件 (git status --porcelain 非空且来自 fix):
不要带着未提交的 fix 直接 push —— 那样推上去的代码仍是红的. 停下, 让用户 review 这些 fix
并 commit 后, 重新走一遍本门禁 再 push. 不要自动 commit (避免意外提交).
Gate 4: 执行 push
三个 gate 全部通过 (无 BLOCK / 无未提交的 lint fix), 才允许:
git push origin main
报告
全部通过并 push 成功:
## push origin main 成功
- openspec-gate: PASSED (本次引入的 change 均已归档 / 无引入)
- lint-gate: PASSED
- 已 push: origin main
被门禁拦截:
## push origin main 被拦截
- 失败 gate: <openspec-gate / lint-gate>
- 命中项:
- <BLOCK / ERROR 行>
- 建议:
- <归档 / 完成实现 / 挪到 worktree / 修 lint 后重试>
红线
永远不要:
- 在 Gate 2 命中 BLOCK (本次 push 引入了未归档的 active change) 时 push origin main
- 在
bun run check(含bun run fix后复检) 仍红时 push - 带着
bun run fix产生的未提交改动直接 push (会把红的代码推上去) - 因 main 上历史遗留的 active change 而硬拦 push (那些只 warn)
- 介入 push origin main 以外的任何分支