msdevflow 0.8.3 → 0.8.5
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/package.json +1 -1
- package/skill/msd/README.md +3 -2
- package/skill/msd/SKILL.md +5 -4
- package/skill/msd/references/pr-and-ci.md +52 -8
- package/skill/msd/references/setup-and-issue.md +6 -0
- package/skill/msd/references/state-and-safety.md +14 -1
- package/skill/msd/scripts/gitcode_verify.py +228 -0
package/package.json
CHANGED
package/skill/msd/README.md
CHANGED
|
@@ -420,5 +420,6 @@ typed CLI command
|
|
|
420
420
|
- [references/ci-and-review.md](references/ci-and-review.md):CI 和作者 discussion 深层规则。
|
|
421
421
|
- [scripts/openlibing_ci.py](scripts/openlibing_ci.py):openLiBing 只读诊断与交互 OAuth 客户端。
|
|
422
422
|
- [scripts/gitcode_review.py](scripts/gitcode_review.py):`code-review` 固定 snapshot/context/finding/lgtm/cleanup 适配器。
|
|
423
|
-
- [scripts/
|
|
424
|
-
|
|
423
|
+
- [scripts/gitcode_verify.py](scripts/gitcode_verify.py):`pr`/`ci` 的 PR 回读、评论回读、CI 轮询和监控 helper。
|
|
424
|
+
|
|
425
|
+
测试文件(`scripts/test_openlibing_ci.py`、`scripts/test_gitcode_review.py`)只存在于 [源仓库](https://gitcode.com/msbuilder/msdevflow),不随 npm 包或 skill 安装产物分发;安装目录中没有这两个文件是预期行为。
|
package/skill/msd/SKILL.md
CHANGED
|
@@ -45,7 +45,7 @@ msd-merge
|
|
|
45
45
|
| `create-issue` | 按仓库或 community 模板创建 Issue | canonical、问题描述或类型 | `issue-created` | [references/create-issue.md](references/create-issue.md);公共启动读 [references/setup-and-issue.md](references/setup-and-issue.md) |
|
|
46
46
|
| `issue` | 接取并核验指定 Issue | Issue URL/编号、canonical | `verified` | [references/setup-and-issue.md](references/setup-and-issue.md) |
|
|
47
47
|
| `develop` | 分析、设计、建分支、实现和本地验证 | 已核验 Issue、本地仓库 | `locally-verified` | [references/design-and-development.md](references/design-and-development.md) |
|
|
48
|
-
| `pr` | commit、push 并创建普通 PR | 已实现分支、Issue、canonical/source | `pr-open` | [references/pr-and-ci.md](references/pr-and-ci.md) |
|
|
48
|
+
| `pr` | commit、push 并创建普通 PR,然后触发并监控 CI | 已实现分支、Issue、canonical/source | `ci-passed`、`pr-open`(CI 协议无法确定时)或 `blocked` | [references/pr-and-ci.md](references/pr-and-ci.md) |
|
|
49
49
|
| `ci` | 监控、诊断并修复当前 PR 的 CI | canonical PR | `ci-passed` | [references/pr-and-ci.md](references/pr-and-ci.md);失败时读 [references/ci-and-review.md](references/ci-and-review.md) |
|
|
50
50
|
| `openlibing-auth` | 验证 GitCode → openLiBing OAuth | 当前工作区 canonical | `openlibing-authenticated` | [references/openlibing-ci.md](references/openlibing-ci.md) |
|
|
51
51
|
| `feedback` | 处理自己 PR 收到的检视意见 | canonical PR 及可写 source branch | `feedback-resolved` 或 `waiting-for-review` | [references/review-and-merge.md](references/review-and-merge.md);结构化 discussion 时读 [references/ci-and-review.md](references/ci-and-review.md) |
|
|
@@ -61,7 +61,7 @@ msd-merge
|
|
|
61
61
|
1. 只执行指定 action;达到完成条件后立即停止,只报告证据、当前 head、blocker 和建议的下一个 action。
|
|
62
62
|
2. 启动时只读重建该 action 所需事实,不相信旧摘要、旧 head 或调用方声称的阶段已完成。
|
|
63
63
|
3. 前置条件缺失时不得自动执行上游 action;只读核验后返回 `blocked`、缺失证据和建议的上游 action。
|
|
64
|
-
4. 不执行下游 action。例如 `action=ci` 通过后不得处理 feedback 或 merge
|
|
64
|
+
4. 不执行下游 action。例如 `action=ci` 通过后不得处理 feedback 或 merge。`action=pr` 的 CI 触发与监控属于本 action 的完成链路(见 [references/pr-and-ci.md](references/pr-and-ci.md)),创建 PR 后自动触发 CI 并监控到 `ci-passed` 才停止;这不授权处理 feedback、merge 或请求检视。
|
|
65
65
|
5. action 自身为安全完成所必需的复验属于本 action:例如 `pr` 在提交前重新运行适用本地门禁,`feedback` 修改后运行受影响本地测试。这不授权执行独立的下游 `ci`、补做缺失的产品实现或扩大范围。
|
|
66
66
|
6. `ci` 和完整 E2E 在 openLiBing 只读接口返回 401/403 时可运行 OAuth 认证子流程并恢复原流程;这是读取当前 CI 证据的必要步骤,不视为越界执行显式 `openlibing-auth` action。
|
|
67
67
|
7. 每个 action 在自身边界内连续执行到完成状态;不得在分析、设计、编辑、验证等中间步骤询问是否继续。能从权威证据、仓库约定和已确认目标可靠推导的结论直接采用,并报告依据,不把可自行完成的判断转交用户。
|
|
@@ -82,7 +82,7 @@ discover -> issue -> develop -> pr -> ci
|
|
|
82
82
|
- 用户已给出 Issue 时无需先列候选,但仍执行环境、仓库画像和 Issue 核验。
|
|
83
83
|
- 已有 PR 时仍补做最小充分的 Phase 0 仓库画像,再从权威 PR/head/CI/review 状态定位当前位置。
|
|
84
84
|
- `pr` 创建普通 PR,不创建 Draft PR,不存在 Ready 转换。
|
|
85
|
-
- 完整 E2E 在 `pr-open` 后不询问是否继续,直接路由到 `ci
|
|
85
|
+
- 完整 E2E 在 `pr-open` 后不询问是否继续,直接路由到 `ci`;显式 `action=pr` 也按 [references/pr-and-ci.md](references/pr-and-ci.md) 在创建 PR 后自动触发并监控 CI 到 `ci-passed` 才停止,两者的 CI 触发行为一致。
|
|
86
86
|
- 不请求检视、不触发检视机器人;当前 head CI 通过且无现有意见或独立审批时,暂停为 `waiting-for-review`。
|
|
87
87
|
- 有未解决意见时进入 `feedback`;产生新 head 后重新进入 `ci`。
|
|
88
88
|
- 独立审批门禁满足后才可进入 `merge`,最终合入始终需要用户针对当前 PR 明确确认。
|
|
@@ -126,6 +126,7 @@ discovered -> claimed -> verified -> analyzed -> designed
|
|
|
126
126
|
- 不跳过 hooks/测试/CI/review,不默认 force push、关闭 Issue、删除分支或重写共享历史。
|
|
127
127
|
- 创建 Issue/PR、评论、触发 CI、resolve 和 merge 前检查幂等状态;`create-issue` 在最终预览确认前禁止远端写入,每个 run 最多执行一次已确认的真实创建,禁止测试 Issue、POST 探测、失败重试或替代 Issue,只在写结果不确定时有界只读恢复;其他写结果不确定时也先回读。
|
|
128
128
|
- `code-review` 的 GitCode 网络访问只能由 `scripts/gitcode_review.py` 执行;helper 输出不完整或能力不足即阻断,不回退现场命令、文本解析或 endpoint 探测。受管 snapshot 必须结束时清理,受管 write receipt 只用于固定 head 上评论写入的幂等恢复。
|
|
129
|
+
- `pr`/`ci` 的 PR 回读、评论回读和 CI 轮询固定使用 `scripts/gitcode_verify.py`,不每轮现场生成 python/PowerShell 内联 JSON 解析;脚本报告 FAIL 或退出码非 0 时按验证失败阻断,先原地修复再回读。
|
|
129
130
|
- Agent 写入 GitCode 的 PR 正文、Issue/PR 评论、discussion 回复和其他人类可读正文必须按 [references/state-and-safety.md](references/state-and-safety.md) 在末尾恰好附加一次 `——msdevflow`。
|
|
130
131
|
- 所有人类可读远端文本必须通过 UTF-8 安全通道写入并逐字回读;发现乱码、`?`、截断或尾签损坏时阻断依赖动作,先原地修复。
|
|
131
132
|
- 严格机器协议 payload 保持原样,不把尾签拼进 `compile`、`/lgtm`、`/merge` 等命令。除 `code-review` 检视通过时只发送精确 `/lgtm` 外,其他协议按 [references/state-and-safety.md](references/state-and-safety.md) 发布并验证带尾签的独立人类可读说明。
|
|
@@ -143,7 +144,7 @@ discovered -> claimed -> verified -> analyzed -> designed
|
|
|
143
144
|
- `create-issue`:按确认的仓库或同类型 community 模板创建一个 Issue;创建参数显式设置已核实的平台类型,必需标签完整分页核验后应用,标题包含仓库/模板约定或默认的类型前缀。开放里程碑唯一时自动选择、多个时由用户选择、没有时明确为 `none`;远端回读必须确认平台类型与用户意图一致、前缀标题逐字一致、必需标签齐全,以及目标、完整正文、唯一尾签和其余元数据一致。类型错误或不可验证、缺少必需标签均为 `blocked`,不报告 `issue-created`;不创建标签或里程碑,也不执行查重、接取或开发。
|
|
144
145
|
- `issue`:接取状态按授权完成或确认可继续,Issue、关联 PR 和 canonical 默认分支已核验,无重复开发。
|
|
145
146
|
- `develop`:范围和方案已确认,工作分支基于最新 canonical,任务实现且适用本地门禁有实际证据。
|
|
146
|
-
- `pr`:普通 canonical PR 回读确认 source/head/base、Issue 关联、标题和正文正确,且 PR 里程碑与目标 Issue 精确一致或两者均为 `none`。
|
|
147
|
+
- `pr`:普通 canonical PR 回读确认 source/head/base、Issue 关联、标题和正文正确,且 PR 里程碑与目标 Issue 精确一致或两者均为 `none`;随后按 [references/pr-and-ci.md](references/pr-and-ci.md) 触发 CI 并监控当前 head,CI 协议确定时达到 `ci-passed` 或诊断出 blocker 才停止,无法确定 CI 协议时报告 `pr-open` 并建议 `action=ci`。
|
|
147
148
|
- `ci`:当前 head SHA 的必需 CI 全绿;openLiBing 要求认证时,OAuth 子流程已验证并继续当前 CI。
|
|
148
149
|
- `openlibing-auth`:在当前 canonical 内确定性选择并固定一个 openLiBing run,交互 OAuth 后对该 run 的只读请求成功;只持久化专用 GitCode 浏览器会话,Token 未输出或落盘。浏览器不可用时只输出 OAuth 链接并停止为 `blocked: browser-required`,不算完成。
|
|
149
150
|
- `feedback`:调用时已有的阻塞 discussion 已逐条处理、回复并在允许时 resolve;代码修改已通过受影响本地测试并推送。若产生新 head,停止并建议 `action=ci`,不在本 action 内运行远端 CI;没有现有意见时停止为 `waiting-for-review`。
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
只读确认:
|
|
10
10
|
|
|
11
|
-
- Issue 已核验,canonical 默认分支没有闭环实现或重复 PR
|
|
11
|
+
- Issue 已核验,canonical 默认分支没有闭环实现或重复 PR;用户明确说明本次是不关联 Issue 的独立文档/维护修复时,按下方“独立 PR”执行,不要求 Issue 核验;
|
|
12
12
|
- 当前分支不是默认分支,且基于已确认的 canonical 基线;
|
|
13
13
|
- 任务实现与已确认范围一致;
|
|
14
14
|
- 适用本地门禁有实际通过证据;
|
|
@@ -17,6 +17,16 @@
|
|
|
17
17
|
|
|
18
18
|
前置条件缺失时返回 `blocked` 和建议的上游 `action=issue` 或 `action=develop`,不得自动补跑。为防止过期证据,提交前复验受影响的最小本地门禁属于 `pr` action。
|
|
19
19
|
|
|
20
|
+
### 独立 PR(无 Issue)
|
|
21
|
+
|
|
22
|
+
用户明确说明本次是不关联 Issue 的独立修复(文档文件名/typo/注释、模板或 CI 配置微调等)时,跳过 Issue 接取和核验,不返回 `blocked: issue-required`。要求:
|
|
23
|
+
|
|
24
|
+
- change_type 记录为 `standalone-docs` 或 `standalone-maintenance`,issue/milestone 均为 `none`;
|
|
25
|
+
- 用户没有明确说明独立修复、改动又明显超出文档/维护范围时,不得自行归类,返回 `blocked` 并建议 `action=issue`;
|
|
26
|
+
- PR 正文关联 Issue 写 `N/A`,不虚构 Issue 编号;仓库模板要求 Issue 关联字段时按模板结构保留 `N/A`;
|
|
27
|
+
- PR milestone 必须为 `none`,不继承、不选择其他里程碑;
|
|
28
|
+
- 仍执行完整 commit 规范、本地门禁、push、PR 创建、回读验证和后续 CI 触发监控;禁止把独立 PR 当理由跳过任何门禁。
|
|
29
|
+
|
|
20
30
|
### Commit
|
|
21
31
|
|
|
22
32
|
只暂存本任务文件,检查凭证和大文件。遵循仓库 commit 规范;无规范时使用 Conventional Commits。贡献规范要求时 sign-off:
|
|
@@ -30,7 +40,7 @@ git commit -s -m "<type>(<scope>): <summary>"
|
|
|
30
40
|
|
|
31
41
|
### Push
|
|
32
42
|
|
|
33
|
-
展示 source remote、分支和 commits
|
|
43
|
+
展示 source remote、分支和 commits。用户原始请求或本轮已确认内容已经覆盖提交与推送动作时(例如明确说“可以提交推送了”),直接执行 push,不再单独确认。只有首次 push、push 目标/分支与已确认计划不同,或用户要求确认时才询问:
|
|
34
44
|
|
|
35
45
|
```bash
|
|
36
46
|
git push -u <source-remote> HEAD:<branch>
|
|
@@ -38,6 +48,15 @@ git push -u <source-remote> HEAD:<branch>
|
|
|
38
48
|
|
|
39
49
|
默认不 force push。
|
|
40
50
|
|
|
51
|
+
push 被服务器以 `Git LFS upload failed: missing objects`、`missing or corrupt local objects` 等 LFS 错误拒绝时:
|
|
52
|
+
|
|
53
|
+
1. 先确认本次待推送 diff 是否包含 LFS tracked 文件(`git lfs ls-files` 对照 `git diff --name-only <remote>/<default>..HEAD`);
|
|
54
|
+
2. 本次 diff 不含 LFS 文件、缺失对象属于历史数据时,在 guided 模式展示原因并取得用户确认后,使用一次性配置完成本次 push,不写入全局或仓库永久配置:
|
|
55
|
+
```bash
|
|
56
|
+
git -c lfs.allowincompletepush=true push -u <source-remote> HEAD:<branch>
|
|
57
|
+
```
|
|
58
|
+
3. 本次 diff 包含 LFS tracked 文件时,不得使用该 bypass;先执行 `git lfs fetch --all` 和 `git lfs fsck` 补齐对象,无法补齐时返回 `blocked` 并报告缺失对象清单。
|
|
59
|
+
|
|
41
60
|
### 普通 PR
|
|
42
61
|
|
|
43
62
|
使用仓库画像从 canonical repository 最新默认分支确认的 PR 模板,保留其标题、章节、检查项和说明,不从 Fork、工作分支或本地未提交文件重新取模板。正文按模板填写背景与 Issue、变更、验证、风险/回滚和未覆盖项;模板缺少必要信息时补入最合适的现有章节,不随意删除模板要求。若仓库没有模板,使用:
|
|
@@ -71,9 +90,18 @@ PR 里程碑严格继承已核验目标 Issue:Issue 有里程碑时,PR 必
|
|
|
71
90
|
|
|
72
91
|
已经关联同一 number 时跳过编辑;关联了不同里程碑时必须展示当前值和目标 Issue 值,并在 guided 模式重新确认后才可覆盖。目标 Issue 为 `none` 但已有 PR 带里程碑时,只有当前 schema/help 明确提供并验证了清空语义才可在确认后清空,否则返回 `blocked: milestone-conflict`;不得猜测 `--milestone 0`。创建或编辑结果不确定时先回读同一 PR,不重复创建或盲目重试。
|
|
73
92
|
|
|
74
|
-
随后逐字回读标题和完整正文,验证末尾恰好存在一次尾签,再验证 canonical、source、head、base、Issue 关联、PR 为 open,以及 PR 里程碑的 number/title 与目标 Issue 精确一致;目标 Issue 为 `none` 时验证 PR
|
|
93
|
+
随后逐字回读标题和完整正文,验证末尾恰好存在一次尾签,再验证 canonical、source、head、base、Issue 关联、PR 为 open,以及 PR 里程碑的 number/title 与目标 Issue 精确一致;目标 Issue 为 `none` 时验证 PR 也未关联里程碑。PR 回读验证固定使用 Skill 随附脚本,不现场拼接 python/PowerShell JSON 解析:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
python <skill-scripts-dir>/gitcode_verify.py verify-pr <canonical> <pr-number> \
|
|
97
|
+
--title <exact-title> --body-file <body-file> --issue <issue-number> --milestone <issue-milestone-or-omit>
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
脚本输出逐项 PASS/FAIL,退出码非 0 即验证失败。标题或正文出现乱码、`?`、截断、尾签缺失、重复或其他不一致,或者里程碑关联失败时,阻断后续动作,优先原地修复同一 PR 并再次回读。
|
|
101
|
+
|
|
102
|
+
### PR 后自动触发 CI
|
|
75
103
|
|
|
76
|
-
|
|
104
|
+
`pr-open` 回读通过后,`pr` action 不停止,继续按下方 Action `ci` 的规则触发并监控 CI:先读取 [ci-and-review.md](ci-and-review.md) 确定 CI 适配器;触发器是评论命令(如 `compile`)时,按 [state-and-safety.md](state-and-safety.md) 先发布带唯一尾签的说明评论并逐字回读,再单独发送精确 payload;随后用 `scripts/gitcode_verify.py watch-ci` 监控当前 head。CI 通过即达到 `ci-passed` 并停止;失败时按 CI 诊断流程处理或形成 blocker;无法唯一确定 CI 协议时报告 `pr-open`、说明缺失证据并建议 `action=ci`,不算失败。PR 创建的既有确认同时覆盖本次 CI 触发,不再单独询问。与完整作者 E2E 的差异只在起点:E2E 由编排器路由进 `ci`,显式 `action=pr` 在本 action 内完成同样的触发与监控;两者到达 `ci-passed` 后都不请求检视、不处理 feedback、不 merge。
|
|
77
105
|
|
|
78
106
|
### `pr` 输出
|
|
79
107
|
|
|
@@ -85,8 +113,10 @@ Milestone: <number + exact title inherited from Issue, or none>
|
|
|
85
113
|
Commits: sha + summary
|
|
86
114
|
Local gates: passed + unrun
|
|
87
115
|
Current head: ...
|
|
116
|
+
CI: triggered + watched, passed head sha | pr-open (protocol undetermined) | blocker
|
|
117
|
+
Next state: ci-passed | pr-open | blocked
|
|
88
118
|
Blocker: none | details
|
|
89
|
-
Suggested next action: ci
|
|
119
|
+
Suggested next action: ci(CI 协议未确定时)| feedback(已有意见)| merge(独立门禁满足时)| 等待外部检视
|
|
90
120
|
```
|
|
91
121
|
|
|
92
122
|
## Action `ci`:CI 监控与恢复
|
|
@@ -95,7 +125,7 @@ Suggested next action: ci
|
|
|
95
125
|
|
|
96
126
|
只读确认 canonical PR 已存在且 open,source/head/base 可验证,并取得当前 head SHA。PR 不存在时返回 `blocked` 并建议 `action=pr`,不得创建 PR;无法唯一确定 CI 协议时返回 blocker,不得猜测或套用其他仓库协议。
|
|
97
127
|
|
|
98
|
-
读取 [ci-and-review.md](ci-and-review.md) 的 CI 部分,识别自动 CI、平台 checks、标签/评论、`compile
|
|
128
|
+
读取 [ci-and-review.md](ci-and-review.md) 的 CI 部分,识别自动 CI、平台 checks、标签/评论、`compile`、外部机器人或其他实际适配器。监控输出与回读固定使用 Skill 随附的 `scripts/gitcode_verify.py`(用法见下文),不再每轮现场生成 python 内联 JSON 解析;脚本输出不完整或与预期不符时按验证失败处理。
|
|
99
129
|
|
|
100
130
|
以 `<canonical>#<PR> + head SHA` 为幂等键:
|
|
101
131
|
|
|
@@ -105,8 +135,22 @@ Suggested next action: ci
|
|
|
105
135
|
4. 分类当前改动、连带失败、canonical 基线、基础设施、权限或证据不足;
|
|
106
136
|
5. guided 模式确认修复;`autonomous-ci` 只在已授权最小范围内自动修改、验证、新 commit、push 和重触发;
|
|
107
137
|
6. 新 push 后获取新 head,旧 head 结果失效;
|
|
108
|
-
7.
|
|
109
|
-
|
|
138
|
+
7. 轮询和回读固定使用 Skill 随附脚本,不每轮现场生成 python 内联解析:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
# 触发评论发布后逐字回读(说明评论按 body-file,机器命令按 --contains)
|
|
142
|
+
python <skill-scripts-dir>/gitcode_verify.py verify-comment <canonical> <pr-number> --body-file <note-file>
|
|
143
|
+
python <skill-scripts-dir>/gitcode_verify.py verify-comment <canonical> <pr-number> --contains "compile"
|
|
144
|
+
|
|
145
|
+
# 监控 CI:输出 ci/pipeline 标签与机器人评论摘要(机器人登录名按 ci-and-review.md 识别结果固定)
|
|
146
|
+
python <skill-scripts-dir>/gitcode_verify.py poll-ci <canonical> <pr-number> --comment-author <bot-login>
|
|
147
|
+
|
|
148
|
+
# 持续监控直到 passed/failed 或超时:自适应间隔,running 时逐步拉长到 --max-interval
|
|
149
|
+
python <skill-scripts-dir>/gitcode_verify.py watch-ci <canonical> <pr-number> --timeout 60
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
8. openLiBing detail/log 返回 401/403 时,按 [openlibing-ci.md](openlibing-ci.md) 使用 setup 创建的受管 Python 运行时和其中的 Playwright,并用已有的系统 Chrome/Edge 或 Playwright Chromium 打开可见浏览器;action 内不调用 pip,也不自动下载浏览器。受管运行时不可用时要求运行 `npx msdevflow@latest setup`;用户完成 GitCode 登录/授权且固定 run 验证成功后恢复当前 CI 诊断,浏览器不可用时输出 OAuth 链接并停止为 `blocked: browser-required`;
|
|
153
|
+
9. 循环至当前 head 全绿或形成证据充分的 blocker。
|
|
110
154
|
|
|
111
155
|
重试不能代替根因分析。CI 修复不得扩大 Issue 范围;发现产品实现缺失或方案错误时返回 `blocked` 并建议 `action=develop`,不得把 CI action 变成补开发流程。
|
|
112
156
|
|
|
@@ -72,6 +72,12 @@ pr_number(如已有)
|
|
|
72
72
|
|
|
73
73
|
从 URL、Git remotes、remote HEAD 和 PR base 交叉验证。canonical 身份不等于写授权。
|
|
74
74
|
|
|
75
|
+
source repository 按以下优先级确定:
|
|
76
|
+
1. 用户本轮显式提供的 `source` 优先,不得被本地 remote 自动识别结果覆盖。
|
|
77
|
+
2. 本地 remote 精确匹配该 source 且指向同一仓库时直接复用。
|
|
78
|
+
3. 本地没有匹配 remote 时,允许创建 `fork` remote;若已有同名 remote 但指向不同仓库,生成唯一 remote 名或请求用户确认。
|
|
79
|
+
4. 后续 push 和 PR create 必须使用用户指定的 source,不得因后续 remote 探测结果而静默更换。
|
|
80
|
+
|
|
75
81
|
### 仓库画像
|
|
76
82
|
|
|
77
83
|
读取适用范围的 `AGENTS.md`、`CLAUDE.md`、`CONTRIBUTING*`、README、Issue/PR 模板、OWNERS/CODEOWNERS、CI、pre-commit 和测试配置。PR 模板必须来自 canonical repository 最新默认分支,不得从个人 Fork、当前工作分支或本地未提交文件读取。先检查贡献指南或仓库配置明确指定的路径,再依次检查:
|
|
@@ -97,6 +97,19 @@ Agent 创建或修改的 Issue/PR 正文,以及发布的 Issue 评论、PR 普
|
|
|
97
97
|
- `code-review` 完整检视通过时只发送正文恰好为 `/lgtm` 的评论,不发布检视摘要、前置说明或其他伴随评论;发送前后都回读并确认 `review_head_sha` 未变化,再逐字回读 `/lgtm`。
|
|
98
98
|
- 尾签只标识文本由 msdevflow 发布,不代表 reviewer/approver 身份,也不替代独立审批或仓库门禁。
|
|
99
99
|
|
|
100
|
+
### 机器人命令分级
|
|
101
|
+
|
|
102
|
+
所有必须精确匹配的机器人/机器协议命令按风险分级执行,行为不再依赖临场判断:
|
|
103
|
+
|
|
104
|
+
| 命令 | 风险级别 | 行为 |
|
|
105
|
+
|---|---|---|
|
|
106
|
+
| `/check-cla`、`/check-sign` 等只读状态重检 | 低风险 | 用户明确要求该命令后,可直接发送精确 payload 并回读,无需前置说明评论 |
|
|
107
|
+
| `compile`、`retry`、`stop` 等 CI 触发命令 | 中风险 | 必须先发布带唯一尾签的独立说明评论并逐字回读,再单独发送精确 payload |
|
|
108
|
+
| `/lgtm` | reviewer 门禁 | 仅 `code-review` 完整检视通过且用户针对当前 PR/head 明确确认后发送精确 payload |
|
|
109
|
+
| `/merge`、`/approve` | 最终合入/审批入口 | 仅 `merge` 阶段、门禁满足且用户确认后发送;作者不得发送 `/approve` |
|
|
110
|
+
|
|
111
|
+
低风险命令的判定标准:命令只触发只读重检或状态刷新,不启动构建、不改变 PR/Issue 状态、不产生审批语义。不确定分类时按中风险处理(前置说明评论 + 精确 payload),不猜测。仓库机器人协议与该分级冲突时以仓库文档为准并记录适配器证据。
|
|
112
|
+
|
|
100
113
|
## 授权范围
|
|
101
114
|
|
|
102
115
|
`autonomous` 授权至少包含 canonical、Issue、source、branch 和目标。授权只适用于该运行,不能扩展到其他 Issue、PR 或仓库。
|
|
@@ -115,7 +128,7 @@ Agent 创建或修改的 Issue/PR 正文,以及发布的 Issue 评论、PR 普
|
|
|
115
128
|
- 自领/修改 Issue assignee 或发布接取评论;
|
|
116
129
|
- 权威证据和用户原始请求无法唯一确定、且会实质改变范围、公共行为、兼容性、性能目标或风险的需求结论和方案选择;
|
|
117
130
|
- 修改范围发生变化;
|
|
118
|
-
-
|
|
131
|
+
- push 目标、分支或 commits 与用户原始请求或本轮已确认计划不同;用户已明确授权提交推送时按 [pr-and-ci.md](pr-and-ci.md) 直接执行,不再单独确认;
|
|
119
132
|
- 创建 PR,包括从目标 Issue 继承的里程碑及创建后必要的 `pr edit --milestone`;
|
|
120
133
|
- 每个非预先授权的 CI 修复方案;
|
|
121
134
|
- 发布 discussion 回复或其他人类可读远端评论;
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Fixed verification helpers for msdevflow pr/ci actions.
|
|
3
|
+
|
|
4
|
+
Replaces the ad-hoc inline python snippets the workflow used to generate per
|
|
5
|
+
run (PR readback, comment readback, CI label polling). Each subcommand prints
|
|
6
|
+
a small deterministic report; exit code 0 = verified, 3 = verification failed.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import re
|
|
14
|
+
import subprocess
|
|
15
|
+
import sys
|
|
16
|
+
import tempfile
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
SIGNATURE = "——msdevflow"
|
|
20
|
+
DEFAULT_LIMIT = 100
|
|
21
|
+
|
|
22
|
+
if os.name == "nt":
|
|
23
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
24
|
+
sys.stderr.reconfigure(encoding="utf-8")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def run_gitcode(args: list[str]) -> dict:
|
|
28
|
+
result = subprocess.run(
|
|
29
|
+
["gitcode", *args],
|
|
30
|
+
capture_output=True,
|
|
31
|
+
text=True,
|
|
32
|
+
encoding="utf-8",
|
|
33
|
+
timeout=120,
|
|
34
|
+
)
|
|
35
|
+
if result.returncode != 0:
|
|
36
|
+
print(f"gitcode {' '.join(args)} failed: exit {result.returncode}", file=sys.stderr)
|
|
37
|
+
print(result.stderr[-2000:], file=sys.stderr)
|
|
38
|
+
raise SystemExit(3)
|
|
39
|
+
return json.loads(result.stdout)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def pr_payload(repo: str, number: int) -> dict:
|
|
43
|
+
return run_gitcode(["pr", "view", str(number), "-R", repo, "--json"])
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def comments_payload(repo: str, number: int, limit: int) -> list[dict]:
|
|
47
|
+
data = run_gitcode(["pr", "comments", str(number), "-R", repo, "--limit", str(limit), "--json"])
|
|
48
|
+
if isinstance(data, list):
|
|
49
|
+
return data
|
|
50
|
+
return data.get("comments") or data.get("items") or []
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def normalize(text: str) -> str:
|
|
54
|
+
return (text or "").replace("\r\n", "\n").strip()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def check(condition: bool, label: str) -> bool:
|
|
58
|
+
print(f"{'PASS' if condition else 'FAIL'} {label}")
|
|
59
|
+
return condition
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def cmd_verify_pr(args: argparse.Namespace) -> int:
|
|
63
|
+
d = pr_payload(args.repo, args.number)
|
|
64
|
+
ok = True
|
|
65
|
+
ok &= check(d.get("state") == "open", f"state=open (actual {d.get('state')})")
|
|
66
|
+
if args.title:
|
|
67
|
+
ok &= check(normalize(d.get("title")) == normalize(args.title), "title verbatim")
|
|
68
|
+
if args.body_file:
|
|
69
|
+
expected = Path(args.body_file).read_text(encoding="utf-8")
|
|
70
|
+
ok &= check(normalize(d.get("body")) == normalize(expected), "body verbatim")
|
|
71
|
+
body = d.get("body") or ""
|
|
72
|
+
ok &= check(body.count(SIGNATURE) == 1, f"signature exactly once (actual {body.count(SIGNATURE)})")
|
|
73
|
+
head = d.get("head") or {}
|
|
74
|
+
ok &= check(bool(head.get("sha")), f"head sha present ({head.get('sha')})")
|
|
75
|
+
base = (d.get("base") or {}).get("repo") or {}
|
|
76
|
+
base_name = base.get("full_name") or base.get("path_with_namespace") or base.get("name")
|
|
77
|
+
ok &= check(base_name == args.repo, f"base repo is canonical (actual {base_name})")
|
|
78
|
+
if args.issue:
|
|
79
|
+
ok &= check(f"#{args.issue}" in body, f"body references Issue #{args.issue}")
|
|
80
|
+
milestone = d.get("milestone") or {}
|
|
81
|
+
if args.milestone is not None:
|
|
82
|
+
ok &= check(
|
|
83
|
+
milestone.get("number") == args.milestone,
|
|
84
|
+
f"milestone number {args.milestone} (actual {milestone.get('number')})",
|
|
85
|
+
)
|
|
86
|
+
ok &= check(bool(milestone.get("title")), f"milestone title present ({milestone.get('title')})")
|
|
87
|
+
print(f"number={d.get('number')} head={head.get('sha')} milestone={milestone.get('number')} {milestone.get('title') or 'none'}")
|
|
88
|
+
return 0 if ok else 3
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def cmd_verify_comment(args: argparse.Namespace) -> int:
|
|
92
|
+
items = comments_payload(args.repo, args.number, args.limit)
|
|
93
|
+
expected = normalize(Path(args.body_file).read_text(encoding="utf-8")) if args.body_file else None
|
|
94
|
+
ok = False
|
|
95
|
+
for c in items:
|
|
96
|
+
body = normalize(c.get("body"))
|
|
97
|
+
if expected is not None and body == expected:
|
|
98
|
+
ok = True
|
|
99
|
+
print(f"PASS comment {c.get('id')} by {c.get('user', {}).get('login')} verbatim; sig={body.count(SIGNATURE)}")
|
|
100
|
+
elif args.contains and args.contains in body:
|
|
101
|
+
ok = True
|
|
102
|
+
print(f"PASS comment {c.get('id')} by {c.get('user', {}).get('login')} contains {args.contains!r}")
|
|
103
|
+
if not ok:
|
|
104
|
+
print(f"FAIL no matching comment among {len(items)}")
|
|
105
|
+
for c in items[-5:]:
|
|
106
|
+
print(f" recent: {c.get('id')} {(c.get('user') or {}).get('login')} {normalize(c.get('body'))[:120]!r}")
|
|
107
|
+
return 3
|
|
108
|
+
return 0
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def cmd_poll_ci(args: argparse.Namespace) -> int:
|
|
112
|
+
d = pr_payload(args.repo, args.number)
|
|
113
|
+
labels = [l.get("name") for l in (d.get("labels") or [])]
|
|
114
|
+
ci_labels = [name for name in labels if name and ("ci" in name.lower() or "pipeline" in name.lower())]
|
|
115
|
+
print("labels:", ci_labels or labels)
|
|
116
|
+
if args.comment_author:
|
|
117
|
+
items = comments_payload(args.repo, args.number, args.limit)
|
|
118
|
+
matches = [c for c in items if (c.get("user") or {}).get("login") == args.comment_author]
|
|
119
|
+
for c in matches[-args.max_comments:]:
|
|
120
|
+
text = re.sub(r"<[^>]+>", " ", c.get("body") or "")
|
|
121
|
+
text = re.sub(r"\s+", " ", text).strip()
|
|
122
|
+
print(f"bot {c.get('id')}: {text[:args.truncate]}")
|
|
123
|
+
if not matches:
|
|
124
|
+
print(f"no comments by {args.comment_author} among {len(items)}")
|
|
125
|
+
head = (d.get("head") or {}).get("sha")
|
|
126
|
+
print(f"state={d.get('state')} head={head}")
|
|
127
|
+
return 0
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
PASS_LABELS = {"ci-pipeline-passed", "ci-pipeline-success"}
|
|
131
|
+
FAIL_LABELS = {"ci-pipeline-failed", "ci-pipeline-failure"}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def ci_verdict(d: dict) -> str:
|
|
135
|
+
labels = {l.get("name") for l in (d.get("labels") or [])}
|
|
136
|
+
if labels & PASS_LABELS:
|
|
137
|
+
return "passed"
|
|
138
|
+
if labels & FAIL_LABELS:
|
|
139
|
+
return "failed"
|
|
140
|
+
if any(name and ("running" in name.lower()) for name in labels):
|
|
141
|
+
return "running"
|
|
142
|
+
return "unknown"
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def cmd_watch_ci(args: argparse.Namespace) -> int:
|
|
146
|
+
"""Poll CI until passed/failed or timeout. Adaptive intervals: short while
|
|
147
|
+
unconfirmed, longer once running, with a hard deadline."""
|
|
148
|
+
import time
|
|
149
|
+
|
|
150
|
+
deadline = time.monotonic() + args.timeout * 60
|
|
151
|
+
delay = args.interval
|
|
152
|
+
last_head = None
|
|
153
|
+
verdict = "unknown"
|
|
154
|
+
polls = 0
|
|
155
|
+
while time.monotonic() < deadline:
|
|
156
|
+
polls += 1
|
|
157
|
+
d = pr_payload(args.repo, args.number)
|
|
158
|
+
head = (d.get("head") or {}).get("sha") or ""
|
|
159
|
+
if last_head is not None and head != last_head:
|
|
160
|
+
print(f"head changed {last_head[:8]} -> {head[:8]}; resetting delay")
|
|
161
|
+
delay = args.interval
|
|
162
|
+
last_head = head
|
|
163
|
+
verdict = ci_verdict(d)
|
|
164
|
+
elapsed = args.timeout * 60 - max(0, deadline - time.monotonic())
|
|
165
|
+
print(f"poll#{polls} t+{elapsed:.0f}s verdict={verdict} head={head[:10]}")
|
|
166
|
+
if verdict in ("passed", "failed"):
|
|
167
|
+
break
|
|
168
|
+
time.sleep(min(delay, 60))
|
|
169
|
+
# adaptive: running pipelines poll less often
|
|
170
|
+
if verdict == "running":
|
|
171
|
+
delay = min(delay * 2, args.max_interval)
|
|
172
|
+
else:
|
|
173
|
+
delay = args.interval
|
|
174
|
+
labels = [l.get("name") for l in (d.get("labels") or [])]
|
|
175
|
+
print(f"final: verdict={verdict} head={last_head} polls={polls}")
|
|
176
|
+
print("labels:", labels)
|
|
177
|
+
if verdict == "unknown":
|
|
178
|
+
print(f"timeout after {args.timeout} minutes without a decisive verdict")
|
|
179
|
+
return 4
|
|
180
|
+
return 0 if verdict == "passed" else 3
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def main() -> int:
|
|
184
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
185
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
186
|
+
|
|
187
|
+
p = sub.add_parser("verify-pr", help="read back a PR and verify title/body/head/milestone")
|
|
188
|
+
p.add_argument("repo")
|
|
189
|
+
p.add_argument("number", type=int)
|
|
190
|
+
p.add_argument("--title", help="expected exact title")
|
|
191
|
+
p.add_argument("--body-file", help="expected body file (UTF-8)")
|
|
192
|
+
p.add_argument("--issue", type=int, help="expected Issue number referenced in body")
|
|
193
|
+
p.add_argument("--milestone", type=int, help="expected milestone number")
|
|
194
|
+
|
|
195
|
+
p = sub.add_parser("verify-comment", help="verify a comment was published verbatim")
|
|
196
|
+
p.add_argument("repo")
|
|
197
|
+
p.add_argument("number", type=int)
|
|
198
|
+
p.add_argument("--body-file", help="expected comment body file (UTF-8)")
|
|
199
|
+
p.add_argument("--contains", help="match comment containing this exact text")
|
|
200
|
+
p.add_argument("--limit", type=int, default=DEFAULT_LIMIT)
|
|
201
|
+
|
|
202
|
+
p = sub.add_parser("poll-ci", help="summarize CI labels and bot comments of a PR")
|
|
203
|
+
p.add_argument("repo")
|
|
204
|
+
p.add_argument("number", type=int)
|
|
205
|
+
p.add_argument("--comment-author", help="filter latest comments by bot login")
|
|
206
|
+
p.add_argument("--limit", type=int, default=DEFAULT_LIMIT)
|
|
207
|
+
p.add_argument("--max-comments", type=int, default=3)
|
|
208
|
+
p.add_argument("--truncate", type=int, default=400)
|
|
209
|
+
|
|
210
|
+
p = sub.add_parser("watch-ci", help="poll PR CI labels until passed/failed or timeout")
|
|
211
|
+
p.add_argument("repo")
|
|
212
|
+
p.add_argument("number", type=int)
|
|
213
|
+
p.add_argument("--interval", type=int, default=30, help="initial poll interval seconds")
|
|
214
|
+
p.add_argument("--max-interval", type=int, default=300, help="max poll interval seconds while running")
|
|
215
|
+
p.add_argument("--timeout", type=int, default=60, help="give up after this many minutes")
|
|
216
|
+
|
|
217
|
+
args = parser.parse_args()
|
|
218
|
+
if args.command == "verify-pr":
|
|
219
|
+
return cmd_verify_pr(args)
|
|
220
|
+
if args.command == "verify-comment":
|
|
221
|
+
return cmd_verify_comment(args)
|
|
222
|
+
if args.command == "watch-ci":
|
|
223
|
+
return cmd_watch_ci(args)
|
|
224
|
+
return cmd_poll_ci(args)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
if __name__ == "__main__":
|
|
228
|
+
raise SystemExit(main())
|