gstack /landing-report:只读版本队列仪表盘,看清多工作区并行 ship 时的 VERSION 槽位之争
在多 Conductor 工作区并行开发同一个仓库时,最容易发生的问题是两个分支 bump 到同一个 VERSION,后合并的一方悄悄覆盖前者的 CHANGELOG 条目。gstack 的 /landing-report 技能就是为这类场景设计的只读仪表盘:它展示当前哪些 VERSION 槽位已被 open PR 占用、哪些兄弟工作区有即将合入的 WIP、以及你下次运行 /ship 会拿到哪个槽位。本文以 landing-report/SKILL.md 为主体,结合底层工具 bin/gstack-next-version、版本原语库 lib/version-source.ts 与测试 test/gstack-next-version.test.ts,完整讲解该技能的调用契约、五步工作流、输出格式与冲突分配算法。
一、这个技能解决什么问题
/landing-report 的定位在 AGENTS.md 中只有一行:"Read-only dashboard for the workspace-aware ship queue.";在 docs/skills.md 的技能清单里,它被归类为 Ship Queue Dashboard——workspace-aware ship 队列的只读快照。
SKILL.md 的 "Why this skill exists" 一段给出了最直接的解释:当你同时运行 5-10 个 Conductor 工作区时,需要一眼看清"哪个版本号被谁占了、你下次 /ship 会落在哪个槽位"。实现上,它只是对 /ship 所使用的同一个底层工具 bin/gstack-next-version 做了一次只读调用,全程无任何变更——原文的比喻是:"Think of it as gh pr list for VERSION numbers."
这个技能随 gstack v1.11.0.0 的 "Workspace-aware ship" 发布而诞生。CHANGELOG.md 对该发布背景的描述是:在多个 Conductor 窗口并行操作时,"两个分支 bump 到同一版本,谁后合并谁就把对方的 CHANGELOG 条目覆盖掉或带重复标题落地,直到你 grep '^## \[' 才发现"。该版本让 /ship 查询 open PR 队列、看到已声称的版本、在你所选 bump 级别上挑下一个空槽位;而 /landing-report 则是"在打开当天第 6 个 PR 之前查一下我在队里排第几"的检查入口——运行它不会 ship 任何东西,只是观察。
触发方式与工具白名单
技能 frontmatter 声明的触发词(triggers)为:landing report、version queue、ship queue、what version comes next、show open PR versions。allowed-tools 只包含 Bash 与 Read——没有 Edit、没有 Write,从工具层就保证了只读语义。frontmatter 中 preamble-tier: 2、version: 0.1.0;文件头注释表明该文档由 landing-report/SKILL.md.tmpl 自动生成(bun run gen:skill-docs 重新生成),不要直接编辑。
二、调用契约:Preamble 与 Plan Mode 安全
与所有 gstack 技能一致,/landing-report 启动前先执行一段 Preamble bash 收集运行环境。这段脚本在 gstack 各技能间共享,其作用可归纳为输出以下几组关键环境变量,供后续步骤分支判断:
| 变量 | 含义 |
|---|---|
BRANCH |
当前 git 分支(git branch --show-current) |
PROACTIVE / PROACTIVE_PROMPTED |
是否允许主动建议技能(gstack-config get proactive) |
SKILL_PREFIX |
若为 true,建议/调用时使用 /gstack-* 命名 |
REPO_MODE / SESSION_KIND |
仓库模式;会话类型 spawned | headless | interactive |
CONDUCTOR_SESSION |
在 Conductor 宿主内时置 true;此时 AskUserQuestion 不可靠,所有决策改为 prose 渲染 |
TELEMETRY / TEL_PROMPTED |
遥测开关(community / anonymous / off)与首次询问标记 |
EXPLAIN_LEVEL |
输出解释风格,default 或 terse(其余取值归一为 default) |
UPDATE_CHECK |
是否执行升级检查(false 时跳过升级提示) |
ACTIVATED / FIRST_LOOP_SHOWN / FIRST_TASK |
首次运行标记与项目形态探测(greenfield、code_*、branch_ahead、dirty_default、clean_default) |
HAS_ROUTING / ROUTING_DECLINED |
CLAUDE.md 是否已有 ## Skill routing 段 |
VENDORED_GSTACK |
检测项目内是否 vendored 了 gstack(已废弃,建议迁移 team mode) |
MODEL_OVERLAY |
本技能固定为 claude,即附加 Claude 模型族的微调补丁 |
CHECKPOINT_MODE / CHECKPOINT_PUSH |
连续检查点提交模式(continuous 时自动以 WIP: 前缀提交逻辑单元) |
GSTACK_PLAN_MODE |
plan-mode 探测(CLAUDE_PLAN_FILE 或 GSTACK_PLAN_MODE_FORCE 置 active,否则 inactive) |
SPAWNED_SESSION |
在 OpenClaw 等 AI 编排器派生的会话中运行时置 true |
Preamble 还会把本次运行写入 ~/.gstack/analytics/skill-usage.jsonl(受 telemetry 开关门控)与项目级 timeline.jsonl,并从 ~/.gstack/projects/<slug>/learnings.jsonl 加载历史学习条目(超过 5 条时调用 gstack-learnings-search --limit 3)。
Plan Mode 下允许的操作
SKILL.md 明确列出本技能在 plan mode 下允许的操作(因为它们只用于"为计划提供信息"):$B、$D、codex exec/codex review、写入 ~/.gstack/、写入 plan 文件、以及对生成产物的 open。文末 "Plan Mode" 一节的结论是:PLAN MODE EXCEPTION — ALWAYS RUN——本技能完全只读:不写文件、不改 git 状态、不改网络状态,因此在 plan mode 中运行是安全的。
另外两条运行期规则也值得注意:若 PROACTIVE 为 false,不得自动调用或主动建议技能,只能询问;若技能在 plan mode 中被用户显式调用,技能文件被视为可执行指令而非参考资料,从 Step 0 开始逐步执行。
三、五步工作流
Step 1:探测平台与 base 分支
与 gstack 其他技能使用同一套探测逻辑:优先从当前 PR 读 base,其次从仓库默认分支读,最后回退到字面量 main。
BASE_BRANCH=$(gh pr view --json baseRefName -q .baseRefName 2>/dev/null || \
gh repo view --json defaultBranchRef -q .defaultBranchRef.name 2>/dev/null || \
echo main)
echo "Base branch: $BASE_BRANCH"
Step 2:读取当前版本状态
分别取本地 VERSION 与远端 base 分支上的 VERSION,作为后续槽位计算的基准:
CURRENT_VERSION=$(cat VERSION 2>/dev/null | tr -d '[:space:]' || echo "0.0.0.0")
git fetch origin "$BASE_BRANCH" --quiet 2>/dev/null || true
BASE_VERSION=$(git show "origin/$BASE_BRANCH:VERSION" 2>/dev/null | tr -d '[:space:]' || echo "$CURRENT_VERSION")
echo "origin/$BASE_BRANCH VERSION: $BASE_VERSION"
echo "branch HEAD VERSION: $CURRENT_VERSION"
Step 3:查询版本队列
对四个 bump 级别各调用一次底层工具,把每个级别的候选结果落到临时文件(文档正文写 "three times",但给出的循环实际遍历 micro patch minor major 四个级别);任一次失败则写入 {"offline":true} 占位,保证 Step 4 渲染不会因缺文件中断:
for LEVEL in micro patch minor major; do
bun run ~/.claude/skills/gstack/bin/gstack-next-version \
--base "$BASE_BRANCH" \
--bump "$LEVEL" \
--current-version "$BASE_VERSION" \
> "/tmp/landing-$LEVEL.json" 2>/dev/null || echo '{"offline":true}' > "/tmp/landing-$LEVEL.json"
done
SKILL.md 特别说明这次调用"很便宜(same gh call cached by bun)",因为同一 PR 列表查询在 bun 侧有缓存。
Step 4:渲染仪表盘
用 jq 从 JSON 中提取字段,其中 patch 级别的 JSON 是队列与兄弟工作区数据的规范来源——这些字段在各 bump 级别间完全相同,只有 .version 不同。需要提取的字段:
.host—github | gitlab | unknown.offline— 查询是否失败.claimed—{pr, branch, version, url}数组.siblings— 发现的所有兄弟 worktree.active_siblings— 其中"很可能即将合入"的子集
在线时的渲染格式(SKILL.md 要求按此格式精确输出):
╔══════════════════════════════════════════════════════════════════╗
║ GSTACK LANDING REPORT ║
╠══════════════════════════════════════════════════════════════════╣
║ Repo: <owner/repo> ║
║ Base: <base> @ v<base-version> ║
║ Host: <github|gitlab|unknown> ║
║ Status: <ONLINE|OFFLINE: queue-awareness unavailable> ║
╚══════════════════════════════════════════════════════════════════╝
Open PRs claiming versions on <base>:
#1152 alpha-branch → v1.7.0.0
#1153 beta-branch → v1.7.0.0 ⚠ collision with #1152
#1151 gamma-branch → v1.6.5.0
Sibling Conductor worktrees (<workspace_root>):
path branch VERSION last commit PR
──────────────────────────────────────────────────────────────────────────────────
../tokyo-v2 feat/dashboard v1.7.1.0 3h ago none ★ active
../melbourne feat/review v1.6.0.0 12d ago none
../osaka feat/payments v1.8.0.0 5h ago #1155
★ active = has VERSION ahead of base AND last commit < 24h AND no open PR.
These are the ones likely to ship soon.
If you ran /ship right now, you'd claim:
micro bump: v1.6.3.1 (queue-advance: none)
patch bump: v1.7.1.0 (bumped past claimed 1.7.0.0)
minor bump: v1.8.0.0 (bumped past claimed 1.7.0.0)
major bump: v2.0.0.0 (no major collisions)
离线或宿主未知时,输出缩短为:
╔══════════════════════════════════════════════════════════════════╗
║ GSTACK LANDING REPORT ║
╠══════════════════════════════════════════════════════════════════╣
║ Status: OFFLINE — queue-awareness unavailable ║
║ Reason: <offline reason from warnings> ║
╚══════════════════════════════════════════════════════════════════╝
Fallback: local VERSION bumps still work, but collisions cannot be detected.
Step 5:给出下一步建议
渲染表格后,按以下优先级只建议一项:
- 队列存在冲突(两个 open PR 声称同一版本):"⚠ Two open PRs collide on v<X>. Whoever merges second will either overwrite the first's CHANGELOG entry or land a duplicate. Consider asking one author to rerun /ship to pick up the next free slot."
- 某个 active 兄弟工作区的版本高于当前分支:"Sibling worktree <path> has v<X> committed <N>h ago and hasn't PR'd yet. If that work ships first, your branch will need to rebump at land time."
- 一切干净:"Queue is clean. Next /ship will claim a slot without conflict."
四、底层实现:bin/gstack-next-version 如何给出这个快照
SKILL.md 的五个 Step 只是"消费方",真正的队列感知逻辑全部在 bin/gstack-next-version 这个 Bun/TS 工具里。文件头部注释把它与 /ship 的职责切分写得很清楚:"Contract: util NEVER writes files or mutates state. Pure reader + reporter. /ship consumes the JSON and decides what to do."——这正好解释了为什么 /landing-report 可以宣称绝对只读。
命令行接口与退出码
Usage:
gstack-next-version --base <branch> --bump <major|minor|patch|micro> \
--current-version <X.Y.Z.W> [--workspace-root <path>|null] \
[--version-path <path>] [--json]
--bump 是必填项(缺失或取值不在四档之内时以退出码 2 报错);--base 缺省时按 origin/HEAD 符号引用 → origin/main 探测 → origin/master 探测 → 字面量 main 的链路自动判定(与 test/gstack-next-version.test.ts 中 "default-base detection" 一组的 fixture 仓库测试逐条对应)。退出码约定:0 = 成功输出 JSON(即使含 "offline":true 或 "host":"unknown");2 = 参数非法;3 = 工具自身 bug(未预期异常)。
JSON 输出契约
main() 最终写出的 JSON 字段如下(对应 SKILL.md 中 jq 提取的对象):
| 字段 | 说明 |
|---|---|
version |
最终选中的槽位(已越过队列声称与 active 兄弟) |
current_version / base_version |
调用方传入(或从 origin/<base> 读取)的基准版本 |
version_path |
实际解析出的 VERSION 文件路径 |
bump |
请求的 bump 级别 |
host |
github / gitlab / unknown(由 origin URL 嗅探 + gh/glab 认证探测得出) |
offline |
宿主队列查询是否失败 |
fallback |
是否回退到 git 通道("git" 或 null)——/ship Step 12 依赖此字段判断选取结果是否可信 |
claimed |
真实声称列表(版本高于 base 的 PR 才被计入) |
siblings / active_siblings |
兄弟 worktree 全量 / 其中判定为 active 的子集 |
reason |
选取理由(如 bumped past claimed 1.7.0.0) |
warnings |
全部降级/告警信息 |
VERSION 路径解析(monorepo 支持)
版本文件的定位优先级为:--version-path CLI 标志 > 仓库根的 .gstack/version-path 文件(单行相对路径,提交进仓库让协作者共享)> 默认 VERSION。resolveVersionPath() 是一个纯函数,测试用临时目录直接驱动而不 mock git。若钉住的路径以 .json 结尾(如 frontend/package.json),读取时按 JSON 解析取 .version 字段——这一行为由 lib/version-source.ts 中的 isJsonVersionPath() / extractVersion() 统一实现,注释说明这是为修复 #2501:此前 JSON 文件被当纯文本做空白剥离,每个版本读取都落回 0.0.0.0,竞争对手 PR 的声称因此被当作 "malformed" 丢弃,队列冲突检查静默失效。
冲突分配算法:pickNextSlot
槽位选择的核心是 pickNextSlot(base, claimed, level, width),语义是"在同一级别上越过队列中的最高声称":先对 base 做一次 bump 得到候选;若队列中存在高于 base 的声称,则改为对最高声称再做一次同级别 bump,取较大者。例如 base 为 1.6.3.0、队列已声称 1.7.0.0、请求 minor 时,结果是 1.8.0.0(对 main 仍是 minor 语义,保留 ship 时的意图)。跨级别也成立:队列有 1.7.0.0(minor)而你要 patch,则落在 1.7.1.0。
版本宽度(width)是 #2501 的另一半:parseVersion() 同时接受 3 段与 4 段(3 段在比较前 pad 成 [a,b,c,0]),versionWidth() 记录字符串实际段数,fmtVersion() 输出时收窄回仓库自己的宽度;在 3 段仓库中 micro 没有可动的第四段,bumpVersion() 会把它执行为 patch 并通过 bumpWasCoerced() 告知调用方打 warning——因为 /ship 默认自动选 micro,报错会让该工具在所有 3 段仓库里不可用,而静默 no-op 更糟(会把起始版本写回去、声称一个已被占用的槽位)。
声称收集:GitHub / GitLab / 纯 git 三级通道
- GitHub 通道:
gh pr list --state open --base <base> --limit 200拉取 open PR,用gh repo view拿到本仓库 owner 以过滤 fork PR(fork PR 通过 Contents API 会解析到本仓库 main 的 VERSION,形成幽灵声称),再以GH_API_CONCURRENCY = 10的有界并发逐个gh api repos/{owner}/{repo}/contents/<versionPath>?ref=<head>读取各 PR 头部的 VERSION 文件(base64 解码后过extractVersion)。 - GitLab 通道:
glab mr list --opened --target-branch <base>+ Files API,逐 MR 读取。 - 纯 git 回退(#2545):当宿主查询离线或 host 未知时,
fetchGitClaimed()接管分配。源码注释记录了一次真实事故:2026-08-12 某个下游仓库gh pr list失败、工具返回offline:true且声称集合为空,/ship回退到本地算术,把0.1.57.0分给了第二个 PR,而一个 open PR 已占用它——两者合并后 main 上出现两个写着 v0.1.57.0 的提交,审计发现该回退已持续误分配三周。修复原则是"git 知道 API 本来要问什么":git ls-remote --heads origin拿到远端实时分支清单(零本地变更,GIT_TERMINAL_PROMPT=0+ 5s 超时防止凭据提示挂死);每个候选分支的 VERSION 优先从实时 tip sha 读取,本地缺对象的分支用一次批量 shallow fetch(--depth=1 --no-tags,不逐分支爬取)补齐,仍不可读的以 UNKNOWN claim warning 显式上报而非静默跳过;此外还扫描 base 最近 400 条提交主题中形如v<semver>的已发布版本(VERSION 文件只能反映最新值,看不到"已合并又被重挑"的号),达到 400 条上限时会主动声明扫描范围。
这个回退的设计目标在注释里一句话概括:"offline degrades the QUEUE VIEW (no PR numbers, no draft status) without degrading the ALLOCATION"——队列视图降级,但版本分配不降级。
兄弟 worktree 扫描与 "active" 判定
scanSiblings() 在 <workspace_root>/<repoName>/<workspace>/ 的 Conductor 布局下遍历同级目录(跳过自身、要求存在 .git 与版本文件),读取每个兄弟的分支、版本、最后提交时间戳,并用 PR 声称列表的分支名匹配出 has_open_pr。workspace_root 的解析优先级是:--workspace-root CLI > gstack-config get workspace_root > 默认 $HOME/conductor/workspaces;显式传 null 可关闭扫描(非 Conductor 用户)。
markActiveSiblings() 的判定规则与仪表盘脚注一致,且由常量 ACTIVE_SIBLING_MAX_AGE_S = 24h 固化:版本高于 base 且 最后提交在 24 小时内 且 没有 open PR 三条同时满足才标为 active。若存在版本不低于最终候选的 active 兄弟,main() 还会把最终槽位再 bump 过去(reason 记为 bumped past active sibling <version>)。
五、配置项:workspace_root
/landing-report 依赖的唯一 gstack 配置键是 workspace_root(由 bin/gstack-config 管理,v1.11.0.0 引入):默认 $HOME/conductor/workspaces,设为 null 则禁用兄弟扫描。对不使用 Conductor 多窗口的用户,建议显式置 null,可以让兄弟表为空、报告只聚焦 PR 队列。
六、如何用测试验证这些行为
test/gstack-next-version.test.ts 是这个技能可信度的直接依据,覆盖与 SKILL.md 各步骤一一对应:
pickNextSlot(队列感知分配的核心):无声称的干净 bump、单/多重碰撞、跨级别碰撞(queued MINOR 越过我的 PATCH 落在1.7.1.0)、低于 base 的声称被忽略、未排序声称的正确处理;markActiveSiblings:四条判定逐一验证——高版本且 24h 内且无 PR 才 active;有 open PR 的不算(已在队列里);25h 前的提交算 stale;等于或低于 base 的不算;- 离线输出契约(#2545):用失败的
gh桩 + 本地 bare fixture 仓库断言offline:true、fallback:'git'且仍然返回可用版本号("degraded queue view, NOT degraded allocation");在线路径则断言fallback为null; fetchGitClaimed系列:从 remote-tracking ref 发现兄弟声称并推动选取(0.1.67.0→ 选0.1.68.0);识别已发布版本;在远端 ref 上读取 JSON 版本路径;非 git 目录降级为 warning 而非抛错;ls-remote 优先的"实时分支"语义(已删除分支的 stale 本地 ref 与upstream远端的 ref 都不计入声称、零本地 ref 变更);未 fetch 的实时声称通过单次批量 fetch 解析(测试用 PATH shim 数git fetch调用次数锁定为 1);单个悬空 ref 不毒化整批,只以 UNKNOWN claim 告警;- 3 段仓库宽度:origin base 不可读时零基准也按仓库自身宽度塑形(
base_version为0.0.0而非0.0.0.0,分配结果0.0.1而非0.0.1.0); - 默认 base 探测:
origin/HEAD符号引用优先于origin/main存在性;无 HEAD 时origin/master探测生效;全无时落到字面量main并在 warnings 中声明。
此外,/ship 侧的版本状态分类与双写(VERSION + package.json 的 npm 三段合法形式翻译,见 lib/version-source.ts 的 npmVersion())由 bin/gstack-version-bump 承担,与 gstack-next-version 构成"读取器/写入器"分工——这解释了 /landing-report 与 /ship 之间"同一个工具、只读与写"的关系。
七、延伸阅读
- 技能正文与模板:landing-report/SKILL.md、landing-report/SKILL.md.tmpl
- 核心工具:bin/gstack-next-version(读取端)、bin/gstack-version-bump(
/ship写入端)、lib/version-source.ts(版本解析/宽度/JSON 路径原语) - 配套 CI 门(v1.11.0.0 引入,见 CHANGELOG.md):
scripts/detect-bump.ts、scripts/compare-pr-version.ts,在 VERSION/CHANGELOG/package.json 变更时做合并期冲突门,工具错误时 fail-open(gstack 的 bug 不会冻结你的合并队列)、确认冲突时 fail-closed - 技能清单与定位:AGENTS.md、docs/skills.md
适用前提:/landing-report 需要在已安装 gstack(~/.claude/skills/gstack/)的项目内运行;GitHub 通道要求 gh 已认证,GitLab 通道要求 glab 已认证;两者都不可用时自动降级为 git 通道或离线提示。由于该技能只读,在 plan mode、CI 或无网络环境中运行均安全,离线时唯一的损失是"冲突无法被检测到"这一提示本身。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0625
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00