Close decision assistant H15 F16 G15 D17

This commit is contained in:
Codex
2026-05-13 09:43:56 +08:00
parent d77dcda753
commit 07687e326f
5 changed files with 461 additions and 8 deletions
+24 -3
View File
@@ -2,7 +2,7 @@
## Current Goal
Automation execution-layer PXX remains closed and its known scope stays frozen. Under explicit user direction, the active product phase is a dense `Decision Assistant` operator workspace with grouped recall, broader replay baselines, note templates, quick-save shortcuts, and concrete OKX announcements/system-status read-only sources. This run closed H14/F15/G14/D16 by adding handoff lane grouping/overdue cleanup visibility, digest proof-aging escalation, template feedback trend history with negative-feedback guardrails, and setup-family promotion-readiness hints. Local validation passed, the current working tree was deployed, and remote smoke confirmed the latest Decision Assistant fields on the live host.
Automation execution-layer PXX remains closed and its known scope stays frozen. Under explicit user direction, the active product phase is a dense `Decision Assistant` operator workspace with grouped recall, broader replay baselines, note templates, quick-save shortcuts, and concrete OKX announcements/system-status read-only sources. This run closed H15/F16/G15/D17 by adding handoff lifecycle / operator resume queue fields, digest proof follow-up queue and resolved states, template guardrail resolution / preferred-template decay fields, and setup-family promotion queue / demotion history hints. Local validation passed, the current working tree was deployed, and remote smoke confirmed the latest Decision Assistant fields on the live host.
## Current Branch
@@ -84,6 +84,10 @@ Automation execution-layer PXX remains closed and its known scope stays frozen.
- latest_decision_assistant_digest_proof_escalation_slice: proof_aging_trend_stale_escalation
- latest_decision_assistant_template_guardrail_slice: feedback_trend_negative_guardrails
- latest_decision_assistant_replay_promotion_slice: setup_family_watchlist_promotion_readiness
- latest_decision_assistant_handoff_lifecycle_slice: handoff_lifecycle_resume_queue
- latest_decision_assistant_digest_followup_slice: proof_escalation_followup_resolved_states
- latest_decision_assistant_template_guardrail_resolution_slice: guardrail_resolution_preferred_decay
- latest_decision_assistant_replay_promotion_queue_slice: setup_family_promotion_queue_demotion_history
- latest_decision_assistant_summary_status: implemented
- latest_decision_assistant_change_summary_status: implemented
- latest_decision_assistant_evidence_status: implemented
@@ -152,10 +156,14 @@ Automation execution-layer PXX remains closed and its known scope stays frozen.
- latest_decision_assistant_digest_proof_escalation_status: implemented
- latest_decision_assistant_template_guardrail_status: implemented
- latest_decision_assistant_replay_promotion_status: implemented
- latest_decision_assistant_handoff_lifecycle_status: implemented
- latest_decision_assistant_digest_followup_status: implemented
- latest_decision_assistant_template_guardrail_resolution_status: implemented
- latest_decision_assistant_replay_promotion_queue_status: implemented
- latest_decision_assistant_workbench_status: visible_local_smoke
- latest_remote_decision_assistant_deploy_status: deployed_current_head
- latest_remote_decision_assistant_smoke_status: passed
- latest_remote_decision_assistant_review_packet_status: handoff_grouping_proof_escalation_feedback_guardrails_promotion_visible
- latest_remote_decision_assistant_review_packet_status: handoff_lifecycle_proof_followup_guardrail_resolution_promotion_queue_visible
- latest_p7_selector_choice: continue_periodic_analysis_decision_monitoring_with_maturity_rollup
- latest_p14_slice: webhook_transport_retry
- latest_p15_pxx_tasklist_status: ready
@@ -234,6 +242,12 @@ Automation execution-layer PXX remains closed and its known scope stays frozen.
## Completed In This Run
- Added handoff lifecycle / operator resume queue fields (`lifecycle_state`, `resume_queue_rank`, `resume_action_label`, `handoff_lifecycle_summary`, `operator_resume_queue`, and `next_resume_*`) plus cockpit resume visibility.
- Added digest proof follow-up queue and resolved-state fields (`proof_follow_up_state`, `proof_follow_up_action_label`, `proof_resolved_state`, `proof_follow_up_queue`, `resolved_proof_count`, and `proof_resolution_state_counts`).
- Added template guardrail resolution / preferred-template decay fields (`guardrail_resolution_status`, `guardrail_resolution_prompt`, `preferred_template_decay`, `guardrail_resolution_queue`, and `preferred_template_decay_status`).
- Added setup-family promotion queue / demotion history fields (`promotion_queue_status`, `promotion_queue_rank`, `promotion_next_action`, `promotion_queue_item`, `demotion_history`, and `demotion_history_count`).
- Passed focused validation: `unittest tests.unit.test_operator_workspace_service`, `unittest tests.unit.test_operator_workspace_service tests.unit.test_api_static_server tests.unit.test_api_server`, full `unittest discover -s tests/unit` (713 tests), `scripts/run_script_static_checks.py`, and `node --check src/web/app.js`.
- Deployed the current working tree to `http://192.168.3.90:8000`; deploy smoke passed 11 endpoints, `run_remote_ops_smoke.py` passed 9 checks, and remote field smoke confirmed handoff lifecycle/resume queue, proof follow-up queue, guardrail resolution/decay, setup-family promotion queue/demotion history, and matching `/static/app.js` UI strings.
- Added handoff lane grouping / overdue cleanup visibility (`handoff_lane_groups`, `overdue_cleanup_count`, `next_cleanup_*`, per-marker `lane_group`, `overdue_cleanup_candidate`, and `cleanup_priority`) plus matching cockpit summaries.
- Added digest proof-aging trend and stale-proof escalation fields (`proof_age_trend`, `proof_escalation_level`, `proof_escalation_prompt`, `proof_aging_trend_counts`, and `stale_proof_escalation_count`).
- Added template feedback trend history and negative-feedback guardrails (`feedback_trend_history`, `negative_feedback_guardrail`, `negative_feedback_guardrail_count`, and guardrail template IDs) with cockpit visibility.
@@ -521,7 +535,7 @@ Automation execution-layer PXX remains closed and its known scope stays frozen.
## Exact Next Step
Continue the Decision Assistant dense operator workspace with the next bounded slice: H15 handoff lane lifecycle summary / operator resume queue, F16 digest proof escalation follow-up queue / resolved proof states, G15 template guardrail resolution / preferred-template decay, and D17 setup-family promotion queue / demotion history. PXX remains closed; do not create P25/P26 automatically, and do not enable real trading.
Continue the Decision Assistant dense operator workspace with the next bounded slice: H16 handoff resume queue persistence / resolved lane state, F17 digest proof follow-up resolution history / stale queue aging, G16 template preference recovery / guardrail audit trail, and D18 setup-family promotion decision history / review lane aging. PXX remains closed; do not create P25/P26 automatically, and do not enable real trading.
## Validation Commands Used Recently
@@ -533,6 +547,13 @@ D:\ai-exchange\.venv\Scripts\python.exe -m unittest discover -s tests/unit
node --check src/web/app.js
D:\ai-exchange\.venv\Scripts\python.exe scripts\deploy_with_password.py --host 192.168.3.90 --user root --port 22 --password <provided>
D:\ai-exchange\.venv\Scripts\python.exe scripts\run_remote_ops_smoke.py --base-url http://192.168.3.90:8000 --attempts 2 --timeout-seconds 20
D:\ai-exchange\.venv\Scripts\python.exe -m unittest tests.unit.test_operator_workspace_service
D:\ai-exchange\.venv\Scripts\python.exe -m unittest tests.unit.test_operator_workspace_service tests.unit.test_api_static_server tests.unit.test_api_server
D:\ai-exchange\.venv\Scripts\python.exe scripts\run_script_static_checks.py
D:\ai-exchange\.venv\Scripts\python.exe -m unittest discover -s tests/unit
node --check src/web/app.js
D:\ai-exchange\.venv\Scripts\python.exe scripts\deploy_with_password.py --host 192.168.3.90 --user root --port 22 --password <provided>
D:\ai-exchange\.venv\Scripts\python.exe scripts\run_remote_ops_smoke.py --base-url http://192.168.3.90:8000 --attempts 2 --timeout-seconds 20
D:\ai-exchange\.venv\Scripts\python.exe -m unittest tests.unit.test_visual_review_service tests.unit.test_run_visual_review tests.unit.test_trading_runtime_service tests.unit.test_api_and_main tests.unit.test_api_server
D:\ai-exchange\.venv\Scripts\python.exe scripts\run_script_static_checks.py
D:\ai-exchange\.venv\Scripts\python.exe -m unittest tests.unit.test_run_until_blocked tests.unit.test_monitoring_alert_rollout tests.unit.test_runtime_script_json
+26 -5
View File
@@ -84,6 +84,10 @@
- [x] Decision Assistant F15 digest proof aging trend / stale proof escalation slice complete
- [x] Decision Assistant G14 template feedback trend history / negative-feedback guardrails slice complete
- [x] Decision Assistant D16 setup-family watchlist prioritization / promotion-readiness hints slice complete
- [x] Decision Assistant H15 handoff lane lifecycle summary / operator resume queue slice complete
- [x] Decision Assistant F16 digest proof escalation follow-up queue / resolved proof states slice complete
- [x] Decision Assistant G15 template guardrail resolution / preferred-template decay slice complete
- [x] Decision Assistant D17 setup-family promotion queue / demotion history slice complete
## Next Up
@@ -99,10 +103,14 @@
- [x] F15 deepen digest recall with proof aging trend / stale proof escalation
- [x] G14 continue review template ergonomics with feedback trend history / negative-feedback guardrails
- [x] D16 extend replay comparison with setup-family watchlist prioritization / promotion-readiness hints
- [ ] H15 continue compact operator workspace polish with handoff lane lifecycle summary / operator resume queue
- [ ] F16 deepen digest recall with proof escalation follow-up queue / resolved proof states
- [ ] G15 continue review template ergonomics with template guardrail resolution / preferred-template decay
- [ ] D17 extend replay comparison with setup-family promotion queue / demotion history
- [x] H15 continue compact operator workspace polish with handoff lane lifecycle summary / operator resume queue
- [x] F16 deepen digest recall with proof escalation follow-up queue / resolved proof states
- [x] G15 continue review template ergonomics with template guardrail resolution / preferred-template decay
- [x] D17 extend replay comparison with setup-family promotion queue / demotion history
- [ ] H16 continue compact operator workspace polish with handoff resume queue persistence / resolved lane state
- [ ] F17 deepen digest recall with proof follow-up resolution history / stale queue aging
- [ ] G16 continue review template ergonomics with template preference recovery / guardrail audit trail
- [ ] D18 extend replay comparison with setup-family promotion decision history / review lane aging
- [x] P8 local closure complete
- [x] P9 local closure complete
- [x] P10 local closure complete
@@ -194,6 +202,10 @@
- [x] latest_decision_assistant_digest_proof_escalation_slice: proof_aging_trend_stale_escalation
- [x] latest_decision_assistant_template_guardrail_slice: feedback_trend_negative_guardrails
- [x] latest_decision_assistant_replay_promotion_slice: setup_family_watchlist_promotion_readiness
- [x] latest_decision_assistant_handoff_lifecycle_slice: handoff_lifecycle_resume_queue
- [x] latest_decision_assistant_digest_followup_slice: proof_escalation_followup_resolved_states
- [x] latest_decision_assistant_template_guardrail_resolution_slice: guardrail_resolution_preferred_decay
- [x] latest_decision_assistant_replay_promotion_queue_slice: setup_family_promotion_queue_demotion_history
- [x] latest_decision_assistant_summary_status: implemented
- [x] latest_decision_assistant_change_summary_status: implemented
- [x] latest_decision_assistant_evidence_status: implemented
@@ -259,10 +271,14 @@
- [x] latest_decision_assistant_digest_proof_escalation_status: implemented
- [x] latest_decision_assistant_template_guardrail_status: implemented
- [x] latest_decision_assistant_replay_promotion_status: implemented
- [x] latest_decision_assistant_handoff_lifecycle_status: implemented
- [x] latest_decision_assistant_digest_followup_status: implemented
- [x] latest_decision_assistant_template_guardrail_resolution_status: implemented
- [x] latest_decision_assistant_replay_promotion_queue_status: implemented
- [x] latest_decision_assistant_workbench_status: visible_local_smoke
- [x] latest_remote_decision_assistant_deploy_status: deployed_current_head
- [x] latest_remote_decision_assistant_smoke_status: passed
- [x] latest_remote_decision_assistant_review_packet_status: handoff_grouping_proof_escalation_feedback_guardrails_promotion_visible
- [x] latest_remote_decision_assistant_review_packet_status: handoff_lifecycle_proof_followup_guardrail_resolution_promotion_queue_visible
- [x] latest_p7_selector_choice: continue_periodic_analysis_decision_monitoring_with_maturity_rollup
- [x] latest_p14_slice: webhook_transport_retry
- [x] latest_p15_pxx_tasklist_status: ready
@@ -340,6 +356,11 @@
## Newly Done
- [x] Added handoff lifecycle / operator resume queue fields and cockpit visibility
- [x] Added digest proof follow-up queue, resolved proof state counts, and group follow-up actions
- [x] Added template guardrail resolution and preferred-template decay fields
- [x] Added setup-family promotion queue and demotion history fields
- [x] Passed focused validation, full unit discovery, script static checks, JS syntax check, deploy smoke, remote ops smoke, and remote field smoke for H15/F16/G15/D17
- [x] Added handoff lane grouping / overdue cleanup visibility and matching cockpit summaries
- [x] Added digest proof-aging trend, stale-proof escalation fields, and summary counts
- [x] Added template feedback trend history and negative-feedback guardrail visibility
@@ -1086,6 +1086,10 @@ def _build_decision_assistant_review_entrypoints(
item["feedback_trend_history"] = outcome.get("feedback_trend_history") or []
item["negative_feedback_guardrail"] = outcome.get("negative_feedback_guardrail")
item["negative_feedback_guardrail_prompt"] = outcome.get("negative_feedback_guardrail_prompt")
item["guardrail_resolution_status"] = outcome.get("guardrail_resolution_status")
item["guardrail_resolution_prompt"] = outcome.get("guardrail_resolution_prompt")
item["preferred_template_decay"] = outcome.get("preferred_template_decay")
item["preferred_template_decay_reason"] = outcome.get("preferred_template_decay_reason")
return {
"status": "active",
"note_count": len(recent_notes),
@@ -1416,6 +1420,21 @@ def _score_template_outcome(*, template_id: str, usage: dict, notes: list[dict])
template_id=template_id,
negative_count=negative_count,
),
"guardrail_resolution_status": _template_guardrail_resolution_status(negative_count=negative_count),
"guardrail_resolution_prompt": _template_guardrail_resolution_prompt(
template_id=template_id,
negative_count=negative_count,
),
"preferred_template_decay": _template_preferred_decay_factor(
negative_count=negative_count,
positive_count=positive_count,
actionable_count=action_count,
),
"preferred_template_decay_reason": _template_preferred_decay_reason(
negative_count=negative_count,
positive_count=positive_count,
actionable_count=action_count,
),
}
@@ -1438,6 +1457,34 @@ def _template_negative_guardrail_prompt(*, template_id: str, negative_count: int
return f"{template_label} has no negative-feedback guardrail; continue to capture outcome notes after reuse."
def _template_guardrail_resolution_status(*, negative_count: int) -> str:
if negative_count:
return "resolution_required"
return "resolved_clear"
def _template_guardrail_resolution_prompt(*, template_id: str, negative_count: int) -> str:
template_label = template_id.replace("_", " ") or "template"
if negative_count:
return f"Capture a resolution note for {template_label} before it becomes a preferred template again."
return f"{template_label} guardrail is resolved; no negative-feedback follow-up is pending."
def _template_preferred_decay_factor(*, negative_count: int, positive_count: int, actionable_count: int) -> float:
if negative_count <= 0:
return 0.0
positive_offset = min(0.4, (positive_count + actionable_count) * 0.1)
return max(0.2, min(1.0, (negative_count * 0.35) - positive_offset))
def _template_preferred_decay_reason(*, negative_count: int, positive_count: int, actionable_count: int) -> str:
if negative_count:
return "negative_feedback_requires_preference_decay"
if positive_count or actionable_count:
return "positive_feedback_keeps_preference"
return "no_feedback_decay_signal"
def _build_template_feedback_loop(*, template_id: str, outcome_status: str, reuse_count: int) -> dict:
template_label = template_id.replace("_", " ") or "template"
if outcome_status == "review_before_reuse":
@@ -1508,6 +1555,17 @@ def _build_template_feedback_aggregation(
for item in template_outcomes.values()
if item.get("negative_feedback_guardrail") == "pause_before_reuse"
]
guardrail_resolution_queue = [
{
"template_id": str(item.get("template_id") or ""),
"status": item.get("guardrail_resolution_status"),
"prompt": item.get("guardrail_resolution_prompt"),
"decay": item.get("preferred_template_decay"),
"reason": item.get("preferred_template_decay_reason"),
}
for item in template_outcomes.values()
if item.get("guardrail_resolution_status") == "resolution_required"
]
rated_template_count = sum(
1
for item in template_outcomes.values()
@@ -1535,6 +1593,13 @@ def _build_template_feedback_aggregation(
feedback_trend_status = "mixed_feedback"
else:
feedback_trend_status = "needs_more_feedback"
recommended_guardrail = str(recommended_template_id or "") in negative_feedback_guardrail_ids
if recommended_guardrail:
preferred_template_decay_status = "decay_recommended_template"
elif guardrail_resolution_queue:
preferred_template_decay_status = "decay_guardrailed_templates"
else:
preferred_template_decay_status = "no_decay_needed"
return {
"status": "active" if template_outcomes else "idle",
"useful_feedback_count": useful_count,
@@ -1550,6 +1615,13 @@ def _build_template_feedback_aggregation(
"feedback_trend_history_count": sum(len(item.get("feedback_trend_history") or []) for item in template_outcomes.values()),
"negative_feedback_guardrail_count": len(negative_feedback_guardrail_ids),
"negative_feedback_guardrail_template_ids": _dedupe(negative_feedback_guardrail_ids)[:5],
"guardrail_resolution_queue": guardrail_resolution_queue[:5],
"guardrail_resolution_count": len(guardrail_resolution_queue),
"preferred_template_decay_status": preferred_template_decay_status,
"preferred_template_decay_template_ids": [item.get("template_id") for item in guardrail_resolution_queue[:5]],
"preferred_template_decay_max": max(
[float(item.get("decay") or 0.0) for item in guardrail_resolution_queue] or [0.0]
),
}
@@ -1785,6 +1857,22 @@ def _build_decision_assistant_digest_history_summary(history: list[dict]) -> dic
for group in groups
if group.get("proof_escalation_level") in {"stale_refresh", "missing_proof"}
]
proof_follow_up_queue = [
{
"kind": str(group.get("kind") or "history"),
"state": group.get("proof_follow_up_state"),
"action_label": group.get("proof_follow_up_action_label"),
"prompt": group.get("proof_escalation_prompt"),
"evidence_anchor": group.get("refresh_evidence_anchor"),
"resolved_state": group.get("proof_resolved_state"),
}
for group in groups
if group.get("proof_follow_up_state") in {"follow_up_required", "watch"}
]
proof_resolution_state_counts = {}
for group in groups:
state = str(group.get("proof_resolved_state") or "unknown")
proof_resolution_state_counts[state] = int(proof_resolution_state_counts.get(state) or 0) + 1
refresh_confirmation_count = sum(
1
for group in groups
@@ -1819,6 +1907,11 @@ def _build_decision_assistant_digest_history_summary(history: list[dict]) -> dic
"stale_proof_escalation_count": len(stale_proof_escalations),
"next_stale_proof_kind": stale_proof_escalations[0].get("kind") if stale_proof_escalations else None,
"next_stale_proof_prompt": stale_proof_escalations[0].get("proof_escalation_prompt") if stale_proof_escalations else None,
"proof_follow_up_queue_count": len(proof_follow_up_queue),
"proof_follow_up_queue": proof_follow_up_queue[:5],
"resolved_proof_count": proof_resolution_state_counts.get("resolved_fresh", 0),
"proof_resolution_state_counts": proof_resolution_state_counts,
"next_proof_follow_up_prompt": proof_follow_up_queue[0].get("prompt") if proof_follow_up_queue else None,
}
@@ -1844,6 +1937,11 @@ def _build_decision_assistant_digest_history_groups(history: list[dict], *, reco
age=age,
freshness_proof_status=freshness_proof_status,
)
proof_follow_up_state = _digest_history_proof_follow_up_state(proof_escalation_level=proof_escalation_level)
proof_resolved_state = _digest_history_proof_resolved_state(
proof_escalation_level=proof_escalation_level,
freshness_proof_status=freshness_proof_status,
)
refresh_confirmation_trail = _digest_history_refresh_confirmation_trail(
kind=kind,
items=items,
@@ -1891,6 +1989,12 @@ def _build_decision_assistant_digest_history_groups(history: list[dict], *, reco
proof_escalation_level=proof_escalation_level,
evidence_anchor=str(refresh_evidence_anchor.get("anchor") or ""),
),
"proof_follow_up_state": proof_follow_up_state,
"proof_follow_up_action_label": _digest_history_proof_follow_up_action_label(
kind=kind,
proof_follow_up_state=proof_follow_up_state,
),
"proof_resolved_state": proof_resolved_state,
"refresh_confirmation_trail": refresh_confirmation_trail,
"latest_confirmation_trail_item": refresh_confirmation_trail[0] if refresh_confirmation_trail else None,
"items": items[:3],
@@ -2094,6 +2198,34 @@ def _digest_history_proof_escalation_prompt(
return f"{kind} proof trend is {proof_age_trend}; no escalation is needed."
def _digest_history_proof_follow_up_state(*, proof_escalation_level: str) -> str:
if proof_escalation_level in {"missing_proof", "stale_refresh"}:
return "follow_up_required"
if proof_escalation_level == "watch":
return "watch"
return "resolved"
def _digest_history_proof_follow_up_action_label(*, kind: str, proof_follow_up_state: str) -> str:
if proof_follow_up_state == "follow_up_required":
return f"Resolve {kind} proof"
if proof_follow_up_state == "watch":
return f"Watch {kind} proof"
return f"{kind} proof resolved"
def _digest_history_proof_resolved_state(*, proof_escalation_level: str, freshness_proof_status: str) -> str:
if proof_escalation_level == "none" and freshness_proof_status == "freshness_proven":
return "resolved_fresh"
if proof_escalation_level == "watch":
return "aging_watch"
if proof_escalation_level == "missing_proof":
return "missing_proof"
if proof_escalation_level == "stale_refresh":
return "pending_refresh"
return "unresolved"
def _digest_history_refresh_evidence_anchor(*, kind: str, items: list[dict], latest_ts) -> dict:
latest = dict(items[0] or {}) if items else {}
title = str(latest.get("title") or kind or "history")
@@ -2405,6 +2537,9 @@ def _build_decision_assistant_scan_handoff_summary(cards: list[dict]) -> dict:
"lane_group": _scan_handoff_lane_group(str(item.get("handoff_status") or "")),
"overdue_cleanup_candidate": str(item.get("handoff_status") or "") in {"blocked_handoff", "refresh_handoff"},
"cleanup_priority": _scan_handoff_cleanup_priority(str(item.get("handoff_status") or "")),
"lifecycle_state": _scan_handoff_lifecycle_state(str(item.get("handoff_status") or "")),
"resume_queue_rank": _scan_handoff_resume_rank(str(item.get("handoff_status") or "")),
"resume_action_label": _scan_handoff_resume_action_label(str(item.get("handoff_status") or "")),
}
for item in cards
if item.get("handoff_marker")
@@ -2416,6 +2551,7 @@ def _build_decision_assistant_scan_handoff_summary(cards: list[dict]) -> dict:
if bool(item.get("overdue_cleanup_candidate"))
]
next_cleanup_marker = dict(cleanup_markers[0] or {}) if cleanup_markers else {}
operator_resume_queue = _build_scan_handoff_resume_queue(markers)
stale_after_values = [
int(item.get("stale_after_seconds") or 0)
for item in markers
@@ -2439,6 +2575,12 @@ def _build_decision_assistant_scan_handoff_summary(cards: list[dict]) -> dict:
"next_cleanup_title": next_cleanup_marker.get("title"),
"next_cleanup_prompt": next_cleanup_marker.get("handoff_prompt"),
"handoff_lane_groups": _build_scan_handoff_lane_groups(markers),
"handoff_lifecycle_summary": _build_scan_handoff_lifecycle_summary(markers),
"operator_resume_queue": operator_resume_queue[:5],
"resume_queue_count": len(operator_resume_queue),
"next_resume_marker": operator_resume_queue[0].get("handoff_marker") if operator_resume_queue else None,
"next_resume_action_label": operator_resume_queue[0].get("resume_action_label") if operator_resume_queue else None,
"next_resume_prompt": operator_resume_queue[0].get("handoff_prompt") if operator_resume_queue else None,
"handoff_stale_policy": "Persisted handoffs older than their stale window should be reopened and refreshed before reuse.",
"handoff_cleanup_recommendation": _scan_handoff_cleanup_recommendation(markers),
"snooze_options": [
@@ -2471,6 +2613,77 @@ def _scan_handoff_cleanup_priority(handoff_status: str) -> int:
return 10
def _scan_handoff_lifecycle_state(handoff_status: str) -> str:
if handoff_status == "next_handoff":
return "resume_next"
if handoff_status == "blocked_handoff":
return "blocked_active"
if handoff_status == "refresh_handoff":
return "refresh_required"
if handoff_status == "context_ready":
return "carry_forward"
return "ready_to_resume"
def _scan_handoff_resume_rank(handoff_status: str) -> int:
if handoff_status == "next_handoff":
return 1
if handoff_status == "blocked_handoff":
return 2
if handoff_status == "refresh_handoff":
return 3
if handoff_status == "ready_handoff":
return 4
return 5
def _scan_handoff_resume_action_label(handoff_status: str) -> str:
if handoff_status == "next_handoff":
return "Resume Next"
if handoff_status == "blocked_handoff":
return "Resume Blocker"
if handoff_status == "refresh_handoff":
return "Resume Refresh"
if handoff_status == "context_ready":
return "Carry Forward"
return "Resume Ready"
def _build_scan_handoff_resume_queue(markers: list[dict]) -> list[dict]:
return sorted(
[
{
"handoff_marker": item.get("handoff_marker"),
"title": item.get("title"),
"target_section": item.get("target_section"),
"lifecycle_state": item.get("lifecycle_state"),
"resume_action_label": item.get("resume_action_label"),
"handoff_prompt": item.get("handoff_prompt"),
"resume_queue_rank": item.get("resume_queue_rank"),
}
for item in markers
if item.get("lifecycle_state") != "carry_forward"
],
key=lambda row: (int(row.get("resume_queue_rank") or 99), str(row.get("title") or "")),
)
def _build_scan_handoff_lifecycle_summary(markers: list[dict]) -> dict:
state_counts: dict[str, int] = {}
for item in markers:
state = str(item.get("lifecycle_state") or "unknown")
state_counts[state] = int(state_counts.get(state) or 0) + 1
return {
"state_counts": state_counts,
"active_count": sum(
int(state_counts.get(state) or 0)
for state in ("resume_next", "blocked_active", "refresh_required", "ready_to_resume")
),
"carry_forward_count": int(state_counts.get("carry_forward") or 0),
"resume_ready_count": int(state_counts.get("resume_next") or 0) + int(state_counts.get("ready_to_resume") or 0),
}
def _build_scan_handoff_lane_groups(markers: list[dict]) -> list[dict]:
lanes: dict[str, list[dict]] = {}
for marker in markers:
@@ -2788,6 +3001,12 @@ def _build_setup_replay_broader_baseline_summary(
"promotion_ready": False,
"promotion_readiness_hint": f"{setup_family} needs at least 5 samples before promotion readiness can be assessed.",
"promotion_blockers": ["insufficient_samples"],
"promotion_queue_status": "collect_samples",
"promotion_queue_rank": 5,
"promotion_next_action": "Collect more replay samples before promotion review.",
"promotion_queue_item": None,
"demotion_history": [],
"demotion_history_count": 0,
"focus_recommendation": f"Collect samples before changing focus for {setup_family}.",
"focus_reasons": ["insufficient_samples"],
"drift_watchlist_items": ["collect_more_replay_samples"],
@@ -2990,6 +3209,12 @@ def _build_setup_family_trend_delta(
"promotion_ready": False,
"promotion_readiness_hint": f"{setup_family} needs more replay samples before promotion readiness can be assessed.",
"promotion_blockers": ["insufficient_samples"],
"promotion_queue_status": "collect_samples",
"promotion_queue_rank": 5,
"promotion_next_action": "Collect more replay samples before promotion review.",
"promotion_queue_item": None,
"demotion_history": [],
"demotion_history_count": 0,
"focus_recommendation": f"Collect samples before changing focus for {setup_family}.",
"focus_reasons": ["insufficient_samples"],
"drift_watchlist_items": ["collect_more_replay_samples"],
@@ -3062,6 +3287,18 @@ def _build_setup_family_trend_delta(
drift_alert=drift_alert,
sample_gap=sample_gap,
)
promotion_queue_status = _setup_family_promotion_queue_status(
promotion_ready=promotion_ready,
drift_alert=drift_alert,
sample_gap=sample_gap,
trend_status=trend_status,
)
demotion_history = _setup_family_demotion_history(
drift_alert=drift_alert,
promotion_blockers=promotion_blockers,
focus_reasons=focus_reasons,
trend_status=trend_status,
)
return {
"status": "active",
"trend_status": trend_status,
@@ -3088,6 +3325,20 @@ def _build_setup_family_trend_delta(
watchlist_status=watchlist_status,
),
"promotion_blockers": promotion_blockers,
"promotion_queue_status": promotion_queue_status,
"promotion_queue_rank": _setup_family_promotion_queue_rank(promotion_queue_status=promotion_queue_status),
"promotion_next_action": _setup_family_promotion_next_action(
setup_family=setup_family,
promotion_queue_status=promotion_queue_status,
),
"promotion_queue_item": _setup_family_promotion_queue_item(
setup_family=setup_family,
promotion_queue_status=promotion_queue_status,
promotion_ready=promotion_ready,
watchlist_priority_score=watchlist_priority_score,
),
"demotion_history": demotion_history[:5],
"demotion_history_count": len(demotion_history),
"focus_recommendation": focus_recommendation,
"focus_reasons": focus_reasons[:4],
"drift_watchlist_items": _dedupe(drift_watchlist_items)[:6],
@@ -3121,6 +3372,111 @@ def _setup_family_promotion_readiness_hint(
return f"{setup_family} remains in {watchlist_status or 'watch'} mode pending more replay evidence."
def _setup_family_promotion_queue_status(
*,
promotion_ready: bool,
drift_alert: bool,
sample_gap: int,
trend_status: str,
) -> str:
if promotion_ready:
return "ready_for_operator_review"
if drift_alert:
return "demotion_review"
if sample_gap > 0:
return "collect_samples"
if trend_status == "mixed_vs_family_peers":
return "watch_before_promotion"
return "hold"
def _setup_family_promotion_queue_rank(*, promotion_queue_status: str) -> int:
return {
"demotion_review": 1,
"ready_for_operator_review": 2,
"watch_before_promotion": 3,
"collect_samples": 4,
"hold": 5,
}.get(str(promotion_queue_status or "hold"), 5)
def _setup_family_promotion_next_action(*, setup_family: str, promotion_queue_status: str) -> str:
if promotion_queue_status == "ready_for_operator_review":
return f"Review {setup_family} for analysis-lane promotion; keep execution permissions unchanged."
if promotion_queue_status == "demotion_review":
return f"Review whether {setup_family} should be demoted or held because drift is active."
if promotion_queue_status == "watch_before_promotion":
return f"Keep {setup_family} on watch until mixed peer deltas resolve."
if promotion_queue_status == "collect_samples":
return f"Collect more {setup_family} replay samples before promotion review."
return f"Hold {setup_family} in the current review lane."
def _setup_family_promotion_queue_item(
*,
setup_family: str,
promotion_queue_status: str,
promotion_ready: bool,
watchlist_priority_score: int,
) -> dict | None:
if promotion_queue_status in {"hold"}:
return None
return {
"setup_family": setup_family,
"status": promotion_queue_status,
"promotion_ready": promotion_ready,
"priority_score": watchlist_priority_score,
"action": _setup_family_promotion_next_action(
setup_family=setup_family,
promotion_queue_status=promotion_queue_status,
),
}
def _setup_family_demotion_history(
*,
drift_alert: bool,
promotion_blockers: list[str],
focus_reasons: list[str],
trend_status: str,
) -> list[dict]:
history = []
if drift_alert:
history.append(
{
"event": "drift_alert",
"severity": "warning",
"reason": "weaker_vs_family_peers",
}
)
for blocker in promotion_blockers[:3]:
history.append(
{
"event": "promotion_blocker",
"severity": "watch",
"reason": blocker,
}
)
if trend_status == "mixed_vs_family_peers":
history.append(
{
"event": "mixed_trend_watch",
"severity": "watch",
"reason": "mixed_family_delta",
}
)
for reason in focus_reasons[:2]:
if reason not in {item.get("reason") for item in history}:
history.append(
{
"event": "focus_reason",
"severity": "info",
"reason": reason,
}
)
return history
def _rank_session_composite(rows: list[dict], *, session_code: str) -> int | None:
if not session_code:
return None
+25
View File
@@ -573,6 +573,11 @@ function renderDecisionAssistant(summary) {
['handoff cleanup prompt', scanHandoffSummary.next_cleanup_prompt || 'n/a'],
['handoff lane groups', formatList((scanHandoffSummary.handoff_lane_groups || []).map((item) => `${item.label || item.status}: ${item.count ?? 0}`))],
['handoff lane overdue', formatList((scanHandoffSummary.handoff_lane_groups || []).filter((item) => Number(item.overdue_count || 0) > 0).map((item) => `${item.label || item.status}: ${item.overdue_count}`))],
['handoff lifecycle', formatJsonInline(scanHandoffSummary.handoff_lifecycle_summary?.state_counts || {})],
['resume queue', String(scanHandoffSummary.resume_queue_count ?? 0)],
['resume queue items', formatList((scanHandoffSummary.operator_resume_queue || []).map((item) => `${item.resume_action_label || 'Resume'}: ${item.title || item.target_section}`))],
['next resume action', scanHandoffSummary.next_resume_action_label || 'n/a'],
['next resume prompt', scanHandoffSummary.next_resume_prompt || 'n/a'],
['handoff cleanup recommendation', scanHandoffSummary.handoff_cleanup_recommendation || 'n/a'],
['default stale after', formatDurationSeconds(scanHandoffSummary.default_stale_after_seconds)],
['saved handoff marker', assistantScanHandoffMarker?.marker || 'n/a'],
@@ -699,6 +704,10 @@ function renderDecisionAssistant(summary) {
['stale proof escalations', String(digestSurface.history_summary?.stale_proof_escalation_count ?? 0)],
['next stale proof kind', digestSurface.history_summary?.next_stale_proof_kind || 'n/a'],
['next stale proof prompt', digestSurface.history_summary?.next_stale_proof_prompt || 'n/a'],
['proof follow-up queue', String(digestSurface.history_summary?.proof_follow_up_queue_count ?? 0)],
['resolved proofs', String(digestSurface.history_summary?.resolved_proof_count ?? 0)],
['proof resolution states', formatJsonInline(digestSurface.history_summary?.proof_resolution_state_counts || {})],
['next proof follow-up', digestSurface.history_summary?.next_proof_follow_up_prompt || 'n/a'],
['confirmation trail items', String(digestSurface.history_summary?.refresh_confirmation_trail_count ?? 0)],
['confirmation trail', formatList((digestSurface.history_summary?.refresh_confirmation_trail || []).map((item) => `${item.kind}:${item.proof_label}`))],
['refresh evidence anchors', String(digestSurface.history_summary?.refresh_evidence_anchor_count ?? 0)],
@@ -786,6 +795,9 @@ function renderDecisionAssistant(summary) {
['proof age trend', item.proof_age_trend || 'n/a'],
['proof escalation', item.proof_escalation_level || 'n/a'],
['proof escalation prompt', item.proof_escalation_prompt || 'n/a'],
['proof follow-up', item.proof_follow_up_state || 'n/a'],
['proof follow-up action', item.proof_follow_up_action_label || 'n/a'],
['proof resolved state', item.proof_resolved_state || 'n/a'],
['latest proof trail', item.latest_confirmation_trail_item?.proof_label || 'n/a'],
['proof trail items', formatList((item.refresh_confirmation_trail || []).map((row) => `${row.title || row.kind}: ${row.proof_label}`))],
['refresh evidence', item.refresh_evidence_anchor || 'n/a'],
@@ -844,6 +856,10 @@ function renderDecisionAssistant(summary) {
['feedback trend samples', String(reviewEntrypoints.template_reuse_metrics?.template_feedback_aggregation?.feedback_trend_history_count ?? 0)],
['negative guardrails', String(reviewEntrypoints.template_reuse_metrics?.template_feedback_aggregation?.negative_feedback_guardrail_count ?? 0)],
['guardrail templates', formatList(reviewEntrypoints.template_reuse_metrics?.template_feedback_aggregation?.negative_feedback_guardrail_template_ids || [])],
['guardrail resolution queue', String(reviewEntrypoints.template_reuse_metrics?.template_feedback_aggregation?.guardrail_resolution_count ?? 0)],
['preferred template decay', reviewEntrypoints.template_reuse_metrics?.template_feedback_aggregation?.preferred_template_decay_status || 'n/a'],
['preferred decay templates', formatList(reviewEntrypoints.template_reuse_metrics?.template_feedback_aggregation?.preferred_template_decay_template_ids || [])],
['max preference decay', formatPercent(reviewEntrypoints.template_reuse_metrics?.template_feedback_aggregation?.preferred_template_decay_max)],
['rated templates', String(reviewEntrypoints.template_reuse_metrics?.template_feedback_aggregation?.rated_template_count ?? 0)],
['template outcome avg', formatPercent(reviewEntrypoints.template_reuse_metrics?.template_outcome_summary?.average_usefulness_score)],
['template feedback gaps', String(reviewEntrypoints.template_reuse_metrics?.template_outcome_summary?.feedback_gap_count ?? 0)],
@@ -899,6 +915,10 @@ function renderDecisionAssistant(summary) {
['feedback prompt', item.feedback_prompt || 'n/a'],
['negative guardrail', item.negative_feedback_guardrail || 'n/a'],
['guardrail prompt', item.negative_feedback_guardrail_prompt || 'n/a'],
['guardrail resolution', item.guardrail_resolution_status || 'n/a'],
['guardrail resolution prompt', item.guardrail_resolution_prompt || 'n/a'],
['preference decay', formatPercent(item.preferred_template_decay)],
['decay reason', item.preferred_template_decay_reason || 'n/a'],
['feedback trend history', formatList((item.feedback_trend_history || []).map((row) => `${row.label || row.signal}: ${formatTimestamp(row.created_ts)}`))],
],
[
@@ -1198,6 +1218,11 @@ function renderDecisionAssistant(summary) {
['promotion readiness', String(Boolean(setupLearningRollup.replay_statistics?.broader_baseline_summary?.setup_family_trend_delta?.promotion_ready))],
['promotion readiness hint', setupLearningRollup.replay_statistics?.broader_baseline_summary?.setup_family_trend_delta?.promotion_readiness_hint || 'n/a'],
['promotion blockers', formatList(setupLearningRollup.replay_statistics?.broader_baseline_summary?.setup_family_trend_delta?.promotion_blockers || [])],
['promotion queue status', setupLearningRollup.replay_statistics?.broader_baseline_summary?.setup_family_trend_delta?.promotion_queue_status || 'n/a'],
['promotion queue rank', String(setupLearningRollup.replay_statistics?.broader_baseline_summary?.setup_family_trend_delta?.promotion_queue_rank ?? 'n/a')],
['promotion next action', setupLearningRollup.replay_statistics?.broader_baseline_summary?.setup_family_trend_delta?.promotion_next_action || 'n/a'],
['demotion history', String(setupLearningRollup.replay_statistics?.broader_baseline_summary?.setup_family_trend_delta?.demotion_history_count ?? 0)],
['demotion reasons', formatList((setupLearningRollup.replay_statistics?.broader_baseline_summary?.setup_family_trend_delta?.demotion_history || []).map((item) => item.reason || item.event))],
['family sample threshold', String(setupLearningRollup.replay_statistics?.broader_baseline_summary?.setup_family_trend_delta?.sample_threshold ?? 'n/a')],
['family sample gap', String(setupLearningRollup.replay_statistics?.broader_baseline_summary?.setup_family_trend_delta?.sample_gap ?? 'n/a')],
['family watchlist items', formatList(setupLearningRollup.replay_statistics?.broader_baseline_summary?.setup_family_trend_delta?.drift_watchlist_items || [])],
@@ -199,6 +199,10 @@ class OperatorWorkspaceServiceTests(unittest.TestCase):
self.assertIn("feedback_trend_status", summary["review_entrypoints"]["template_reuse_metrics"]["template_feedback_aggregation"])
self.assertIn("negative_feedback_guardrail_count", summary["review_entrypoints"]["template_reuse_metrics"]["template_feedback_aggregation"])
self.assertIn("negative_feedback_guardrail_template_ids", summary["review_entrypoints"]["template_reuse_metrics"]["template_feedback_aggregation"])
self.assertIn("guardrail_resolution_queue", summary["review_entrypoints"]["template_reuse_metrics"]["template_feedback_aggregation"])
self.assertIn("guardrail_resolution_count", summary["review_entrypoints"]["template_reuse_metrics"]["template_feedback_aggregation"])
self.assertIn("preferred_template_decay_status", summary["review_entrypoints"]["template_reuse_metrics"]["template_feedback_aggregation"])
self.assertIn("preferred_template_decay_template_ids", summary["review_entrypoints"]["template_reuse_metrics"]["template_feedback_aggregation"])
self.assertEqual(summary["review_entrypoints"]["note_templates"][0]["reuse_count"], 1)
self.assertEqual(summary["review_entrypoints"]["note_templates"][0]["reuse_status"], "saved_reuse")
self.assertEqual(summary["review_entrypoints"]["note_templates"][0]["outcome_status"], "useful_signal")
@@ -208,6 +212,10 @@ class OperatorWorkspaceServiceTests(unittest.TestCase):
self.assertIn("feedback_trend_history", summary["review_entrypoints"]["note_templates"][0])
self.assertIn("negative_feedback_guardrail", summary["review_entrypoints"]["note_templates"][0])
self.assertIn("negative_feedback_guardrail_prompt", summary["review_entrypoints"]["note_templates"][0])
self.assertIn("guardrail_resolution_status", summary["review_entrypoints"]["note_templates"][0])
self.assertIn("guardrail_resolution_prompt", summary["review_entrypoints"]["note_templates"][0])
self.assertIn("preferred_template_decay", summary["review_entrypoints"]["note_templates"][0])
self.assertIn("preferred_template_decay_reason", summary["review_entrypoints"]["note_templates"][0])
self.assertTrue(any(item["template_id"] == "next_review" for item in summary["review_entrypoints"]["note_templates"]))
self.assertTrue(any(item["template_id"] == "queue_follow_up" for item in summary["review_entrypoints"]["note_templates"]))
self.assertTrue(any(item["reuse_hint"] for item in summary["review_entrypoints"]["note_templates"]))
@@ -227,6 +235,11 @@ class OperatorWorkspaceServiceTests(unittest.TestCase):
self.assertIn("stale_proof_escalation_count", summary["digest_surface"]["history_summary"])
self.assertIn("next_stale_proof_kind", summary["digest_surface"]["history_summary"])
self.assertIn("next_stale_proof_prompt", summary["digest_surface"]["history_summary"])
self.assertIn("proof_follow_up_queue_count", summary["digest_surface"]["history_summary"])
self.assertIn("proof_follow_up_queue", summary["digest_surface"]["history_summary"])
self.assertIn("resolved_proof_count", summary["digest_surface"]["history_summary"])
self.assertIn("proof_resolution_state_counts", summary["digest_surface"]["history_summary"])
self.assertIn("next_proof_follow_up_prompt", summary["digest_surface"]["history_summary"])
self.assertIn("refresh_prompts", summary["digest_surface"]["history_summary"])
self.assertIn("digest_history_groups", summary)
self.assertIn("group_status", summary["digest_history_groups"][0])
@@ -245,6 +258,9 @@ class OperatorWorkspaceServiceTests(unittest.TestCase):
self.assertIn("proof_age_trend", summary["digest_history_groups"][0])
self.assertIn("proof_escalation_level", summary["digest_history_groups"][0])
self.assertIn("proof_escalation_prompt", summary["digest_history_groups"][0])
self.assertIn("proof_follow_up_state", summary["digest_history_groups"][0])
self.assertIn("proof_follow_up_action_label", summary["digest_history_groups"][0])
self.assertIn("proof_resolved_state", summary["digest_history_groups"][0])
self.assertIn("refresh_confirmation_trail", summary["digest_history_groups"][0])
self.assertIn("latest_confirmation_trail_item", summary["digest_history_groups"][0])
self.assertTrue(summary["digest_history_preview"])
@@ -284,6 +300,12 @@ class OperatorWorkspaceServiceTests(unittest.TestCase):
self.assertIn("promotion_ready", summary["setup_learning_rollup"]["replay_statistics"]["broader_baseline_summary"]["setup_family_trend_delta"])
self.assertIn("promotion_readiness_hint", summary["setup_learning_rollup"]["replay_statistics"]["broader_baseline_summary"]["setup_family_trend_delta"])
self.assertIn("promotion_blockers", summary["setup_learning_rollup"]["replay_statistics"]["broader_baseline_summary"]["setup_family_trend_delta"])
self.assertIn("promotion_queue_status", summary["setup_learning_rollup"]["replay_statistics"]["broader_baseline_summary"]["setup_family_trend_delta"])
self.assertIn("promotion_queue_rank", summary["setup_learning_rollup"]["replay_statistics"]["broader_baseline_summary"]["setup_family_trend_delta"])
self.assertIn("promotion_next_action", summary["setup_learning_rollup"]["replay_statistics"]["broader_baseline_summary"]["setup_family_trend_delta"])
self.assertIn("promotion_queue_item", summary["setup_learning_rollup"]["replay_statistics"]["broader_baseline_summary"]["setup_family_trend_delta"])
self.assertIn("demotion_history", summary["setup_learning_rollup"]["replay_statistics"]["broader_baseline_summary"]["setup_family_trend_delta"])
self.assertIn("demotion_history_count", summary["setup_learning_rollup"]["replay_statistics"]["broader_baseline_summary"]["setup_family_trend_delta"])
self.assertIn("sample_threshold", summary["setup_learning_rollup"]["replay_statistics"]["broader_baseline_summary"]["setup_family_trend_delta"])
self.assertIn("sample_gap", summary["setup_learning_rollup"]["replay_statistics"]["broader_baseline_summary"]["setup_family_trend_delta"])
self.assertTrue(summary["setup_learning_rollup"]["replay_statistics"]["broader_baseline_summary"]["setup_family_trend_delta"]["drift_watchlist_items"])
@@ -316,12 +338,20 @@ class OperatorWorkspaceServiceTests(unittest.TestCase):
self.assertIn("next_cleanup_marker", summary["scan_handoff_summary"])
self.assertIn("next_cleanup_prompt", summary["scan_handoff_summary"])
self.assertIn("handoff_lane_groups", summary["scan_handoff_summary"])
self.assertIn("handoff_lifecycle_summary", summary["scan_handoff_summary"])
self.assertIn("operator_resume_queue", summary["scan_handoff_summary"])
self.assertIn("resume_queue_count", summary["scan_handoff_summary"])
self.assertIn("next_resume_marker", summary["scan_handoff_summary"])
self.assertIn("next_resume_prompt", summary["scan_handoff_summary"])
self.assertIn("handoff_cleanup_recommendation", summary["scan_handoff_summary"])
self.assertTrue(summary["scan_handoff_summary"]["snooze_options"])
self.assertIn("snooze_seconds", summary["scan_handoff_summary"]["handoff_markers"][0])
self.assertIn("lane_group", summary["scan_handoff_summary"]["handoff_markers"][0])
self.assertIn("overdue_cleanup_candidate", summary["scan_handoff_summary"]["handoff_markers"][0])
self.assertIn("cleanup_priority", summary["scan_handoff_summary"]["handoff_markers"][0])
self.assertIn("lifecycle_state", summary["scan_handoff_summary"]["handoff_markers"][0])
self.assertIn("resume_queue_rank", summary["scan_handoff_summary"]["handoff_markers"][0])
self.assertIn("resume_action_label", summary["scan_handoff_summary"]["handoff_markers"][0])
self.assertTrue(summary["external_context_summary"]["operator_questions"])
self.assertTrue(summary["external_context_summary"]["source_cards"])
self.assertEqual(summary["external_context_summary"]["exchange_status"]["status"], "active")