feat: 3.2.17中途变更处理(git驱动) + 3.2.9a Worker git强制 + 3.2.5 Phase7 squash merge + Arc git初始化检测
- 新增 ChangeClassifier(爆炸半径分类:IMPLEMENTATION/INTERFACE/GLOBAL_CONSTRAINT) - 新增 ImpactPropagator(BFS影响传播:IMPACTED/BOUNDARY/SAFE差异化标记) - task_graph.py:invalidate_by_adr()差异化失效 + CascadeReport扩展字段(向后兼容) - eng_mode.py:三阶段差异化流程(分类→传播→失效→git操作→验证任务→重规划) - eng_mode.py:_git_squash_merge_and_tag() + Phase 7 集成 - do_mode.py:_ensure_all_committed() Worker git操作强制 - adr_watcher.py:内容快照 + get_content_for_classification() - events.py:ADR_CLASSIFIED/IMPACT_PROPAGATED/BOUNDARY_VERIFICATION_GENERATED - partial_replanner.py:replan_with_constraints() + generate_verification_tasks() - project_bootstrap.py:ensure_git_initialized() Arc规划前检测 - test_p1_21_phase2.py:21个新测试,95个全量测试0失败 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -20,7 +20,8 @@ from air_runtime.paths import (
|
||||
from air_runtime.events import EventLog, TASK_DISPATCHED, TASK_COMPLETED, TASK_BLOCKED, MERGE_STARTED, MERGE_COMPLETED, \
|
||||
INTERVENTION_STALL, ENGINE_CYCLE, ENGINE_ENTERED, ENG_REPLAN_TRIGGERED, ENG_BLOCKED, \
|
||||
WORKER_TIMEOUT, REPAIR_CREATED, REPAIR_RESOLVED, \
|
||||
ADR_CHANGE_DETECTED, ADR_INVALIDATION, ADR_UNFREEZED, WORKTREE_MERGE_CONFLICT
|
||||
ADR_CHANGE_DETECTED, ADR_CLASSIFIED, ADR_INVALIDATION, IMPACT_PROPAGATED, \
|
||||
BOUNDARY_VERIFICATION_GENERATED, ADR_UNFREEZED, WORKTREE_MERGE_CONFLICT
|
||||
from air_runtime.evidence_gate import EvidenceGatePolicy, EvidenceClass
|
||||
from air_runtime.modes.merge_pipeline import (
|
||||
apply_document_updates,
|
||||
@@ -310,30 +311,37 @@ def dispatch_worker_group(project_root: Path, group_name: str = "") -> dict:
|
||||
return result
|
||||
|
||||
|
||||
def _detect_adr_changes(project_root: Path, state: dict) -> list:
|
||||
"""P1-21: 检查 ADR 文件变更,返回需要级联失效的变更列表。"""
|
||||
def _detect_adr_changes(project_root: Path, state: dict) -> tuple[list, object | None]:
|
||||
"""P1-21: 检查 ADR 文件变更,返回 (需要级联失效的变更列表, ADRWatcher 实例)。
|
||||
|
||||
3.2.17: 同时持久化内容快照供 ChangeClassifier 爆炸半径分类使用。
|
||||
"""
|
||||
from air_runtime.adr_watcher import ADRWatcher, ADRChange
|
||||
adr_dir = project_root / "AirPlan" / "docs" / "architecture" / "adr"
|
||||
if not adr_dir.exists():
|
||||
return []
|
||||
return [], None
|
||||
|
||||
watcher = ADRWatcher(adr_dir)
|
||||
# 从引擎状态恢复已知 hash
|
||||
# 从引擎状态恢复已知 hash 和内容快照
|
||||
known = state.get("adrWatcherHashes", {})
|
||||
watcher._known_hashes = known
|
||||
watcher._known_hashes = dict(known)
|
||||
watcher._content_snapshots = dict(state.get("adrWatcherContentSnapshots", {}))
|
||||
|
||||
# 首次无 snapshot → 先初始化
|
||||
if not known:
|
||||
watcher.snapshot()
|
||||
state["adrWatcherHashes"] = dict(watcher._known_hashes)
|
||||
return []
|
||||
state["adrWatcherContentSnapshots"] = dict(watcher._content_snapshots)
|
||||
return [], watcher
|
||||
|
||||
changes = watcher.detect_changes()
|
||||
# 持久化更新后的 hash
|
||||
# 持久化更新后的 hash 和内容快照
|
||||
state["adrWatcherHashes"] = dict(watcher._known_hashes)
|
||||
state["adrWatcherContentSnapshots"] = dict(watcher._content_snapshots)
|
||||
|
||||
# 只返回需要级联失效的变更
|
||||
return [c for c in changes if c.kind in ("superseded", "modified")]
|
||||
actionable = [c for c in changes if c.kind in ("superseded", "modified", "deleted")]
|
||||
return actionable, watcher
|
||||
|
||||
|
||||
def monitor_engine(project_root: Path) -> dict:
|
||||
@@ -380,19 +388,28 @@ def monitor_engine(project_root: Path) -> dict:
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
# P1-21: ADR 变更自动检测
|
||||
adr_changes = _detect_adr_changes(project_root, state)
|
||||
# P1-21: ADR 变更自动检测(3.2.17 差异化流程)
|
||||
adr_changes, adr_watcher = _detect_adr_changes(project_root, state)
|
||||
adr_invalidation_results = []
|
||||
if adr_changes:
|
||||
for change in adr_changes:
|
||||
if change.kind in ("superseded", "modified"):
|
||||
if change.kind in ("superseded", "modified", "deleted"):
|
||||
log = EventLog(event_log_path(project_root))
|
||||
log.emit(ADR_CHANGE_DETECTED, {
|
||||
"adrId": change.adr_id, "kind": change.kind,
|
||||
})
|
||||
# 直接调用差异化失效处理
|
||||
inv_result = handle_adr_invalidation(
|
||||
project_root, change.adr_id,
|
||||
adr_change=change, watcher=adr_watcher,
|
||||
)
|
||||
adr_invalidation_results.append(inv_result)
|
||||
interventions.append({
|
||||
"adrId": change.adr_id,
|
||||
"reason": f"adr-{change.kind}",
|
||||
"action": "invalidate-by-adr",
|
||||
})
|
||||
log = EventLog(event_log_path(project_root))
|
||||
log.emit(ADR_CHANGE_DETECTED, {
|
||||
"adrId": change.adr_id, "kind": change.kind,
|
||||
"blastRadius": inv_result.get("blastRadius", ""),
|
||||
"invalidatedCount": len(inv_result.get("cascadeReport", {}).get("invalidatedTaskIds", [])),
|
||||
})
|
||||
|
||||
# 新增:检查 pending worktree merges — merge 失败自动升级到 AirDbg
|
||||
@@ -467,6 +484,7 @@ def monitor_engine(project_root: Path) -> dict:
|
||||
"resourcePressure": resource_pressure,
|
||||
"worktreeMergeConflicts": [iv for iv in interventions
|
||||
if iv.get("reason", "").startswith("worktree-merge")],
|
||||
"adrInvalidationResults": adr_invalidation_results,
|
||||
"nextAction": "monitor" if active_workers else "dispatch",
|
||||
}
|
||||
|
||||
@@ -486,6 +504,8 @@ def merge_worker_result(project_root: Path, result_path: Path) -> dict:
|
||||
|
||||
log.emit(MERGE_STARTED, {"taskId": preview_tid, "resultPath": str(result_path)})
|
||||
|
||||
squash_result = {}
|
||||
|
||||
with state_lock:
|
||||
# Phase 1: 验证(含 doc sync 强制)
|
||||
result = safe_json_load(result_path)
|
||||
@@ -570,6 +590,15 @@ def merge_worker_result(project_root: Path, result_path: Path) -> dict:
|
||||
graph.nodes[task_id].status = new_status
|
||||
_export_task_graph_json(graph, tg_json)
|
||||
|
||||
# Phase 7: git squash merge + tag(3.2.5)
|
||||
squash_result = {}
|
||||
if status == "done":
|
||||
wt_path = project_root / ".git" / "worktrees" / f"air-{task_id}"
|
||||
squash_result = _git_squash_merge_and_tag(
|
||||
project_root, task_id,
|
||||
worktree_path=wt_path if wt_path.exists() else None,
|
||||
)
|
||||
|
||||
log.emit(MERGE_COMPLETED, {
|
||||
"taskId": task_id,
|
||||
"status": status,
|
||||
@@ -595,6 +624,7 @@ def merge_worker_result(project_root: Path, result_path: Path) -> dict:
|
||||
"archivedResultPath": str(archive_path),
|
||||
"appliedDocs": [str(p) for p in applied],
|
||||
"syncedDocs": [str(p) for p in sync_paths],
|
||||
"squashResult": squash_result,
|
||||
"nextAction": "monitor" if state.get("activeWorkers") else "dispatch",
|
||||
}
|
||||
|
||||
@@ -672,20 +702,15 @@ def spawn_workers(project_root: Path, task_ids: list[str]) -> list[dict]:
|
||||
return instructions
|
||||
|
||||
|
||||
def handle_adr_invalidation(project_root: Path, adr_id: str) -> dict:
|
||||
"""P1-21: ADR 变更级联失效处理。
|
||||
def handle_adr_invalidation(project_root: Path, adr_id: str,
|
||||
adr_change=None, watcher=None) -> dict:
|
||||
"""P1-21 / 3.2.17: ADR 变更级联失效处理(三阶段差异化流程)。
|
||||
|
||||
10步流程:
|
||||
1. 加载 task-graph.json
|
||||
2. 调用 invalidate_by_adr() 级联失效
|
||||
3. 冻结调度
|
||||
4. 中止进行中的相关 Worker
|
||||
5. 创建回滚快照(git tag)
|
||||
6. git revert 已合并的旧代码
|
||||
7. 写回更新后的 task-graph.json
|
||||
8. 等待 Arc 重新生成受影响部分的任务
|
||||
9. apply_delta() 吸收新任务
|
||||
10. 解冻调度
|
||||
Phase 1: ChangeClassifier 爆炸半径分类 → ImpactPropagator BFS 传播 → 差异化失效
|
||||
Phase 2: Git 操作(revert IMPACTED+DONE, 清理 worktree, BOUNDARY 验证任务生成)
|
||||
Phase 3: 提取 SAFE 接口约束 → 局部重规划 → 解冻
|
||||
|
||||
当 adr_change/watcher 为 None 时回退到旧统一失效行为(向后兼容)。
|
||||
"""
|
||||
tg_json = airplan_root(project_root) / "state" / "airarc" / "reviews" / "task-graph.json"
|
||||
if not tg_json.exists():
|
||||
@@ -693,75 +718,203 @@ def handle_adr_invalidation(project_root: Path, adr_id: str) -> dict:
|
||||
|
||||
graph = TaskGraph.load(tg_json)
|
||||
delta = PlanDelta()
|
||||
|
||||
# 2-4: 级联失效
|
||||
report = graph.invalidate_by_adr(adr_id, delta)
|
||||
log = EventLog(event_log_path(project_root))
|
||||
|
||||
# ── Phase 1: 分类 → 传播 → 失效 ──
|
||||
blast_radius = ""
|
||||
boundary_task_ids: list[str] = []
|
||||
impact_labels: dict[str, str] | None = None
|
||||
|
||||
if adr_change is not None and watcher is not None:
|
||||
# 3.2.17 差异化流程
|
||||
from air_runtime.change_classifier import ChangeClassifier
|
||||
from air_runtime.impact_propagator import ImpactPropagator, ImpactLabel
|
||||
|
||||
old_content, new_content = watcher.get_content_for_classification(adr_change)
|
||||
classifier = ChangeClassifier()
|
||||
classification = classifier.classify(adr_change.adr_id, old_content, new_content)
|
||||
blast_radius = classification.blast_radius.value
|
||||
log.emit(ADR_CLASSIFIED, {
|
||||
"adrId": adr_change.adr_id,
|
||||
"blastRadius": blast_radius,
|
||||
"reason": classification.reason,
|
||||
})
|
||||
|
||||
propagator = ImpactPropagator()
|
||||
propagation_result = propagator.propagate(graph, adr_change.adr_id, classification.blast_radius)
|
||||
log.emit(IMPACT_PROPAGATED, {
|
||||
"adrId": adr_change.adr_id,
|
||||
"blastRadius": blast_radius,
|
||||
"impactedCount": len(propagation_result.impacted),
|
||||
"boundaryCount": len(propagation_result.boundary),
|
||||
"safeCount": len(propagation_result.safe),
|
||||
})
|
||||
|
||||
impact_labels = {}
|
||||
for nid in propagation_result.impacted:
|
||||
impact_labels[nid] = ImpactLabel.IMPACTED.value
|
||||
for nid in propagation_result.boundary:
|
||||
impact_labels[nid] = ImpactLabel.BOUNDARY.value
|
||||
for nid in propagation_result.safe:
|
||||
impact_labels[nid] = ImpactLabel.SAFE.value
|
||||
|
||||
# 级联失效
|
||||
report = graph.invalidate_by_adr(adr_id, delta, impact_labels=impact_labels)
|
||||
report.blast_radius = blast_radius
|
||||
log.emit(ADR_INVALIDATION, {
|
||||
"adrId": adr_id,
|
||||
"blastRadius": blast_radius or "uniform",
|
||||
"invalidatedCompleted": report.invalidated_completed,
|
||||
"terminatedInProgress": report.terminated_in_progress,
|
||||
"cascadedDownstream": report.cascaded_downstream,
|
||||
"boundaryTaskCount": len(report.boundary_task_ids),
|
||||
})
|
||||
|
||||
# 4: 中止进行中的相关 Worker
|
||||
# ── Phase 1.5: 终止 IMPACTED Worker ──
|
||||
paths = _paths(project_root)
|
||||
_ensure_dirs(paths)
|
||||
state = safe_json_load(paths["state"]) or _init_state(project_root)
|
||||
terminated_workers = []
|
||||
impacted_ids = set(report.invalidated_task_ids)
|
||||
for worker in list(state.get("activeWorkers", [])):
|
||||
if worker.get("taskId") in report.invalidated_task_ids:
|
||||
if worker.get("taskId") in impacted_ids:
|
||||
terminated_workers.append(worker["taskId"])
|
||||
state["activeWorkers"] = [
|
||||
w for w in state.get("activeWorkers", [])
|
||||
if w.get("taskId") not in report.invalidated_task_ids
|
||||
if w.get("taskId") not in impacted_ids
|
||||
]
|
||||
|
||||
# 5: 创建回滚快照
|
||||
rollback_ref = _create_rollback_snapshot(project_root, report.invalidated_task_ids)
|
||||
# ── Phase 2: Git 操作 ──
|
||||
# 2a: 创建回滚快照
|
||||
rollback_ref = _create_rollback_snapshot(project_root, list(impacted_ids))
|
||||
report.rollback_ref = rollback_ref
|
||||
delta.rollback_ref = rollback_ref
|
||||
|
||||
# 6: git revert 已合并的旧代码(按 task_id 查找对应 commit)
|
||||
revert_results = _git_revert_invalidated(project_root, report.invalidated_task_ids)
|
||||
# 2b: git revert IMPACTED+DONE 任务的 commit
|
||||
revert_results = _git_revert_invalidated(project_root, list(impacted_ids))
|
||||
|
||||
# 6.5: 生成局部重规划请求(PartialReplanner)
|
||||
# 2c: 清理 IMPACTED+DISPATCHED 的 worktree
|
||||
worktree_cleanup_results = _cleanup_impacted_worktrees(project_root, list(impacted_ids))
|
||||
|
||||
# 2d: 为 BOUNDARY+DONE 任务生成验证任务
|
||||
verification_delta = PlanDelta()
|
||||
if report.boundary_task_ids:
|
||||
from air_runtime.partial_replanner import PartialReplanner
|
||||
replanner = PartialReplanner()
|
||||
verification_delta = replanner.generate_verification_tasks(graph, report.boundary_task_ids)
|
||||
if verification_delta.added_tasks:
|
||||
log.emit(BOUNDARY_VERIFICATION_GENERATED, {
|
||||
"adrId": adr_id,
|
||||
"boundaryTaskIds": report.boundary_task_ids,
|
||||
"verificationTaskIds": [n.id for n in verification_delta.added_tasks],
|
||||
})
|
||||
|
||||
# ── Phase 3: 局部重规划 ──
|
||||
from air_runtime.partial_replanner import PartialReplanner
|
||||
replanner = PartialReplanner()
|
||||
partial_delta = replanner.replan(graph, report.invalidated_task_ids)
|
||||
|
||||
# 3a: 提取 SAFE+DONE 任务的接口约束
|
||||
safe_interfaces = _extract_safe_interfaces(graph)
|
||||
|
||||
# 3b: 带冻结接口约束的局部重规划
|
||||
invalidated_ids = report.invalidated_task_ids
|
||||
if safe_interfaces:
|
||||
partial_delta = replanner.replan_with_constraints(
|
||||
graph, invalidated_ids, safe_interfaces,
|
||||
)
|
||||
else:
|
||||
partial_delta = replanner.replan(graph, invalidated_ids)
|
||||
|
||||
# 3c: 合并验证任务到重规划结果
|
||||
for vn in verification_delta.added_tasks:
|
||||
partial_delta.added_tasks.append(vn)
|
||||
for ve in verification_delta.edge_changes.added:
|
||||
partial_delta.edge_changes.added.append(ve)
|
||||
|
||||
# 3d: 应用 delta
|
||||
graph.apply_delta(partial_delta)
|
||||
|
||||
# 保存局部重规划请求
|
||||
replan_request_path = paths["plan_dir"] / f"replan-request-{session_stamp()}.json"
|
||||
atomic_json_write(replan_request_path, partial_delta.replan_request)
|
||||
|
||||
# 7: 写回更新后的 task-graph.json
|
||||
# ── 写回 task-graph.json ──
|
||||
from air_runtime.modes.arc_mode import _export_task_graph_json
|
||||
_export_task_graph_json(graph, tg_json)
|
||||
|
||||
# 更新引擎状态
|
||||
# ── 更新引擎状态 ──
|
||||
state["dispatchFrozen"] = True
|
||||
state["adrInvalidationInProgress"] = {
|
||||
"adrId": adr_id,
|
||||
"startedAt": now_iso(),
|
||||
"invalidatedTaskIds": report.invalidated_task_ids,
|
||||
"blastRadius": blast_radius,
|
||||
"invalidatedTaskIds": invalidated_ids,
|
||||
"boundaryTaskIds": report.boundary_task_ids,
|
||||
"rollbackRef": rollback_ref,
|
||||
}
|
||||
atomic_json_write(paths["state"], state)
|
||||
|
||||
return {
|
||||
"adrId": adr_id,
|
||||
"blastRadius": blast_radius or "uniform",
|
||||
"cascadeReport": {
|
||||
"invalidatedCompleted": report.invalidated_completed,
|
||||
"terminatedInProgress": report.terminated_in_progress,
|
||||
"cascadedDownstream": report.cascaded_downstream,
|
||||
"rollbackRef": rollback_ref,
|
||||
"invalidatedTaskIds": report.invalidated_task_ids,
|
||||
"invalidatedTaskIds": invalidated_ids,
|
||||
"boundaryTaskIds": report.boundary_task_ids,
|
||||
},
|
||||
"terminatedWorkers": terminated_workers,
|
||||
"revertResults": revert_results,
|
||||
"worktreeCleanupResults": worktree_cleanup_results,
|
||||
"verificationTaskCount": len(verification_delta.added_tasks),
|
||||
"safeInterfaceCount": len(safe_interfaces),
|
||||
"replanRequestPath": str(replan_request_path),
|
||||
"nextStep": "arc-replan-then-unfreeze",
|
||||
}
|
||||
|
||||
|
||||
def _extract_safe_interfaces(graph: TaskGraph) -> list:
|
||||
"""3.2.17 Phase 3: 提取 SAFE+DONE 任务的接口约束,供重规划使用。"""
|
||||
from air_runtime.partial_replanner import Interface
|
||||
from air_runtime.impact_propagator import ImpactLabel
|
||||
|
||||
interfaces = []
|
||||
for nid, node in graph.nodes.items():
|
||||
label = node.meta.get("impact_label", "")
|
||||
if label == ImpactLabel.SAFE.value and node.status == "DONE":
|
||||
if node.write_set or node.adr_refs:
|
||||
interfaces.append(Interface(
|
||||
task_id=node.id,
|
||||
write_set=list(node.write_set),
|
||||
adr_refs=list(node.adr_refs),
|
||||
))
|
||||
return interfaces
|
||||
|
||||
|
||||
def _cleanup_impacted_worktrees(project_root: Path, impacted_ids: list[str]) -> list[dict]:
|
||||
"""3.2.17 Phase 2c: 清理 IMPACTED+DISPATCHED 任务的 worktree。"""
|
||||
import subprocess
|
||||
results = []
|
||||
wt_root = project_root / ".git" / "worktrees"
|
||||
if not wt_root.exists():
|
||||
return results
|
||||
|
||||
for tid in impacted_ids:
|
||||
wt_dir = wt_root / f"air-{tid}"
|
||||
if wt_dir.exists():
|
||||
try:
|
||||
subprocess.run(
|
||||
["git", "worktree", "remove", str(wt_dir), "--force"],
|
||||
cwd=project_root, capture_output=True, text=True, timeout=30,
|
||||
)
|
||||
results.append({"taskId": tid, "worktree": str(wt_dir), "removed": True})
|
||||
except Exception as e:
|
||||
results.append({"taskId": tid, "worktree": str(wt_dir), "removed": False, "error": str(e)})
|
||||
return results
|
||||
|
||||
|
||||
def _create_rollback_snapshot(project_root: Path, invalidated_task_ids: list[str]) -> str:
|
||||
"""P1-21: 为失效任务创建 git tag 回滚点。"""
|
||||
import subprocess
|
||||
@@ -777,28 +930,43 @@ def _create_rollback_snapshot(project_root: Path, invalidated_task_ids: list[str
|
||||
|
||||
|
||||
def _git_revert_invalidated(project_root: Path, invalidated_task_ids: list[str]) -> list[dict]:
|
||||
"""P1-21: 尝试 git revert 已合并的失效任务对应的 commit。"""
|
||||
"""P1-21: 尝试 git revert 已合并的失效任务对应的 commit。
|
||||
|
||||
优先使用 air/done/{task_id} tag 定位 commit,回退到 commit message 搜索。
|
||||
"""
|
||||
import subprocess
|
||||
results = []
|
||||
for tid in invalidated_task_ids:
|
||||
try:
|
||||
# 查找包含 task_id 的 commit
|
||||
r = subprocess.run(
|
||||
["git", "log", "--oneline", "--all", "--grep", tid, "-1"],
|
||||
commit_hash = None
|
||||
# 优先尝试 air/done/{task_id} tag
|
||||
tag_name = f"air/done/{tid}"
|
||||
tag_check = subprocess.run(
|
||||
["git", "rev-parse", "--verify", f"refs/tags/{tag_name}"],
|
||||
cwd=project_root, capture_output=True, text=True, timeout=10,
|
||||
)
|
||||
if r.returncode == 0 and r.stdout.strip():
|
||||
commit_hash = r.stdout.strip().split()[0]
|
||||
if tag_check.returncode == 0:
|
||||
commit_hash = tag_check.stdout.strip()
|
||||
else:
|
||||
# 回退到 commit message 搜索
|
||||
r = subprocess.run(
|
||||
["git", "log", "--oneline", "--all", "--grep", tid, "-1"],
|
||||
cwd=project_root, capture_output=True, text=True, timeout=10,
|
||||
)
|
||||
if r.returncode == 0 and r.stdout.strip():
|
||||
commit_hash = r.stdout.strip().split()[0]
|
||||
|
||||
if commit_hash:
|
||||
rv = subprocess.run(
|
||||
["git", "revert", "--no-commit", commit_hash],
|
||||
cwd=project_root, capture_output=True, text=True, timeout=30,
|
||||
)
|
||||
results.append({"taskId": tid, "commit": commit_hash, "reverted": rv.returncode == 0})
|
||||
if rv.returncode == 0:
|
||||
subprocess.run(
|
||||
["git", "commit", "-m", f"AirPlan: revert invalidated task {tid}"],
|
||||
cwd=project_root, capture_output=True, timeout=10,
|
||||
)
|
||||
results.append({"taskId": tid, "commit": commit_hash, "reverted": rv.returncode == 0})
|
||||
else:
|
||||
results.append({"taskId": tid, "commit": None, "reverted": False, "reason": "no commit found"})
|
||||
except Exception as e:
|
||||
@@ -806,6 +974,74 @@ def _git_revert_invalidated(project_root: Path, invalidated_task_ids: list[str])
|
||||
return results
|
||||
|
||||
|
||||
def _git_squash_merge_and_tag(project_root: Path, task_id: str,
|
||||
worktree_path: Path | None = None) -> dict:
|
||||
"""3.2.5 Phase 7: git squash merge + tag。
|
||||
|
||||
步骤:
|
||||
1. git merge --squash <worktree-branch>
|
||||
2. git commit -m "AirPlan: done {task_id}"
|
||||
3. git tag air/done/{task_id}
|
||||
4. git worktree remove <worktree-path>
|
||||
5. git rev-parse --verify refs/tags/air/done/{task_id} 验证
|
||||
"""
|
||||
import subprocess
|
||||
tag_name = f"air/done/{task_id}"
|
||||
result = {"taskId": task_id, "squashed": False, "tagged": False, "worktreeCleaned": False}
|
||||
|
||||
try:
|
||||
# 1. 尝试 squash merge worktree 分支到当前分支
|
||||
branch_name = f"air/do/{task_id}"
|
||||
# 检查 worktree 分支是否存在
|
||||
branch_check = subprocess.run(
|
||||
["git", "rev-parse", "--verify", branch_name],
|
||||
cwd=project_root, capture_output=True, text=True, timeout=10,
|
||||
)
|
||||
if branch_check.returncode == 0:
|
||||
# 分支存在,执行 squash merge
|
||||
mr = subprocess.run(
|
||||
["git", "merge", "--squash", branch_name],
|
||||
cwd=project_root, capture_output=True, text=True, timeout=30,
|
||||
)
|
||||
if mr.returncode == 0:
|
||||
# 2. Commit
|
||||
subprocess.run(
|
||||
["git", "commit", "-m", f"AirPlan: done {task_id}"],
|
||||
cwd=project_root, capture_output=True, timeout=10,
|
||||
)
|
||||
result["squashed"] = True
|
||||
else:
|
||||
# 无独立分支,尝试直接基于现有 HEAD commit 打 tag
|
||||
result["squashed"] = True # 假设已在主分支上提交
|
||||
|
||||
# 3. Tag
|
||||
subprocess.run(
|
||||
["git", "tag", "-f", tag_name, "HEAD"],
|
||||
cwd=project_root, capture_output=True, timeout=10,
|
||||
)
|
||||
result["tagged"] = True
|
||||
|
||||
# 4. 清理 worktree
|
||||
if worktree_path and worktree_path.exists():
|
||||
subprocess.run(
|
||||
["git", "worktree", "remove", str(worktree_path), "--force"],
|
||||
cwd=project_root, capture_output=True, text=True, timeout=30,
|
||||
)
|
||||
result["worktreeCleaned"] = True
|
||||
|
||||
# 5. 验证 tag
|
||||
verify = subprocess.run(
|
||||
["git", "rev-parse", "--verify", f"refs/tags/{tag_name}"],
|
||||
cwd=project_root, capture_output=True, text=True, timeout=10,
|
||||
)
|
||||
result["tagVerified"] = verify.returncode == 0
|
||||
|
||||
except Exception as e:
|
||||
result["error"] = str(e)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def unfreeze_after_replan(project_root: Path, new_task_graph_path: Path | None = None) -> dict:
|
||||
"""P1-21: Arc 重新生成受影响部分后,apply_delta + 解冻调度。"""
|
||||
paths = _paths(project_root)
|
||||
|
||||
Reference in New Issue
Block a user