Merge remote-tracking branch 'gitea/main' into codex/github-private-backup-readback-20260627
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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?: {
|
||||
|
||||
@@ -1,3 +1,31 @@
|
||||
## 2026-06-27|D1J 修復候選升級合約:受控自動化閉環可視化
|
||||
|
||||
**背景**: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-27|00:58 reboot SOP 實際修復:188 MOMO backup core 假紅收斂
|
||||
|
||||
**時間與來源**:
|
||||
|
||||
Reference in New Issue
Block a user