Merge remote-tracking branch 'gitea/main' into codex/github-private-backup-readback-20260627

This commit is contained in:
Your Name
2026-06-27 11:32:11 +08:00
7 changed files with 309 additions and 0 deletions

View File

@@ -841,6 +841,98 @@ class RepairCandidateService:
"assign_km_and_playbook_trust_writeback_owner",
"rerun_repair_candidate_gate_after_owner_release",
],
"controlled_automation_closure": {
"schema_version": "repair_candidate_controlled_automation_closure_v1",
"status": "owner_review_ready_no_write_rehearsal",
"runtime_write_gate": "closed_until_owner_release",
"no_write_rehearsal": {
"rehearsal_id": (
f"repair-rehearsal:{project_id}:{incident_id}:{lane}"
if incident_id
else f"repair-rehearsal:{project_id}:unbound:{lane}"
),
"status": "ready_for_owner_review",
"route_id": route or "--",
"input_refs": {
"source_work_item_id": work_item_id or None,
"evidence_refs": list(evidence_refs),
"coverage_key": coverage_gap.get("coverage_key"),
},
"planned_steps": [
"render_repair_command_without_execution",
"render_rollback_command_without_execution",
"check_route_allowlist_and_target_selector",
"produce_verifier_rehearsal_plan",
],
"writes_runtime_state": False,
"executes_command": False,
},
"verifier_writeback_plan": {
"verifier_id": (
f"verifier-plan:{project_id}:{incident_id}:{lane}"
if incident_id
else f"verifier-plan:{project_id}:unbound:{lane}"
),
"status": "planned_not_executed",
"checks": list(verifier_plan),
"result_states": ["healthy", "degraded", "failed", "rollback_required"],
"writes_after_execution": [
"incident_timeline",
"auto_repair_execution_result",
"km_draft",
"playbook_trust_event",
],
"writes_runtime_state": False,
},
"automation_asset_ledger": [
{
"asset_type": "KM",
"asset_id": (
f"km-draft:{project_id}:{incident_id}:{lane}"
if incident_id
else f"km-draft:{project_id}:unbound:{lane}"
),
"owner": "Hermes",
"status": "draft_required_after_verifier",
"next_action": "prepare_km_draft_from_incident_evidence",
},
{
"asset_type": "PlayBook",
"asset_id": (
f"playbook-draft:{project_id}:{incident_id}:{lane}"
if incident_id
else f"playbook-draft:{project_id}:unbound:{lane}"
),
"owner": "OpenClaw",
"status": "owner_review_ready",
"next_action": "review_service_specific_repair_steps",
},
{
"asset_type": "ScriptOrAnsible",
"asset_id": route or "--",
"owner": "Executor lane",
"status": "route_allowlist_review_required",
"next_action": "confirm_check_mode_and_blast_radius",
},
{
"asset_type": "Verifier",
"asset_id": (
f"verifier-plan:{project_id}:{incident_id}:{lane}"
if incident_id
else f"verifier-plan:{project_id}:unbound:{lane}"
),
"owner": "PostExecutionVerifier",
"status": "planned_not_executed",
"next_action": "owner_review_post_apply_verifier",
},
],
"operator_next_actions": [
"review_no_write_rehearsal_inputs",
"approve_or_reject_owner_release",
"fill_blast_radius_maintenance_window_and_rollback_owner",
"promote_to_controlled_apply_only_after_all_gates_pass",
],
},
}
def _promotion_contract_field(

View File

@@ -359,6 +359,20 @@ async def test_candidate_blocked_observe_only_prompts_repair_playbook_draft() ->
assert "maintenance_window" in promotion_contract["blocked_fields"]
assert promotion_contract["runtime_execution_authorized"] is False
assert promotion_contract["runtime_write_allowed"] is False
closure = promotion_contract["controlled_automation_closure"]
assert closure["schema_version"] == "repair_candidate_controlled_automation_closure_v1"
assert closure["status"] == "owner_review_ready_no_write_rehearsal"
assert closure["runtime_write_gate"] == "closed_until_owner_release"
assert closure["no_write_rehearsal"]["status"] == "ready_for_owner_review"
assert closure["no_write_rehearsal"]["executes_command"] is False
assert closure["verifier_writeback_plan"]["status"] == "planned_not_executed"
assert "incident_timeline" in closure["verifier_writeback_plan"]["writes_after_execution"]
assert [item["asset_type"] for item in closure["automation_asset_ledger"]] == [
"KM",
"PlayBook",
"ScriptOrAnsible",
"Verifier",
]
assert result.metadata["repair_candidate_promotion_contract"] == promotion_contract
assert "promotion=6/11" in result.metadata["repair_candidate_promotion_summary"]
assert "runtime=false" in result.metadata["repair_candidate_promotion_summary"]

View File

@@ -9807,6 +9807,22 @@
"blast_radius": "影響範圍",
"km_writeback_owner": "KM owner",
"playbook_trust_owner": "PlayBook trust owner"
},
"closure": {
"title": "受控自動化閉環",
"subtitle": "AI 已把下一步拆成無寫入演練、Verifier 與資產沉澱;這裡只呈現可審查的閉環,不代表已執行命令。",
"rehearsal": {
"title": "無寫入演練",
"meta": "已規劃 {steps} 個演練步驟"
},
"verifier": {
"title": "Verifier 回寫",
"meta": "已規劃 {checks} 個驗證檢查"
},
"writeback": {
"title": "資產沉澱",
"meta": "已建立 {assets} 個資產槽位"
}
}
},
"flow": {

View File

@@ -9807,6 +9807,22 @@
"blast_radius": "影響範圍",
"km_writeback_owner": "KM owner",
"playbook_trust_owner": "PlayBook trust owner"
},
"closure": {
"title": "受控自動化閉環",
"subtitle": "AI 已把下一步拆成無寫入演練、Verifier 與資產沉澱;這裡只呈現可審查的閉環,不代表已執行命令。",
"rehearsal": {
"title": "無寫入演練",
"meta": "已規劃 {steps} 個演練步驟"
},
"verifier": {
"title": "Verifier 回寫",
"meta": "已規劃 {checks} 個驗證檢查"
},
"writeback": {
"title": "資產沉澱",
"meta": "已建立 {assets} 個資產槽位"
}
}
},
"flow": {

View File

@@ -1121,6 +1121,15 @@ function promotionStatusTone(status: string | null | undefined): WorkStatus {
return "blocked";
}
function automationClosureTone(status: string | null | undefined): WorkStatus {
const normalized = String(status ?? "").toLowerCase();
if (normalized.includes("ready")) return "in_progress";
if (normalized.includes("planned") || normalized.includes("draft")) return "watching";
if (normalized.includes("blocked") || normalized.includes("failed")) return "blocked";
if (normalized.includes("executed") || normalized.includes("healthy")) return "live";
return "watching";
}
function closureGateTone(status: string | null | undefined): WorkStatus {
const normalized = String(status ?? "").toLowerCase();
if (normalized === "passed" || normalized === "ready") return "live";
@@ -4203,6 +4212,48 @@ function RepairCandidateDraftPanel({
)
.filter((field): field is string => Boolean(field))
.slice(0, 8);
const controlledClosure = promotionContract?.controlled_automation_closure;
const noWriteRehearsal = controlledClosure?.no_write_rehearsal;
const verifierWritebackPlan = controlledClosure?.verifier_writeback_plan;
const controlledClosureCards = [
{
key: "rehearsal",
icon: Activity,
title: t("promotion.closure.rehearsal.title"),
status: noWriteRehearsal?.status,
primary: noWriteRehearsal?.rehearsal_id,
detail: noWriteRehearsal?.route_id,
meta: t("promotion.closure.rehearsal.meta", {
steps: noWriteRehearsal?.planned_steps?.length ?? 0,
}),
},
{
key: "verifier",
icon: SearchCheck,
title: t("promotion.closure.verifier.title"),
status: verifierWritebackPlan?.status,
primary: verifierWritebackPlan?.verifier_id,
detail: verifierWritebackPlan?.result_states?.slice(0, 3).join(" / "),
meta: t("promotion.closure.verifier.meta", {
checks: verifierWritebackPlan?.checks?.length ?? 0,
}),
},
{
key: "writeback",
icon: Database,
title: t("promotion.closure.writeback.title"),
status: controlledClosure?.status,
primary: controlledClosure?.runtime_write_gate,
detail: controlledClosure?.automation_asset_ledger
?.map((asset) => asset.asset_type)
.filter(Boolean)
.slice(0, 4)
.join(" / "),
meta: t("promotion.closure.writeback.meta", {
assets: controlledClosure?.automation_asset_ledger?.length ?? 0,
}),
},
];
const closureReadiness = chain?.automation_handoff?.closure_readiness;
const closureAvailable = Boolean(!promotionAvailable && closureReadiness);
const closureReady = toCount(closureReadiness?.ready_count);
@@ -4382,6 +4433,69 @@ function RepairCandidateDraftPanel({
) : null}
</div>
</div>
{controlledClosure ? (
<div className="mt-3 border border-[#9bb6d9] bg-[#eef5ff] p-3">
<div className="flex flex-wrap items-start justify-between gap-2">
<div>
<p className="text-xs font-semibold text-[#1f5b9b]">{t("promotion.closure.title")}</p>
<p className="mt-1 text-[11px] leading-5 text-[#475569]">
{t("promotion.closure.subtitle")}
</p>
</div>
<span className="border border-[#9bb6d9] bg-white px-2 py-0.5 font-mono text-[10px] font-semibold text-[#1f5b9b]">
{controlledClosure.runtime_write_gate ?? "--"}
</span>
</div>
<div className="mt-3 grid gap-2 md:grid-cols-3">
{controlledClosureCards.map((card) => {
const tone = automationClosureTone(card.status);
const Icon = card.icon;
const ToneIcon = statusConfig[tone].icon;
return (
<div key={card.key} className="min-w-0 border border-[#c7d7ef] bg-white px-3 py-2">
<div className="flex items-start justify-between gap-2">
<div className="min-w-0">
<div className="flex items-center gap-2">
<Icon className="h-3.5 w-3.5 shrink-0 text-brand-accent" aria-hidden="true" />
<p className="break-words text-xs font-semibold text-[#141413]">{card.title}</p>
</div>
<p className="mt-1 break-all font-mono text-[11px] leading-4 text-[#5f5b52]">
{card.primary ?? "--"}
</p>
<p className="mt-1 break-words text-[11px] leading-4 text-[#77736a]">
{card.detail ?? "--"}
</p>
</div>
<span className={cn("inline-flex shrink-0 items-center gap-1 border px-1.5 py-0.5 font-mono text-[10px] font-semibold", statusConfig[tone].className)}>
<ToneIcon className="h-3 w-3" aria-hidden="true" />
{card.status ?? "--"}
</span>
</div>
<p className="mt-2 border-t border-[#e0ddd4] pt-2 text-[11px] leading-4 text-[#5f5b52]">
{card.meta}
</p>
</div>
);
})}
</div>
<div className="mt-3 grid gap-2 md:grid-cols-4">
{(controlledClosure.automation_asset_ledger ?? []).map((asset) => (
<div key={`${asset.asset_type}-${asset.asset_id}`} className="min-w-0 border border-[#d8d3c7] bg-[#faf9f3] px-3 py-2">
<p className="font-mono text-[10px] font-semibold uppercase text-[#77736a]">
{asset.asset_type ?? "--"}
</p>
<p className="mt-1 break-all font-mono text-[11px] leading-4 text-[#141413]">
{asset.asset_id ?? "--"}
</p>
<p className="mt-1 text-[11px] leading-4 text-[#5f5b52]">
{asset.owner ?? "--"} · {asset.status ?? "--"}
</p>
</div>
))}
</div>
</div>
) : null}
</div>
) : closureAvailable ? (
<div className="p-3">

View File

@@ -376,6 +376,35 @@ export interface AwoooPStatusChain {
owner_review_required?: boolean | null;
forbidden_until_promoted?: string[];
next_steps?: string[];
controlled_automation_closure?: {
schema_version?: string | null;
status?: string | null;
runtime_write_gate?: string | null;
no_write_rehearsal?: {
rehearsal_id?: string | null;
status?: string | null;
route_id?: string | null;
planned_steps?: string[];
writes_runtime_state?: boolean | null;
executes_command?: boolean | null;
} | null;
verifier_writeback_plan?: {
verifier_id?: string | null;
status?: string | null;
checks?: string[];
result_states?: string[];
writes_after_execution?: string[];
writes_runtime_state?: boolean | null;
} | null;
automation_asset_ledger?: Array<{
asset_type?: string | null;
asset_id?: string | null;
owner?: string | null;
status?: string | null;
next_action?: string | null;
}>;
operator_next_actions?: string[];
} | null;
} | null;
} | null;
source_refs?: {

View File

@@ -1,3 +1,31 @@
## 2026-06-27D1J 修復候選升級合約:受控自動化閉環可視化
**背景**Telegram / AwoooP 告警卡已能指出 `repair_candidate_draft_ready_owner_review`,但 Work Items / Approvals 仍主要呈現「需人工」與長文字,操作員看不到 AI 已準備好的無寫入演練、Verifier 與 KM / PlayBook / Script 資產沉澱槽位,容易被誤判為 AI Agent 沒有任何自動化進展。
**完成內容**
- `repair_candidate_promotion_contract_v1` 新增 `controlled_automation_closure`
- `no_write_rehearsal`:固定 rehearsal id、route、input refs、planned steps明確 `executes_command=false``writes_runtime_state=false`
- `verifier_writeback_plan`:固定 verifier id、checks、result states 與執行後應回寫的 timeline / auto-repair / KM / PlayBook trust 欄位。
- `automation_asset_ledger`:固定 KM、PlayBook、ScriptOrAnsible、Verifier 四類資產槽位,帶 asset id、owner、status、next action。
- `/zh-TW/awooop/work-items` 的修復候選草案處置板新增「受控自動化閉環」三段卡無寫入演練、Verifier 回寫、資產沉澱;保留原有 promotion readiness、blocked fields 與 runtime gate=0。
- `AwoooPStatusChain` 型別同步支援新 closure contract避免前端只拿到長文字。
**本地驗證**
- `apps/api/venv/bin/python -m pytest apps/api/tests/test_repair_candidate_service.py apps/api/tests/test_awooop_operator_timeline_labels.py -q``77 passed`
- `apps/api/venv/bin/python -m py_compile apps/api/src/services/repair_candidate_service.py apps/api/src/services/platform_operator_service.py`:通過。
- `python3 -m json.tool apps/web/messages/zh-TW.json` / `apps/web/messages/en.json`:通過。
- i18n key mirror`14284 / 14284`diff `0`
- `pnpm --filter @awoooi/web typecheck`:通過。
- `python3 scripts/security/source-control-owner-response-guard.py --root .``SOURCE_CONTROL_OWNER_RESPONSE_GUARD_OK`
- `python3 scripts/security/security-mirror-progress-guard.py --root .``SECURITY_MIRROR_PROGRESS_GUARD_OK`
- `git diff --check`:通過。
**完成度 / 邊界**
- 修復候選升級合約可視化:本地 `100%`
- Work Items 受控閉環 UI本地 `100%`
- 真實 executor / Ansible apply / SSH / Telegram 實發 / KM 寫入 / PlayBook trust 寫入:仍 `0 / false`,本段沒有開 runtime gate。
- 這段把「下一步要自動化什麼」從長文字變成可讀回的 contract 與 UI 卡片;下一段必須接 owner release / no-write rehearsal persistence / verifier result capture不可再只新增靜態治理卡。
## 2026-06-2700:58 reboot SOP 實際修復188 MOMO backup core 假紅收斂
**時間與來源**