refactor(task-worktree): 10项改进+清理旧版兼容
- 默认基线 v2-on-ui → master - regen() 新增 ram_baseline.py --update/--write - run_gates() 先 build 再 ci_local(修顺序错误) - done 门禁失败时自动调 postmerge 补绿 - audit_residue 增加 git 冲突标记检测(JSON/TSV/CSV) - plugin_trees() 同时扫描 .dsh-worktrees + .agents/worktrees - harvest_memory() 只保留 dsh-mnemon,移除 workbuddy-ai 兼容 - done --dry-run 新增台账文件碰撞预检 - done 统一补绿链:regen→build→ci→postmerge - SKILL.md 同步更新
This commit is contained in:
@@ -77,7 +77,7 @@ def _main_worktree_of(start: Path):
|
||||
_SCRIPT_ROOT = Path(__file__).resolve().parents[4]
|
||||
REPO_ROOT = _main_worktree_of(_SCRIPT_ROOT) or _SCRIPT_ROOT
|
||||
WT_ROOT = Path(os.environ.get("DTU_WT_ROOT") or (REPO_ROOT.parent / "_wt"))
|
||||
BASE_BRANCH = os.environ.get("DTU_TASK_BASE", "v2-on-ui")
|
||||
BASE_BRANCH = os.environ.get("DTU_TASK_BASE", "master")
|
||||
|
||||
STATE_DIR = WT_ROOT / ".state"
|
||||
LOCKS_DIR = STATE_DIR / "locks"
|
||||
@@ -86,11 +86,21 @@ LOCKS_DIR = STATE_DIR / "locks"
|
||||
# /worktree)把任务树建在**仓库内**这个命名空间下,与本 skill 的仓外 _wt/ 刻意分开。
|
||||
# .gitignore 里预置同名规则,正是为了让插件的 ensureIgnored() 见到就跳过、
|
||||
# 不在会话中途往这份受控文件里追加一行。
|
||||
PLUGIN_ROOT = REPO_ROOT / ".dsh-worktrees"
|
||||
PLUGIN_NAMESPACE = "worktree" # 插件侧 WORKTREE_NAMESPACE:name 即分支尾段
|
||||
PLUGIN_SUBDIR = PLUGIN_ROOT / PLUGIN_NAMESPACE
|
||||
# DSH worktree 插件实际使用两个位置(历史演进):
|
||||
# 旧版 dsh-task-worktree → .dsh-worktrees/worktree/
|
||||
# 新版内置 create_worktree → .agents/worktrees/
|
||||
# plugin_trees() 同时扫描两处,list 统一展示。
|
||||
PLUGIN_ROOT_LEGACY = REPO_ROOT / ".dsh-worktrees"
|
||||
PLUGIN_NAMESPACE_LEGACY = "worktree"
|
||||
PLUGIN_SUBDIR_LEGACY = PLUGIN_ROOT_LEGACY / PLUGIN_NAMESPACE_LEGACY
|
||||
|
||||
PLUGIN_ROOT_AGENTS = REPO_ROOT / ".agents" / "worktrees"
|
||||
|
||||
PLUGIN_LOCK_PREFIX = "pw" # 插件树在锁 payload 里的持有者前缀
|
||||
|
||||
# 兼容旧引用:外部若直接读 PLUGIN_SUBDIR 仍指向 legacy 路径
|
||||
PLUGIN_SUBDIR = PLUGIN_SUBDIR_LEGACY
|
||||
|
||||
|
||||
# --------------------------------------------------------------------- 基础工具
|
||||
|
||||
@@ -472,29 +482,50 @@ def junction_label(st) -> str:
|
||||
|
||||
|
||||
def plugin_trees():
|
||||
""".dsh-worktrees/worktree/ 下的工作树(事实源 = git worktree list,不读插件 manifest)。
|
||||
"""扫描两处 DSH 插件工作树(事实源 = git worktree list,不读插件 manifest)。
|
||||
|
||||
位置:
|
||||
- .dsh-worktrees/worktree/ (旧版 dsh-task-worktree 插件)
|
||||
- .agents/worktrees/ (新版内置 create_worktree 工具)
|
||||
|
||||
manifest.json 是插件私有格式(MANIFEST_VERSION 1,字段随版本变),而 git 的注册表
|
||||
同时覆盖「插件正常建的树」和「manifest 丢了 / 树被手工删」的残留——桥接命令要看的
|
||||
是真实存在的东西。
|
||||
"""
|
||||
try:
|
||||
base = PLUGIN_SUBDIR.resolve()
|
||||
except OSError:
|
||||
bases = []
|
||||
for candidate in (PLUGIN_SUBDIR_LEGACY, PLUGIN_ROOT_AGENTS):
|
||||
try:
|
||||
resolved = candidate.resolve()
|
||||
if resolved.is_dir():
|
||||
bases.append(resolved)
|
||||
except OSError:
|
||||
continue
|
||||
if not bases:
|
||||
return []
|
||||
trees = []
|
||||
seen_paths = set()
|
||||
for ent in worktree_entries():
|
||||
p = ent["path"]
|
||||
try:
|
||||
rp = p.resolve()
|
||||
except OSError:
|
||||
continue
|
||||
if rp == base or base not in rp.parents:
|
||||
matched_base = None
|
||||
for base in bases:
|
||||
if rp == base or base in rp.parents:
|
||||
matched_base = base
|
||||
break
|
||||
if matched_base is None:
|
||||
continue
|
||||
# 去重(同一棵树不会出现在两个 base 下,但防御性检查)
|
||||
path_key = str(rp)
|
||||
if path_key in seen_paths:
|
||||
continue
|
||||
seen_paths.add(path_key)
|
||||
dirty = (len(git_out("status", "--porcelain", cwd=p, check=False).splitlines())
|
||||
if p.is_dir() else -1)
|
||||
trees.append({
|
||||
"name": rp.relative_to(base).as_posix(),
|
||||
"name": rp.relative_to(matched_base).as_posix(),
|
||||
"path": str(p),
|
||||
"branch": ent.get("branch") or "(detached)",
|
||||
"dirty": dirty,
|
||||
@@ -605,30 +636,37 @@ def regen(cwd: Path) -> None:
|
||||
run_script("scripts/gen_task_table.py", ["--write"], cwd, "重生成 FreeRTOS 任务表")
|
||||
if list((Path(cwd) / "MDK-ARM").glob("**/*.map")):
|
||||
run_script("scripts/check_mem_numbers.py", ["--write"], cwd, "同步内存/Flash 快照")
|
||||
run_script("scripts/ram_baseline.py", ["--update"], cwd, "追加 RAM 趋势基线行")
|
||||
run_script("scripts/ram_baseline.py", ["--write"], cwd, "同步 RAM 预算台账受管区")
|
||||
else:
|
||||
print("[skip] 未找到 .map,跳过内存快照同步(需先 /keil-build)")
|
||||
print("[skip] 未找到 .map,跳过内存快照与 RAM 台账同步(需先 /keil-build)")
|
||||
|
||||
|
||||
def run_gates(cwd: Path, skip_build: bool) -> bool:
|
||||
ok = run_script("scripts/ci_local.py", [], cwd, "静态门禁 + 主机侧纯逻辑单测 本地等价 CI")
|
||||
if skip_build:
|
||||
# 先构建再跑门禁:ci_local.py 含 mem-budget 钩子,需要新鲜 .map 才能正确比对台账。
|
||||
# 旧版先 ci 后 build 导致合并后 .map 仍是旧的 → mem-budget 必红。
|
||||
ok = True
|
||||
if not skip_build:
|
||||
got, info = lock_acquire("build", "__done__", wait=900)
|
||||
if not got:
|
||||
print("!! build 锁被 {} 占用(UV4 不能并发构建),跳过 /keil-build".format(info.get("task")))
|
||||
return False
|
||||
# 告知 /keil-build「调用方已持锁」:新版 keil_build.py 自持自释(见其 docstring),
|
||||
# 读到本变量即跳过,避免自我死锁;**旧版根本不看此变量、也不会自锁**,
|
||||
# 故此处对「工作树里还是旧脚本」的在途任务同样安全(向后兼容)。
|
||||
os.environ["DTU_LOCK_HELD"] = "build"
|
||||
try:
|
||||
if not run_script(".dsh/skills/keil-build/scripts/keil_build.py", [], cwd,
|
||||
"/keil-build(0 错 0 警)"):
|
||||
ok = False
|
||||
finally:
|
||||
os.environ.pop("DTU_LOCK_HELD", None)
|
||||
lock_release("build", "__done__", force=True)
|
||||
else:
|
||||
print("[skip] 按 --skip-build 跳过 /keil-build")
|
||||
return ok
|
||||
got, info = lock_acquire("build", "__done__", wait=900)
|
||||
if not got:
|
||||
print("!! build 锁被 {} 占用(UV4 不能并发构建),跳过 /keil-build".format(info.get("task")))
|
||||
return False
|
||||
# 告知 /keil-build「调用方已持锁」:新版 keil_build.py 自持自释(见其 docstring),
|
||||
# 读到本变量即跳过,避免自我死锁;**旧版根本不看此变量、也不会自锁**,
|
||||
# 故此处对「工作树里还是旧脚本」的在途任务同样安全(向后兼容)。
|
||||
os.environ["DTU_LOCK_HELD"] = "build"
|
||||
try:
|
||||
if not run_script(".dsh/skills/keil-build/scripts/keil_build.py", [], cwd,
|
||||
"/keil-build(0 错 0 警)"):
|
||||
ok = False
|
||||
finally:
|
||||
os.environ.pop("DTU_LOCK_HELD", None)
|
||||
lock_release("build", "__done__", force=True)
|
||||
# 构建完成(或跳过后)再跑静态门禁 + 单测,此时 .map 是新鲜的
|
||||
if not run_script("scripts/ci_local.py", [], cwd, "静态门禁 + 主机侧纯逻辑单测 本地等价 CI"):
|
||||
ok = False
|
||||
return ok
|
||||
|
||||
|
||||
@@ -668,45 +706,75 @@ def audit_residue(base_before: str, branch: str, cwd: Path) -> None:
|
||||
print(" - " + f)
|
||||
if len(both) > 30:
|
||||
print(" …(其余 {} 个略)".format(len(both) - 30))
|
||||
hits = []
|
||||
hits_dup = []
|
||||
hits_marker = []
|
||||
conflict_markers = ("<<<<<<<", "=======", ">>>>>>>")
|
||||
structured_exts = (".json", ".tsv", ".csv")
|
||||
for f in both:
|
||||
p = cwd / f
|
||||
if not p.is_file():
|
||||
continue
|
||||
try:
|
||||
lines = p.read_text(encoding="utf-8", errors="replace").splitlines()
|
||||
content = p.read_text(encoding="utf-8", errors="replace")
|
||||
lines = content.splitlines()
|
||||
except OSError:
|
||||
continue
|
||||
# 检查 git 冲突标记(结构化文件尤其危险,会静默损坏 JSON/TSV)
|
||||
ext = os.path.splitext(f)[1].lower()
|
||||
if ext in structured_exts or any(m in content for m in conflict_markers):
|
||||
marker_lines = [i + 1 for i, line in enumerate(lines)
|
||||
if any(line.strip().startswith(m) for m in conflict_markers)]
|
||||
if marker_lines:
|
||||
hits_marker.append((f, marker_lines))
|
||||
# 检查连续重复行(并集合并的经典症状)
|
||||
dups = [i + 1 for i in range(1, len(lines))
|
||||
if lines[i] == lines[i - 1] and lines[i].strip()]
|
||||
if dups:
|
||||
hits.append((f, dups))
|
||||
if hits:
|
||||
hits_dup.append((f, dups))
|
||||
if hits_marker:
|
||||
print()
|
||||
print("!! 发现 git 冲突标记(结构化文件损坏风险极高):")
|
||||
for f, lns in hits_marker:
|
||||
print(" - {} 第 {} 行".format(f, ", ".join(str(l) for l in lns[:8])))
|
||||
if hits_dup:
|
||||
print()
|
||||
print("!! 连续重复行(并集合并残留嫌疑):")
|
||||
for f, dups in hits:
|
||||
for f, dups in hits_dup:
|
||||
print(" - {} 第 {} 行".format(f, ", ".join(str(d) for d in dups[:8])))
|
||||
else:
|
||||
print("[ok] 未检测到连续重复行")
|
||||
if not hits_marker and not hits_dup:
|
||||
print("[ok] 未检测到冲突标记或连续重复行")
|
||||
print("提示:权威判据是 base / 两个 tip 三方对比(git show <ref>:<file>),别只看结果文件。")
|
||||
|
||||
|
||||
def harvest_memory(st: dict) -> None:
|
||||
"""任务工作树会被删除,先把它自己的记忆归并回主工作树,避免丢上下文"""
|
||||
src = Path(st["path"]) / ".workbuddy-ai" / "memory"
|
||||
if not src.is_dir():
|
||||
"""任务工作树会被删除,先把它的 dsh-mnemon 记忆归并回主工作树,避免丢上下文。
|
||||
|
||||
只处理 .mnemon/ 目录(dsh-mnemon 插件)。目录级合并:只拷贝目标不存在的子项,
|
||||
不覆盖已有内容。若无 .mnemon/ 则静默跳过。
|
||||
"""
|
||||
wt = Path(st["path"])
|
||||
src_mn = wt / ".mnemon"
|
||||
if not src_mn.is_dir():
|
||||
return
|
||||
dst_dir = REPO_ROOT / ".workbuddy-ai" / "memory"
|
||||
dst_dir.mkdir(parents=True, exist_ok=True)
|
||||
for f in sorted(src.glob("*.md")):
|
||||
text = f.read_text(encoding="utf-8", errors="replace").strip()
|
||||
if not text:
|
||||
continue
|
||||
dst = dst_dir / f.name
|
||||
marker = "## [task-worktree 归并自任务 {} / {}]".format(st["id"], st["slug"])
|
||||
with dst.open("a", encoding="utf-8", newline="\n") as fh:
|
||||
fh.write("\n" + marker + "\n\n" + text + "\n")
|
||||
print("[ok] 记忆归并 {} -> {}".format(f.name, dst))
|
||||
dst_mn = REPO_ROOT / ".mnemon"
|
||||
dst_mn.mkdir(parents=True, exist_ok=True)
|
||||
import shutil
|
||||
count = 0
|
||||
for item in sorted(src_mn.iterdir()):
|
||||
target = dst_mn / item.name
|
||||
try:
|
||||
if item.is_dir():
|
||||
if not target.exists():
|
||||
shutil.copytree(str(item), str(target))
|
||||
count += 1
|
||||
else:
|
||||
if not target.exists():
|
||||
shutil.copy2(str(item), str(target))
|
||||
count += 1
|
||||
except OSError as e:
|
||||
print("[warn] 记忆拷贝失败 {}: {}".format(item.name, e))
|
||||
if count:
|
||||
print("[ok] 记忆归并(mnemon) {} 个条目 -> {}".format(count, dst_mn))
|
||||
|
||||
|
||||
def teardown_worktree(st: dict, force: bool = False, base: str = None) -> None:
|
||||
@@ -989,6 +1057,25 @@ def cmd_done(a) -> int:
|
||||
raise Fail("预演发现冲突:到任务工作树里 `git merge {}` 解决并提交后重跑 done".format(base))
|
||||
tree = (r.stdout.splitlines() or ["?"])[0].strip()
|
||||
print("[ok] 无冲突,预演结果树 {}".format(tree))
|
||||
|
||||
# 台账类生成物碰撞预检:双方都改过的已知生成文件在合并后大概率需要重算
|
||||
ledger_files = (
|
||||
"scripts/ram_baseline.tsv",
|
||||
"scripts/size_attestation.json",
|
||||
"docs/内存管理/RAM预算台账.md",
|
||||
"docs/内存管理/RAM使用分析与优化方案.md",
|
||||
)
|
||||
mb = git_out("merge-base", base, branch, cwd=main, check=False)
|
||||
task_changed = set(filter(None, git_out("diff", "--name-only", mb, branch, cwd=main, check=False).splitlines()))
|
||||
base_changed = set(filter(None, git_out("diff", "--name-only", mb, base, cwd=main, check=False).splitlines()))
|
||||
collided = sorted(f for f in ledger_files if f in task_changed and f in base_changed)
|
||||
if collided:
|
||||
print()
|
||||
print("[warn] 以下 {} 个台账/凭证文件双方都改过,合并后需 postmerge 重算:".format(len(collided)))
|
||||
for f in collided:
|
||||
print(" - " + f)
|
||||
print(" (done 会自动调 postmerge 补绿;若 --dry-run 请留意此提示)")
|
||||
|
||||
if a.dry_run:
|
||||
print()
|
||||
print("--dry-run:到此为止,未做任何改动。")
|
||||
@@ -1007,6 +1094,8 @@ def cmd_done(a) -> int:
|
||||
print("=== 合并残留审计 ===")
|
||||
audit_residue(base_before, branch, cwd=main)
|
||||
|
||||
# 统一补绿链:regen + build + ci + postmerge 收敛到一条路径。
|
||||
# regen 先跑(任务表/内存快照),然后 build+ci,最后 postmerge 兜底凭证与文档。
|
||||
print("=== 重生成受管产物 ===")
|
||||
regen(main)
|
||||
|
||||
@@ -1017,13 +1106,46 @@ def cmd_done(a) -> int:
|
||||
print("=== 门禁 ===")
|
||||
gates_ok = run_gates(main, skip_build=a.skip_build)
|
||||
|
||||
if not gates_ok:
|
||||
# 无论门禁是否通过,都跑一次 postmerge 确保凭证/文档全库一致。
|
||||
# 门禁已通过时 postmerge 检测到新鲜凭证会直接跳过(零开销)。
|
||||
print("=== postmerge 补绿(凭证 + 文档全库)===")
|
||||
pm_args = argparse.Namespace(force=False, skip_mem=False, coverage=False, skip_docs=False)
|
||||
try:
|
||||
pm_rc = cmd_postmerge(pm_args)
|
||||
except Exception as e:
|
||||
print("[warn] postmerge 异常:{}".format(e))
|
||||
pm_rc = EXIT_FAIL
|
||||
|
||||
# postmerge 完成后检查是否有回填产物需要提交
|
||||
dirty = git_out("diff", "--name-only", "HEAD", cwd=main, check=False).strip()
|
||||
if dirty and not gates_ok:
|
||||
print()
|
||||
print("!! 门禁未全绿:工作树与分支**保留**,未清场。")
|
||||
print("!! 门禁未全绿且 postmerge 产生了回填文件,工作树与分支**保留**。")
|
||||
print(" 主干已含合并提交 {}".format(base_after[:12]))
|
||||
print(" 可选:a) 在主工作树修正后追加提交(推荐)")
|
||||
print(" ——体积凭证/台账红就用 `postmerge` 自动诊断回填,别手抄补链")
|
||||
print(" b) 回退本次合并:git -C \"{}\" reset --hard {}".format(main, base_before[:12]))
|
||||
print("[i] 以下文件待提交(属 Ask first):")
|
||||
for line in dirty.splitlines():
|
||||
if line.strip():
|
||||
print(" - " + line.strip())
|
||||
print(" 建议:git add <上面列的文件> && git commit -F <正文文件>")
|
||||
print(" 提交后重跑 `done {}` 完成清场。".format(a.task_id))
|
||||
return EXIT_FAIL
|
||||
elif not gates_ok:
|
||||
print()
|
||||
print("!! 门禁未全绿,工作树与分支**保留**,未清场。")
|
||||
print(" 主干已含合并提交 {}".format(base_after[:12]))
|
||||
print(" 请手动排查后重跑 `done {}`。".format(a.task_id))
|
||||
print(" 或回退本次合并:git -C \"{}\" reset --hard {}".format(main, base_before[:12]))
|
||||
return EXIT_FAIL
|
||||
|
||||
# 门禁通过 + postmerge 完成 → 若有回填文件则提示提交后再清场
|
||||
if dirty:
|
||||
print()
|
||||
print("[i] postmerge 已回填,以下文件待提交(属 Ask first):")
|
||||
for line in dirty.splitlines():
|
||||
if line.strip():
|
||||
print(" - " + line.strip())
|
||||
print(" 建议:git add <上面列的文件> && git commit -F <正文文件>")
|
||||
print(" 提交后重跑 `done {}` 完成清场。".format(a.task_id))
|
||||
return EXIT_FAIL
|
||||
|
||||
print("=== 清场 ===")
|
||||
|
||||
Reference in New Issue
Block a user