@haiyangbg/buildbeat 0.0.0 → 1.20.0
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/CHANGELOG.md +296 -0
- package/LICENSE +21 -0
- package/README.en.md +288 -0
- package/README.md +283 -4
- package/SKILL.md +303 -0
- package/bin/buildbeat.js +5 -0
- package/bin/solobaton.js +6 -0
- package/docs/CAPABILITY-MATRIX.md +50 -0
- package/docs/CHECKS.md +326 -0
- package/docs/CLI-PILOT-2026-08-23.md +25 -0
- package/docs/CLI-STRATEGY-2026-08.md +55 -0
- package/docs/CLI.md +233 -0
- package/docs/EXECUTION-PLAN.md +487 -0
- package/docs/LEGACY-V1.16-MIGRATION.md +54 -0
- package/docs/PHASE1-PILOT-2026-08-24.md +32 -0
- package/docs/PHASE2-BUILDBEAT-PILOT-2026-08-25.md +75 -0
- package/docs/PHASE2-PILOT-2026-08-25.md +88 -0
- package/docs/PHASE2-PILOT-PREFLIGHT-2026-08-25.md +42 -0
- package/docs/PHASE4-STABILITY-AUDIT-2026-08-25.md +33 -0
- package/docs/PHASE4-V1.20-PILOT-2026-08-25.md +52 -0
- package/docs/RELEASING.md +117 -0
- package/docs/ROADMAP.md +873 -0
- package/example/.buildbeat/manifest.json +45 -0
- package/example/AGENTS.md +19 -0
- package/example/ARCHITECTURE.md +39 -0
- package/example/BUILDBEAT.md +17 -0
- package/example/CLAUDE.md +7 -0
- package/example/README.md +53 -0
- package/example/contracts/PROTOCOL.md +38 -0
- package/example/pm/NOW.md +22 -0
- package/example/pm/adr/ADR-0001-local-first-sqlite.md +25 -0
- package/example/pm/adr/README.md +7 -0
- package/example/pm/archive//344/270/200/346/234/237/evidence/gate1.md +5 -0
- package/example/pm/archive//344/270/200/346/234/237/evidence/gate2.md +5 -0
- package/example/pm/archive//344/270/200/346/234/237/evidence/gate3.md +5 -0
- package/example/pm/archive//344/270/200/346/234/237/evidence/gate4.md +5 -0
- package/example/pm/archive//344/270/200/346/234/237/evidence/implementation.md +5 -0
- package/example/pm/decisions.md +20 -0
- package/example/pm/status//344/272/247/345/223/201.md +20 -0
- package/example/pm/status//345/205/250/346/240/210.md +15 -0
- package/example/pm/status//346/265/213/350/257/225.md +15 -0
- package/example/pm//344/270/200/346/234/237-/347/234/213/346/235/277.md +97 -0
- package/example/standards/CODE.md +18 -0
- package/example/standards/DESIGN.md +34 -0
- package/example/standards/REVIEW.md +16 -0
- package/example/standards/STACK.md +31 -0
- package/lessons.md +119 -0
- package/package.json +48 -7
- package/src/cli.js +323 -0
- package/src/constants.js +199 -0
- package/src/doctor.js +267 -0
- package/src/planner.js +251 -0
- package/src/project.js +839 -0
- package/src/upgrader.js +1249 -0
- package/src/writer.js +534 -0
- package/templates/.claude/agents/reviewer.md +62 -0
- package/templates/AGENTS.md +64 -0
- package/templates/ARCHITECTURE.md +50 -0
- package/templates/BUILDBEAT.md +13 -0
- package/templates/CLAUDE.md +7 -0
- package/templates/contracts/PROTOCOL.md +32 -0
- package/templates/gitignore.template +19 -0
- package/templates/pm/NOW.md +26 -0
- package/templates/pm/adr/ADR-0000-template.md +25 -0
- package/templates/pm/adr/README.md +15 -0
- package/templates/pm/changes/README.md +44 -0
- package/templates/pm/decisions.md +12 -0
- package/templates/pm/status/README.md +32 -0
- package/templates/pm//345/275/223/346/234/237/347/234/213/346/235/277.md +62 -0
- package/templates/scripts/bus-check.sh +1850 -0
- package/templates/scripts/design-preview.sh +44 -0
- package/templates/scripts/drift-check.sh +112 -0
- package/templates/scripts/pre-commit.sh +74 -0
- package/templates/scripts/verify-status.sh +105 -0
- package/templates/standards/CODE.md +23 -0
- package/templates/standards/DESIGN.md +36 -0
- package/templates/standards/REVIEW.md +20 -0
- package/templates/standards/STACK.md +37 -0
- package/templates//346/214/207/346/214/245/345/217/260.md +35 -0
package/lessons.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# 反模式与实战教训(全部真实发生,已脱敏通用化)
|
|
2
|
+
|
|
3
|
+
> 读法:每条 = 症状 → 根因 → 总线里对应的解药。解药编号对应 SKILL.md §4 十条规则。
|
|
4
|
+
|
|
5
|
+
## 1. SSOT 腐烂(最隐蔽、最致命)
|
|
6
|
+
|
|
7
|
+
**症状**:看板头部停在四周前的状态;"当前线上版本"在四份文档里出现三个互相矛盾的值;NOW 指针从 30 行薄文件长成 9 段流水账;新一期没建新看板,直接往上一期看板里塞嵌套引文"续命"。
|
|
8
|
+
**根因**:协调文档天然 append-only,没有任何机制强制压缩/归档;每个事实(版本/决策/进度)被复写进 N 处,改的时候必漏。
|
|
9
|
+
**解药**:规则⑨(单点事实)+ 规则①(看板名不写死)+ 换期压缩仪式。**会腐烂的事实(线上版本/在途数量/当前轨道)一律不写进文档,只放"怎么查"的指针。**
|
|
10
|
+
|
|
11
|
+
## 2. 读过期 race(治了写冲突,漏了读 staleness)
|
|
12
|
+
|
|
13
|
+
**症状**:A 会话开工时读了看板上的决策,干到一半用户改了主意并由 PM 更新了看板;A 会话不知道,按旧决策把活干完上线,只能事后拍板"不回滚"。
|
|
14
|
+
**根因**:开工护栏只在会话开始跑一次;长会话中途不会重读总线。
|
|
15
|
+
**解药**:规则④后半句——**部署/改契约/migration 等不可逆动作前必须重跑 bus-check**,bus-check 打印"最近拍板 3 条"正是给这一刻看的。
|
|
16
|
+
|
|
17
|
+
## 3. 静态稿拍板 → 返工螺旋(单项目最大成本来源)
|
|
18
|
+
|
|
19
|
+
**症状**:用户对着静态设计稿拍板通过;实现上线后用户见到真渲染,推翻自己,整层 UI 返工;某次改版存活仅 2 天就被下一期拍板回退;设计终签之后用户对着真机又推翻 4 条已"演进接受"的走查裁决。
|
|
20
|
+
**根因**:人对静态 artboard 的判断不可靠,见到可点的真东西才表真态;流程却假设"Gate2 拍板是终态"。
|
|
21
|
+
**解药**:规则⑩——Gate2 拍板对象必须是真渲染可点原型;终签也必须含真渲染走查(教训:spec 数值全对,渲染出来卡片仍然"偏平/缺卡/换行")。
|
|
22
|
+
|
|
23
|
+
## 4. 域切得过细(按职能切,不按物理边界切)
|
|
24
|
+
|
|
25
|
+
**症状**:起步按人类公司职能切 6 个域(PM/前端/后端/设计/测试/运维);两个月内前端+后端合并、设计+测试合并,收敛到 4 个;期间契约 ping-pong、会话间信息差、人被编排成本压垮。
|
|
26
|
+
**根因**:域的本质是"独立上下文+独立核查",不是"模拟一个公司组织架构图"。物理上同仓同镜像的"前后端"切成两个域,只制造交接;真正需要隔离的是"写者"与"审者"。
|
|
27
|
+
**解药**:§2——3 域起步,切域只看物理边界与核查需要;合并丢掉的"天然独立核查"用 reviewer subagent + 测试域独立核两端补回。
|
|
28
|
+
|
|
29
|
+
## 5. "当前线上 vX"声明漂移
|
|
30
|
+
|
|
31
|
+
**症状**:运维 README 写"当前线上 v0.3.x",实际线上早已 v0.4.x;PM 文档表格写某服务 v0.2.18,bus-check 实查是 v0.2.20。
|
|
32
|
+
**根因**:版本是变化最快的事实,写进文档的瞬间就开始腐烂。
|
|
33
|
+
**解药**:规则⑨——线上版本唯一查询口是脚本实查(bus-check 调 live-status 钩子);文档要写版本必须带"写就时点"字样。
|
|
34
|
+
|
|
35
|
+
## 6. 走查漏掉非主路径 UI 面
|
|
36
|
+
|
|
37
|
+
**症状**:整期设计走查+E2E 全绿后上线,用户真机一点,发现一个**独立弹窗**整个没按新设计重做——走查只覆盖了主组件。
|
|
38
|
+
**根因**:走查清单按"页面"组织,独立弹窗/二级浮层/空错态不在清单上。
|
|
39
|
+
**解药**:走查范围 = **所有 UI 面**(独立弹窗也算);每个可见界面四态(加载/空/错误/移动)必处理、必走查;规则⑧带图对比让"漏走查"无处藏身。
|
|
40
|
+
|
|
41
|
+
## 7. 状态条目膨胀,下游读不动
|
|
42
|
+
|
|
43
|
+
**症状**:单条状态 entry 长到一屏放不下,信息密度高但检索性差;下游会话开工要先啃几千字。
|
|
44
|
+
**根因**:写状态的会话倾向把全部细节倒进去自证完成;没有长度纪律。
|
|
45
|
+
**解药**:状态条目只写「做了什么 + hash + 证据指针」,长篇分析放报告文件挂链接;换期压缩仪式把历史截走。
|
|
46
|
+
|
|
47
|
+
## 8. 流程只管"怎么做对",不管"做的是不是对的事"
|
|
48
|
+
|
|
49
|
+
**症状**:风险清单里 P1 质量问题(直接影响用户信任)挂了一个月;同期连续数期资源全部投给 UI 改版——因为人在回路,人挑了顺手有趣的活。
|
|
50
|
+
**根因**:立项环节没有任何机制要求对照风险清单/路线图。
|
|
51
|
+
**解药**:立项模板加一栏"对照风险清单/优先级建议,为什么先做这个";每期收尾加一条"上线 N 天数据回看"挂账。流程越精密,越要防"精密地做不重要的事"。
|
|
52
|
+
|
|
53
|
+
## 9. 设计产物自相矛盾
|
|
54
|
+
|
|
55
|
+
**症状**:设计包终版 HTML 的**标题**写方案 A,**实际渲染**的是方案 B;实现会话按标题选了 A,返工。
|
|
56
|
+
**根因**:设计稿没有 manifest,"终版"靠文件名约定,人没核对标题与渲染一致。
|
|
57
|
+
**解药**:设计交付硬要求——标题/文件名必须与实渲染一致;Gate2 用渲染实物拍板天然兜底(规则⑩)。
|
|
58
|
+
|
|
59
|
+
## 10. 自动化便利与安全红线打架
|
|
60
|
+
|
|
61
|
+
**症状**:Stop hook 自动 push 一切已 commit 内容,省心;但一旦误 commit 凭据,会在无人审查窗口下立刻出网。
|
|
62
|
+
**根因**:自动化没有配套闸门。
|
|
63
|
+
**解药**:装自动 push 前先装 secret 扫描;红线写成**可执行口径**("凭据不入 git、不出本机、600 权限"),而不是不可执行的口号("凭据绝不写进任何文件"——本地 .env 事实上必须存在,口径与实践不符的规则必被持续违反)。
|
|
64
|
+
|
|
65
|
+
## 11. 状态文档里的 commit hash 是幽灵
|
|
66
|
+
|
|
67
|
+
**症状**:上游状态文件写着「code-complete,hash 9c3xxxx」;下游会话接手,一查——git 里**查无此 hash**,改动其实整个躺在工作树没提交,所谓 hash 是臆造/本地曾有后被重置的幽灵。
|
|
68
|
+
**根因**:规则③只要求「状态行带 hash」,没要求 hash **可解析**;写状态的会话把"打算提交"当成"已提交",下游不核它就成了假事实。
|
|
69
|
+
**解药**:证据制完成延伸到 hash 本身——**接手 code-complete 第一步 `git cat-file -t <hash>` 核 hash 真实存在**;查无此 hash 一律按"未完成"处理,回工作树找改动。规则②的"独立核查再信"不止适用契约,适用一切上游声明。(此检查已机器化:bus-check「幽灵 hash 核验」逐个核 status 里的 hash,`--strict` 下挂 pre-commit 直接拦。)
|
|
70
|
+
|
|
71
|
+
## 12. 多会话共用工作树 → 构建产物混入他人 WIP
|
|
72
|
+
|
|
73
|
+
**症状**:部署构建直接 `tar` 打包工作树,把**另一个会话没写完的数据库 migration** 烤进了生产镜像,容器启动时自动执行了半成品 SQL。
|
|
74
|
+
**根因**:多会话共编下工作树永远不干净——"我构建那一刻的目录内容" ≠ "我提交的代码";打包工作树等于把并行会话的 WIP 一起发布。
|
|
75
|
+
**解药**:**构建/打包一律取 `git archive HEAD`**,产物只来自已提交内容;推镜像/发布前抽查产物文件清单(`tar tzf`)有无 stray 文件。这是红线"不 `git add -A`"在构建侧的镜像:提交只 stage 自己的,构建只取已提交的。
|
|
76
|
+
|
|
77
|
+
## 13. 平台侧配置漂移(git ≠ 生产的另一半)
|
|
78
|
+
|
|
79
|
+
**症状**:某天第三方登录突然报凭据无效——部署平台控制台上的 env/secret 被改过,但**没重新部署**,容器还跑旧值;另一次实查发现线上镜像 tag 在 git 里找不到对应 tag,没人说得清线上跑的是哪份代码。
|
|
80
|
+
**根因**:部署平台的 env/secret 与镜像配置**不在 git 里**,控制台一改就产生第二事实源;"改了配置"与"配置生效"之间还隔一次部署,没有机制把这个缺口暴露出来。
|
|
81
|
+
**解药**:**漂移检测基线**(`templates/scripts/drift-check.sh`)——对平台侧配置做 env 指纹(🔴只存 sha256 指纹不存值)+ 镜像 tag↔git tag 锚定,基线存 `bus-baseline.json`;bus-check 每次开工自动比对,红字报「+新增 / -删除 / ≠值变 / git 无此 tag」;改配置或部署后跑 `--update-baseline`,把"确认已生效"变成显式动作。
|
|
82
|
+
|
|
83
|
+
## 14. UI 元注释复发(模型爱给"做的人"写字)
|
|
84
|
+
|
|
85
|
+
**症状**:新看板上线,用户在界面上抓到 6 处口径脚注/"数据来源:xxx"/示意标记之类**给开发者看的解释性文字**,返工清除;这类问题若只做一次性整改,下一个新页面必再长出来——模型出 UI 天然爱自证。
|
|
86
|
+
**根因**:模型倾向把实现解释、数据口径、调试提示写进可见界面(训练里"解释自己"是美德,产品里是噪音);流程只在用户抓到时修一次,没有常设关卡。
|
|
87
|
+
**解药**:升格为常设红线(§1.5「界面零元注释」)+ 写进 reviewer 审查清单(第 6 条,发现通常判 P1),每次上线核查门自动查;确需解释的信息走设计稿定的 hover/帮助入口。通用原则:**用户对同类问题给过一次纠正,第二次就不该靠用户抓——把"修一次"升格为"每次必查的关卡"(红线/清单/hook),让流程替人记住偏好。**
|
|
88
|
+
|
|
89
|
+
## 15. 上下文载体绑死单一厂商(且与标准名撞脸)
|
|
90
|
+
|
|
91
|
+
**症状**:装载入口只提供 `CLAUDE.md`,于是用 Codex CLI / Gemini CLI / Aider / Zed 开的会话**根本装载不到总线规则**——开工护栏(规则④)、状态分写(规则⑦)、红线在那些会话里静默失效,且失效时没有任何报错,人以为规则在跑。另一半症状在命名:根目录 `Agent.md`(全栈总图,按需读)与各子仓 `AGENTS.md`(开放标准,自动装载)仅差一个 S、语义相反,人和 AI 反复误判前者也会被自动装载。
|
|
92
|
+
|
|
93
|
+
**根因**:把某个厂商的装载约定当成物理常量写进方法论(原 SKILL §3 原话:「`CLAUDE.md` 是工具装载约定,永远在项目根」),方法论因此绑死单一工具——而本方法论的核心场景恰恰是**并行多会话**,用户极可能一个会话用 A 工具、另一个用 B 工具,厂商锁在这里的杀伤面比单会话场景大一个量级。命名那半则是新造文件名时没去避让既有标准的近似名。
|
|
94
|
+
|
|
95
|
+
**解药**:装载入口一律用开放标准 **`AGENTS.md`**——层叠语义由标准定义(向上收集沿途所有 `AGENTS.md` 合并、就近优先),「根写全局 / 子仓写局部」是白捡的,不用自己发明。厂商专属文件只留**一行指针**指过去,🔴 绝不复制内容(复制 = 自造第 1 条 SSOT 腐烂),🔴 也不用符号链接(Windows 上 git 默认 `core.symlinks=false`,clone 出来静默退化成内容是路径字符串的普通文件,装载照样失效且照样无声)。全栈总图改名 `ARCHITECTURE.md`,与标准名彻底拉开。**并明确拒绝** gitignore 的本地覆盖文件(`AGENTS.override.md` 之类):这份文件装的是红线与护栏,允许一份不进 git 的覆盖 = 给绕过护栏开后门,reviewer 与 pre-commit 都看不见它。通用原则:**方法论里凡是「某工具的约定」,都必须能被替换掉;替换不掉的地方就是厂商锁,迟早在换工具那天要债。**
|
|
96
|
+
|
|
97
|
+
## 16. 核查门按小任务重复全审,防线反过来吞掉交付
|
|
98
|
+
|
|
99
|
+
**症状**:一个阶段被拆成规格、契约、键空间、设计输入、批准回写等多个小任务,每个任务收尾都生成一份完整 reviewer 报告,整改后再全文复核,合并前和阶段门又重新全审。同一条语义链被核三四次;报告体量开始超过真正交付物,主会话的大量时间花在复制 P0/P1/P2 原文、回写 status 和解释「本次通过不代表下一门通过」。
|
|
100
|
+
|
|
101
|
+
**根因**:把三种本应分开的东西揉成一个开关:① 每次提交都该跑的机器检查;② 实现中突发的高风险语义 delta;③ 里程碑候选的完整四方一致性核查。再用「任务结束」「文件数」而不是**风险状态是否变化 / 候选 hash 是否变化**做触发条件,任务拆得越细,审查次数反而越多;P2 也被当成重新跑全流程的理由。流程于是优化了审查产量,没有优化风险发现率。
|
|
102
|
+
|
|
103
|
+
**解药**:规则⑥改成「机器闸常驻 + 高风险 delta 定向核 + 里程碑候选一次全核」。gitleaks/bus-check 等轻量机器闸每次提交照跑;受影响测试按变更批次跑,里程碑候选跑全量并留证据。只有冻结契约对外语义、鉴权/租户/Secret/fail-closed、持久化键空间或不可逆副作用变化才立即核 `base..candidate`;完整 reviewer 绑定精确候选 hash 集,同一 hash 与同一份绿证据在合并前直接复用。P0/P1 阻塞,P2 默认挂账;首轮问题原文只存一次,后续用 finding closure 表收口。实现期新自由度集中记在同一份「实现语义清单」,到里程碑一次消费。通用原则:**核查强度跟风险变化走,不跟任务数和文档行数走;写者≠审者保留,重复全审不是独立性。**
|
|
104
|
+
|
|
105
|
+
## 17. 追踪项被当成任务边界,人不断说“继续”和“批准”
|
|
106
|
+
|
|
107
|
+
**症状**:需求被拆成十几个可追踪条目后,会话每交一份文档、一个 commit、一次 reviewer 结论或一条 status 就结束,把接力棒交还给人;同一目标内明明还有安全工作,却要人反复说“继续”。另一边,十几个验收条件被原样呈现成十几个审批项,用户分轮回答后永久台账再留下“3/14、11/14、14/14”多条部分进度。形式上每一步都可审计,项目吞吐却被人工调度和确认请求吃掉。
|
|
108
|
+
|
|
109
|
+
**根因**:把三种粒度混成一种:需求/验收项是**追踪粒度**,工作包是**执行粒度**,Gate/真实取舍是**审批粒度**。流程没有显式 `objective / in_scope / terminal_condition`,agent 就把“一个产物完成”当终止条件;又没有区分冻结前可逆草案与冻结后语义,于是任何草案选择都被当成必须立即人批。状态日志和决策台账反过来奖励“多收尾、多问、多记”,却不奖励用户级结果。
|
|
110
|
+
|
|
111
|
+
**解药**:每轮只认领一个用户级工作包,通常覆盖多个 ID / 文档 / commit;范围内仍有安全可逆工作就继续,子产物只报中间进展。任务只因目标带证据完成、真实人类阻塞或明确检查点而结束。审批分 `STOP_NOW / BATCH_AT_GATE / NO_APPROVAL`:越 Gate/扩范围/改冻结契约/不可逆动作/风险接受立即停;冻结前可逆取舍进看板决策收件箱,到门前默认一次问 2–5 个(确实只有 1 个就单项);事实、推导约束、status/归档/P2 自主做。永久台账只记收敛后的真实决策包,status 只在工作包/里程碑/真实阻塞更新。通用原则:**细粒度用来追踪,不是用来制造更多任务终点和人批门。**
|
|
112
|
+
|
|
113
|
+
## 18. 未稳定候选过早送审,一个 reviewer 变成连续状态风暴
|
|
114
|
+
|
|
115
|
+
**症状**:写者刚提交首个功能切片就宣称“候选已固定”并启动 reviewer;reviewer 还在跑,写者自己的边界自查又连续发现幂等恢复、429 退避、demo 模式与测试隔离等问题,几分钟内 candidate hash 连变数次、工作树重新变脏。主会话把这些自发现修补称为“只核 delta”,reviewer 状态卡与整改播报不断刷屏;旧候选尚无完整结论,却已经长出一条 milestone → delta → closure 的伪链。
|
|
116
|
+
|
|
117
|
+
**根因**:规则只说“稳定候选”,没有把稳定变成可核前置;又把“高风险领域”误写成“实现中一碰就立即派 reviewer”。于是 reviewer 代替了写者应先完成的自查,而 `risk-delta` 被滥用于首次 milestone 前的普通候选收敛。subagent 还分批播报 findings/进度,视觉上一个 reviewer 像开了多轮审查。
|
|
118
|
+
|
|
119
|
+
**解药**:引入 **review-ready** 四项硬前置:工作包实现与写者自查完成;所有候选仓 `HEAD=candidate` 且工作树干净;受影响/全量 L3 与真渲染证据绿;无已知待修或计划改 hash。首次 milestone 前的鉴权/Secret/fail-closed 等自发现问题先集中进实现语义清单并自行收敛,不送审;只有修改已冻结对外契约或不可逆副作用才提前 `STOP_NOW + risk-delta`。每工作包每 Gate 默认一次 milestone,P0/P1 合并修完后一次 closure,P2 不复核。reviewer 单次静默核完再返回;返回前 candidate 改变就标 `SUPERSEDED` 并停止,不得把连续修补包装成 delta 链。通用原则:**独立审查应消费稳定候选,不能成为写者边实现边找问题的后台 lint。**
|
package/package.json
CHANGED
|
@@ -1,18 +1,59 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@haiyangbg/buildbeat",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
5
|
-
"
|
|
3
|
+
"version": "1.20.0",
|
|
4
|
+
"description": "BuildBeat: a Git-based, human-gated engineering delivery protocol for humans and AI sessions.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"buildbeat": "bin/buildbeat.js",
|
|
8
|
+
"solobaton": "bin/solobaton.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin/",
|
|
12
|
+
"docs/",
|
|
13
|
+
"example/",
|
|
14
|
+
"src/",
|
|
15
|
+
"templates/",
|
|
16
|
+
"CHANGELOG.md",
|
|
17
|
+
"LICENSE",
|
|
18
|
+
"README.md",
|
|
19
|
+
"README.en.md",
|
|
20
|
+
"SKILL.md",
|
|
21
|
+
"lessons.md"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"check:docs": "bash tests/check-docs.sh",
|
|
25
|
+
"test": "node --test tests/*.test.js",
|
|
26
|
+
"test:scripts": "bash tests/test-scripts.sh",
|
|
27
|
+
"test:skill-only": "bash tests/skill-only.test.sh",
|
|
28
|
+
"test:plugin": "bash tests/plugin-marketplace.test.sh",
|
|
29
|
+
"pack:check": "npm pack --dry-run",
|
|
30
|
+
"prepublishOnly": "npm test && npm run test:scripts && npm run test:skill-only && npm run test:plugin && npm run check:docs && npm run pack:check"
|
|
31
|
+
},
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=20"
|
|
34
|
+
},
|
|
6
35
|
"repository": {
|
|
7
36
|
"type": "git",
|
|
8
37
|
"url": "git+https://github.com/HaiYangBG1/BuildBeat.git"
|
|
9
38
|
},
|
|
10
39
|
"homepage": "https://github.com/HaiYangBG1/BuildBeat#readme",
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
|
|
40
|
+
"bugs": {
|
|
41
|
+
"url": "https://github.com/HaiYangBG1/BuildBeat/issues"
|
|
42
|
+
},
|
|
14
43
|
"publishConfig": {
|
|
15
44
|
"access": "public",
|
|
16
45
|
"registry": "https://registry.npmjs.org/"
|
|
17
|
-
}
|
|
46
|
+
},
|
|
47
|
+
"keywords": [
|
|
48
|
+
"ai-coding",
|
|
49
|
+
"file-first",
|
|
50
|
+
"human-in-the-loop",
|
|
51
|
+
"scaffold",
|
|
52
|
+
"software-delivery",
|
|
53
|
+
"delivery-protocol",
|
|
54
|
+
"ai-sessions",
|
|
55
|
+
"git-workflow"
|
|
56
|
+
],
|
|
57
|
+
"author": "HaiYangBG",
|
|
58
|
+
"license": "MIT"
|
|
18
59
|
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { createInterface } from "node:readline/promises";
|
|
2
|
+
|
|
3
|
+
import { CLI_VERSION, OUTPUT_SCHEMA_VERSION } from "./constants.js";
|
|
4
|
+
import { formatDoctor, runDoctor } from "./doctor.js";
|
|
5
|
+
import { buildPlan, formatPlan } from "./planner.js";
|
|
6
|
+
import { applyUpgrade, buildUpgradePlan, formatUpgradePlan } from "./upgrader.js";
|
|
7
|
+
import { applyScaffold, WriteError } from "./writer.js";
|
|
8
|
+
|
|
9
|
+
const HELP = `BuildBeat CLI v${CLI_VERSION} (Wave 2 source candidate)
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
buildbeat doctor [path] [--json]
|
|
13
|
+
buildbeat init [path] [--dry-run] [--layout default|compact] [--json] [--yes]
|
|
14
|
+
buildbeat adopt [path] [--dry-run] [--layout default|compact] [--json] [--yes]
|
|
15
|
+
buildbeat upgrade [path] [--dry-run] [--json] [--force] [--major]
|
|
16
|
+
buildbeat version
|
|
17
|
+
|
|
18
|
+
init/adopt write only after a blocker-free plan and confirmation; --yes skips
|
|
19
|
+
only that prompt. upgrade is schema-2-only and requires a clean target-root Git
|
|
20
|
+
worktree. diff and uninstall remain unavailable.
|
|
21
|
+
|
|
22
|
+
Legacy compatibility: the published npm package and solobaton executable alias
|
|
23
|
+
remain available during the BuildBeat namespace migration.`;
|
|
24
|
+
|
|
25
|
+
class UsageError extends Error {
|
|
26
|
+
constructor(message) {
|
|
27
|
+
super(message);
|
|
28
|
+
this.name = "UsageError";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function parse(args) {
|
|
33
|
+
if (args.length === 0) {
|
|
34
|
+
return {
|
|
35
|
+
command: "help",
|
|
36
|
+
target: ".",
|
|
37
|
+
json: false,
|
|
38
|
+
dryRun: false,
|
|
39
|
+
layout: null,
|
|
40
|
+
yes: false,
|
|
41
|
+
force: false,
|
|
42
|
+
major: false,
|
|
43
|
+
targetProvided: false,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const options = {
|
|
47
|
+
command: args[0],
|
|
48
|
+
target: ".",
|
|
49
|
+
json: false,
|
|
50
|
+
dryRun: false,
|
|
51
|
+
yes: false,
|
|
52
|
+
force: false,
|
|
53
|
+
major: false,
|
|
54
|
+
layout: null,
|
|
55
|
+
help: false,
|
|
56
|
+
targetProvided: false,
|
|
57
|
+
};
|
|
58
|
+
let targetSeen = false;
|
|
59
|
+
for (let index = 1; index < args.length; index += 1) {
|
|
60
|
+
const token = args[index];
|
|
61
|
+
if (token === "--json") {
|
|
62
|
+
options.json = true;
|
|
63
|
+
} else if (token === "--dry-run") {
|
|
64
|
+
options.dryRun = true;
|
|
65
|
+
} else if (token === "--yes") {
|
|
66
|
+
options.yes = true;
|
|
67
|
+
} else if (token === "--force") {
|
|
68
|
+
options.force = true;
|
|
69
|
+
} else if (token === "--major") {
|
|
70
|
+
options.major = true;
|
|
71
|
+
} else if (token === "--help" || token === "-h") {
|
|
72
|
+
options.help = true;
|
|
73
|
+
} else if (token === "--layout") {
|
|
74
|
+
index += 1;
|
|
75
|
+
if (index >= args.length) {
|
|
76
|
+
throw new UsageError("--layout requires default or compact.");
|
|
77
|
+
}
|
|
78
|
+
options.layout = args[index];
|
|
79
|
+
} else if (token.startsWith("--layout=")) {
|
|
80
|
+
options.layout = token.slice("--layout=".length);
|
|
81
|
+
} else if (token.startsWith("-")) {
|
|
82
|
+
throw new UsageError(`Unknown option: ${token}`);
|
|
83
|
+
} else if (!targetSeen) {
|
|
84
|
+
options.target = token;
|
|
85
|
+
targetSeen = true;
|
|
86
|
+
options.targetProvided = true;
|
|
87
|
+
} else {
|
|
88
|
+
throw new UsageError(`Unexpected argument: ${token}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (options.layout && !["default", "compact"].includes(options.layout)) {
|
|
92
|
+
throw new UsageError("--layout must be default or compact.");
|
|
93
|
+
}
|
|
94
|
+
return options;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function confirmPlan(io, plan) {
|
|
98
|
+
if (typeof io.confirm === "function") {
|
|
99
|
+
return Boolean(await io.confirm(plan));
|
|
100
|
+
}
|
|
101
|
+
if (!io.stdin?.isTTY || !io.stderr) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
const readline = createInterface({ input: io.stdin, output: io.stderr });
|
|
105
|
+
try {
|
|
106
|
+
const answer = await readline.question("Apply this BuildBeat plan? [y/N] ");
|
|
107
|
+
return /^(?:y|yes)$/i.test(answer.trim());
|
|
108
|
+
} finally {
|
|
109
|
+
readline.close();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function outputJson(io, value) {
|
|
114
|
+
io.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function commandLabel(value) {
|
|
118
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
return value
|
|
122
|
+
.replace(/[\u0000-\u001f\u007f]+/g, " ")
|
|
123
|
+
.replace(/\s+/g, " ")
|
|
124
|
+
.trim()
|
|
125
|
+
.slice(0, 80) || null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function writeError(io, message, { json = false, code = "usage", command = null } = {}) {
|
|
129
|
+
if (json) {
|
|
130
|
+
outputJson(io, {
|
|
131
|
+
schemaVersion: OUTPUT_SCHEMA_VERSION,
|
|
132
|
+
command: commandLabel(command),
|
|
133
|
+
cliVersion: CLI_VERSION,
|
|
134
|
+
ok: false,
|
|
135
|
+
error: { code, message },
|
|
136
|
+
});
|
|
137
|
+
} else {
|
|
138
|
+
io.stderr.write(`Error: ${message}\n`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export async function run(
|
|
143
|
+
args,
|
|
144
|
+
io = { stdin: process.stdin, stdout: process.stdout, stderr: process.stderr },
|
|
145
|
+
) {
|
|
146
|
+
let options;
|
|
147
|
+
try {
|
|
148
|
+
options = parse(args);
|
|
149
|
+
} catch (error) {
|
|
150
|
+
writeError(io, error.message, {
|
|
151
|
+
json: args.includes("--json"),
|
|
152
|
+
command: args[0] || null,
|
|
153
|
+
});
|
|
154
|
+
return 2;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (options.help || options.command === "help" || options.command === "--help") {
|
|
158
|
+
io.stdout.write(`${HELP}\n`);
|
|
159
|
+
return 0;
|
|
160
|
+
}
|
|
161
|
+
if (options.command === "version" || options.command === "--version" || options.command === "-v") {
|
|
162
|
+
if (
|
|
163
|
+
options.targetProvided ||
|
|
164
|
+
options.json ||
|
|
165
|
+
options.dryRun ||
|
|
166
|
+
options.yes ||
|
|
167
|
+
options.force ||
|
|
168
|
+
options.major ||
|
|
169
|
+
options.layout
|
|
170
|
+
) {
|
|
171
|
+
writeError(io, "version does not accept a path or options.", {
|
|
172
|
+
json: options.json,
|
|
173
|
+
command: options.command,
|
|
174
|
+
});
|
|
175
|
+
return 2;
|
|
176
|
+
}
|
|
177
|
+
io.stdout.write(`${CLI_VERSION}\n`);
|
|
178
|
+
return 0;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
if (options.command === "doctor") {
|
|
183
|
+
if (options.layout || options.dryRun || options.yes || options.force || options.major) {
|
|
184
|
+
throw new UsageError("doctor accepts only [path] and --json.");
|
|
185
|
+
}
|
|
186
|
+
const report = runDoctor(options.target);
|
|
187
|
+
if (options.json) {
|
|
188
|
+
outputJson(io, report);
|
|
189
|
+
} else {
|
|
190
|
+
io.stdout.write(`${formatDoctor(report)}\n`);
|
|
191
|
+
}
|
|
192
|
+
return report.ok ? 0 : 1;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (options.command === "init" || options.command === "adopt") {
|
|
196
|
+
if (options.force || options.major) {
|
|
197
|
+
throw new UsageError(`${options.command} does not accept --force or --major.`);
|
|
198
|
+
}
|
|
199
|
+
if (options.dryRun && options.yes) {
|
|
200
|
+
throw new UsageError("--yes cannot be combined with --dry-run.");
|
|
201
|
+
}
|
|
202
|
+
const layout = options.layout || (options.command === "adopt" ? "compact" : "default");
|
|
203
|
+
const now = new Date();
|
|
204
|
+
const plan = buildPlan({
|
|
205
|
+
mode: options.command,
|
|
206
|
+
target: options.target,
|
|
207
|
+
layout,
|
|
208
|
+
preview: options.dryRun,
|
|
209
|
+
now,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
if (options.dryRun || !plan.ready) {
|
|
213
|
+
if (options.json) {
|
|
214
|
+
outputJson(io, plan);
|
|
215
|
+
} else {
|
|
216
|
+
io.stdout.write(`${formatPlan(plan)}\n`);
|
|
217
|
+
}
|
|
218
|
+
return plan.ready ? 0 : 1;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (!options.json) {
|
|
222
|
+
io.stdout.write(`${formatPlan(plan)}\n`);
|
|
223
|
+
} else if (options.yes || io.stdin?.isTTY) {
|
|
224
|
+
io.stderr.write(`${formatPlan(plan)}\n`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (!options.yes) {
|
|
228
|
+
const confirmed = await confirmPlan(io, plan);
|
|
229
|
+
if (confirmed === null) {
|
|
230
|
+
writeError(
|
|
231
|
+
io,
|
|
232
|
+
"Interactive confirmation is unavailable. Re-run with --yes only after reviewing --dry-run output.",
|
|
233
|
+
{
|
|
234
|
+
json: options.json,
|
|
235
|
+
code: "confirmation_required",
|
|
236
|
+
command: options.command,
|
|
237
|
+
},
|
|
238
|
+
);
|
|
239
|
+
return 2;
|
|
240
|
+
}
|
|
241
|
+
if (!confirmed) {
|
|
242
|
+
if (options.json) {
|
|
243
|
+
outputJson(io, { ...plan, cancelled: true });
|
|
244
|
+
} else {
|
|
245
|
+
io.stdout.write("\nCancelled. No files changed.\n");
|
|
246
|
+
}
|
|
247
|
+
return 0;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const result = applyScaffold(plan, { now });
|
|
252
|
+
if (options.json) {
|
|
253
|
+
outputJson(io, result);
|
|
254
|
+
} else {
|
|
255
|
+
io.stdout.write(`\n${formatPlan(result)}\n`);
|
|
256
|
+
}
|
|
257
|
+
return 0;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (options.command === "upgrade") {
|
|
261
|
+
if (options.layout || options.yes) {
|
|
262
|
+
throw new UsageError("upgrade accepts only [path], --dry-run, --json, --force, and --major.");
|
|
263
|
+
}
|
|
264
|
+
const now = new Date();
|
|
265
|
+
const plan = buildUpgradePlan({
|
|
266
|
+
target: options.target,
|
|
267
|
+
preview: options.dryRun,
|
|
268
|
+
force: options.force,
|
|
269
|
+
major: options.major,
|
|
270
|
+
now,
|
|
271
|
+
});
|
|
272
|
+
if (options.dryRun || !plan.ready || plan.upToDate) {
|
|
273
|
+
if (options.json) {
|
|
274
|
+
outputJson(io, plan);
|
|
275
|
+
} else {
|
|
276
|
+
io.stdout.write(`${formatUpgradePlan(plan)}\n`);
|
|
277
|
+
}
|
|
278
|
+
return plan.ready ? 0 : 1;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (options.json) {
|
|
282
|
+
io.stderr.write(`${formatUpgradePlan(plan)}\n`);
|
|
283
|
+
} else {
|
|
284
|
+
io.stdout.write(`${formatUpgradePlan(plan)}\n`);
|
|
285
|
+
}
|
|
286
|
+
const result = applyUpgrade(plan, { now });
|
|
287
|
+
if (options.json) {
|
|
288
|
+
outputJson(io, result);
|
|
289
|
+
} else {
|
|
290
|
+
io.stdout.write(`\n${formatUpgradePlan(result)}\n`);
|
|
291
|
+
}
|
|
292
|
+
return result.doctor?.ok === false ? 1 : 0;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (["uninstall", "diff"].includes(options.command)) {
|
|
296
|
+
writeError(
|
|
297
|
+
io,
|
|
298
|
+
`${options.command} is reserved by the lifecycle contract but is not enabled in this CLI build.`,
|
|
299
|
+
{
|
|
300
|
+
json: options.json,
|
|
301
|
+
code: "command_not_available",
|
|
302
|
+
command: options.command,
|
|
303
|
+
},
|
|
304
|
+
);
|
|
305
|
+
return 2;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
throw new UsageError(`Unknown command: ${options.command}`);
|
|
309
|
+
} catch (error) {
|
|
310
|
+
if (error instanceof UsageError) {
|
|
311
|
+
writeError(io, error.message, { json: options.json, command: options.command });
|
|
312
|
+
return 2;
|
|
313
|
+
}
|
|
314
|
+
writeError(io, error.message, {
|
|
315
|
+
json: options.json,
|
|
316
|
+
code: error instanceof WriteError ? error.code : "runtime_error",
|
|
317
|
+
command: options.command,
|
|
318
|
+
});
|
|
319
|
+
return 1;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export { HELP };
|