menu

云手SAAS规范流程

  • date_range 27/07/2021 15:19
    点击量:
    info
    sort
    leetcode
    label
    云原生
    数字孪生
    微服务

并行线工作流:一条线 = 一个终端 tab = 一个 worktree = 一个 claude 会话

用户 2026-08-02 拍板(ISO1 出路①),2026-08-09 合仓后复核并补齐。 工作目录就是这条线的边界。


★ 全流程总表:干活 → 收口(2026-08-12 补立)

★★补这张表的原因:本文档原先只覆盖 ④⑤⑥⑨(开工位/看板/合回/退役), 而 ①②③⑦⑧⑩ 散落在 docs/任务清单.mddocs/HANDOFF.mddocs/发版流程.md、 交付回执模板里。⇒ 没有一处能一眼看到全流程 —— 2026-08-12 架构线因此漏了第 ③ 步(登记了编号却没派工), 表现是「任务有编号,而工位看板上什么都没有」,且没有任何地方会报错

阶段 落点
① 方案设计 docs/任务-*.md / kubernetes/任务规格-*.md 架构线
② 任务编码 docs/任务清单.md 登记编号 ★动手前先查,别撞号 架构线
派工 docs/HANDOFF.md 写一条「XX 交编码线」 架构线
④ 开工位 line-worktree.sh new <线名> + 私有测试库(§9.2,脚本不做) 编码线
⑤ 看板 line-worktree.sh list(★显示的是开着哪些工位,不是任务列表) 脚本实时算
— 干活 — 每天至少跑一次 check-line-collisions.py(§9.3) 编码线
合回 main §4 那四步,第 ④ 步(rebase 后重跑 check)最容易漏 编码线
写回执 docs/交付回执-<编号>-给设计线.md七段必填 编码线
更新 HANDOFF 把那条从「交编码线」改成「已闭环」 编码线
⑨ 退役工位 line-worktree.sh rm <线名> + 手工回收测试库(§9.6) 编码线
⑩ 发版(攒够 20 条) docs/发版流程.md / scripts/release-all.sh 谁发谁负责

🔴 收口那三步(⑦⑧⑨)有一道闸

scripts/check-line-closeout.sh <编号>        # 例:GW4

为什么单独给收口做闸:⑦⑧⑨ 全都是「不做也不会有任何东西报错」的步骤 —— 活已经合进 main 了、功能也上线了,少一份回执、HANDOFF 少改一行、工位没退役, 系统一切正常。⇒ 它们是这套流程里唯一靠自觉的一段,而自觉在忙的时候必失效。

⚠️ 已知缺口:主工作区落后时 §4 第 ⑤ 步走不通

git merge --ff-only feat/<线名> 要求主工作区能快进。 而主工作区常常落后远端十几到几十个提交(别的线在推),且带着未提交改动 ⇒ 拒绝快进。

★2026-08-12 架构线实测撞上:主工作区落后 28 个提交、有别的线的未提交改动, 于是改用「临时工位 cherry-pick + push origin HEAD:main」绕过去。

  • 绕过去本身可以(它保住了「只推自己的文件」这条),
  • 🔴 但代价必须知道:那条路跳过了 §4 的两次 make check。 跑分包测试 ≠ make check —— 后者还含契约测试、OpenAPI 基线、四道 repo 闸。
  • ⇒ 走这条路时,必须在回执第 5 段(偏离规格与未验前提)里如实写下来, 不要当作”应该没事”。

0. 为什么要这样(不是洁癖)

全机只有一个物理工作目录时,两条线编辑同一个文件,谁先提交谁把两份都带走。 内容不丢,丢的是「这行为什么这么写」—— git 里它指向一条不相干的提交说明, 而那是几个月后唯一能查的东西。★2026-08-02 一天内发生 3 次。

★而 ISO1 已论证过:这做不成 pre-commit 闸 ——「这半是别人的」这个信息 在 git 里根本不存在。所以只能从物理隔离入手。


1. 开一条线(3 步)

在主仓开新终端 tab,然后:

cd ~/dockersofts/CloudHandStack
scripts/line-worktree.sh new <线名> [简述]      # 例:new bc9 generic-quota

它会:

动作 为什么(都是实测过的静默失败)
~/dockersofts/CloudHandStack-lines/<线名> 物理隔离
具名分支 feat/<线名>[-<简述>] detached 下提交没有任何 ref 指着,工作区一删就只剩 reflog
软链主仓 .env 不软链的话 docker compose config 退出码 0,19 个变量静默变空
软链 apps/agent-ui/node_modulesweb/node_modules 不软链的话这条线的 make check 必红在前端闸
.WORKTREE-README 并加进 .git/info/exclude 不排除的话它会污染 git status——那是各条线互查占用的唯一视野

最后 cd 过去起 claude:

cd ~/dockersofts/CloudHandStack-lines/<线名> && claude

cd 再起 claude,不要在主仓起了再 cd。claude 起在哪,哪就是它的项目根; 起错了它整场会话都以主仓为根,边界当场就没了。


2. 在线上能做什么、不能做什么

  命令
✅ 可以 编辑、go buildgo testmake checkmake docker-buildscripts/stack.sh buildscripts/stack.sh status
🔴 绝不 docker compose up / down / restartscripts/stack.sh up / down / publish-plugin

两个失效都不报错、退出码 0

  • compose 项目名跟着目录名走 ⇒ 在工位里 up另起一整套容器,而不是管现有的;
  • 数据卷是相对路径./data 等)⇒ 解析成工位下的空目录

合起来的观感是「站点数据没了」,而真站点好好的 —— 极难当场看懂。

⇒ 部署一律回主仓做。


3. 边界靠什么保证(★分清「有闸」和「靠自觉」)

三层,2026-08-09 逐层验过:

是什么 验证
物理隔离 各线各目录,git worktree 天然成立
fail-CLOSED 守卫 stack.sh_refuse_in_worktree():比 --git-dir--git-common-dir绝对路径,不等就拒 工位里实测二者确实不同(.git/worktrees/au1 vs .git);status 只读故意放行
.WORKTREE-README 说明为什么不能跑 compose 建线时自动写入

★守卫的两个设计点值得知道:

  • 判据用脚本自己所在的目录,不是 $(pwd) —— 从别处调用时 pwd 是调用者的, 守卫会去检查一个不相干的地方,★而那种失效的方向是放行
  • 判定不出来就拦(fail-CLOSED)。误拦的代价是一条提示 + 一个环境变量 (AYG_ALLOW_WORKTREE_DEPLOY=1),误放的代价是上面那套「像是数据没了」。

🔴 它保证什么

  1. docker compose 没有任何闸 —— 守卫只在 stack.sh 里。 这条路上唯一的防线就是 .WORKTREE-README 那张纸。
  2. git -C <主仓> … 照样能用 —— 工位挡不住你显式指向主仓。
  3. claude 能读写工位以外的绝对路径 —— 边界是「默认在哪」,不是沙箱。
  4. node_modules 是软链,不是副本 —— 某条线 npm ci 会动到共享的那一份 (与 .env 软链同一个取舍)。改 package.json 的线要意识到这点。

4. 合回 main(★第 ④ 步最容易漏)

W=~/dockersofts/CloudHandStack-lines/<线名>
cd $W && make check                                   # ② 本地全绿
git -C $W rebase main                                 # ③ 冲突在自己线上解
cd $W && make check                                   # ④ ★rebase 后**重跑**
git -C ~/dockersofts/CloudHandStack merge --ff-only feat/<线名>   # ⑤ 主目录只做快进
  • 第 ④ 步不能省:「绿」是 rebase 那次的绿,rebase 之后代码已经不是那份了。
  • 第 ⑤ 步用 --ff-only:它拒绝就说明 ③ 没做干净,回去重做,不要用普通 merge 糊过去。

⚠️ 多条线一起合回时这套要重做多遍 —— 见 §9.5。


5. 退役

cd ~/dockersofts/CloudHandStack
scripts/line-worktree.sh rm <线名>
  • 未提交改动会拒绝删 —— 那正是这套隔离要保住的东西。
  • 分支用 git branch -d不是 -D):没全并时它会拒,等于免费多一道确认。 ★这道确认拦的是「以为合过了、其实没合」—— 那种情况下 -D 会安静地把活删掉, 而工作区已经先一步被拿走,两步都成功,东西没了,一条错误信息都没有。

🔴 ★它不回收测试库 —— line-worktree.sh 不知道 §9.2 那个库的存在。 多工位场景下漏这一步会攒一堆孤儿库,见 §9.6。


6. 查看所有线

scripts/line-worktree.sh list

它会额外替你看两件 git worktree list 一眼扫不出来的事:

  • 有没有 (detached HEAD) —— 那种工位的提交没有任何 ref 指着;
  • 有没有 prunable 登记(目录已不存在)—— 清之前要先确认 HEAD 在别处可达。

7. 桌面端:New 会话怎么跟工位对上

Claude Code 按工作目录给会话建项目 —— ~/.claude/projects/ 下的目录名 就是工作目录路径编码来的:

/Users/andrewyang/dockersofts/CloudHandStack           → -Users-andrewyang-dockersofts-CloudHandStack
/Users/andrewyang/dockersofts/CloudHandStack-lines/bc9 → -Users-andrewyang-dockersofts-CloudHandStack-lines-bc9

换个工作目录 = 换一个项目 = 侧边栏里另起一组。 这就是「一个会话 = 一个工位」的全部机制,没有别的开关要拧。

起会话的方式 怎么绑到这条线
终端 cd <工位> && claude
桌面端 New → 工作目录选 ~/dockersofts/CloudHandStack-lines/<线名>

先建工位、再 New 会话 —— 反过来的话 New 的时候那个目录还不存在。

★工位根目录为什么不带点

原先叫 .agenthands-lines(点开头 = 隐藏目录)。 2026-08-09 改成 <仓名>-lines,理由是功能性的:macOS 的文件选择器 默认看不见隐藏目录,桌面端 New 会话每次都得 Cmd+Shift+. 或手贴路径 —— 而「开一条线」正是这套流程里唯一的高频动作,不该有摩擦。

★同一份文档早先写过「工位根目录刻意不改名」,那句针对的是改称呼 (与「网关改叫 aiproxy 但运行时仍是 cliproxy」同一条取舍)。 这次不是称呼问题,是选不中目录,所以结论反过来 —— 记在这里免得被改回去。

默认值是 ${REPO}-lines,仓库将来改名会跟着走。要放别处: AGENTHANDS_WORKTREE_BASE=<路径>


8. 还有一个不是这套流程建的工位

CloudHandStack/.claude/worktrees/worktree-session-boundary-<hash>

这是 Claude Code 自己的隔离工位isolation: worktree),会出现在 git worktree list 与侧边栏里。别把它当成一条业务线,也别用 line-worktree.sh rm 去动它。

🔴 ★★它不经过 line-worktree.sh,所以 §1 那五件事一件都没做 —— 最要命的是缺 node_modules 软链:在这种工位里 make check 会红在前端闸, 退出码 2web/node_modules 是空的 —— 这道闸跑不了)。 2026-08-09 实测撞到过;补上两条软链后 check-consolecheck-agent-ui 均退 0:

R=~/dockersofts/CloudHandStack; W=$R/.claude/worktrees/<那个目录>; ln -sf $R/.env $W/.env; for n in apps/agent-ui web; do ln -sfn $R/$n/node_modules $W/$n/node_modules; done

9. 同时开多个工位(★以 8 个为例)

单工位的每一步照 §1–§7,本节只写多开之后才出现的那些事。

9.0 先回答「这几条能不能并行」——不能跳过

能并行 ⟺ 文件集合不相交。 与甘特图上是否同期、方案拆得多小,都无关。 判据表在 docs/任务清单.md 的「📁 文件领地总表」,排期先看那张表

★物理隔离治得了「谁把谁的改动卷进提交」,★★治不了「两条线改同一个文件、 最后 rebase 时全撞在一起」 —— 后者只是把冲突从提交时推迟到合并时,还更难解。

认领每条线之前三步,顺序不能反(git log 那步最容易漏,它发现的是「已经做完了」):

git log --all --grep=<编号> && git status && git worktree list

🔴 ★2026-08-09 实测:本仓当前凑不出 8 条互不相交的线。 领地表里标 🟢 的本仓任务只有 ONB1 · DL1(P1/P2) · SK2 · TOS1 四条(AU1 已被占用), 其余不是 🔴 独占(ENT1 跨 port、SDK1 剩余项独占 adapter/httpapi/**) 就是 🟡 要排队(IAM1ENT1DX1VA2engine/core), DUN1ENT1 同在 billingsync ⇒ 串行,SB2 暂缓。 要凑到 8 只能把 QG1/CP3BC4/BC8RB1 这三组仓外任务算进来 —— ★而它们在另外的仓cliproxy-fork / andrewyg-agentplugin / docker_cliproxy), 不是本仓的 worktree,本文这套流程管不到它们。

先按领地表定出真正能同开的条数,再决定开几个工位。「凑够 8 个」不是目标。

9.1 一次性前提

git -C ~/dockersofts/CloudHandStack status --porcelain

必须输出为空。 非空说明主仓里还有人在写 —— 那正是工位模型要消灭的状态。

(cd ~/dockersofts/CloudHandStack/web && npm ci) && (cd ~/dockersofts/CloudHandStack/apps/agent-ui && npm ci)

各工位的 node_modules软链到主仓这一份的。主仓这份是空的 ⇒ 所有工位的 make check 一起红在前端闸(退出码 2,不是 1 —— 别去查代码)。

9.2 🔴 每条线一个私有测试库(★这一步 line-worktree.sh 不做,少了它必炸)

Makefile 已经把包级并行钉成 GO_TEST_PACKAGE_PARALLELISM ?= 1,注释写明理由: controlstore / httpapi 都会对同一个 AGENTHANDS_TEST_PG_DSN 做真实清库, 并行会让两个包彼此删掉对方的夹具,表现为偶发红而非真实回归

★★-p 1 只约束一个 go test 进程内部。 8 个工位是 8 个进程-p 1 对它们之间毫无作用 ⇒ 同一个坑原样重现, ★而它的表现是「偶发红」—— 人会去查自己刚写的代码,真实原因是隔壁工位在清库。

2026-08-09 实测(两个方向都验了,只验一边分不出「真成立」还是「检查失灵」):

场景 controlstore httpapi
并发 + 共用一个库 退出码 1TestCrossAgentReleaseRejectedAgainstRealStore FAIL) 退出码 1TestControlAPIEndToEnd FAIL)
并发 + 各自一个库 退出码 0 退出码 0

⇒ 在这个 tab 里导出,就一行(★不能写进 .env —— 那是各条线软链共用的 同一个文件,写进去等于又回到同一个库):

export AGENTHANDS_TEST_PG_DSN="$(scripts/test-db.sh)"

scripts/test-db.sh当前工位目录名算出库名(主仓 → agenthands_test_main, 工位 …-lines/onb6agenthands_test_onb6),不存在就建,存在就直接给 DSN。

  • 为什么按工位而不是按 PID/时间戳:按 PID = 每次跑都是新库 = 每次重跑全部迁移, 且垃圾库无限增长。按工位 = 同一工位反复跑复用同一个库(快),不同工位天然隔离(安全)。 ⇒ 隔离粒度要对齐真实的并发单位,而这里的并发单位就是工位。
  • 🔴 ★建库必须 -U postgresagenthands 角色 rolcreatedb=f,用它会得到 ERROR: permission denied to create database(实测)。脚本已经这么做。
  • 属主必须是 agenthands:属主不对时表会建成 postgres 的, 而各 store 用的 CREATE TABLE IF NOT EXISTS静默跳过已存在的表 ⇒ 错误属主永久留存,报出来的是 permission denied(指向权限,不指向属主)。 2026-08-10 发 v1.0.11 时因此误诊过一轮。
  • 库名必须含 test:契约测试有硬保险,不含就 t.Fatalf 拒绝跑 —— 那些测试会 TRUNCATE 整张表,指向生产库会清空真实会话与租户。 命名规则 agenthands_test_<工位> 天然满足。
  • 不必预先建表:各 store 用 CREATE TABLE IF NOT EXISTS 自举,空库直接能跑(实测)。

其他用法:scripts/test-db.sh --name 只看库名 · --list 列出所有测试库并标出 哪些已经没有对应工位(可回收)· --drop 删掉本工位那个库(夹具脏了想重来)。

🔴 2026-08-10:共享库 agenthands_test 已退役,并加了闸

本节的约定 2026-08-09 就写在这里了,仍然被绕过 —— 因为它要人做两件手工事 (建库 + 手打一长串 export),而 release-all.sh 自己推导 DSN 时 默认推出来的正是共享库。发 v1.0.11 当晚连红三次,每次红的包都不一样。

⇒ 三件一起做,缺任何一件都还会被绕过:

   
自动 scripts/test-db.sh 一行搞定建库 + 取 DSN;release-all.sh 改成调它
make check 第一项 check-test-db-isolation:DSN 指向 agenthands_test 直接红(毫秒级,而它要防的故障要等十几分钟才显形,且显形时报的是别的包)
退役 共享库已 ALTER DATABASE … RENAME TO agenthands_test_retired_20260810 ——改名不删:残留引用会立刻报 database "agenthands_test" does not exist,而数据仍可回滚

★不设 DSN 的后果不是 skip 是 fail:35 个用例会故意红。 一个绿色的 ok 配零个用例,比红色的失败危险得多。

9.3 批量开(在主仓 tab 里跑,线名换成你自己那批)

cd ~/dockersofts/CloudHandStack && for L in onb1:onboarding dl1:retention sk2:ablate tos1:tos; do N="${L%%:*}"; S="${L##*:}"; bash scripts/line-worktree.sh new "$N" "$S" || { echo "🔴 建线失败:$N —— 停下来看"; break; }; docker exec langfuse-postgres-1 psql -U postgres -d postgres -c "CREATE DATABASE agenthands_test_${N} OWNER agenthands;" || { echo "🔴 建库失败:$N —— 停下来看"; break; }; done

★ 用 || break 不用 || true半成品工位比没有工位更糟 —— 它长得跟好的一样,要到跑 make check 时才以「连不上数据库」的形状暴露。 ★★同理不要用 >/dev/null 吃掉错误:写这份文档时我就这么干过一次, psql 报了 permission denied而后面那行 ✓ 已建 照样打印了

9.4 核对工位真的齐了(★三份清单条数必须一致)

cd ~/dockersofts/CloudHandStack && echo "— worktree —" && bash scripts/line-worktree.sh list && echo "— 分支 —" && git branch --list 'feat/*' && echo "— 测试库 —" && docker exec langfuse-postgres-1 psql -U postgres -d postgres -tAc "SELECT datname FROM pg_database WHERE datname LIKE 'agenthands_test_%' ORDER BY 1;"

对不上就是 §9.3 在某条线上断了。★每个工位再各开一个 tab,照 §9.2 export 后 cd … && claude

9.4.1 干活期间:每天至少跑一次撞车检查

python3 ~/dockersofts/CloudHandStack/scripts/check-line-collisions.py

★它补的是隔离之后丢掉的那个信号:以前两条线重复造同一个东西, 是靠共用一个目录、在 git status 里看见对方的未跟踪文件才发现的 (2026-08-02 的 line-worktree.sh vs worktree-line.sh 就是这么抓到的)。 隔离之后各人只看得见自己那棵树。

⚠️ 它不在 make check,得手动跑;且只看得见已落盘的文件 —— 两条线都还在想的时候它看不见。

9.5 🔴 合回 main 是串行的,每条都要重来一遍 rebase + 重跑 check

main 每接受一条线就往前走一格 ⇒ 后面每条线的 rebase 基线都变了 ⇒ ★②③④ 必须重跑,不能拿开工时那次绿来交差。

一条一条来,每条都等 merge --ff-only 成功再动下一条:

cd ~/dockersofts/CloudHandStack-lines/<线名> && make check && git rebase main && make check && git -C ~/dockersofts/CloudHandStack merge --ff-only feat/<线名>-<简述> && echo "✅ <线名> 已合入 main"
  • 顺序建议:领地最大的先合(它最可能与别人冲突,早暴露早解),文档类最后合。
  • 时间要说在前面:N 条线 × (rebase + 重跑 make check) 是串行成本, make check 每次几分钟 ⇒ 合并窗口是小时级的,不是「八条一起点一下」。 把它当成一个独立时段来排,别排在下班前十分钟。

9.6 批量退役并回收(★rm 不管测试库,得自己删)

cd ~/dockersofts/CloudHandStack && for N in onb1 dl1 sk2 tos1; do bash scripts/line-worktree.sh rm "$N" || { echo "🔴 $N 退役被拒 —— 多半是没合干净或还有未提交改动,停下来看"; break; }; docker exec langfuse-postgres-1 psql -U postgres -d postgres -c "DROP DATABASE IF EXISTS agenthands_test_${N};" && echo "  ✓ 已删库 agenthands_test_${N}"; done

★ 同样用 || break退役被拒是信号不是噪音 —— 它通常意味着这条线的活根本没合进 main(branch -d 拒绝了),强删就是把活丢了。

9.7 收工核对(★四条都要归零)

cd ~/dockersofts/CloudHandStack && bash scripts/line-worktree.sh list && git branch --list 'feat/*' && docker exec langfuse-postgres-1 psql -U postgres -d postgres -tAc "SELECT datname FROM pg_database WHERE datname LIKE 'agenthands_test_%';" && bash scripts/check-stray-test-containers.sh
看什么 期望 不对时怎么读
worktree list 只剩主仓 + Claude Code 自建那个(§8) 多出来的是没退役干净
feat/* 还剩的那条就是没合干净的,去查它,别删
agenthands_test_* 孤儿库(agenthands_testagenthands_bc8_testcliproxy_billing_test 是常驻的,不在此模式内)
残留测试容器 退出 0 有残留就 make clean-stray-test-containers。★它的代价不只是占内存 —— 它会在下一次跑测试时占住端口,而那时的报错是「连不上数据库」,人会去查数据库配置

9.8 ⚠️ 核对结果有保质期 —— 2026-08-09 写这节时当场撞到

写 §9.7 的过程中,feat/au1-audit-subagent几分钟内从「1 条未合回 main」 变成「0 条」—— 另一条线正好在这期间合了。同一时段 main 还从 ad60360 走到 78ead609(工位根目录由 .agenthands-lines 改成 <仓名>-lines), ★git worktree list 里那条线的路径都变了

我据此先写下「抓到一个真实残留」,是错的 —— 那是一条正在飞的线,不是残留。

⇒ ★★判读 §9.7 的四条清单时,先分清「残留」和「在飞」

现象 是残留 是在飞
feat/* 还有分支 工位目录已删、且 main..分支 > 0 工位还在、有人正在里面干活
agenthands_test_* 还有库 对应工位已退役 对应工位还开着

判据是「工位还在不在」,不是「分支还在不在」。 拿分支当判据会把活人当死人。

★★而这件事本身就是 §9.5 那条的现场证据:你 rebase 的那个 main, 在你跑 make check 的这几分钟里可能已经又往前走了。merge --ff-only 被拒不是意外,是常态;被拒就回去重做 ③,别换成普通 merge。


评论:


技术文章推送

手机、电脑实用软件分享

微信搜索公众号: AndrewYG的算法世界
wechat 微信公众号:AndrewYG的算法世界

热门文章