From a282c6d2412fe16cb0906aa3f2c5af42a126f604 Mon Sep 17 00:00:00 2001 From: yihong guo Date: Thu, 2 Jul 2026 11:16:23 +0700 Subject: [PATCH] feat: add quality panel closeout guidance --- .../references/control-record-map.md | 4 +- .../briefloop/references/status-and-gates.md | 6 +- .../briefloop/references/version-matrix.md | 3 + CHANGELOG.md | 9 + .../references/control-record-map.md | 4 +- .../briefloop/references/status-and-gates.md | 6 +- .../briefloop/references/version-matrix.md | 3 + scripts/check_briefloop_skill_freshness.py | 4 + src/multi_agent_brief/outputs/finalize.py | 6 + .../product/quality_closeout.py | 188 ++++++++++++++++++ .../product/quality_panel.py | 41 +++- src/multi_agent_brief/status.py | 16 ++ tests/test_finalize_delivery_gate.py | 11 + tests/test_quality_panel.py | 105 ++++++++++ 14 files changed, 401 insertions(+), 5 deletions(-) create mode 100644 src/multi_agent_brief/product/quality_closeout.py diff --git a/.agents/skills/briefloop/references/control-record-map.md b/.agents/skills/briefloop/references/control-record-map.md index 2d25ca57..f03206a9 100644 --- a/.agents/skills/briefloop/references/control-record-map.md +++ b/.agents/skills/briefloop/references/control-record-map.md @@ -29,7 +29,9 @@ Owning commands for recent control-tool projections: - `briefloop quality summarize --workspace ` writes `quality_panel.json`, source-bound `quality_summary.md`, and static - `quality_panel.html`. + `quality_panel.html`. `finalize_report.json` and `status --json` may + recommend this post-finalize closeout, but they do not write these artifacts + automatically. - `multi-agent-brief approval init` and `multi-agent-brief approval record` write `human_approval_ledger.json` with event-log linkage. - `multi-agent-brief release check` reads `human_approval_ledger.json` and diff --git a/.agents/skills/briefloop/references/status-and-gates.md b/.agents/skills/briefloop/references/status-and-gates.md index 6e9d974d..61db5ff9 100644 --- a/.agents/skills/briefloop/references/status-and-gates.md +++ b/.agents/skills/briefloop/references/status-and-gates.md @@ -103,9 +103,13 @@ projection. Stage-scoped gate authority lives under experimental product-quality audit/control projections. - Write them with `briefloop quality summarize --workspace `. +- After successful finalize, `finalize_report.json` and `status --json` may + project `quality_panel_closeout` as a post-finalize recommendation to run + `briefloop quality summarize --workspace `. This recommendation + is not a gate, delivery approval, release approval, or automatic writer. - `quality_summary.md` and `quality_panel.html` must be rendered from the sibling `quality_panel.json` and carry its SHA-256 binding. -- They may be included in the audit bundle when valid. +- They may be included in the audit bundle when valid. They remain excluded from reader-facing delivery bundles. - They do not run gates, replace gate reports, create a quality score, repair artifacts, approve delivery, decide release eligibility, or prove truth. - If stale or hand-edited, rerun `briefloop quality summarize`; do not patch diff --git a/.agents/skills/briefloop/references/version-matrix.md b/.agents/skills/briefloop/references/version-matrix.md index f4df4dbf..c1f75764 100644 --- a/.agents/skills/briefloop/references/version-matrix.md +++ b/.agents/skills/briefloop/references/version-matrix.md @@ -118,6 +118,9 @@ Historical implementation name: MABW - `briefloop quality summarize --workspace ` - artifacts: `quality_panel.json`, `quality_summary.md`, `quality_panel.html` + - `quality_panel_closeout` in finalize/status is a post-finalize + recommendation to run `briefloop quality summarize --workspace + `, not an automatic writer - quality summary and HTML are SHA-bound projections of `quality_panel.json` - audit bundle inclusion is allowed when present and valid; delivery bundle diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b6c0f35..1179a561 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- **Quality Panel real-run closeout guidance**: finalize reports and + `status --json` now project a post-finalize Quality Panel closeout + recommendation pointing operators to + `briefloop quality summarize --workspace `. The Quality Panel and + summary/HTML renderers also show the audit/delivery bundle separation for + these artifacts. This is operator follow-up guidance only; finalize does not + auto-generate Quality Panel artifacts, Quality Panel remains audit-bundle + material when valid, and it does not approve delivery, decide release + readiness, run gates, repair content, or prove report correctness. - **Citation Profile Split**: packaged ReportTemplates can now declare `reader_contract.citation_profile` values (`executive`, `analyst`, or `audit`) so finalize reports and bundle manifests record the resolved diff --git a/integrations/hermes-plugin/mabw/skills/briefloop/references/control-record-map.md b/integrations/hermes-plugin/mabw/skills/briefloop/references/control-record-map.md index 2d25ca57..f03206a9 100644 --- a/integrations/hermes-plugin/mabw/skills/briefloop/references/control-record-map.md +++ b/integrations/hermes-plugin/mabw/skills/briefloop/references/control-record-map.md @@ -29,7 +29,9 @@ Owning commands for recent control-tool projections: - `briefloop quality summarize --workspace ` writes `quality_panel.json`, source-bound `quality_summary.md`, and static - `quality_panel.html`. + `quality_panel.html`. `finalize_report.json` and `status --json` may + recommend this post-finalize closeout, but they do not write these artifacts + automatically. - `multi-agent-brief approval init` and `multi-agent-brief approval record` write `human_approval_ledger.json` with event-log linkage. - `multi-agent-brief release check` reads `human_approval_ledger.json` and diff --git a/integrations/hermes-plugin/mabw/skills/briefloop/references/status-and-gates.md b/integrations/hermes-plugin/mabw/skills/briefloop/references/status-and-gates.md index 6e9d974d..61db5ff9 100644 --- a/integrations/hermes-plugin/mabw/skills/briefloop/references/status-and-gates.md +++ b/integrations/hermes-plugin/mabw/skills/briefloop/references/status-and-gates.md @@ -103,9 +103,13 @@ projection. Stage-scoped gate authority lives under experimental product-quality audit/control projections. - Write them with `briefloop quality summarize --workspace `. +- After successful finalize, `finalize_report.json` and `status --json` may + project `quality_panel_closeout` as a post-finalize recommendation to run + `briefloop quality summarize --workspace `. This recommendation + is not a gate, delivery approval, release approval, or automatic writer. - `quality_summary.md` and `quality_panel.html` must be rendered from the sibling `quality_panel.json` and carry its SHA-256 binding. -- They may be included in the audit bundle when valid. +- They may be included in the audit bundle when valid. They remain excluded from reader-facing delivery bundles. - They do not run gates, replace gate reports, create a quality score, repair artifacts, approve delivery, decide release eligibility, or prove truth. - If stale or hand-edited, rerun `briefloop quality summarize`; do not patch diff --git a/integrations/hermes-plugin/mabw/skills/briefloop/references/version-matrix.md b/integrations/hermes-plugin/mabw/skills/briefloop/references/version-matrix.md index f4df4dbf..c1f75764 100644 --- a/integrations/hermes-plugin/mabw/skills/briefloop/references/version-matrix.md +++ b/integrations/hermes-plugin/mabw/skills/briefloop/references/version-matrix.md @@ -118,6 +118,9 @@ Historical implementation name: MABW - `briefloop quality summarize --workspace ` - artifacts: `quality_panel.json`, `quality_summary.md`, `quality_panel.html` + - `quality_panel_closeout` in finalize/status is a post-finalize + recommendation to run `briefloop quality summarize --workspace + `, not an automatic writer - quality summary and HTML are SHA-bound projections of `quality_panel.json` - audit bundle inclusion is allowed when present and valid; delivery bundle diff --git a/scripts/check_briefloop_skill_freshness.py b/scripts/check_briefloop_skill_freshness.py index 62560838..f5ae975c 100644 --- a/scripts/check_briefloop_skill_freshness.py +++ b/scripts/check_briefloop_skill_freshness.py @@ -26,6 +26,7 @@ "quality_panel.json", "quality_summary.md", "quality_panel.html", + "quality_panel_closeout", "approval init", "approval record", "release check", @@ -76,6 +77,8 @@ "Reader Template Conformance is warning-only", "Citation profiles split reader and audit citation surfaces", "Support-Calibrated Wording is warning-only", + "quality_panel_closeout", + "excluded from reader-facing delivery bundles", ], "references/control-record-map.md": [ "quality_panel.json", @@ -86,6 +89,7 @@ "Support-Calibrated Wording is a status / Quality Panel projection", "release_readiness_report.json", "resolved citation profile", + "post-finalize closeout", ], "references/repo-development.md": [ "check_product_baseline.py", diff --git a/src/multi_agent_brief/outputs/finalize.py b/src/multi_agent_brief/outputs/finalize.py index 05beed78..6fbb6802 100644 --- a/src/multi_agent_brief/outputs/finalize.py +++ b/src/multi_agent_brief/outputs/finalize.py @@ -28,6 +28,7 @@ resolve_workspace_policy_gate_adapter, ) from multi_agent_brief.product.citation_profile import resolve_workspace_citation_profile +from multi_agent_brief.product.quality_closeout import quality_panel_closeout_projection from multi_agent_brief.product.template_renderer import render_reader_markdown_with_template from multi_agent_brief.product.template_conformance import project_workspace_report_template_conformance @@ -92,6 +93,7 @@ class FinalizeResult: citation_profile_delivery_exposes_local_paths: bool = False citation_profile_audit_bundle_keeps_trace: bool = True citation_profile_warnings: list[str] = field(default_factory=list) + quality_panel_closeout: dict[str, Any] = field(default_factory=dict) def to_dict(self) -> dict[str, Any]: data = asdict(self) @@ -103,6 +105,10 @@ def to_dict(self) -> dict[str, Any]: data["reader_clean"] = _empty_reader_clean_report() if data["audit_binding"] is None: data["audit_binding"] = _empty_audit_binding_report() + if not data["quality_panel_closeout"]: + data["quality_panel_closeout"] = quality_panel_closeout_projection( + finalize_report=data, + ) return data diff --git a/src/multi_agent_brief/product/quality_closeout.py b/src/multi_agent_brief/product/quality_closeout.py new file mode 100644 index 00000000..60dbdd5c --- /dev/null +++ b/src/multi_agent_brief/product/quality_closeout.py @@ -0,0 +1,188 @@ +from __future__ import annotations + +import hashlib +import json +from pathlib import Path +from typing import Any, Mapping + +QUALITY_PANEL_CLOSEOUT_COMMAND = "briefloop quality summarize --workspace " +QUALITY_PANEL_CLOSEOUT_BOUNDARY = ( + "post_finalize_quality_projection_only_not_gate_delivery_or_release_authority" +) +QUALITY_PANEL_CLOSEOUT_ARTIFACTS = ( + "output/intermediate/quality_panel.json", + "output/intermediate/quality_summary.md", + "output/intermediate/quality_panel.html", +) + + +def quality_panel_closeout_projection( + *, + workspace: str | Path | None = None, + finalize_report: Mapping[str, Any] | None = None, + generated_by_quality_summarize: bool = False, + artifact_registry: Mapping[str, Any] | None = None, +) -> dict[str, Any]: + """Project the post-finalize Quality Panel closeout surface. + + The projection is advisory. It does not create gate, delivery, or release + authority and it does not write the Quality Panel artifacts. + """ + + finalize_ready = _finalize_report_passed(finalize_report) + status = "recommended" + reason = "run_briefloop_quality_summarize_after_finalize" + if not finalize_ready: + status = "not_ready" + reason = "finalize_report_not_passed" + elif generated_by_quality_summarize: + status = "generated" + reason = "quality_summarize_generated_projection" + + present: list[str] = [] + missing: list[str] = [] + invalid: list[str] = [] + if workspace is not None: + ws = Path(workspace).expanduser().resolve() + for artifact in QUALITY_PANEL_CLOSEOUT_ARTIFACTS: + if (ws / artifact).exists(): + present.append(artifact) + else: + missing.append(artifact) + if status == "recommended" and not missing: + validation_reason = _quality_artifacts_validation_reason(ws) + registry_reason = _quality_artifact_registry_reason(artifact_registry) + if validation_reason is None and registry_reason is None: + status = "complete" + reason = "quality_projection_artifacts_valid" + else: + status = "stale_or_invalid" + reason = validation_reason or registry_reason or "quality_projection_artifacts_invalid" + invalid = list(QUALITY_PANEL_CLOSEOUT_ARTIFACTS) + + return { + "status": status, + "reason": reason, + "command": QUALITY_PANEL_CLOSEOUT_COMMAND, + "artifacts": list(QUALITY_PANEL_CLOSEOUT_ARTIFACTS), + "present_artifacts": present, + "missing_artifacts": missing, + "invalid_artifacts": invalid, + "audit_bundle": "included_when_present_and_valid", + "delivery_bundle": "excluded", + "runtime_effect": "operator_followup_only", + "boundary": QUALITY_PANEL_CLOSEOUT_BOUNDARY, + "gate_authority": False, + "delivery_authority": False, + "release_authority": False, + } + + +def validate_quality_panel_closeout_payload(payload: Any) -> str | None: + if not isinstance(payload, dict): + return "quality_panel_closeout_schema_error:not_object" + if payload.get("boundary") != QUALITY_PANEL_CLOSEOUT_BOUNDARY: + return "quality_panel_closeout_schema_error:boundary" + if payload.get("runtime_effect") != "operator_followup_only": + return "quality_panel_closeout_schema_error:runtime_effect" + if payload.get("status") not in {"not_ready", "recommended", "complete", "generated", "stale_or_invalid"}: + return "quality_panel_closeout_schema_error:status" + if payload.get("command") != QUALITY_PANEL_CLOSEOUT_COMMAND: + return "quality_panel_closeout_schema_error:command" + if payload.get("artifacts") != list(QUALITY_PANEL_CLOSEOUT_ARTIFACTS): + return "quality_panel_closeout_schema_error:artifacts" + for field in ("present_artifacts", "missing_artifacts", "invalid_artifacts"): + values = payload.get(field) + if not isinstance(values, list) or any(not isinstance(item, str) for item in values): + return f"quality_panel_closeout_schema_error:{field}" + if payload.get("audit_bundle") != "included_when_present_and_valid": + return "quality_panel_closeout_schema_error:audit_bundle" + if payload.get("delivery_bundle") != "excluded": + return "quality_panel_closeout_schema_error:delivery_bundle" + for field in ("gate_authority", "delivery_authority", "release_authority"): + if payload.get(field) is not False: + return f"quality_panel_closeout_schema_error:{field}" + return None + + +def _finalize_report_passed(finalize_report: Mapping[str, Any] | None) -> bool: + if not isinstance(finalize_report, Mapping): + return False + if str(finalize_report.get("status") or "").strip() != "pass": + return False + reader_clean = finalize_report.get("reader_clean") + if isinstance(reader_clean, Mapping): + return str(reader_clean.get("status") or "").strip() == "pass" + return False + + +def _quality_artifact_registry_reason(artifact_registry: Mapping[str, Any] | None) -> str | None: + if not isinstance(artifact_registry, Mapping): + return "quality_projection_artifact_registry_missing" + artifacts = artifact_registry.get("artifacts") + if not isinstance(artifacts, Mapping): + return "quality_projection_artifact_registry_missing" + for artifact_id in ("quality_panel", "quality_summary", "quality_panel_html"): + record = artifacts.get(artifact_id) + if not isinstance(record, Mapping): + return f"quality_projection_artifact_registry_missing:{artifact_id}" + if str(record.get("status") or "").strip() != "valid": + return f"quality_projection_artifact_registry_not_valid:{artifact_id}" + return None + + +def _quality_artifacts_validation_reason(workspace: Path) -> str | None: + panel_path = workspace / "output" / "intermediate" / "quality_panel.json" + summary_path = workspace / "output" / "intermediate" / "quality_summary.md" + html_path = workspace / "output" / "intermediate" / "quality_panel.html" + try: + panel_payload = json.loads(panel_path.read_text(encoding="utf-8")) + except OSError: + return "quality_panel_unreadable" + except UnicodeDecodeError: + return "quality_panel_unreadable" + except json.JSONDecodeError: + return "quality_panel_parse_error" + if not isinstance(panel_payload, dict): + return "quality_panel_invalid:not_object" + + try: + from multi_agent_brief.product.quality_panel import ( + render_quality_panel_html, + render_quality_summary, + validate_quality_panel_payload, + validate_quality_panel_html, + validate_quality_summary_markdown, + ) + + panel_reason = validate_quality_panel_payload(panel_payload) + if panel_reason: + return f"quality_panel_invalid:{panel_reason}" + panel_sha256 = _sha256_file(panel_path) + summary_text = summary_path.read_text(encoding="utf-8") + html_text = html_path.read_text(encoding="utf-8") + summary_reason = validate_quality_summary_markdown(summary_text) + if summary_reason: + return f"quality_summary_invalid:{summary_reason}" + html_reason = validate_quality_panel_html(html_text) + if html_reason: + return f"quality_panel_html_invalid:{html_reason}" + if summary_text != render_quality_summary(panel_payload, quality_panel_sha256=panel_sha256): + return "quality_summary_stale_or_hand_edited" + if html_text != render_quality_panel_html(panel_payload, quality_panel_sha256=panel_sha256): + return "quality_panel_html_stale_or_hand_edited" + except OSError: + return "quality_projection_artifact_unreadable" + except UnicodeDecodeError: + return "quality_projection_artifact_unreadable" + except Exception as exc: + return f"quality_projection_artifact_invalid:{type(exc).__name__}" + return None + + +def _sha256_file(path: Path) -> str: + h = hashlib.sha256() + with path.open("rb") as f: + for chunk in iter(lambda: f.read(1024 * 1024), b""): + h.update(chunk) + return h.hexdigest() diff --git a/src/multi_agent_brief/product/quality_panel.py b/src/multi_agent_brief/product/quality_panel.py index 348e1740..8e37a3a6 100644 --- a/src/multi_agent_brief/product/quality_panel.py +++ b/src/multi_agent_brief/product/quality_panel.py @@ -21,6 +21,10 @@ validate_guidance_manifestation_projection_payload, ) from multi_agent_brief.product.materiality_selection import validate_materiality_selection_payload +from multi_agent_brief.product.quality_closeout import ( + quality_panel_closeout_projection, + validate_quality_panel_closeout_payload, +) from multi_agent_brief.product.support_wording import validate_support_wording_payload from multi_agent_brief.product.template_conformance import validate_report_template_conformance_payload from multi_agent_brief.product.trajectory_regulation import validate_trajectory_regulation_payload @@ -135,6 +139,13 @@ def build_quality_panel(workspace: str | Path) -> dict[str, Any]: if isinstance(workspace_status.get("support_wording"), dict) else {} ) + finalize_report = _read_json_mapping(ws / _INTERMEDIATE / "finalize_report.json") or {} + closeout = quality_panel_closeout_projection( + workspace=ws, + finalize_report=finalize_report, + generated_by_quality_summarize=True, + artifact_registry=registry_payload, + ) control_integrity = { "run_integrity": run_integrity.get("status") or "unknown", "reference_eligible": bool(run_integrity.get("reference_eligible")), @@ -184,6 +195,7 @@ def build_quality_panel(workspace: str | Path) -> dict[str, Any]: "materiality_selection": materiality_selection, "report_template_conformance": report_template_conformance, "support_wording": support_wording, + "quality_panel_closeout": closeout, "recommended_actions": recommended_actions, "non_goals": [ "quality_score", @@ -245,6 +257,8 @@ def render_quality_summary( template_conformance = template_conformance if isinstance(template_conformance, Mapping) else {} support_wording = panel_payload.get("support_wording") support_wording = support_wording if isinstance(support_wording, Mapping) else {} + closeout = panel_payload.get("quality_panel_closeout") + closeout = closeout if isinstance(closeout, Mapping) else {} actions = panel_payload.get("recommended_actions") actions = actions if isinstance(actions, list) else [] @@ -319,6 +333,13 @@ def render_quality_summary( f"- Support wording status: `{_text(support_wording.get('status')) or 'unknown'}`", f"- Support wording warnings: `{_support_wording_warning_count(support_wording)}`", "", + "## Quality Closeout And Bundle Separation", + "", + f"- Quality closeout status: `{_text(closeout.get('status')) or 'unknown'}`", + f"- Closeout command: `{_text(closeout.get('command')) or 'unknown'}`", + f"- Audit bundle: `{_text(closeout.get('audit_bundle')) or 'unknown'}`", + f"- Delivery bundle: `{_text(closeout.get('delivery_bundle')) or 'unknown'}`", + "", "## Recommended Next Actions", "", ]) @@ -391,6 +412,8 @@ def render_quality_panel_html( template_conformance = template_conformance if isinstance(template_conformance, Mapping) else {} support_wording = panel_payload.get("support_wording") support_wording = support_wording if isinstance(support_wording, Mapping) else {} + closeout = panel_payload.get("quality_panel_closeout") + closeout = closeout if isinstance(closeout, Mapping) else {} actions = panel_payload.get("recommended_actions") actions = actions if isinstance(actions, list) else [] overall_status = _text(panel_payload.get("overall_status")) or "unknown" @@ -514,6 +537,15 @@ def render_quality_panel_html( ), ], ), + _html_section( + "Quality Closeout And Bundle Separation", + [ + ("Closeout status", _text(closeout.get("status")) or "unknown"), + ("Closeout command", _text(closeout.get("command")) or "unknown"), + ("Audit bundle", _text(closeout.get("audit_bundle")) or "unknown"), + ("Delivery bundle", _text(closeout.get("delivery_bundle")) or "unknown"), + ], + ), _html_actions(actions), ] ) @@ -627,6 +659,13 @@ def validate_quality_panel_payload(payload: Any) -> str | None: support_wording_error = validate_support_wording_payload(support_wording) if support_wording_error: return f"quality_panel_schema_error:support_wording:{support_wording_error}" + closeout = payload.get("quality_panel_closeout") + if closeout is not None: + if not isinstance(closeout, dict): + return "quality_panel_schema_error:quality_panel_closeout" + closeout_error = validate_quality_panel_closeout_payload(closeout) + if closeout_error: + return f"quality_panel_schema_error:quality_panel_closeout:{closeout_error}" recommended_actions = payload.get("recommended_actions") if not isinstance(recommended_actions, list): return "quality_panel_schema_error:recommended_actions" @@ -1564,7 +1603,7 @@ def _read_json_mapping(path: Path) -> dict[str, Any] | None: return None try: payload = json.loads(path.read_text(encoding="utf-8")) - except (OSError, json.JSONDecodeError): + except (OSError, UnicodeDecodeError, json.JSONDecodeError): return None return payload if isinstance(payload, dict) else None diff --git a/src/multi_agent_brief/status.py b/src/multi_agent_brief/status.py index d03c7e42..5f7e543b 100644 --- a/src/multi_agent_brief/status.py +++ b/src/multi_agent_brief/status.py @@ -28,6 +28,7 @@ from multi_agent_brief.product.guidance_manifestation import project_workspace_guidance_manifestation from multi_agent_brief.product.materiality_selection import project_workspace_materiality_selection from multi_agent_brief.product.policy_projection import project_workspace_policy_profile +from multi_agent_brief.product.quality_closeout import quality_panel_closeout_projection from multi_agent_brief.product.support_wording import project_workspace_support_wording from multi_agent_brief.product.template_conformance import project_workspace_report_template_conformance from multi_agent_brief.product.template_projection import project_workspace_report_template @@ -72,6 +73,7 @@ def build_workspace_status(workspace: str | Path) -> dict[str, Any]: "guidance_manifestation": {}, "materiality_selection": {}, "support_wording": {}, + "quality_panel_closeout": {}, "timing": {}, "stale_or_unknown": [], "suggested_next_command": None, @@ -115,6 +117,11 @@ def build_workspace_status(workspace: str | Path) -> dict[str, Any]: ) ) payload["reader_clean"] = _reader_clean_summary(finalize_report) + payload["quality_panel_closeout"] = quality_panel_closeout_projection( + workspace=ws, + finalize_report=finalize_report.get("payload") if finalize_report.get("status") == "present" else None, + artifact_registry=registry.get("payload") if registry.get("status") == "present" else None, + ) payload["improvement"] = _improvement_summary(ws, manifest) payload["feedback"] = _feedback_summary(feedback_issues, repair_plan) payload["experiment_080"] = project_assessment_target_status( @@ -222,6 +229,7 @@ def format_workspace_status(status: dict[str, Any]) -> str: guidance_manifestation = status.get("guidance_manifestation") or {} materiality_selection = status.get("materiality_selection") or {} support_wording = status.get("support_wording") or {} + quality_panel_closeout = status.get("quality_panel_closeout") or {} events = status.get("events") or {} timing = status.get("timing") or {} run_integrity = workflow.get("run_integrity") if isinstance(workflow.get("run_integrity"), dict) else {} @@ -253,6 +261,11 @@ def format_workspace_status(status: dict[str, Any]) -> str: *_format_experiment_080_lines(experiment_080), f"[status] quality_gate: {gate.get('status') or 'unknown'}", f"[status] reader_clean: {reader.get('status') or 'unknown'}", + ( + "[status] quality_panel_closeout: " + f"{quality_panel_closeout.get('status') or 'unknown'} " + f"command={quality_panel_closeout.get('command') or ''}" + ), ( "[status] improvement: " f"ledger={improvement.get('ledger_present')} " @@ -807,6 +820,9 @@ def _suggested_next_command(workspace: Path, status: dict[str, Any]) -> str: return f"multi-agent-brief run --workspace {workspace} --recipe fast-rerun --skip-doctor" if current_stage == "finalize": return f"/mabw deliver {workspace}" + quality_closeout = status.get("quality_panel_closeout") or {} + if quality_closeout.get("status") in {"recommended", "stale_or_invalid"}: + return f"briefloop quality summarize --workspace {workspace}" if current_stage == "auditor" and gate.get("status") != "pass": return f"multi-agent-brief gates check --workspace {workspace} --stage auditor" if current_stage: diff --git a/tests/test_finalize_delivery_gate.py b/tests/test_finalize_delivery_gate.py index 1eeb1d1e..be1b973f 100644 --- a/tests/test_finalize_delivery_gate.py +++ b/tests/test_finalize_delivery_gate.py @@ -482,6 +482,17 @@ def test_finalize_cli_strips_src_markers_after_subagent_rewrite(tmp_path: Path, assert (output_dir / "delivery" / "brief.md").exists() assert "[finalize] Delivery snapshot:" in captured.out assert (intermediate / "finalize_report.json").exists() + report = json.loads((intermediate / "finalize_report.json").read_text(encoding="utf-8")) + closeout = report["quality_panel_closeout"] + assert closeout["status"] == "recommended" + assert closeout["command"] == "briefloop quality summarize --workspace " + assert closeout["runtime_effect"] == "operator_followup_only" + assert closeout["delivery_bundle"] == "excluded" + assert closeout["delivery_authority"] is False + assert closeout["release_authority"] is False + assert not (intermediate / "quality_panel.json").exists() + assert not (intermediate / "quality_summary.md").exists() + assert not (intermediate / "quality_panel.html").exists() def test_finalize_cli_fails_without_writing_when_active_repair_open(tmp_path: Path, capsys): diff --git a/tests/test_quality_panel.py b/tests/test_quality_panel.py index 73304a23..27726dee 100644 --- a/tests/test_quality_panel.py +++ b/tests/test_quality_panel.py @@ -12,6 +12,7 @@ import pytest from multi_agent_brief.cli.main import main +from multi_agent_brief.status import build_workspace_status, format_workspace_status from multi_agent_brief.product.quality_panel import ( QUALITY_PANEL_HTML_BOUNDARY, QUALITY_PANEL_BOUNDARY, @@ -417,11 +418,114 @@ def test_quality_panel_builds_incomplete_projection_without_writing(tmp_path: Pa assert payload["overall_status"] == "incomplete" assert payload["source_evidence"]["source_pack_status"] == "missing" assert payload["control_integrity"]["fact_layer_status"] == "missing" + assert payload["quality_panel_closeout"]["status"] == "not_ready" assert payload["recommended_actions"][0]["action"] == "materialize_durable_source_evidence" assert not quality_panel_path(ws).exists() assert validate_quality_panel_payload(payload) is None +def test_status_recommends_quality_closeout_after_finalize_report_pass(tmp_path: Path) -> None: + ws = _workspace(tmp_path) + _write_finalize_report(ws) + + status = build_workspace_status(ws) + closeout = status["quality_panel_closeout"] + + assert closeout["status"] == "recommended" + assert closeout["command"] == "briefloop quality summarize --workspace " + assert closeout["runtime_effect"] == "operator_followup_only" + assert closeout["delivery_authority"] is False + assert closeout["release_authority"] is False + assert "quality_panel.json" in "\n".join(closeout["missing_artifacts"]) + assert "quality_panel_closeout: recommended" in format_workspace_status(status) + + +def test_quality_summarize_marks_closeout_generated_and_then_complete(tmp_path: Path) -> None: + ws = _workspace(tmp_path) + _write_finalize_report(ws) + + panel = write_quality_panel(workspace=ws) + summary = write_quality_summary(workspace=ws, panel_payload=panel) + html = write_quality_panel_html(workspace=ws, panel_payload=panel) + + assert panel["quality_panel_closeout"]["status"] == "generated" + assert panel["quality_panel_closeout"]["audit_bundle"] == "included_when_present_and_valid" + assert panel["quality_panel_closeout"]["delivery_bundle"] == "excluded" + assert panel["quality_panel_closeout"]["gate_authority"] is False + assert validate_quality_panel_payload(panel) is None + assert summary["path"] == "output/intermediate/quality_summary.md" + assert html["path"] == "output/intermediate/quality_panel.html" + + pre_registry_status = build_workspace_status(ws) + assert pre_registry_status["quality_panel_closeout"]["status"] == "stale_or_invalid" + + assert main(["state", "check", "--workspace", str(ws), "--json"]) == 0 + status = build_workspace_status(ws) + assert status["quality_panel_closeout"]["status"] == "complete" + assert not status["quality_panel_closeout"]["missing_artifacts"] + + summary_text = quality_summary_path(ws).read_text(encoding="utf-8") + html_text = quality_panel_html_path(ws).read_text(encoding="utf-8") + assert "## Quality Closeout And Bundle Separation" in summary_text + assert "- Delivery bundle: `excluded`" in summary_text + assert "Quality Closeout And Bundle Separation" in html_text + + +def test_quality_closeout_rejects_stale_or_hand_edited_quality_artifacts(tmp_path: Path) -> None: + ws = _workspace(tmp_path) + _write_finalize_report(ws) + panel = write_quality_panel(workspace=ws) + write_quality_summary(workspace=ws, panel_payload=panel) + write_quality_panel_html(workspace=ws, panel_payload=panel) + assert main(["state", "check", "--workspace", str(ws), "--json"]) == 0 + assert build_workspace_status(ws)["quality_panel_closeout"]["status"] == "complete" + + html_path = quality_panel_html_path(ws) + html_path.write_text( + html_path.read_text(encoding="utf-8").replace("Run integrity", "Run integrity edited", 1), + encoding="utf-8", + ) + status = build_workspace_status(ws) + + closeout = status["quality_panel_closeout"] + assert closeout["status"] == "stale_or_invalid" + assert closeout["reason"] == "quality_panel_html_stale_or_hand_edited" + assert "quality_panel.html" in "\n".join(closeout["invalid_artifacts"]) + assert status["suggested_next_command"].startswith("briefloop quality summarize --workspace ") + + +def test_quality_panel_handles_corrupt_finalize_report_utf8_without_crashing(tmp_path: Path) -> None: + ws = _workspace(tmp_path) + (ws / "output" / "intermediate" / "finalize_report.json").write_bytes(b"\xff\xfe") + + status = build_workspace_status(ws) + panel = build_quality_panel(ws) + + assert status["quality_panel_closeout"]["status"] == "not_ready" + assert panel["quality_panel_closeout"]["status"] == "not_ready" + assert validate_quality_panel_payload(panel) is None + + +def test_quality_panel_payload_validator_rejects_forged_closeout_authority(tmp_path: Path) -> None: + ws = _workspace(tmp_path) + _write_finalize_report(ws) + payload = build_quality_panel(ws) + + forged_delivery_bundle = json.loads(json.dumps(payload)) + forged_delivery_bundle["quality_panel_closeout"]["delivery_bundle"] = "included" + assert validate_quality_panel_payload(forged_delivery_bundle) == ( + "quality_panel_schema_error:quality_panel_closeout:" + "quality_panel_closeout_schema_error:delivery_bundle" + ) + + forged_delivery_authority = json.loads(json.dumps(payload)) + forged_delivery_authority["quality_panel_closeout"]["delivery_authority"] = True + assert validate_quality_panel_payload(forged_delivery_authority) == ( + "quality_panel_schema_error:quality_panel_closeout:" + "quality_panel_closeout_schema_error:delivery_authority" + ) + + def test_quality_panel_surfaces_reader_template_conformance_without_authority(tmp_path: Path) -> None: ws = _workspace(tmp_path) (ws / "report_spec.yaml").write_text( @@ -612,6 +716,7 @@ def test_quality_panel_html_renders_static_audit_attachment_without_external_ass assert "

Gate Findings

" in html assert "

Claim And Support Risk

" in html assert "

Reader Clean And Citation Hygiene

" in html + assert "

Quality Closeout And Bundle Separation

" in html assert "

Recommended Next Actions

" in html lower = html.lower() assert "