docs(skills): 明确 @build/@test/@task 为文档别名并新增 precheck/postmerge 命令
CI / 静态门禁 (pre-commit) (push) Failing after 3m45s
CI / 主机侧单测 (MSVC) (push) Failing after 43s

AGENTS.md「构建与验证」新增总口径:@build / @test / @task 都是本仓库的
文档别名、不是 shell 命令(2026-10-30 实测:bash 敲 @task / @build 即
command not found,dsh task 被解析成 profile 名报错);`=` 后的命令行
才是可执行形态,DSH 会话显式调用为 /keil-build、/serial-boot-test、
/task-worktree。

- AGENTS.md / docs/README.md / 并行任务工作树方案.md:@task 三处定义位
  由「短命令」统一改为「文档别名」,并写入两种实测失败形态
- keil-build / serial-boot-test SKILL.md:定义位点破别名性质;@test 在
  skill 内原先没有定义句,补口径 blockquote
- task-worktree SKILL.md 与 reference/:whenToUse、标题、症状表、插件桥
  去 @task 前缀改指 list / 本 skill;frontmatter 能力清单同步
- task_worktree.py 新增 precheck(树内只读自查:BOM 漂移 / CR / U+FFFD、
  docs 逐档门禁、--msgfile 消息 BOM/CRLF/行长/禁词检查,自动补
  ci-artifacts/)与 postmerge(主树体积凭证自动诊断:新鲜跳过、红先短链
  再只读复检裁定是否升全链;check_doc_links --all;列文件不代提交)
- done 快路径打印与门禁拦截提示均指向 postmerge

实测:precheck 对坏消息文件抓到 CRLF、行长 95>80、禁词×3(rc=1);
postmerge 端到端「凭证同码新鲜→跳链→doc --all 绿(2967 章节引用 /
214 文档)→列 dirty 文件」rc=0;check_skill_size.py 与 py_compile 通过;
frontmatter 改动后 DSH 已用新值真加载过一轮。无 .c/.h 改动,不涉及体积。
This commit is contained in:
2026-10-30 10:25:05 +08:00
parent a157b443cb
commit 2be49dc9dd
9 changed files with 176 additions and 148 deletions
@@ -970,6 +970,8 @@ def cmd_done(a) -> int:
raise Fail("任务分支相对 {} 没有新提交,无需合并".format(base))
if already and ahead == 0:
print("[i] {} 已合入 {},跳过合并,直接清场".format(branch, base))
print("[i] 该路径在门禁之前就返回——不替你复绿;上一次门禁没跑到的尾巴,"
"清场后去主树跑 `postmerge`(或手工 ci_local + check_doc_links --all)")
print("=== 清场 ===")
teardown_worktree(st, force=a.force, base=base)
print("\n完成(分支此前已合入)。")
@@ -1020,6 +1022,7 @@ def cmd_done(a) -> int:
print("!! 门禁未全绿:工作树与分支**保留**,未清场。")
print(" 主干已含合并提交 {}".format(base_after[:12]))
print(" 可选:a) 在主工作树修正后追加提交(推荐)")
print(" ——体积凭证/台账红就用 `postmerge` 自动诊断回填,别手抄补链")
print(" b) 回退本次合并:git -C \"{}\" reset --hard {}".format(main, base_before[:12]))
return EXIT_FAIL
@@ -1031,6 +1034,130 @@ def cmd_done(a) -> int:
return EXIT_OK
# ------------------------------------------------- 树内预检 / 合并后补绿(机械化)
HYGIENE_BINARY = (".png", ".jpg", ".gif", ".ico", ".ttf", ".bin", ".hex", ".elf",
".axf", ".o", ".crf", ".lst", ".zip", ".gz", ".pdf", ".cur")
CR_EXEMPT = (".bat", ".cmd") # .gitattributes 强制 CRLF
def _git_bytes(*args, cwd=None):
r = subprocess.run(["git"] + [str(x) for x in args], cwd=str(cwd or REPO_ROOT),
capture_output=True, stdin=subprocess.DEVNULL)
return r.stdout if r.returncode == 0 else None
def _changed_paths(cwd: Path):
q = ("-c", "core.quotePath=false") # 中文文件名不许被八进制转义出去
tracked = git(*q, "diff", "--name-only", "HEAD", cwd=cwd, check=False).stdout
others = git(*q, "ls-files", "--others", "--exclude-standard", cwd=cwd, check=False).stdout
return ([p.strip() for p in tracked.splitlines() if p.strip()],
[p.strip() for p in others.splitlines() if p.strip()])
def cmd_precheck(a) -> int:
"""树内自查(只读):字节体检 + docs 逐档门禁 + 可选提交信息检查。绝不代改。"""
cwd = Path.cwd()
(cwd / "ci-artifacts").mkdir(exist_ok=True) # 新树没有 ci-artifacts(G 节:ENOENT 坑)
tracked, untracked = _changed_paths(cwd)
print("[i] {}:改动 {} 个(tracked {} + untracked {})".format(
cwd, len(tracked) + len(untracked), len(tracked), len(untracked)))
bom3, cr, fffd = bytes.fromhex("efbbbf"), b"\r", bytes.fromhex("efbfbd")
problems, docs = 0, []
for p in tracked + untracked:
f = cwd / p
if not f.is_file() or p.lower().endswith(HYGIENE_BINARY):
continue
b = f.read_bytes()
bad = []
if cr in b and not p.lower().endswith(CR_EXEMPT):
bad.append("CR")
n = b.count(fffd)
if n:
bad.append("U+FFFD x{}".format(n))
if p in tracked:
hb = _git_bytes("show", "HEAD:" + p, cwd=cwd)
if hb is not None and hb.startswith(bom3) and not b.startswith(bom3):
bad.append("BOM 漂移(HEAD 有 BOM、工作区没了——bom-guard 会拦)")
if p.startswith("docs/") and p.endswith(".md"):
docs.append(p)
if bad:
problems += 1
print("!! {}:{}".format(p, " / ".join(bad)))
for d in docs:
if not run_script("scripts/check_doc_links.py", [d], cwd, "文档门禁 " + d, tail=6):
problems += 1
if a.msgfile:
m = Path(a.msgfile)
if not m.is_file():
raise EnvError("消息文件不存在:{}".format(m))
mb = m.read_bytes()
if mb.startswith(bom3):
problems += 1
print("!! 消息带 BOM(commit -F 要 UTF-8 无 BOM)")
if cr in mb:
problems += 1
print("!! 消息含 CRLF")
ml = mb.decode("utf-8", "replace").splitlines()
for i, ln in enumerate(ml):
lim = 100 if i == 0 else 80
if len(ln) > lim:
problems += 1
print("!! 消息第 {} 行长 {} > {}".format(i + 1, len(ln), lim))
break
for w in a.ban:
c = mb.decode("utf-8", "replace").lower().count(w.lower())
print("[i] 禁词 {!r}:{} 次".format(w, c))
if c:
problems += 1
print("完成:问题 {} 个".format(problems))
return EXIT_OK if problems == 0 else EXIT_FAIL
def cmd_postmerge(a) -> int:
"""主树合并后补绿:体积凭证自动诊断(短链/全链)+ 文档全量。列文件,不提交。
裁定外包给现有门禁:凭证红 → 先短链(--update 重写)→ 用只读的
check_mem_numbers 复检裁定数字是否真变——红了才升级全链(--write 族)。
"""
main = main_worktree()
if Path.cwd().resolve() != main.resolve():
raise EnvError("postmerge 必须在主工作树跑:{}".format(main))
if not list((main / "MDK-ARM").glob("**/*.map")):
raise EnvError("主树没有 .map——先 @build 0/0 再回来(C.5 的前提)")
gate = "scripts/check_size_gate.py"
if not a.force and run_script(gate, ["--local"], main, "体积凭证检查(同码)", tail=8):
print("[ok] 凭证同码新鲜,无需补链")
else:
run_script("scripts/ram_baseline.py", ["--update"], main, "短链:重写体积凭证", tail=6)
mem_ok = (a.skip_mem or run_script("scripts/check_mem_numbers.py", [], main,
"受管区一致性复检(只读)", tail=8))
if not mem_ok:
print("[i] 快照与 .map 真漂了 → 升级全链(数字变了才动受管区)")
run_script("scripts/check_mem_numbers.py", ["--write"], main, "同步内存快照")
run_script("scripts/ram_baseline.py", ["--write"], main, "同步 RAM 预算台账")
if a.coverage:
run_script("scripts/check_test_coverage.py", ["--update-baseline"], main,
"覆盖率基线刷新", tail=6)
if not run_script(gate, ["--local"], main, "凭证复检(期望绿)", tail=8):
print("!! 凭证复检仍红——贴输出查 check_size_gate 原因,别硬提交")
return EXIT_FAIL
ok = True
if not a.skip_docs:
ok = run_script("scripts/check_doc_links.py", ["--all"], main, "文档全库检查", tail=8)
dirty = git("-c", "core.quotePath=false", "diff", "--name-only", "HEAD",
cwd=main, check=False).stdout.splitlines()
dirty = [x.strip() for x in dirty if x.strip()]
if dirty:
print()
print("[i] 回填产物(提交属 Ask first,本命令不代提交):")
for p in dirty:
print(" - " + p)
print(" 建议:git add <上面列的文件> && git commit -F <正文文件>")
return EXIT_OK if ok else EXIT_FAIL
def cmd_abort(a) -> int:
st = load_state(a.task_id)
wt = Path(st["path"])
@@ -1116,6 +1243,18 @@ def build_parser():
p.add_argument("task_id")
p.add_argument("--force", action="store_true")
p.set_defaults(func=cmd_abort)
p = sub.add_parser("precheck", help="树内自查(只读):字节体检 + docs 逐档门禁 + 可选消息检查")
p.add_argument("--msgfile", default=None, help="附带检查提交信息文件(BOM/CRLF/行长/禁词)")
p.add_argument("--ban", action="append", default=[], help="禁词,可重复")
p.set_defaults(func=cmd_precheck)
p = sub.add_parser("postmerge", help="主树合并后补绿:体积凭证自动诊断(短/全链)+ 文档全量;不代提交")
p.add_argument("--force", action="store_true", help="凭证新鲜也强制走一遍 --update")
p.add_argument("--coverage", action="store_true", help="附带覆盖率基线刷新")
p.add_argument("--skip-docs", action="store_true", help="跳过 check_doc_links --all")
p.add_argument("--skip-mem", action="store_true", help="只挪凭证,跳过受管区复检/全链")
p.set_defaults(func=cmd_postmerge)
return ap