Merge authoritative main into the task branch
Why: include the current integration baseline before verification. Evidence: fetched origin/main=$(git rev-parse origin/main). Impact: integrate upstream changes without rewriting task history. The backend gitlink stays at 89acd16, which already contains the pinned identity CLI acabb85. Refs: task-sync Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -3,7 +3,9 @@
|
||||
本仓开发遵循 Development-Guidelines(见 `deps/development-guidelines`,gitlink 固定版本):
|
||||
核心规则与术语见其 `core/`,按任务触发阅读 `standards/` 专题;通用交付依据为 `workflows/delivery.md`。
|
||||
本项目的需求、设计试验、实施调整及交付**必读 [policy/delivery.md](policy/delivery.md)**:
|
||||
默认逐阶段人工评审,阶段内迭代、按影响回退;仅在用户明确授权范围内连续推进。
|
||||
需求、方案、实施与验收默认逐阶段人工评审,阶段内迭代、按影响回退;代码 Review 与 PR
|
||||
合入由智能体负责,解决冲突且当前 HEAD 的 CI 通过后执行合入,无需另请人工操作。
|
||||
具体评审、版本及授权条件见 policy/delivery.md;其他阶段仅在用户明确授权范围内连续推进。
|
||||
|
||||
## UI 设计门禁(强制)
|
||||
|
||||
@@ -42,7 +44,8 @@
|
||||
共享主检出或推送另一任务。主检出 pull/merge 的 post-merge 钩子仅提示待办,不自动写入或推送。
|
||||
`make task-close` 会提交、推送并可能创建 PR,须先核对闭合范围和对应授权。闭合在主检出运行→建
|
||||
`feature/card-close-*` 分支+PR,任务分支内加 `FOLD=--fold` 随当前 PR 合入。
|
||||
precheck 的 `[cards]` 行报告欠账,验收前应清零;闭合 PR 由所有者合入。
|
||||
precheck 的 `[cards]` 行报告欠账,验收前应清零;闭合 PR 同样由智能体按
|
||||
policy/delivery.md 完成 Review、冲突处置及当前 HEAD 的 CI 核验后合入。
|
||||
|
||||
## 证据与验证门禁(强制)
|
||||
|
||||
|
||||
+2
-2
@@ -16,7 +16,7 @@
|
||||
| standards/verification.md | 验收 | 本地 make test-governance + 组合测试;当前证据与历史 scratch 记录分开 |
|
||||
| standards/multi-repo.md | 跨仓/gitlink | manual;受控历史迁移通道已于 2026-09-30 行使一次(documents/records/2026-09-30-history-migration.md) |
|
||||
| 工作区回收 | 交付终点 | advisory(task-finish);负例:合入前 finish 拒绝输出 |
|
||||
| policy/delivery.md(项目收紧) | 需求、长期方向、试验、实施迭代及交付 | manual:人工评审和有范围的连续推进授权;方向主记录及任务收尾建议;任务卡提供记录,post-merge 默认只提示,行为回归见 tests/test_task.py |
|
||||
| policy/delivery.md(项目收紧) | 需求、长期方向、试验、实施迭代及交付 | manual(人工/Agent 执行):前序人工决策与有范围的连续推进授权;智能体完成 Review、冲突处置和当前 HEAD 的 CI 核验后合入;任务卡提供记录,post-merge 默认只提示,行为回归见 tests/test_task.py |
|
||||
| standards/tool-adaptation.md(draft.8) | 为工具创建规则/钩子实例 | 本组合 CLI/git 级实例已登记:policy/tool-adaptation-matrix.md(A1–A7 逐行定级+正负例证据) |
|
||||
| 同上(zcode 实例) | zcode 客户端写路径 | policy/tool-adaptation-zcode.md:PreToolUse 写守卫;脚本负例与客户端接线分开,客户端 pending-live |
|
||||
| core/baseline.md「规范层级」+ documentation.md「记忆分层/机制化」(draft.9) | 层级效力、记忆权威、经验机制化 | manual:本仓 policy/ 仅收紧不放宽;记忆四层按条文执行(任务卡=任务记忆,documents/records=项目记忆,Dev 仓 AGENTS.md 指针已就位) |
|
||||
@@ -26,7 +26,7 @@
|
||||
| 原规则/章节 | 项目采用方式 | 原因与风险 | 负责人/复查条件 |
|
||||
|---|---|---|---|
|
||||
| baseline 硬边界 1(授权范围) | 高后果双钥匙:改写历史/解除分支保护/删归档/发布——Agent 只准备,用户执行(AGENTS.md 条文) | 单人+Agent 场景下消除单点;风险=用户不在场时高后果操作排队 | weichen;首次触发时复查 |
|
||||
| delivery.md §2、design-verification.md 的轻量流程默认做法 | 按 [policy/delivery.md](delivery.md) 默认逐阶段人工评审;小任务简化篇幅,阶段内可迭代;明确授权时在指定范围连续推进 | 保留需求、方案和实施决策权;按变化影响重审,避免每条命令审批及全流程重复返工 | weichen;阶段等待或试验边界影响交付时复查 |
|
||||
| delivery.md §2、design-verification.md 的轻量流程默认做法 | 按 [policy/delivery.md](delivery.md) 保留需求、方案、实施与验收的人工节点;代码 Review 与 PR 合入交由智能体,冲突解决且当前 HEAD 的 CI 通过后合入;小任务简化篇幅,阶段内可迭代 | 2026-10-12 用户明确要求智能体 Review 后合入,合入前解决冲突且 CI 通过;其他人工决策与高后果双钥匙边界保留 | weichen;Review、CI 或平台准入条件变化时复查 |
|
||||
|
||||
- 历史记录(2026-09-30)报告 11 个组合仓 main 禁直推、拒绝陈旧候选;当前平台能力须独立重验,本地脚本测试不替代远端证据。
|
||||
- 当前规范内容以 gitlink 及其 VERSION 为准。治理可靠性修复的命令与证据约定见 [开发门禁](governance.md),本地与客户端/平台能力分别登记;本轮不改变分支保护、发布授权或通用库版本。
|
||||
|
||||
+47
-11
@@ -2,21 +2,24 @@
|
||||
|
||||
适用:本项目的需求、修复、规范修改和其他交付任务。依据固定版本的
|
||||
[通用交付流程](../deps/development-guidelines/workflows/delivery.md),本项目收紧为
|
||||
**默认逐阶段人工评审;阶段内迭代,按影响回退,按增量交付**。
|
||||
**需求、方案、实施与验收默认逐阶段人工评审;代码 Review 与 PR 合入由智能体完成;
|
||||
阶段内迭代,按影响回退,按增量交付**。
|
||||
本文件是阶段与评审的权威表述;启动、同步、命令证据和回收操作见
|
||||
[开发门禁](governance.md)。规则的执行能力见 [能力矩阵](tool-adaptation-matrix.md)。
|
||||
|
||||
## 授权与阶段评审
|
||||
|
||||
任务卡默认记为 `manual`。完成当前阶段产物并取得人工评审结论后,才进入下一阶段。
|
||||
任务卡默认记为 `manual`。阶段 1–7 完成当前阶段产物并取得人工评审结论后,才进入下一阶段;
|
||||
阶段 8–9 由智能体执行 Review 和合入,条件见下文「智能体 Review 与 PR 合入」。
|
||||
评审记录包含阶段、对象版本、评审人、时间、结论、意见及可追溯依据;结论使用
|
||||
未评审、通过、需修改、拒绝、不适用。人工结论与命令执行证据分开记录;Agent 自审、
|
||||
测试通过、工具写入或用户沉默均不能代替人工批准,也不能由作者代签独立审核。
|
||||
测试通过、工具写入或用户沉默均不能代替阶段 1–7 的人工批准,也不能由作者代签独立审核。
|
||||
|
||||
只有用户明确要求自动或连续推进时,才将对应范围记为 `authorized-continuous`,记录
|
||||
授权来源、覆盖阶段、允许动作、交付终点、资源约束和停止条件。未覆盖阶段仍逐阶段评审。
|
||||
授权来源、覆盖阶段、允许动作、交付终点、资源约束和停止条件。未覆盖的人工节点仍逐阶段
|
||||
人工评审;智能体 Review/合入按阶段 8–9 的职责与条件执行。
|
||||
授权连续推进不免除需求分析、试验判定、测试、代码 Review、证据和平台准入;
|
||||
既有独立审核要求及高后果操作双钥匙仍有效。单独的合入授权不补齐尚不存在的前序产物。
|
||||
既有独立审核要求及高后果操作双钥匙仍有效。智能体合入职责不补齐尚不存在的前序产物。
|
||||
|
||||
阶段内已授权的调查、编辑、自检、缺陷修复和等价实现可以持续进行,无需逐命令批准。
|
||||
需求拆解、评估、需求表可以共用一份任务卡,但分别保留阶段结论。小任务缩短篇幅;
|
||||
@@ -34,11 +37,12 @@
|
||||
| 5 实施计划 | 关联需求的工作项、依赖顺序、责任人、文件或模块范围、测试方法与环境、完成条件及停止条件 | 人工批准当前实施和测试计划;远期工作可保留粗粒度 |
|
||||
| 6 实施与自检 | 可审查变更、局部测试、计划偏差与迭代记录 | 人工确认当前增量实现产物,必要设计调整已重新获批 |
|
||||
| 7 正式测试与验收 | 需求到证据映射;正常、异常、边界、回归结果及缺项 | 人工审阅当前候选的验证结果;正式验收前同步并固定被测对象 |
|
||||
| 8 代码 Review | 需求符合性、正确性、维护性、测试充分性及问题闭合记录 | 完成必要重放、整改、补测和复审,由有权评审者放行当前版本 |
|
||||
| 9 合入 | 最新 base/head、必要检查及评审、冲突处置、合入授权 | 合入前再次同步核验;经 PR 合入后取得真实合入结果 |
|
||||
| 8 代码 Review | 智能体审阅者、对象版本、需求符合性、正确性、维护性、测试充分性及问题闭合记录 | 智能体完成必要重放、整改、补测和复审;当前版本 Review 通过,平台要求的独立批准已满足 |
|
||||
| 9 合入 | 最新 base/head、当前 HEAD 的 CI、Review、冲突处置和合入结果 | 智能体再次同步,确认冲突已解决、Review 有效且 CI 全部通过,经 PR 执行合入并核验真实结果;无需另请人工合入 |
|
||||
| 10 合入核验与收口 | 实际合入内容及组件版本、验证依据、成果保存位置、写者状态、清理清单、任务归档安排及适用规划的进展与下一步建议 | 清理前取得人工批准或对应连续推进授权;安全回收后核对结果、闭合任务卡 |
|
||||
|
||||
表中的人工节点仅在明确的连续推进授权范围内按授权执行;阶段产物及验证条件保留。
|
||||
阶段 1–7 和阶段 10 的人工节点仅在明确的连续推进授权范围内按授权执行;
|
||||
阶段 8–9 按智能体职责执行;各阶段产物及验证条件保留。
|
||||
正式测试和最终 Review 的顺序不限制设计阶段试验、实施期测试或提前审阅。
|
||||
文本合并无冲突不证明组合兼容;合入后仍核验真实内容与被测候选是否一致。
|
||||
|
||||
@@ -122,7 +126,7 @@
|
||||
|
||||
| 变化 | 执行动作与评审边界 |
|
||||
|---|---|
|
||||
| 已批准范围内的内部组织、等价实现、局部缺陷修正 | 当前阶段继续调整、自测并记录;阶段出口统一人工评审 |
|
||||
| 已批准范围内的内部组织、等价实现、局部缺陷修正 | 当前阶段继续调整、自测并记录;按阶段表由人工或智能体完成出口评审 |
|
||||
| 技术路线、模块职责、公开契约、数据或权限设计、外部影响,或计划的关键依赖、交付顺序和资源约束变化 | 暂停受影响工作,修订方案或计划,经人工重审后继续;已有明确授权覆盖该类调整时按其边界执行 |
|
||||
| 目标、范围、优先级或验收条件变化 | 返回需求评估和需求表,取得对应批准后更新下游方案、计划和验证 |
|
||||
| 影响不明 | 先调查;仍无法判定时提交人工决定,不自行归入细节调整 |
|
||||
@@ -137,13 +141,45 @@
|
||||
|
||||
## 合入、清理与恢复
|
||||
|
||||
合入与清理分别核对授权及条件。仅回收本任务登记工作区,按开发门禁核对当前权威目标、
|
||||
### 智能体 Review 与 PR 合入
|
||||
|
||||
已获实施及交付授权的任务,代码 Review 和 PR 合入默认由智能体负责,包括任务卡闭合 PR;
|
||||
本条即该范围内的合入执行授权,条件满足后直接合入,无需用户再次批准或亲手点击。
|
||||
用户明确限定为只读、方案、本地修改、草稿 PR 或禁止合入时,以该任务边界为准。
|
||||
前序阶段的必要人工结论仍须齐备;合入职责不扩大需求、业务实施、清理或高后果操作授权。
|
||||
|
||||
智能体按以下顺序执行:
|
||||
|
||||
1. 完成 Review,记录审阅者及其身份、base/head、结论、问题处置和必要重放依据。
|
||||
要求独立审阅时由未参与本次实现的审阅智能体完成;平台要求的非作者批准须由符合
|
||||
平台身份及权限条件的账号给出,切换智能体角色不等于非作者账号批准。
|
||||
2. 合入前以 evidence 记录 `make task-sync`,将最新权威目标纳入任务分支,解决全部
|
||||
文本及组件冲突,检查组合兼容性。无法在已批准需求和方案内判断冲突取舍时,暂停合入
|
||||
并提交具体决策;无冲突或仅文本合并成功不能代替语义兼容核验。
|
||||
3. 同步、冲突解决或整改导致 head/base 变化时,重新判断 Review 与测试的有效性,完成
|
||||
受影响的复审和补测,推送新 HEAD,并等待该 HEAD 的 CI;旧版本绿色结果不能放行新版本。
|
||||
4. 核验当前待合入 HEAD 的全部必需 CI,包括本仓 CI 的测试、构建及适用的其他作业,
|
||||
记录完整 SHA、运行链接、执行身份和结果(GATE)。缺检查、排队、运行中、失败、取消、
|
||||
适用检查被跳过或来源无法核验时均不得合入;本地 SELF 测试不能替代 CI 通过。
|
||||
5. 再查最新 base/head、冲突状态、有效 Review、CI 及平台准入,满足后通过平台 PR 接口
|
||||
执行合入。最终推进须受平台陈旧候选保护、合并队列或等价的目标分支串行与版本核验保护;
|
||||
目标再前进时重新同步和核验,不绕过平台拒绝、不降低检查或批准要求。
|
||||
6. 查询真实合入结果,记录 PR、被核验 HEAD、合入 SHA、目标分支和内容/组件核对结果。
|
||||
超时或断连先查询 PR 及目标状态,再决定是否重试;合入与后续清理分别记录。
|
||||
|
||||
本流程由智能体执行,任务卡模板和本地提示不构成平台强制门禁。平台能力按
|
||||
[能力矩阵](tool-adaptation-matrix.md) 核验;分支保护变更仍由用户按双钥匙边界执行。
|
||||
|
||||
### 清理与恢复
|
||||
|
||||
清理另行核对授权及条件。仅回收本任务登记工作区,按开发门禁核对当前权威目标、
|
||||
父子仓成果、未跟踪和 ignored 文件、独有提交与引用、活动 Git 操作以及所有写者退出。
|
||||
无法证明成果已保存时保留现场;本地删除与远端分支删除分别报告。
|
||||
|
||||
清理前保存任务主记录与证据。执行回收时将命令输出用 evidence 的 `RECEIPT` 模式保存到
|
||||
待删目录之外;回收后的结果写入后续闭合记录,不能向已删除的任务卡补写收据。
|
||||
任务卡须回填真实主 PR 和完成状态并归档;闭合 PR 的创建、合入均依当前授权执行。
|
||||
任务卡须回填真实主 PR 和完成状态并归档;闭合 PR 的创建核对当前闭合授权,合入按上述
|
||||
智能体 Review 与 PR 合入流程执行。
|
||||
post-merge 默认只提示,不因 pull 或 merge 自动提交、推送或创建闭合 PR。
|
||||
|
||||
中断或交接时记录当前有效版本、批准范围、未决假设、最新试验结论、剩余事项及下一动作。
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
|
||||
本文件细化根 AGENTS.md 的启动、验证和回收规则。通用语义以固定的
|
||||
`deps/development-guidelines` 为准;门禁实现及回归用例随本仓版本管理。
|
||||
需求表、设计试验、迭代调整、阶段产物与人工评审按 [迭代交付](delivery.md) 执行。
|
||||
任务卡记录批准来源与对象版本;本地 evidence 仅记录命令,不能证明人工批准。
|
||||
需求表、设计试验、迭代调整、阶段产物、人工决策及智能体 Review/合入按
|
||||
[迭代交付](delivery.md) 执行。任务卡分别记录人工批准、智能体审阅者及对象版本;
|
||||
本地 evidence 仅记录 SELF 命令,不能证明人工批准或提升为 CI/独立审阅证据。
|
||||
|
||||
## 启动与恢复
|
||||
|
||||
@@ -42,6 +43,10 @@ make task-start NAME=feature/my-task PRECHECK_RECEIPT=/tmp/my-task-precheck.json
|
||||
GATE 引用受信平台的运行链接、执行身份、完整 SHA 和结果;REPLAY 引用独立审阅者的
|
||||
环境、命令、对象和结果。审核者核验这些来源,不能只读作者填入的标签。
|
||||
|
||||
PR 合入由智能体执行:先完成有效 Review,再同步并解决冲突,核验当前 HEAD 的全部
|
||||
必需 CI 通过及平台准入后合入;具体步骤与失败出口见 [迭代交付](delivery.md#智能体-review-与-pr-合入)。
|
||||
闭合 PR 使用同一流程,不以任务卡已回填或本地测试通过代替 CI。
|
||||
|
||||
收据中的摘要检测损坏,不是签名或身份凭据。保存的是完整脱敏输出及其 SHA-256;
|
||||
命令含敏感值时展示脱敏形式,并记录原命令摘要。执行前后对象分别记录;命令改变 HEAD
|
||||
时不把执行前 SHA 冒充执行后对象。不能读取执行后状态时明确记录缺项。
|
||||
|
||||
@@ -20,7 +20,8 @@
|
||||
4. 批准数暂不随此修复自动调整。若另行启用非作者批准,先确认有可用且符合权限要求的
|
||||
独立审核账号;不能把切换 Agent 角色当独立账号,也不能盲设阈值使单账号仓库无法交付。
|
||||
5. 在隔离的测试 PR 上验证:无检查、检查失败和陈旧候选均不可合入;合法且检查通过的
|
||||
PR 可进入所有者审核。记录 head/base、运行链接及实际拒绝结果,标 GATE/REPLAY。
|
||||
PR 可由智能体完成 Review 并按 [迭代交付](delivery.md#智能体-review-与-pr-合入) 执行合入。
|
||||
记录 head/base、运行链接及实际拒绝结果,标 GATE/REPLAY。
|
||||
|
||||
任一判据未满足时暂停合入并保留测试对象。配置调整失败由所有者对照保存的配置恢复;
|
||||
不把“配置 API 返回成功”写成“门禁已有效”。其他组合仓分别核验,不外推 Dev 的结果。
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
| A5 回收 | 本地命令检查;写者退出由 owner 确认 | 登记根目录、锁/操作状态、父子仓与唯一文件、合入核验 | `tests/test_task.py` |
|
||||
| A6 秘密 | 本地脱敏及 CI 获取边界;权限与宿主隔离另验 | 命令/输出/摘要脱敏,凭据不持久写 Git 配置 | `tests/test_evidence.py`、`tests/test_ci_credentials.py` |
|
||||
| A7 证据 | 本地 SELF 收据;外部来源独立核验 | 首次捕获、完整脱敏输出/摘要、对象记录、原样导入 | `tests/test_evidence.py`;CI/审阅链接另核 |
|
||||
| A8 阶段评审与迭代 | manual;模板和提示不构成批准身份校验或全流程阻断 | policy/delivery.md;任务卡记录需求、方案/计划、评审与试验迭代;post-merge 默认仅提示 | `tests/test_task.py` 核对模板及钩子不自动写入;人工核对批准范围和版本 |
|
||||
| A8 阶段评审与迭代 | manual(人工/Agent 执行);前序人工决策,智能体 Review/合入;模板和提示不构成批准身份校验或全流程阻断 | policy/delivery.md;任务卡分别记录阶段结论、Review、冲突、当前 HEAD 的 CI 和合入;post-merge 默认仅提示 | `tests/test_task.py` 核对模板及钩子不自动写入;智能体核对 Review、冲突及匹配 HEAD 的 CI/平台准入;人工批准范围仍须核验 |
|
||||
| A9 长期方向与后续任务 | manual;模板不校验规划有效性或自动调度任务 | policy/delivery.md;PROJECT.md 提供规划入口,任务卡关联规划/里程碑并记录下一步建议 | `tests/test_task.py` 核对生成字段与未授权默认值;人工核对当前规划、进展、重复任务和建议依据 |
|
||||
|
||||
## 失败处理
|
||||
|
||||
+371
-63
@@ -1253,7 +1253,17 @@ env_config() {
|
||||
ENV_DIR="$RUNTIME/$name"
|
||||
ENV_PID="$ENV_DIR/svc.pid"
|
||||
ENV_LOG="$ENV_DIR/svc.log"
|
||||
ENV_BIN_DIR="$ENV_DIR/bin"
|
||||
# U2:releases/ 不可变版本目录 + current 原子指针。已切换实例经 current 解析
|
||||
# 产物(进程 cmdline 保留未解析路径——指针切换不破坏 pidcheck 匹配,也彻底
|
||||
# 消除 exe=(deleted) 覆盖孤儿);legacy 实例沿用原路径(首次 deploy 领养,
|
||||
# svc_release_adopt_legacy)。
|
||||
if [ -L "$ENV_DIR/current" ]; then
|
||||
ENV_BIN_DIR="$ENV_DIR/current/bin"
|
||||
ENV_PLUGINS="$ENV_DIR/current/plugins"
|
||||
else
|
||||
ENV_BIN_DIR="$ENV_DIR/bin"
|
||||
ENV_PLUGINS="$ENV_DIR/plugins"
|
||||
fi
|
||||
ENV_BIN="$ENV_BIN_DIR/agent-system"
|
||||
# 按实例重置内存态——批量 update/restart 逐服务解析时不串用上一服务。
|
||||
# DB_*:防跨服务串库;SVC_PIDCHECK/EXEC/VERSION/SOURCE:防 pid_alive 与
|
||||
@@ -1599,6 +1609,256 @@ install_artifacts() { # SRCBIN SRCPLUGINS:agent 产物复制进服务目录。
|
||||
c_ok "[$ENV_NAME] 产物就位:$ENV_BIN(plugins:$ENV_DIR/plugins)"
|
||||
}
|
||||
|
||||
# ---- U2:不可变版本目录(releases/)+ current 原子指针 + 切换 journal + 失败回退 ----
|
||||
# 布局:releases/<id>/{bin/agent-system, plugins/, htdocs/, release.json};
|
||||
# current → releases/<id>(相对符号链接,temp+rename 原子切换);deploy.journal
|
||||
# 记录切换过程(staging→switched→done/rolledback/blocked),任意步骤中断后由
|
||||
# svc_deploy_journal_resolve 判定并恢复实际状态。legacy 目录永不删除:旧进程
|
||||
# cmdline 指向 legacy 路径,pidcheck 兼容到自然换版。
|
||||
|
||||
svc_release_cur() { [ -L "$ENV_DIR/current" ] && readlink "$ENV_DIR/current" || true; }
|
||||
svc_release_cur_id() { local c; c="$(svc_release_cur)"; printf '%s' "${c##*/}"; }
|
||||
|
||||
# svc_release_adopt_legacy:legacy 布局(bin/ 直接落服务目录)→ 领养为首个
|
||||
# release(幂等:已有 current 即跳过;无产物即跳过——全新部署无需领养)。
|
||||
svc_release_adopt_legacy() {
|
||||
[ -L "$ENV_DIR/current" ] && return 0
|
||||
[ -x "$ENV_DIR/bin/agent-system" ] || return 0
|
||||
# 注意:bash 5.3 set -u 下同一 local 行不得引用本行先声明变量(拆行)。
|
||||
local id="legacy-$(date -u +%Y%m%dT%H%M%SZ)"
|
||||
local dir="$ENV_DIR/releases/$id"
|
||||
mkdir -p "$dir/bin" || return 1
|
||||
cp -f "$ENV_DIR/bin/agent-system" "$dir/bin/agent-system" || return 1
|
||||
if [ -d "$ENV_DIR/plugins" ]; then
|
||||
cp -r "$ENV_DIR/plugins" "$dir/plugins" || return 1
|
||||
else
|
||||
mkdir -p "$dir/plugins"
|
||||
fi
|
||||
[ -d "$ENV_DIR/htdocs" ] && cp -r "$ENV_DIR/htdocs" "$dir/htdocs" || true
|
||||
printf '{"id":"%s","source":"legacy","created":"%s"}\n' "$id" "$(date -u +%FT%TZ)" > "$dir/release.json"
|
||||
ln -s "releases/$id" "$ENV_DIR/.current.new" && mv -Tf "$ENV_DIR/.current.new" "$ENV_DIR/current" || {
|
||||
rm -f "$ENV_DIR/.current.new"; return 1; }
|
||||
c_info "[$ENV_NAME] 领养 legacy 制品为首个 release:releases/$id"
|
||||
}
|
||||
|
||||
# svc_release_id:源标签净化的版本目录名(rdm-ddc09eb-20261012T131500)。
|
||||
svc_release_id() {
|
||||
local label
|
||||
label="$(printf '%s' "${SRC_LABEL:-manual}" | tr '@' '-' | tr -cd 'a-zA-Z0-9._-' | cut -c1-40)"
|
||||
printf '%s-%s' "$label" "$(date -u +%Y%m%dT%H%M%SZ)"
|
||||
}
|
||||
|
||||
# svc_release_stage ID SRCBIN SRCPLUGINS [HTDOCS]:候选产物入不可变版本目录。
|
||||
# stdout 回显 release 目录(供门禁/探测引用)。
|
||||
svc_release_stage() {
|
||||
local id="$1" srcbin="$2" srcplug="$3" htdocs="${4:-}"
|
||||
local dir="$ENV_DIR/releases/$id"
|
||||
rm -rf "$dir"
|
||||
mkdir -p "$dir/bin" || return 1
|
||||
cp -f "$srcbin" "$dir/bin/agent-system" || return 1
|
||||
if [ -d "$srcplug" ]; then
|
||||
cp -r "$srcplug" "$dir/plugins" || return 1
|
||||
else
|
||||
mkdir -p "$dir/plugins"
|
||||
fi
|
||||
if [ -n "$htdocs" ] && [ -d "$htdocs" ]; then
|
||||
cp -r "$htdocs" "$dir/htdocs" || return 1
|
||||
fi
|
||||
printf '{"id":"%s","source":"%s","created":"%s"}\n' \
|
||||
"$id" "${SRC_LABEL:-}" "$(date -u +%FT%TZ)" > "$dir/release.json"
|
||||
printf '%s' "$dir"
|
||||
}
|
||||
|
||||
# svc_release_switch ID:原子切换 current(temp 符号链接 + rename)。
|
||||
svc_release_switch() {
|
||||
ln -s "releases/$1" "$ENV_DIR/.current.new" || return 1
|
||||
mv -Tf "$ENV_DIR/.current.new" "$ENV_DIR/current" || {
|
||||
rm -f "$ENV_DIR/.current.new"; return 1; }
|
||||
}
|
||||
|
||||
# svc_release_gc [KEEP]:裁剪历史 release(保留最近 KEEP 个,缺省 5;current 与
|
||||
# legacy 永不删除)。失败静默——目录非关键路径。
|
||||
svc_release_gc() {
|
||||
local keep="${1:-5}" cur id dirs=()
|
||||
[ -d "$ENV_DIR/releases" ] || return 0
|
||||
cur="$(svc_release_cur_id)"
|
||||
while IFS= read -r d; do dirs+=("$(basename "$d")"); done \
|
||||
< <(ls -1dt "$ENV_DIR/releases"/*/ 2>/dev/null)
|
||||
local n=${#dirs[@]} i rmv
|
||||
for ((i = keep; i < n; i++)); do
|
||||
rmv="${dirs[$i]}"
|
||||
[ "$rmv" = "$cur" ] && continue
|
||||
case "$rmv" in legacy-*) continue ;; esac
|
||||
rm -rf "$ENV_DIR/releases/$rmv"
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
# ---- 切换 journal(deploy.journal):python3 原子写;缺 python3 时降级为无 journal ----
|
||||
svc_journal_path() { printf '%s' "$ENV_DIR/deploy.journal"; }
|
||||
|
||||
svc_journal_write() { # PHASE [k=v ...]
|
||||
local phase="$1"; shift
|
||||
command -v python3 >/dev/null 2>&1 || return 0
|
||||
python3 - "$phase" "$(svc_journal_path)" "$@" <<'PYJ'
|
||||
import json, os, sys, time
|
||||
phase, out, args = sys.argv[1], sys.argv[2], sys.argv[3:]
|
||||
j = {"phase": phase, "ts": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())}
|
||||
for kv in args:
|
||||
if "=" in kv:
|
||||
k, v = kv.split("=", 1)
|
||||
j[k] = v
|
||||
tmp = out + ".new"
|
||||
with open(tmp, "w") as f:
|
||||
f.write(json.dumps(j, ensure_ascii=False) + "\n")
|
||||
os.replace(tmp, out)
|
||||
PYJ
|
||||
}
|
||||
|
||||
svc_journal_get() { # KEY → 值(journal 缺失/损坏 → 空)
|
||||
local f; f="$(svc_journal_path)"
|
||||
[ -f "$f" ] || return 0
|
||||
python3 -c 'import json,sys
|
||||
try:
|
||||
d = json.load(open(sys.argv[1])); v = d.get(sys.argv[2], "")
|
||||
print(v if isinstance(v, str) else json.dumps(v))
|
||||
except Exception:
|
||||
pass' "$f" "$1" 2>/dev/null
|
||||
}
|
||||
svc_journal_phase() { svc_journal_get phase; }
|
||||
svc_journal_clear() { rm -f "$(svc_journal_path)"; }
|
||||
|
||||
# svc_db_schema_version:SQLite 档当前 schema(schema_migrations 最大 id;
|
||||
# 无库/无表/pg → 空)。只读连接,绝不写库。
|
||||
svc_db_schema_version() {
|
||||
local db="$ENV_DATA/agent-system.db"
|
||||
[ -f "$db" ] || return 0
|
||||
python3 -c 'import sqlite3,sys
|
||||
try:
|
||||
con = sqlite3.connect("file:" + sys.argv[1] + "?mode=ro", uri=True)
|
||||
print(con.execute("select max(id) from schema_migrations").fetchone()[0])
|
||||
con.close()
|
||||
except Exception:
|
||||
pass' "$db" 2>/dev/null
|
||||
}
|
||||
|
||||
# svc_release_schema_supported ID:release.json 的 schema_at_commit(无则空)。
|
||||
svc_release_schema_supported() {
|
||||
local f="$ENV_DIR/releases/$1/release.json"
|
||||
[ -f "$f" ] || return 0
|
||||
python3 -c 'import json,sys
|
||||
try:
|
||||
print(json.load(open(sys.argv[1])).get("schema_at_commit") or "")
|
||||
except Exception:
|
||||
pass' "$f" 2>/dev/null
|
||||
}
|
||||
|
||||
# svc_release_commit_schema ID:成功提交时把当前库 schema 记入 release.json
|
||||
# (回退时据此判定旧制品可否运行;pg/无库静默跳过)。
|
||||
svc_release_commit_schema() {
|
||||
local f="$ENV_DIR/releases/$1/release.json" v
|
||||
v="$(svc_db_schema_version)"
|
||||
[ -n "$v" ] || return 0
|
||||
python3 -c 'import json,sys
|
||||
try:
|
||||
d = json.load(open(sys.argv[1])); d["schema_at_commit"] = sys.argv[2]
|
||||
open(sys.argv[1], "w").write(json.dumps(d, ensure_ascii=False) + "\n")
|
||||
except Exception:
|
||||
pass' "$f" "$v" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# svc_rollback_deploy REASON:切换失败 → 回退上一完整版本并恢复期望运行态。
|
||||
# schema 阻断(库 schema 超过上一 release 提交时点)不自动回退启动、绝不触碰
|
||||
# 业务数据库——登记 blocked 待受控迁移(U2 约束:不能靠覆盖数据库「回滚」)。
|
||||
svc_rollback_deploy() {
|
||||
local reason="$1"
|
||||
local prev new desired pid cur_schema prev_schema
|
||||
prev="$(svc_journal_get prev)"
|
||||
new="$(svc_journal_get new)"
|
||||
desired="$(svc_journal_get desired)"
|
||||
pid="$(read_pid)"
|
||||
if [ -n "$pid" ] && pid_alive "$pid"; then
|
||||
c_warn "[$ENV_NAME] 回退前停止新制品进程 pid=$pid"
|
||||
proc_stop "$pid" || true
|
||||
rm -f "$ENV_PID"
|
||||
fi
|
||||
if [ -z "$prev" ] || [ ! -d "$ENV_DIR/releases/$prev" ]; then
|
||||
c_err "[$ENV_NAME] 无可回退版本(prev 缺失,$reason)——保持停止;清理:rm 前先取证"
|
||||
svc_journal_write blocked reason="无 prev release:$reason"
|
||||
hist_event deploy fail "$pid" "回退无 prev($reason)"
|
||||
return 1
|
||||
fi
|
||||
cur_schema="$(svc_db_schema_version)"
|
||||
prev_schema="$(svc_release_schema_supported "$prev")"
|
||||
if [ -n "$cur_schema" ] && [ -n "$prev_schema" ] \
|
||||
&& [ "$cur_schema" -gt "$prev_schema" ] 2>/dev/null; then
|
||||
c_err "[$ENV_NAME] schema 已前进(库 v$cur_schema > 上一 release 提交 v$prev_schema):旧制品无法运行,禁止自动回退"
|
||||
c_err " 不触碰业务数据库。处理:deploy 换可运行制品(受控迁移后),或人工处置;登记 blocked"
|
||||
svc_journal_write blocked reason="schema v$cur_schema > prev v$prev_schema($reason)"
|
||||
hist_event deploy fail "$pid" "回退阻断:schema 前进($reason)"
|
||||
return 1
|
||||
fi
|
||||
svc_release_switch "$prev" || {
|
||||
svc_journal_write blocked reason="指针回退失败:$reason"
|
||||
return 1
|
||||
}
|
||||
c_ok "[$ENV_NAME] 已回退上一版本:releases/$prev($reason)"
|
||||
# 指针已切回:重解析产物路径到回退后的 release。
|
||||
ENV_BIN_DIR="$ENV_DIR/current/bin"
|
||||
ENV_PLUGINS="$ENV_DIR/current/plugins"
|
||||
ENV_BIN="$ENV_BIN_DIR/agent-system"
|
||||
hist_event deploy rollback "$pid" "new=${new:-?} prev=$prev($reason)"
|
||||
svc_journal_write rolledback "prev=$prev" "new=${new:-}" "reason=$reason"
|
||||
if [ "$desired" = "running" ]; then
|
||||
c_info "[$ENV_NAME] 按原期望态恢复运行(上一版本)"
|
||||
# 登记一致性:清掉新部署的源标签——回退后的 record_state 须回落到 svc.env
|
||||
# 里上一版的 source(否则实际跑旧制品、登记却写新标签)。
|
||||
SRC_LABEL=""
|
||||
SVC_SOURCE="$(svc_get source)"
|
||||
agent_do_start || { svc_journal_clear; return 1; }
|
||||
fi
|
||||
svc_journal_clear
|
||||
return 0
|
||||
}
|
||||
|
||||
# svc_deploy_journal_resolve [ACTION]:生命周期入口处理中断的切换。
|
||||
# switched/verifying:指针已切但未经完整验证 → 回退上一完整版本(无 prev 的
|
||||
# 首次部署中断 → 保持停止)。blocked:拒绝除 deploy 外的动作(显式换制品即
|
||||
# 解除路径)。staging:未切换,清记录(孤儿 release 目录无害,随 gc 裁剪)。
|
||||
svc_deploy_journal_resolve() {
|
||||
local action="${1:-}" phase
|
||||
[ -f "$(svc_journal_path)" ] || return 0
|
||||
phase="$(svc_journal_phase)"
|
||||
case "$phase" in
|
||||
""|done|rolledback)
|
||||
svc_journal_clear; return 0 ;;
|
||||
staging)
|
||||
c_warn "[$ENV_NAME] 发现中断的暂存(staging,未切换)——忽略"
|
||||
svc_journal_clear; return 0 ;;
|
||||
blocked)
|
||||
if [ "$action" = "deploy" ]; then
|
||||
c_warn "[$ENV_NAME] 存在 blocked 切换记录,本次 deploy 尝试跨越(换可运行制品)"
|
||||
return 0
|
||||
fi
|
||||
c_err "[$ENV_NAME] 切换被阻断($(svc_journal_get reason)):先 deploy 换可运行制品,或受控迁移后再操作"
|
||||
return 1 ;;
|
||||
switched|verifying)
|
||||
c_warn "[$ENV_NAME] 发现中断的切换($phase,new=$(svc_journal_get new))——回退上一完整版本"
|
||||
svc_rollback_deploy "中断的切换(journal=$phase)" || return 1
|
||||
return 0 ;;
|
||||
esac
|
||||
return 0
|
||||
}
|
||||
|
||||
# svc_deploy_recycle_wt:deploy 各失败/成功出口统一回收构建 worktree(尽力而为)。
|
||||
svc_deploy_recycle_wt() {
|
||||
case "${SRC_KINDSRC:-}" in
|
||||
dev) [ -n "${wt:-}" ] && recycle_worktree "$SRC_SHA" ;;
|
||||
rdm) [ -n "${wt:-}" ] && recycle_worktree "rdm-$SRC_SHA" "$RDM_SRC_DIR" ;;
|
||||
esac
|
||||
return 0
|
||||
}
|
||||
|
||||
# materialize_dev_commit FULL SHA TARGET:隔离 worktree 物化+构建;stdout 只回显 worktree
|
||||
# 路径(提示信息走 stderr)。TARGET=build(agent)| frontend(static)。
|
||||
materialize_dev_commit() {
|
||||
@@ -1868,33 +2128,26 @@ agent_do_deploy() {
|
||||
return 1
|
||||
fi
|
||||
hist_event deploy begin "$(read_pid)" "from=$SRC_LABEL"
|
||||
# U1/U2(构建前置):先构建/物化候选并过身份门禁,再停旧服务——构建失败或候选
|
||||
# 不合格时旧服务继续运行、旧制品与登记不动(2026-10-12 rdm1/rdmcand 事故:停服
|
||||
# →覆盖→启动即拒,只留「登记版本≠磁盘制品」的混合状态且无恢复路径)。
|
||||
local wt RDM_UI_OK=0
|
||||
# U2(不可变版本目录 + 失败回退):构建 → 暂存 release → 门禁(身份/环境 DSN)
|
||||
# → 停旧 → current 原子切换 → 启动与 UI 验证 → 成功提交(schema 记录 + journal
|
||||
# 清理 + release 裁剪);任一步失败回退上一完整版本并恢复期望运行态
|
||||
# (svc_rollback_deploy;库 schema 已前进时阻断,绝不覆盖业务数据库)。
|
||||
local wt relid staged srcbin srcplug srchtdocs="" prev_id desired
|
||||
svc_release_adopt_legacy \
|
||||
|| { hist_event deploy fail "$(read_pid)" "legacy 领养失败($SRC_LABEL)"; return 1; }
|
||||
case "$SRC_KINDSRC" in
|
||||
local)
|
||||
c_info "[$ENV_NAME] 构建当前工作区(make build)——唯一写主检出构建产物的路径"
|
||||
make -C "$ROOT" build || { hist_event deploy fail "$(read_pid)" "构建失败($SRC_LABEL)"; return 1; }
|
||||
svc_deploy_identity_gate "$BIN" \
|
||||
|| { hist_event deploy fail "$(read_pid)" "候选身份门禁拒绝($SRC_LABEL)"; return 1; }
|
||||
do_stop_quiet
|
||||
install_artifacts "$BIN" "$ROOT/plugins" \
|
||||
|| { hist_event deploy fail "$(read_pid)" "产物复制失败"; return 1; }
|
||||
;;
|
||||
make -C "$ROOT" build || {
|
||||
c_err "[$ENV_NAME] 构建失败($SRC_LABEL)——旧服务保持运行、制品与登记不变"
|
||||
hist_event deploy fail "$(read_pid)" "构建失败($SRC_LABEL)"
|
||||
return 1
|
||||
}
|
||||
srcbin="$BIN"; srcplug="$ROOT/plugins" ;;
|
||||
dev)
|
||||
wt="$(materialize_dev_commit "$SRC_FULL" "$SRC_SHA" build)" \
|
||||
|| { hist_event deploy fail "$(read_pid)" "物化/构建失败($SRC_LABEL)"; return 1; }
|
||||
if ! svc_deploy_identity_gate "$wt/bin/agent-system"; then
|
||||
hist_event deploy fail "$(read_pid)" "候选身份门禁拒绝($SRC_LABEL)"
|
||||
recycle_worktree "$SRC_SHA"
|
||||
return 1
|
||||
fi
|
||||
do_stop_quiet
|
||||
install_artifacts "$wt/bin/agent-system" "$wt/plugins" \
|
||||
|| { hist_event deploy fail "$(read_pid)" "产物复制失败"; return 1; }
|
||||
recycle_worktree "$SRC_SHA"
|
||||
;;
|
||||
srcbin="$wt/bin/agent-system"; srcplug="$wt/plugins" ;;
|
||||
release)
|
||||
local relbin="$REL_ROOT/build/$REL_VERSION/agent-system"
|
||||
if [ ! -f "$relbin" ]; then
|
||||
@@ -1903,65 +2156,98 @@ agent_do_deploy() {
|
||||
hist_event deploy fail "$(read_pid)" "制品缺失($SRC_LABEL)"
|
||||
return 1
|
||||
fi
|
||||
svc_deploy_identity_gate "$relbin" \
|
||||
|| { hist_event deploy fail "$(read_pid)" "候选身份门禁拒绝($SRC_LABEL)"; return 1; }
|
||||
do_stop_quiet
|
||||
materialize_release || { hist_event deploy fail "$(read_pid)" "物化/校验失败($SRC_LABEL)"; return 1; }
|
||||
;;
|
||||
local mf="$REL_ROOT/versions/$REL_VERSION/manifest.json" want have
|
||||
if [ -f "$mf" ]; then
|
||||
want="$(sed -n 's/.*"agent-system": *"\([0-9a-f]\{64\}\)".*/\1/p' "$mf" | head -1)"
|
||||
have="$(sha256sum "$relbin" | cut -d' ' -f1)"
|
||||
if [ -n "$want" ] && [ "$want" != "$have" ]; then
|
||||
c_err "[$ENV_NAME] sha256 校验失败:制品与 manifest 不符($relbin)——拒绝部署"
|
||||
hist_event deploy fail "$(read_pid)" "sha256 校验失败($SRC_LABEL)"
|
||||
return 1
|
||||
fi
|
||||
[ -n "$want" ] && c_ok "[$ENV_NAME] sha256 校验通过(manifest:$REL_VERSION)"
|
||||
else
|
||||
c_warn "[$ENV_NAME] 无 manifest 可校验:$mf"
|
||||
fi
|
||||
srcbin="$relbin"; srcplug="$REL_ROOT/build/$REL_VERSION/plugins" ;;
|
||||
rdm)
|
||||
wt="$(materialize_rdm_commit "$SRC_FULL" "$SRC_SHA")" \
|
||||
|| { hist_event deploy fail "$(read_pid)" "物化/构建失败($SRC_LABEL)"; return 1; }
|
||||
# RDM 组合插件编译进二进制(provides 无声明式资产)——插件目录按空集安装。
|
||||
# RDM 组合插件编译进二进制(provides 无声明式资产)——插件目录按空集。
|
||||
mkdir -p "$wt/.svc-empty-plugins"
|
||||
if ! svc_deploy_identity_gate "$wt/bin/agent-system-rdm"; then
|
||||
hist_event deploy fail "$(read_pid)" "候选身份门禁拒绝($SRC_LABEL)"
|
||||
recycle_worktree "rdm-$SRC_SHA" "$RDM_SRC_DIR"
|
||||
return 1
|
||||
fi
|
||||
do_stop_quiet
|
||||
install_artifacts "$wt/bin/agent-system-rdm" "$wt/.svc-empty-plugins" \
|
||||
|| { hist_event deploy fail "$(read_pid)" "产物复制失败"; return 1; }
|
||||
# RDM 自带零构建工作台(web/)落 htdocs/;收尾自动部署伴生 ${name}web。
|
||||
if [ -d "$wt/web" ]; then
|
||||
rm -rf "$ENV_DIR/htdocs"
|
||||
if cp -r "$wt/web" "$ENV_DIR/htdocs"; then
|
||||
RDM_UI_OK=1
|
||||
c_ok "[$ENV_NAME] RDM 工作台就位:$ENV_DIR/htdocs(将自动部署伴生 $(rdm_companion_name "$ENV_NAME"))"
|
||||
else
|
||||
c_warn "[$ENV_NAME] RDM web/ 工作台复制失败"
|
||||
fi
|
||||
else
|
||||
c_warn "[$ENV_NAME] RDM 源无 web/ 工作台目录"
|
||||
fi
|
||||
recycle_worktree "rdm-$SRC_SHA" "$RDM_SRC_DIR"
|
||||
;;
|
||||
srcbin="$wt/bin/agent-system-rdm"; srcplug="$wt/.svc-empty-plugins"
|
||||
[ -d "$wt/web" ] && srchtdocs="$wt/web" ;;
|
||||
esac
|
||||
# 新产物承接能力复核(环境 DSN 交付):不支持即失败——绑定保持可恢复,不降级启动。
|
||||
if [ "$DB_MODE" = pg ] && [ "$DB_SOURCE" != conffile ] && ! bin_env_dsn_supported "$ENV_BIN"; then
|
||||
relid="$(svc_release_id)"
|
||||
staged="$(svc_release_stage "$relid" "$srcbin" "$srcplug" ${srchtdocs:+"$srchtdocs"})" \
|
||||
|| { hist_event deploy fail "$(read_pid)" "产物暂存失败($SRC_LABEL)"; svc_deploy_recycle_wt; return 1; }
|
||||
if ! svc_deploy_identity_gate "$staged/bin/agent-system"; then
|
||||
hist_event deploy fail "$(read_pid)" "候选身份门禁拒绝($SRC_LABEL)"
|
||||
rm -rf "$staged"
|
||||
svc_deploy_recycle_wt
|
||||
return 1
|
||||
fi
|
||||
# 新产物承接能力复核(环境 DSN 交付;候选上、停服前):不支持即失败——绑定
|
||||
# 保持可恢复,不降级启动。
|
||||
if [ "$DB_MODE" = pg ] && [ "$DB_SOURCE" != conffile ] && ! bin_env_dsn_supported "$staged/bin/agent-system"; then
|
||||
c_err "[$ENV_NAME] 新产物不消费环境变量 DSN(AGENT_SYSTEM_DSN)——部署中止;旧绑定保留在 svc.env"
|
||||
c_err " 恢复:换部署版本,或改用携带 dsn 的配置文件(scripts/svc.sh $ENV_NAME config <file.json>)"
|
||||
hist_event deploy fail "$(read_pid)" "新产物不支持环境 DSN($SRC_LABEL)"
|
||||
rm -rf "$staged"
|
||||
return 1
|
||||
fi
|
||||
prev_id="$(svc_release_cur_id)"
|
||||
desired="stopped"; pid_alive "$(read_pid)" 2>/dev/null && desired="running"
|
||||
svc_journal_write staging "new=$relid" "prev=$prev_id" "desired=$desired"
|
||||
do_stop_quiet
|
||||
svc_release_switch "$relid" || {
|
||||
svc_journal_write blocked reason="current 指针切换失败"
|
||||
hist_event deploy fail "$(read_pid)" "current 指针切换失败($SRC_LABEL)"
|
||||
return 1
|
||||
}
|
||||
svc_journal_write switched "new=$relid" "prev=$prev_id" "desired=$desired"
|
||||
c_ok "[$ENV_NAME] 已切换 release:releases/$relid(prev:${prev_id:-无})"
|
||||
# 指针已切:重解析产物路径(env_config 解析时 current 可能尚不存在)。
|
||||
ENV_BIN_DIR="$ENV_DIR/current/bin"
|
||||
ENV_PLUGINS="$ENV_DIR/current/plugins"
|
||||
ENV_BIN="$ENV_BIN_DIR/agent-system"
|
||||
if ! agent_do_start; then
|
||||
svc_rollback_deploy "新制品启动失败($SRC_LABEL)" || true
|
||||
hist_event deploy fail "$(read_pid)" "启动失败已回退($SRC_LABEL)"
|
||||
return 1
|
||||
fi
|
||||
agent_do_start || { hist_event deploy fail "$(read_pid)" "启动失败($SRC_LABEL)"; return 1; }
|
||||
case "$SRC_KINDSRC" in
|
||||
rdm)
|
||||
if [ "$RDM_UI_OK" != 1 ]; then
|
||||
c_err "[$ENV_NAME] RDM 工作台未就位(ui-missing):缺少可伺服的 htdocs/"
|
||||
hist_event deploy fail "$(read_pid)" "ui-missing(htdocs 未就位)"
|
||||
return 1
|
||||
fi
|
||||
if ! deploy_rdm_companion_ui "$(rdm_companion_name "$ENV_NAME")" "$ENV_DIR/htdocs"; then
|
||||
hist_event deploy fail "$(read_pid)" "ui-missing(伴生部署失败)"
|
||||
# 顶层 htdocs → current/htdocs 符号链接:伴生经它伺服,指针切换自动换内容
|
||||
# (legacy 实体目录内容已随领养保入 release,替换不丢)。
|
||||
if [ -n "$srchtdocs" ]; then
|
||||
if [ ! -L "$ENV_DIR/htdocs" ]; then
|
||||
rm -rf "$ENV_DIR/htdocs"
|
||||
ln -s current/htdocs "$ENV_DIR/htdocs"
|
||||
fi
|
||||
c_ok "[$ENV_NAME] RDM 工作台就位:releases/$relid/htdocs → $ENV_DIR/htdocs(伴生 $(rdm_companion_name "$ENV_NAME"))"
|
||||
if ! deploy_rdm_companion_ui "$(rdm_companion_name "$ENV_NAME")" "$ENV_DIR/htdocs"; then
|
||||
svc_rollback_deploy "RDM 伴生工作台部署失败(ui-missing)" || true
|
||||
hist_event deploy fail "$(read_pid)" "ui-missing(伴生部署失败,已回退)"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
svc_rollback_deploy "RDM 源无 web/ 工作台(ui-missing)" || true
|
||||
hist_event deploy fail "$(read_pid)" "ui-missing(源无 web/,已回退)"
|
||||
return 1
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
if ! assert_ui_ready; then
|
||||
hist_event deploy fail "$(read_pid)" "ui-missing"
|
||||
svc_rollback_deploy "UI 探活失败(ui-missing)" || true
|
||||
hist_event deploy fail "$(read_pid)" "ui-missing(已回退)"
|
||||
return 1
|
||||
fi
|
||||
hist_event deploy ok "$(read_pid)" "from=$SRC_LABEL"
|
||||
svc_release_commit_schema "$relid"
|
||||
svc_journal_clear
|
||||
svc_release_gc
|
||||
svc_deploy_recycle_wt
|
||||
hist_event deploy ok "$(read_pid)" "from=$SRC_LABEL release=$relid"
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -2380,6 +2666,9 @@ agent_do_start() {
|
||||
local exec_args
|
||||
if ! agent_has_explicit_cfg && [ -n "$(svc_get exec)" ]; then
|
||||
exec_args="$(svc_get exec)"
|
||||
# U2:release 治理实例的持久化 exec 里 legacy plugins 路径跟随 current
|
||||
#(指针切换后插件目录随 release 解析)。
|
||||
[ -L "$ENV_DIR/current" ] && exec_args="${exec_args//$ENV_DIR\/plugins/$ENV_PLUGINS}"
|
||||
c_info "[$ENV_NAME] 复用上次配置(svc.env exec=;显式参数可覆盖):$(mask_dsn "$exec_args")"
|
||||
else
|
||||
local args="" explicit_auth="${CLI_AUTH:-$(env_var "$ENV_NAME" AUTH)}"
|
||||
@@ -2398,7 +2687,7 @@ agent_do_start() {
|
||||
c_warn "[$ENV_NAME] 该产物不支持部分启动参数(旧版本二进制),已自动省略:${args:-<无>} → ${adjusted:-<无>}"
|
||||
fi
|
||||
args="$adjusted"
|
||||
exec_args="--addr $ENV_ADDR --data $ENV_DATA --plugins ${CLI_PLUGINS:-$ENV_DIR/plugins} $args $CLI_EXTRA"
|
||||
exec_args="--addr $ENV_ADDR --data $ENV_DATA --plugins ${CLI_PLUGINS:-$ENV_PLUGINS} $args $CLI_EXTRA"
|
||||
exec_args="$(printf '%s' "$exec_args" | sed 's/[[:space:]]*$//')"
|
||||
fi
|
||||
SVC_EXEC="$exec_args"
|
||||
@@ -4405,6 +4694,15 @@ cmd_update() {
|
||||
failed=$((failed + 1))
|
||||
continue
|
||||
fi
|
||||
# U2:批内同样处理中断切换(deploy 可跨越 blocked)。
|
||||
if ! svc_deploy_journal_resolve deploy; then
|
||||
hist_event update fail "$(read_pid)" "from=$from to=$tov(中断切换处置失败)"
|
||||
c_err "[$name] 更新失败(deploy.journal 处置),其余服务继续"
|
||||
svc_unlock
|
||||
CLI_FROM=""
|
||||
failed=$((failed + 1))
|
||||
continue
|
||||
fi
|
||||
if agent_do_deploy; then
|
||||
hist_event update ok "$(read_pid)" "from=$from to=$tov"
|
||||
c_ok "[$name] 已更新:$from → $tov"
|
||||
@@ -4479,6 +4777,11 @@ EOF
|
||||
c_warn "[$n] 生命周期锁被占用(deploy/手动操作进行中),本轮跳过"
|
||||
skipped=$((skipped + 1)); continue
|
||||
fi
|
||||
# U2:拉起前处置中断的切换(switched → 回退上一完整版本再按期望态启动)。
|
||||
if ! svc_deploy_journal_resolve recover; then
|
||||
c_warn "[$n] 存在阻断的切换记录,本轮跳过(见 deploy.journal)"
|
||||
failed=$((failed + 1)); rc=1; svc_unlock; continue
|
||||
fi
|
||||
c_info "[$n] recover:autostart=1 且进程已死,重新拉起(复用持久化配置)"
|
||||
if do_start; then
|
||||
hist_event recover ok "$(read_pid)" "autostart revival"
|
||||
@@ -4700,6 +5003,11 @@ main() {
|
||||
case "$action" in
|
||||
start|restart|deploy|init) svc_manifest_migrate || exit 1 ;;
|
||||
esac
|
||||
# U2:入口处理中断的切换(deploy.journal)。blocked 拒绝除 deploy 外的动作
|
||||
#(显式换制品即解除路径);start/restart 触发回退判定。
|
||||
case "$action" in
|
||||
start|restart|deploy) svc_deploy_journal_resolve "$action" || exit 1 ;;
|
||||
esac
|
||||
case "$action" in
|
||||
deploy)
|
||||
case "$SVC_KIND" in
|
||||
|
||||
+8
-4
@@ -178,7 +178,8 @@ cmd_start() {
|
||||
- 工作区:${wt}(worktree);分支:${name}
|
||||
${parent_note}
|
||||
- 授权与交付终点:<本次允许的动作与终点;未授权事项写明>
|
||||
- 交付模式:manual(逐阶段人工评审);明确授权后可记为 authorized-continuous
|
||||
- 交付模式:manual(前序人工决策,智能体 Review/合入);其他阶段明确授权后可记为 authorized-continuous
|
||||
- 合入执行:agent(已授权交付范围内,Review 通过、冲突解决且当前 HEAD 的 CI 通过后合入)
|
||||
- 自动推进授权:无;授权范围:无;授权来源:无;授权终点:无;停止条件:无(未授权)
|
||||
- 当前阶段:需求拆解;当前增量:I1(待定义)
|
||||
- 流程依据:仓库根目录 policy/delivery.md(归档后仍按仓库根目录定位)
|
||||
@@ -256,7 +257,10 @@ ${parent_note}
|
||||
|
||||
## 合入与清理
|
||||
|
||||
- 合入记录:NOT_RUN(授权依据、PR、合入 SHA、验证来源)
|
||||
- Review 记录:NOT_RUN(审阅智能体/平台身份、base/head、结论、问题闭合及重放依据)
|
||||
- 冲突记录:NOT_RUN(最新目标同步、冲突处置、组合兼容核验及复审/补测)
|
||||
- CI 记录:NOT_RUN(当前 HEAD 完整 SHA、全部必需检查、运行链接、执行身份及 GATE 结果)
|
||||
- 合入记录:NOT_RUN(交付授权范围、PR、被核验 HEAD、合入 SHA、目标分支及内容核验)
|
||||
- 清理记录:NOT_RUN(授权依据、写者退出确认、worktree/分支/任务卡处置)
|
||||
EOF
|
||||
(cd "$wt" && IMPORT_RECEIPT="$PRECHECK_RECEIPT" bash "${SCRIPT_DIR}/evidence.sh")
|
||||
@@ -458,9 +462,9 @@ for p in json.load(sys.stdin):
|
||||
fi
|
||||
if [ -n "$newbranch" ]; then git checkout -q "${TARGET_BRANCH}"; fi
|
||||
if [ -n "$pr_url" ]; then
|
||||
echo "[pr] ${pr_url}(合入权在所有者)"
|
||||
echo "[pr] ${pr_url}(智能体按 policy/delivery.md 完成 Review、解决冲突且当前 HEAD 的 CI 通过后合入)"
|
||||
else
|
||||
echo "[next] 建 PR:${web_base}/${owner}/${repo}/pulls/new?head=${headbr}(合入权在所有者)"
|
||||
echo "[next] 建 PR:${web_base}/${owner}/${repo}/pulls/new?head=${headbr}(智能体按 policy/delivery.md 完成 Review、解决冲突且当前 HEAD 的 CI 通过后合入)"
|
||||
fi
|
||||
echo "[closed] ${n} 张已合入卡片已回填归档,提交于 ${headbr} 并推送"
|
||||
}
|
||||
|
||||
+6
-4
@@ -796,12 +796,14 @@ class SvcDbRecoveryTests(unittest.TestCase):
|
||||
env = self.rdm_env(remote)
|
||||
result = self.svc("rdm1", "deploy", "--from", "rdm", "--port", "18841", env=env)
|
||||
d = self.runtime("rdm1")
|
||||
# 组合二进制复制改名落位、插件目录空集、工作台静态资产落 htdocs/。
|
||||
self.assertTrue((d / "bin" / "agent-system").exists())
|
||||
self.assertTrue((d / "bin" / "agent-system").stat().st_mode & 0o111)
|
||||
# U2:组合产物入不可变 release,经 current 指针解析;插件空集、工作台随 release。
|
||||
active = d / "current"
|
||||
self.assertTrue(active.is_symlink(), "current 指针须就位")
|
||||
self.assertTrue((active / "bin" / "agent-system").exists())
|
||||
self.assertTrue((active / "bin" / "agent-system").stat().st_mode & 0o111)
|
||||
self.assertEqual(self.kv("rdm1", "kind"), "agent")
|
||||
self.assertTrue(self.kv("rdm1", "source").startswith("rdm@"))
|
||||
self.assertEqual(list((d / "plugins").iterdir()), [])
|
||||
self.assertEqual(list((active / "plugins").iterdir()), [])
|
||||
self.assertIn("rdm workbench", (d / "htdocs" / "index.html").read_text())
|
||||
self.assertIn("args:", self.serve_record()) # 健康通过即登记启动参数
|
||||
# 伴生 static 自动部署并探活 200。
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
"""svc.sh U2 回归:不可变版本目录(releases/)+ current 原子切换 + deploy.journal
|
||||
+ 失败回退(2026-10-12 统一部署方案 U2)。
|
||||
|
||||
覆盖:release 布局与 current 解析(pidcheck 经未解析路径跨版本稳定)、启动失败
|
||||
回退上一完整版本并恢复期望态、构建失败旧服务保持运行、中断切换经 start/recover
|
||||
入口回退、schema 前进阻断回退(不触碰业务库)、legacy 布局兼容。夹具复用
|
||||
tests/test_svc_identity.py(IdentityTests:manifest 治理 + 源仓轨道 + noid 桩)。
|
||||
"""
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
SOURCE = Path(__file__).resolve().parents[1]
|
||||
|
||||
_spec = importlib.util.spec_from_file_location("tsr", SOURCE / "tests" / "test_svc_recover.py")
|
||||
tsr = importlib.util.module_from_spec(_spec)
|
||||
_spec.loader.exec_module(tsr)
|
||||
|
||||
_spec2 = importlib.util.spec_from_file_location("tsi", SOURCE / "tests" / "test_svc_identity.py")
|
||||
tsi = importlib.util.module_from_spec(_spec2)
|
||||
_spec2.loader.exec_module(tsi)
|
||||
|
||||
|
||||
class U2ReleaseTests(tsi.IdentityTests):
|
||||
"""manifest 治理 + identity 桩 + dev 轨道源仓(make_src_repo 等)。"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
# --from local 需要根为 git 仓(local@HEAD 作源标签);recover 夹具不初始化。
|
||||
self.git_ok(self.root, "init", "--initial-branch=main", ".")
|
||||
self.git_ok(self.root, "add", ".")
|
||||
self.git_ok(self.root, "commit", "-m", "u2 fixture")
|
||||
|
||||
# ---- 夹具 ----
|
||||
|
||||
def cur_release(self, name):
|
||||
c = self.runtime(name) / "current"
|
||||
return c.resolve().name if c.is_symlink() else None
|
||||
|
||||
def active_bin(self, name):
|
||||
return self.runtime(name) / "current" / "bin" / "agent-system"
|
||||
|
||||
def cmdline_of(self, pid):
|
||||
return Path("/proc/%d/cmdline" % pid).read_bytes().replace(b"\0", b" ").decode()
|
||||
|
||||
def journal(self, name):
|
||||
f = self.runtime(name) / "deploy.journal"
|
||||
return json.loads(f.read_text()) if f.exists() else None
|
||||
|
||||
def die_stub(self):
|
||||
"""identity 可用、serve 即退的桩(模拟新制品启动失败)。"""
|
||||
p = self.root / "stub-agent-system-die"
|
||||
if not p.exists():
|
||||
stub = tsr.STUB
|
||||
head, sep, tail = stub.partition(" serve)")
|
||||
assert sep, "STUB 缺 serve 分支锚点"
|
||||
_, sep2, tail2 = tail.partition(' wait "$child"\n ;;')
|
||||
assert sep2, "STUB 缺 serve 分支结尾锚点"
|
||||
p.write_text(head + ' serve)\n shift\n'
|
||||
+ ' echo "die-stub: serve exits immediately" >&2\n'
|
||||
+ " exit 1 ;;" + tail2)
|
||||
p.chmod(0o755)
|
||||
return p
|
||||
|
||||
def set_makefile(self, stub):
|
||||
(self.root / "Makefile").write_text(
|
||||
"build:\n\tmkdir -p bin\n\tcp %s bin/agent-system\n" % stub)
|
||||
|
||||
def bump_schema(self, name, version):
|
||||
"""把库 schema_migrations 推进到 version(桩库无该表则建;模拟新制品
|
||||
已迁移数据库)。"""
|
||||
import sqlite3
|
||||
db = self.runtime(name) / "data" / "agent-system.db"
|
||||
con = sqlite3.connect(str(db))
|
||||
con.execute(
|
||||
"CREATE TABLE IF NOT EXISTS schema_migrations"
|
||||
"(id INTEGER PRIMARY KEY, name TEXT, applied_at TEXT)")
|
||||
for i in (1, 2, 3, version):
|
||||
con.execute(
|
||||
"INSERT OR IGNORE INTO schema_migrations(id,name,applied_at)"
|
||||
" VALUES(?, 'u2-test', '2026-10-12T00:00:00Z')", (i,))
|
||||
con.commit()
|
||||
con.close()
|
||||
|
||||
# ---- T1:release 布局 + 启动失败回退 + 期望态恢复 ----
|
||||
|
||||
def test_U2_layout_switch_and_rollback_on_start_failure(self):
|
||||
self.make_src_repo() # 预克隆 srccache(本用例走 --from local,仅保环境一致)
|
||||
self.seed_agent("u2a", 9461)
|
||||
self.start_ok("u2a") # legacy 运行中(期望态=running)
|
||||
self.assertTrue(self.running("u2a"))
|
||||
|
||||
# v1 部署:legacy 领养 → current 切换 → 经 current 启动
|
||||
self.set_makefile(self.stub)
|
||||
result = self.svc("u2a", "deploy", "--from", "local")
|
||||
self.assertIn("已切换 release", result.stdout)
|
||||
self.assertIn("领养 legacy", result.stdout)
|
||||
cur1 = self.cur_release("u2a")
|
||||
self.assertTrue(cur1 and cur1.startswith("local-"), cur1)
|
||||
self.assertTrue(self.active_bin("u2a").exists())
|
||||
releases = sorted((self.runtime("u2a") / "releases").iterdir())
|
||||
self.assertEqual(len(releases), 2, "legacy + v1 两个 release")
|
||||
pid = self.pid_of("u2a")
|
||||
self.assertIn("current/bin/agent-system", self.cmdline_of(pid),
|
||||
"进程 cmdline 须走 current 未解析路径(pidcheck 跨版本稳定)")
|
||||
self.assertIn("current/bin", self.kv("u2a", "pidcheck") or "")
|
||||
v1_bin = self.active_bin("u2a").read_bytes()
|
||||
self.assertEqual(v1_bin, Path(self.stub).read_bytes(), "v1 制品 = 可用桩")
|
||||
self.assertIsNone(self.journal("u2a"), "成功后 journal 清理")
|
||||
|
||||
# v2 部署:serve 即退 → 回退 v1 并恢复运行
|
||||
self.set_makefile(self.die_stub())
|
||||
result = self.svc("u2a", "deploy", "--from", "local", check=False)
|
||||
self.assertNotEqual(result.returncode, 0, result.stdout)
|
||||
self.assertIn("已回退上一版本", result.stdout)
|
||||
self.assertTrue(self.running("u2a"), "回退须按原期望态恢复运行")
|
||||
pid2 = self.pid_of("u2a")
|
||||
self.assertNotEqual(pid2, pid)
|
||||
self.assertIn("current/bin/agent-system", self.cmdline_of(pid2))
|
||||
self.assertEqual(self.active_bin("u2a").read_bytes(), v1_bin, "回退后制品=v1")
|
||||
hist = (self.runtime("u2a") / "history.jsonl").read_text()
|
||||
self.assertIn('"event":"deploy"', hist)
|
||||
self.assertIn("rollback", hist)
|
||||
self.assertIsNone(self.journal("u2a"))
|
||||
self.svc("u2a", "stop")
|
||||
|
||||
# ---- T2:构建失败 → 旧服务保持运行、current 不动 ----
|
||||
|
||||
def test_U2_build_failure_keeps_old_running(self):
|
||||
self.seed_agent("u2b", 9462)
|
||||
self.set_makefile(self.stub)
|
||||
self.svc("u2b", "deploy", "--from", "local")
|
||||
cur1 = self.cur_release("u2b")
|
||||
pid = self.pid_of("u2b")
|
||||
(self.root / "Makefile").write_text("build:\n\texit 1\n")
|
||||
result = self.svc("u2b", "deploy", "--from", "local", check=False)
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn("构建失败", result.stdout)
|
||||
self.assertTrue(self.running("u2b"), "构建失败旧服务必须保持运行")
|
||||
self.assertEqual(self.pid_of("u2b"), pid)
|
||||
self.assertEqual(self.cur_release("u2b"), cur1, "current 指针不动")
|
||||
self.svc("u2b", "stop")
|
||||
|
||||
# ---- T3:中断切换(journal switched)→ start 入口回退并恢复 ----
|
||||
|
||||
def test_U2_journal_interrupt_resolved_on_start(self):
|
||||
self.seed_agent("u2c", 9463)
|
||||
self.set_makefile(self.stub)
|
||||
self.svc("u2c", "deploy", "--from", "local")
|
||||
v1 = self.cur_release("u2c")
|
||||
self.svc("u2c", "stop")
|
||||
# 伪造中断于「指针已切」:current → 假 release,journal switched
|
||||
fake = self.runtime("u2c") / "releases" / "fake-interrupt"
|
||||
(fake / "bin").mkdir(parents=True)
|
||||
shutil.copy2(self.die_stub(), fake / "bin" / "agent-system")
|
||||
(self.runtime("u2c") / "current").unlink()
|
||||
(self.runtime("u2c") / "current").symlink_to("releases/fake-interrupt")
|
||||
(self.runtime("u2c") / "deploy.journal").write_text(json.dumps({
|
||||
"phase": "switched", "new": "fake-interrupt", "prev": v1,
|
||||
"desired": "running", "ts": "2026-10-12T00:00:00Z"}))
|
||||
|
||||
result = self.svc("u2c", "start")
|
||||
self.assertIn("中断的切换", result.stdout)
|
||||
self.assertIn("已回退上一版本", result.stdout)
|
||||
self.assertTrue(self.running("u2c"))
|
||||
self.assertEqual(self.cur_release("u2c"), v1)
|
||||
self.assertIsNone(self.journal("u2c"))
|
||||
self.svc("u2c", "stop")
|
||||
|
||||
# ---- T4:schema 前进 → 回退阻断,不触碰业务库 ----
|
||||
|
||||
def test_U2_schema_advance_blocks_rollback(self):
|
||||
self.seed_agent("u2d", 9464)
|
||||
self.set_makefile(self.stub)
|
||||
self.svc("u2d", "deploy", "--from", "local")
|
||||
v1 = self.cur_release("u2d")
|
||||
self.svc("u2d", "stop")
|
||||
# v1 提交时点 schema=3;库被「新制品」推进到 5
|
||||
rf = self.runtime("u2d") / "releases" / v1 / "release.json"
|
||||
data = json.loads(rf.read_text())
|
||||
data["schema_at_commit"] = "3"
|
||||
rf.write_text(json.dumps(data))
|
||||
self.bump_schema("u2d", 5)
|
||||
fake = self.runtime("u2d") / "releases" / "fake-advance"
|
||||
(fake / "bin").mkdir(parents=True)
|
||||
shutil.copy2(self.die_stub(), fake / "bin" / "agent-system")
|
||||
(self.runtime("u2d") / "current").unlink()
|
||||
(self.runtime("u2d") / "current").symlink_to("releases/fake-advance")
|
||||
(self.runtime("u2d") / "deploy.journal").write_text(json.dumps({
|
||||
"phase": "switched", "new": "fake-advance", "prev": v1,
|
||||
"desired": "running", "ts": "2026-10-12T00:00:00Z"}))
|
||||
|
||||
result = self.svc("u2d", "start", check=False)
|
||||
self.assertNotEqual(result.returncode, 0, result.stdout)
|
||||
self.assertIn("禁止自动回退", result.stdout)
|
||||
self.assertFalse(self.running("u2d"), "阻断后保持停止")
|
||||
j = self.journal("u2d")
|
||||
self.assertEqual(j["phase"], "blocked")
|
||||
# 业务库不被回写:schema 仍是 5
|
||||
import sqlite3
|
||||
con = sqlite3.connect(str(self.runtime("u2d") / "data" / "agent-system.db"))
|
||||
self.assertEqual(
|
||||
con.execute("select max(id) from schema_migrations").fetchone()[0], 5)
|
||||
con.close()
|
||||
# blocked 下普通动作被拒;deploy 是解除路径(此处仅验证拒绝语义)
|
||||
result = self.svc("u2d", "restart", check=False)
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn("切换被阻断", result.stdout)
|
||||
|
||||
# ---- T5:legacy 布局未迁移 → 裸 start/restart 仍走原路径(兼容) ----
|
||||
|
||||
def test_U2_legacy_layout_bare_lifecycle_still_works(self):
|
||||
self.seed_agent("u2e", 9465)
|
||||
self.start_ok("u2e")
|
||||
self.assertFalse((self.runtime("u2e") / "current").exists())
|
||||
self.assertTrue((self.runtime("u2e") / "bin" / "agent-system").exists())
|
||||
self.svc("u2e", "stop")
|
||||
self.start_ok("u2e")
|
||||
self.assertTrue(self.running("u2e"))
|
||||
self.svc("u2e", "stop")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
+1
-1
@@ -604,7 +604,7 @@ class TaskLifecycleTests(unittest.TestCase):
|
||||
self.assertTrue((wt / ".task/feature-new.md").is_file())
|
||||
card = (wt / ".task/feature-new.md").read_text()
|
||||
self.assertIn("make precheck", card)
|
||||
self.assertIn("交付模式:manual(逐阶段人工评审)", card)
|
||||
self.assertIn("交付模式:manual(前序人工决策,智能体 Review/合入)", card)
|
||||
self.assertIn("authorized-continuous", card)
|
||||
self.assertIn("流程依据:仓库根目录 policy/delivery.md", card)
|
||||
for field in ("授权范围", "授权来源", "授权终点", "停止条件"):
|
||||
|
||||
Reference in New Issue
Block a user