msdevflow 0.8.5 → 0.8.6

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "msdevflow",
3
- "version": "0.8.5",
3
+ "version": "0.8.6",
4
4
  "description": "Install the msdevflow GitCode skill and its runtime dependencies",
5
5
  "type": "module",
6
6
  "bin": {
@@ -421,5 +421,6 @@ typed CLI command
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
423
  - [scripts/gitcode_verify.py](scripts/gitcode_verify.py):`pr`/`ci` 的 PR 回读、评论回读、CI 轮询和监控 helper。
424
+ - [scripts/gitcode_issue.py](scripts/gitcode_issue.py):`create-issue` 的 schema/标签/里程碑/模板批量只读探测和 Issue 回读 helper。
424
425
 
425
426
  测试文件(`scripts/test_openlibing_ci.py`、`scripts/test_gitcode_review.py`)只存在于 [源仓库](https://gitcode.com/msbuilder/msdevflow),不随 npm 包或 skill 安装产物分发;安装目录中没有这两个文件是预期行为。
@@ -127,6 +127,7 @@ discovered -> claimed -> verified -> analyzed -> designed
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
129
  - `pr`/`ci` 的 PR 回读、评论回读和 CI 轮询固定使用 `scripts/gitcode_verify.py`,不每轮现场生成 python/PowerShell 内联 JSON 解析;脚本报告 FAIL 或退出码非 0 时按验证失败阻断,先原地修复再回读。
130
+ - `create-issue` 的 schema 探测、标签/里程碑分页和创建后回读固定使用 `scripts/gitcode_issue.py prepare` / `inspect`,不逐条现场调用只读 CLI 命令;批量只读探测走单次 `python` 调用,避免每条命令单独触发权限确认。
130
131
  - Agent 写入 GitCode 的 PR 正文、Issue/PR 评论、discussion 回复和其他人类可读正文必须按 [references/state-and-safety.md](references/state-and-safety.md) 在末尾恰好附加一次 `——msdevflow`。
131
132
  - 所有人类可读远端文本必须通过 UTF-8 安全通道写入并逐字回读;发现乱码、`?`、截断或尾签损坏时阻断依赖动作,先原地修复。
132
133
  - 严格机器协议 payload 保持原样,不把尾签拼进 `compile`、`/lgtm`、`/merge` 等命令。除 `code-review` 检视通过时只发送精确 `/lgtm` 外,其他协议按 [references/state-and-safety.md](references/state-and-safety.md) 发布并验证带尾签的独立人类可读说明。
@@ -35,7 +35,7 @@ GitCode CLI 版本和命令集合可能不同。工作流必须探测能力,
35
35
  <gitcode-command> schema "pr merge"
36
36
  ```
37
37
 
38
- 若无 schema,对应使用 `<command> --help`。只探测当前 action 所需命令;`create-issue` 至少需要 `milestone list` 的显式 repo、JSON 和分页,`label list` 的显式 repo、JSON 和完整分页,`issue create` 的显式 repo、title、body-file、issue-type、JSON 和 dry-run,`issue view` 的 JSON 回读能力,以及 `issue edit` 的显式 repo、label、title、body-file、milestone number、yes 和 JSON。平台 `issue_type` 不在 typed view 输出时须用已验证的同 CLI 单 Issue GET JSON 回读;类型枚举和名称/ID 映射只采用当前官方文档、CLI 实现或仓库类型配置支持的只读路径,不猜 endpoint。`--issue-type` 的 enterprise 标注不是目标仓库支持证据;类型无法明确设置或回读时在创建前阻断,不能省略参数创建默认 Bug。标签完整分页证明必需标签缺失时也必须阻断,不降级为 `none`。官方 npm GitCode CLI 的 typed `issue create --title` 是 `create-issue` 唯一支持的非 ASCII 标题创建通道;不得调用 `api .../issues --method POST` 测试或创建 Issue。`pr` 至少需要读取关联 Issue 和 PR milestone 的 JSON 能力,以及 `pr edit` 的显式 repo、milestone number 和 JSON;typed `pr create` 不支持 milestone 时,必须在同一次已确认的 PR action 中以 `pr edit --milestone` 关联并回读。`openlibing-auth` 至少需要可有界列举 canonical PR、读取 PR head/labels/comments 的 JSON 能力。完整作者 E2E 记录仓库参数(`-R`/`--repo`)、JSON、body-file/comment-file/stdin、dry-run、fork、assignee、discussion/resolve、正文 edit 和标题 edit 能力。涉及人类可读远端文本时,必须确认 [state-and-safety.md](state-and-safety.md) 要求的 UTF-8 文件、stdin 或 `api --input <utf8-json-file>` 安全通道;Windows PowerShell 5.1 下只有 `--body`/`--comment`/`--title` 等字符串参数而入口为 `.cmd` wrapper 时,不把非 ASCII 文本直接放进命令行参数。
38
+ 若无 schema,对应使用 `<command> --help`。只探测当前 action 所需命令;`create-issue` 至少需要 `milestone list` 的显式 repo、JSON 和分页,`label list` 的显式 repo、JSON 和完整分页,`issue create` 的显式 repo、title、body-file、issue-type、JSON 和 dry-run,`issue view` 的 JSON 回读能力,以及 `issue edit` 的显式 repo、label、title、body-file、milestone number、yes 和 JSON。这些只读探测和回读固定通过 `scripts/gitcode_issue.py prepare` / `inspect` 一次完成,不逐条现场调用 schema 和分页命令,避免每条命令单独触发权限确认。平台 `issue_type` 不在 typed view 输出时须用已验证的同 CLI 单 Issue GET JSON 回读;类型枚举和名称/ID 映射只采用当前官方文档、CLI 实现或仓库类型配置支持的只读路径,不猜 endpoint。`--issue-type` 的 enterprise 标注不是目标仓库支持证据;类型无法明确设置或回读时在创建前阻断,不能省略参数创建默认 Bug。标签完整分页证明必需标签缺失时也必须阻断,不降级为 `none`。官方 npm GitCode CLI 的 typed `issue create --title` 是 `create-issue` 唯一支持的非 ASCII 标题创建通道;不得调用 `api .../issues --method POST` 测试或创建 Issue。`pr` 至少需要读取关联 Issue 和 PR milestone 的 JSON 能力,以及 `pr edit` 的显式 repo、milestone number 和 JSON;typed `pr create` 不支持 milestone 时,必须在同一次已确认的 PR action 中以 `pr edit --milestone` 关联并回读。`openlibing-auth` 至少需要可有界列举 canonical PR、读取 PR head/labels/comments 的 JSON 能力。完整作者 E2E 记录仓库参数(`-R`/`--repo`)、JSON、body-file/comment-file/stdin、dry-run、fork、assignee、discussion/resolve、正文 edit 和标题 edit 能力。涉及人类可读远端文本时,必须确认 [state-and-safety.md](state-and-safety.md) 要求的 UTF-8 文件、stdin 或 `api --input <utf8-json-file>` 安全通道;Windows PowerShell 5.1 下只有 `--body`/`--comment`/`--title` 等字符串参数而入口为 `.cmd` wrapper 时,不把非 ASCII 文本直接放进命令行参数。
39
39
 
40
40
  `code-review` 是能力降级顺序的专用例外:Agent 不直接探测或组合 GitCode 命令,只调用 Skill 随附的 `scripts/gitcode_review.py`。setup 必须验收 `issue view`、`pr view`、`pr comments`、`pr comment` 和 `api`;helper 内部只使用 [code-review.md](code-review.md) 固定的数据路径,以 `pulls/<number>/files` 而不是 typed `pr diff --json` 读取完整 changed-files。helper 的 `snapshot`/`context`/`finding`/`lgtm` 返回能力不足、未知 schema 或不完整响应时,停止为 `blocked: review-capability-required` 或 `review-incomplete`;不得退回 raw CLI、网页 raw URL、`curl`、现场 `python -c`/PowerShell JSON、展示文本 grep 或其他 endpoint 试探。
41
41
 
@@ -135,17 +135,22 @@ Duplicate search: not performed by design
135
135
 
136
136
  本 Skill 固定使用 setup 已验收的官方 npm GitCode CLI typed command,不通过 Issue collection API 创建。能力验证只能使用 `schema`、`--help` 和 typed command 自带的 `--dry-run`;`api .../issues --method POST` 没有 dry-run 语义,禁止用于创建、试错或验证。
137
137
 
138
- 使用前只读探测:
138
+ 使用前只读探测固定使用 Skill 随附脚本一次完成,不逐条现场调用 schema 和分页命令:
139
139
 
140
140
  ```bash
141
- <gitcode-command> schema "label list"
142
- <gitcode-command> schema "milestone list"
143
- <gitcode-command> schema "issue create"
144
- <gitcode-command> schema "issue view"
145
- <gitcode-command> schema "issue edit"
141
+ MSDEVFLOW_GITCODE_COMMAND=<gitcode-command> python <skill-scripts-dir>/gitcode_issue.py prepare <canonical> --template-ref <canonical-remote>/<default-branch>
146
142
  ```
147
143
 
148
- 必须具备 `issue create --issue-type`、完整的标签分页、同 Issue 标签/里程碑编辑和平台类型 JSON 回读能力;具体类型的适用性仍按“平台类型”核验,不能只看 flag 是否存在。
144
+ 该命令一次返回:`issue create`/`issue view`/`issue edit`/`label list`/`milestone list` 的 schema flags、全部分页标签(含精确数量,避免把第一页截断误判为“标签不存在”)、开放里程碑列表,以及 `.gitcode` 模板树。缺少 `issue create --issue-type`、完整标签分页或同 Issue 编辑能力时在创建前阻断;具体类型的适用性仍按“平台类型”核验,不能只看 flag 是否存在。
145
+
146
+ 创建后的权威回读同样固定使用脚本一次完成:
147
+
148
+ ```bash
149
+ MSDEVFLOW_GITCODE_COMMAND=<gitcode-command> python <skill-scripts-dir>/gitcode_issue.py inspect <canonical> <issue-number> \
150
+ --expect-type <platform-issue-type> --expect-label <required-label> --expect-milestone <milestone-number> --expect-title "<exact-title>"
151
+ ```
152
+
153
+ 脚本用 typed `issue view` 加单 Issue GET API 读取平台类型、标签、里程碑和正文,逐项输出 PASS/FAIL,退出码非 0 即回读失败。命令本身不创建、编辑或删除任何远端对象;只有脚本报告全部 PASS 才能继续。
149
154
 
150
155
  将最终正文写入唯一的系统临时文件,编码固定为 UTF-8 无 BOM。官方 npm GitCode CLI 的 `issue create --title` 是本 Skill 已验收的中文标题通道;Windows `.cmd` wrapper 也直接使用 typed command,不得为中文标题切换到 API POST。最终预览前必须用固定仓库、带前缀标题、正文文件和已验证的平台类型执行本地 dry-run:
151
156
 
@@ -175,7 +180,7 @@ dry-run 不得产生新 Issue number/URL;否则视为能力异常并停止。
175
180
 
176
181
  ## 回读、恢复与完成
177
182
 
178
- 创建返回 Issue number/URL 后立即回读目标与类型;仅在该门禁通过后应用已确认元数据,随后再次用权威 JSON 完整验证:
183
+ 创建返回 Issue number/URL 后,先用 `gitcode_issue.py inspect` 回读目标与类型;仅在该门禁通过后应用已确认元数据,随后再次运行带 `--expect-*` 的 `inspect` 完整验证:
179
184
 
180
185
  - repository 和 Issue number/URL;
181
186
  - state 为 open;
@@ -0,0 +1,235 @@
1
+ #!/usr/bin/env python3
2
+ """Fixed read-only helpers for msd create-issue.
3
+
4
+ Batches the read-only probes that create-issue otherwise issues as dozens of
5
+ separate CLI calls (schema, full label/milestone pagination, Issue readback),
6
+ so the workflow can invoke one `python` script instead of many gitcode
7
+ commands. Read-only: never creates, edits or deletes anything.
8
+
9
+ Subcommands:
10
+ prepare schema flags + all label pages + all milestone pages + template tree
11
+ inspect read back one Issue via typed view and authoritative GET API
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import argparse
16
+ import json
17
+ import os
18
+ import shutil
19
+ import subprocess
20
+ import sys
21
+ from pathlib import Path
22
+
23
+ if os.name == "nt":
24
+ sys.stdout.reconfigure(encoding="utf-8")
25
+ sys.stderr.reconfigure(encoding="utf-8")
26
+
27
+ DEFAULT_PAGE_SIZE = 100
28
+ GITCODE_COMMAND = os.environ.get("MSDEVFLOW_GITCODE_COMMAND", "gitcode")
29
+
30
+
31
+ def resolve_command() -> str:
32
+ selected = shutil.which(GITCODE_COMMAND)
33
+ if not selected:
34
+ print(f"gitcode command not found: {GITCODE_COMMAND}", file=sys.stderr)
35
+ raise SystemExit(3)
36
+ return selected
37
+
38
+
39
+ def invocation(arguments: list[str]) -> list[str]:
40
+ exe = resolve_command()
41
+ if os.name == "nt" and Path(exe).suffix.lower() in {".cmd", ".bat"}:
42
+ # .cmd wrappers cannot be exec'd directly by subprocess.
43
+ return [
44
+ "powershell.exe", "-NoProfile", "-Command",
45
+ "& { $exe = $args[0]; $rest = @($args | Select-Object -Skip 1); & $exe @rest; exit $LASTEXITCODE }",
46
+ exe, *arguments,
47
+ ]
48
+ return [exe, *arguments]
49
+
50
+
51
+ def run(args: list[str], allow_failure: bool = False) -> str:
52
+ result = subprocess.run(
53
+ invocation(args),
54
+ capture_output=True,
55
+ text=True,
56
+ encoding="utf-8",
57
+ timeout=180,
58
+ )
59
+ if result.returncode != 0:
60
+ if allow_failure:
61
+ return ""
62
+ print(f"{GITCODE_COMMAND} {' '.join(args)} failed: exit {result.returncode}", file=sys.stderr)
63
+ print(result.stderr[-2000:], file=sys.stderr)
64
+ raise SystemExit(3)
65
+ return result.stdout
66
+
67
+
68
+ def run_json(args: list[str], allow_failure: bool = False):
69
+ text = run(args, allow_failure=allow_failure)
70
+ if not text.strip():
71
+ return None
72
+ return json.loads(text)
73
+
74
+
75
+ def json_list(value) -> list:
76
+ if isinstance(value, list):
77
+ return value
78
+ if isinstance(value, dict):
79
+ for key in ("items", "data", "labels", "milestones", "comments"):
80
+ if isinstance(value.get(key), list):
81
+ return value[key]
82
+ return []
83
+
84
+
85
+ def page_all(kind: str, repo: str, page_size: int, max_pages: int) -> list:
86
+ """Read every page of a list command. Stops on empty page, short page
87
+ (< page_size), repeated page, or max_pages."""
88
+ seen_page_signatures: set[str] = set()
89
+ items: list = []
90
+ for page in range(1, max_pages + 1):
91
+ payload = run_json(
92
+ [
93
+ kind, "list", "-R", repo,
94
+ "--limit", str(page_size),
95
+ "--per-page", str(page_size),
96
+ "--page", str(page),
97
+ "--json",
98
+ ]
99
+ )
100
+ batch = json_list(payload)
101
+ signature = json.dumps(sorted(str(item.get("id") or item.get("number")) for item in batch))
102
+ if not batch:
103
+ break
104
+ if signature in seen_page_signatures:
105
+ print(f"warning: {kind} list page {page} repeats page {page - 1}; stopping", file=sys.stderr)
106
+ break
107
+ seen_page_signatures.add(signature)
108
+ items.extend(batch)
109
+ if len(batch) < page_size:
110
+ break
111
+ return items
112
+
113
+
114
+ def cmd_prepare(args: argparse.Namespace) -> int:
115
+ report: dict = {"repository": args.repo}
116
+
117
+ # schema flags for the commands create-issue needs
118
+ schemas: dict[str, list[str]] = {}
119
+ for name in ("issue create", "issue view", "issue edit", "label list", "milestone list"):
120
+ payload = run_json(["schema", name], allow_failure=True)
121
+ flags = []
122
+ if isinstance(payload, dict) and isinstance(payload.get("flags"), list):
123
+ flags = [flag.get("name") for flag in payload["flags"] if isinstance(flag, dict)]
124
+ schemas[name] = flags
125
+ report["schemas"] = schemas
126
+
127
+ labels = page_all("label", args.repo, args.page_size, args.max_pages)
128
+ report["labels"] = [item.get("name") for item in labels]
129
+ report["label_count"] = len(labels)
130
+
131
+ milestones = page_all("milestone", args.repo, args.page_size, args.max_pages)
132
+ report["open_milestones"] = [
133
+ {
134
+ "number": item.get("number") or item.get("id"),
135
+ "title": item.get("title"),
136
+ "state": item.get("state"),
137
+ "due_on": item.get("due_on") or item.get("due_date") or "",
138
+ }
139
+ for item in milestones
140
+ if str(item.get("state", "")).lower() in ("active", "open", "")
141
+ ]
142
+ report["milestone_count"] = len(milestones)
143
+
144
+ if args.template_ref:
145
+ result = subprocess.run(
146
+ ["git", "ls-tree", "-r", "--name-only", args.template_ref, "--", ".gitcode"],
147
+ capture_output=True,
148
+ text=True,
149
+ encoding="utf-8",
150
+ timeout=120,
151
+ )
152
+ report["gitcode_tree"] = (
153
+ [line for line in result.stdout.splitlines() if line.strip()]
154
+ if result.returncode == 0
155
+ else None
156
+ )
157
+ else:
158
+ report["gitcode_tree"] = None
159
+
160
+ print(json.dumps(report, ensure_ascii=False, indent=2))
161
+ return 0
162
+
163
+
164
+ def cmd_inspect(args: argparse.Namespace) -> int:
165
+ view = run_json(["issue", "view", str(args.number), "-R", args.repo, "--json"]) or {}
166
+ api = run_json(["api", f"repos/{args.repo}/issues/{args.number}"]) or {}
167
+
168
+ labels = [l.get("name") if isinstance(l, dict) else l for l in (api.get("labels") or view.get("labels") or [])]
169
+ type_detail = api.get("issue_type_detail") or {}
170
+ platform_type = api.get("issue_type") or type_detail.get("title") or ""
171
+ milestone = api.get("milestone") or view.get("milestone") or {}
172
+ body = view.get("body") or api.get("body") or ""
173
+ title = view.get("title") or api.get("title") or ""
174
+
175
+ report = {
176
+ "number": view.get("number") or api.get("number"),
177
+ "state": view.get("state") or api.get("state"),
178
+ "title": title,
179
+ "platform_issue_type": platform_type,
180
+ "labels": labels,
181
+ "milestone_number": milestone.get("number"),
182
+ "milestone_title": milestone.get("title"),
183
+ "signature_count": body.count("——msdevflow"),
184
+ "body": body,
185
+ }
186
+ print(json.dumps(report, ensure_ascii=False, indent=2))
187
+
188
+ ok = True
189
+
190
+ def check(condition: bool, label: str) -> None:
191
+ nonlocal ok
192
+ ok = ok and condition
193
+ print(f"{'PASS' if condition else 'FAIL'} {label}")
194
+
195
+ check(bool(platform_type), f"platform type present ({platform_type or 'empty'})")
196
+ check(len(labels) == len(set(labels)), "labels unique")
197
+ check(report["signature_count"] == 1, f"signature exactly once ({report['signature_count']})")
198
+ if args.expect_type:
199
+ check(platform_type == args.expect_type, f"platform type == {args.expect_type}")
200
+ if args.expect_label:
201
+ for name in args.expect_label:
202
+ check(name in labels, f"label present: {name}")
203
+ if args.expect_milestone is not None:
204
+ check(milestone.get("number") == args.expect_milestone, f"milestone number == {args.expect_milestone}")
205
+ if args.expect_title:
206
+ check(title == args.expect_title, "title verbatim")
207
+ return 0 if ok else 3
208
+
209
+
210
+ def main() -> int:
211
+ parser = argparse.ArgumentParser(description=__doc__)
212
+ sub = parser.add_subparsers(dest="command", required=True)
213
+
214
+ p = sub.add_parser("prepare", help="batch schema + labels + milestones + template tree")
215
+ p.add_argument("repo")
216
+ p.add_argument("--page-size", type=int, default=DEFAULT_PAGE_SIZE)
217
+ p.add_argument("--max-pages", type=int, default=50)
218
+ p.add_argument("--template-ref", help="git ref to list .gitcode template tree from, e.g. upstream/HEAD")
219
+
220
+ p = sub.add_parser("inspect", help="read back one Issue via view + GET API")
221
+ p.add_argument("repo")
222
+ p.add_argument("number", type=int)
223
+ p.add_argument("--expect-type", help="expected platform issue_type")
224
+ p.add_argument("--expect-label", action="append", default=[], help="expected label (repeatable)")
225
+ p.add_argument("--expect-milestone", type=int, help="expected milestone number")
226
+ p.add_argument("--expect-title", help="expected exact title")
227
+
228
+ args = parser.parse_args()
229
+ if args.command == "prepare":
230
+ return cmd_prepare(args)
231
+ return cmd_inspect(args)
232
+
233
+
234
+ if __name__ == "__main__":
235
+ raise SystemExit(main())