Complete broker read model sync groundwork
This commit is contained in:
@@ -132,6 +132,7 @@ def run_autonomous_cycle(env=None, runner=subprocess.run, db_probe=None, schema_
|
||||
"schema": None,
|
||||
"next_actions": [],
|
||||
"action_plan": [],
|
||||
"continuation_decision": "continue",
|
||||
}
|
||||
|
||||
offline_env = dict(env)
|
||||
@@ -371,6 +372,7 @@ def _parse_json(value: str):
|
||||
def _finish(summary: dict, exit_code: int, output_json: bool) -> int:
|
||||
summary["next_actions"] = build_cycle_next_actions(summary)
|
||||
summary["action_plan"] = build_cycle_action_plan(summary["next_actions"])
|
||||
summary["continuation_decision"] = classify_cycle_continuation(summary)
|
||||
if output_json:
|
||||
print(json.dumps(summary, ensure_ascii=False, sort_keys=True))
|
||||
return exit_code
|
||||
@@ -437,6 +439,15 @@ def build_cycle_next_actions(summary: dict) -> list[str]:
|
||||
return ["continue_offline_automation"]
|
||||
|
||||
|
||||
def classify_cycle_continuation(summary: dict) -> str:
|
||||
mode = summary.get("mode")
|
||||
if mode in {"waiting_for_database", "offline_checks_failed", "migration_failed", "backfill_failed", "pipeline_failed"}:
|
||||
return "blocked"
|
||||
if mode == "pipeline_completed":
|
||||
return "advance"
|
||||
return "continue"
|
||||
|
||||
|
||||
def build_cycle_action_plan(next_actions: list[str]) -> list[dict]:
|
||||
plan = []
|
||||
for action in next_actions:
|
||||
|
||||
@@ -58,6 +58,8 @@ def build_backtest_mode_from_env(env=None) -> str:
|
||||
|
||||
def build_backtest_subject_type_from_env(env=None) -> str:
|
||||
env = os.environ if env is None else env
|
||||
if build_backtest_session_codes_from_env(env=env):
|
||||
return "signal"
|
||||
value = str(env.get("BACKTEST_SUBJECT_TYPE") or "").strip()
|
||||
return value or BACKTEST_SUBJECT_PLAN_REPLAY
|
||||
|
||||
@@ -85,6 +87,14 @@ def build_replay_config_from_env(env=None) -> BacktestReplayConfig:
|
||||
)
|
||||
|
||||
|
||||
def build_backtest_session_codes_from_env(env=None) -> list[str]:
|
||||
env = os.environ if env is None else env
|
||||
raw = str(env.get("BACKTEST_SESSION_CODES") or "").strip()
|
||||
if not raw:
|
||||
return []
|
||||
return [item.strip() for item in raw.split(",") if item.strip()]
|
||||
|
||||
|
||||
def build_backtest_timeframe_set_from_env(env=None) -> dict:
|
||||
env = os.environ if env is None else env
|
||||
bias_tf = str(env.get("AI_ICT_BIAS_TIMEFRAME") or env.get("BACKTEST_BIAS_TIMEFRAME") or "1m")
|
||||
@@ -111,6 +121,7 @@ def run_backtest_once(
|
||||
candidate_model_code: Optional[str] = None,
|
||||
replay_config: Optional[BacktestReplayConfig] = None,
|
||||
timeframe_set: Optional[dict] = None,
|
||||
session_codes: Optional[list[str]] = None,
|
||||
) -> int:
|
||||
run_id, outcomes = service.run_backtest(
|
||||
instrument_id=instrument_id,
|
||||
@@ -123,6 +134,7 @@ def run_backtest_once(
|
||||
mode=backtest_mode,
|
||||
replay_config=replay_config,
|
||||
timeframe_set=timeframe_set,
|
||||
session_codes=session_codes,
|
||||
)
|
||||
summary = summarize_backtest_outcomes(outcomes, subject_type=subject_type)
|
||||
quality_gate_result = evaluate_backtest_quality(
|
||||
@@ -138,6 +150,7 @@ def run_backtest_once(
|
||||
"trading_plan_id": trading_plan_id,
|
||||
"candidate_model_code": candidate_model_code,
|
||||
"timeframe_set": timeframe_set,
|
||||
"session_codes": session_codes,
|
||||
"summary": summary,
|
||||
"quality_gate": quality_gate_result,
|
||||
"quality_gate_required": require_quality_gate,
|
||||
@@ -172,6 +185,7 @@ def main(env=None) -> int:
|
||||
candidate_model_code=build_backtest_candidate_model_code_from_env(env=env),
|
||||
replay_config=build_replay_config_from_env(env=env),
|
||||
timeframe_set=build_backtest_timeframe_set_from_env(env=env),
|
||||
session_codes=build_backtest_session_codes_from_env(env=env),
|
||||
)
|
||||
except SQLAlchemyError as exc:
|
||||
summary = {
|
||||
|
||||
@@ -79,6 +79,7 @@ def run_trading_delivery_rehearsal(env=None, runner=subprocess.run, cwd: Path =
|
||||
"blockers": [],
|
||||
"manual_decisions": [],
|
||||
"promotion_decision": "hold",
|
||||
"continuation_decision": "continue",
|
||||
"policies": {
|
||||
"reference_migration": (
|
||||
"auto_apply"
|
||||
@@ -501,10 +502,35 @@ def _finalize_trading_rehearsal(summary: dict) -> dict:
|
||||
summary["passed"] = summary["passed"] and len(summary["blockers"]) == 0
|
||||
if not summary["passed"]:
|
||||
summary["promotion_decision"] = "hold"
|
||||
summary["continuation_decision"] = classify_rehearsal_continuation(summary)
|
||||
summary["finished_ts"] = datetime.now(timezone.utc).isoformat()
|
||||
return summary
|
||||
|
||||
|
||||
def classify_rehearsal_continuation(summary: dict) -> str:
|
||||
mode = summary.get("mode")
|
||||
if mode == "rehearsal_completed":
|
||||
return "advance"
|
||||
if mode in {
|
||||
"reference_migration_deferred",
|
||||
"reference_migration_conflicts_blocked",
|
||||
"reference_migration_manual_apply_required",
|
||||
"checklist_drift_failed",
|
||||
"historical_repair_scan_failed",
|
||||
"historical_repair_apply_failed",
|
||||
"historical_repair_rescan_failed",
|
||||
"historical_repair_safe_candidates_remain",
|
||||
"historical_repair_manual_apply_required",
|
||||
"autonomous_cycle_failed",
|
||||
"deploy_or_health_failed",
|
||||
"post_deploy_readiness_failed",
|
||||
"post_deploy_reports_failed",
|
||||
"reference_migration_apply_failed",
|
||||
}:
|
||||
return "blocked"
|
||||
return "continue"
|
||||
|
||||
|
||||
def main(env=None) -> int:
|
||||
env = build_trading_rehearsal_env(env=env)
|
||||
summary = run_trading_delivery_rehearsal(env=env)
|
||||
|
||||
@@ -0,0 +1,491 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from scripts.run_trading_delivery_rehearsal import run_trading_delivery_rehearsal
|
||||
|
||||
SESSION_PATH = ROOT / "SESSION.md"
|
||||
TASKS_PATH = ROOT / "TASKS.md"
|
||||
RUNNER_STATUS_PATH = ROOT / ".claude" / "automation_status.json"
|
||||
RUNNER_RESUME_WATCH_PATHS = [
|
||||
ROOT / "src",
|
||||
ROOT / "scripts",
|
||||
ROOT / ".env",
|
||||
ROOT / "SESSION.md",
|
||||
ROOT / "TASKS.md",
|
||||
]
|
||||
STAGE_SEQUENCE = ["P0-2", "P0-3", "P0-4", "P0-5", "P1-1", "P1-2", "P1-3"]
|
||||
STAGE_TITLES = {
|
||||
"P0-1": "实时市场真值层",
|
||||
"P0-2": "真实账户与订单只读同步",
|
||||
"P0-3": "盘中持续调度与运行监督",
|
||||
"P0-4": "决策包与主动提醒",
|
||||
"P0-5": "真实交易映射与盘后闭环",
|
||||
"P1-1": "辅助可信度与模型白名单",
|
||||
"P1-2": "操作体验与运营工具",
|
||||
"P1-3": "live adapter 前准备",
|
||||
}
|
||||
STAGE_GOALS = {
|
||||
"P0-2": [
|
||||
"建立 `BrokerReadModelService`",
|
||||
"读取真实账户余额 / 持仓 / 挂单 / 成交",
|
||||
"扩展 `RiskContext`",
|
||||
"建立 reconciliation 读模型",
|
||||
"前端展示真实 `open position / pending order`",
|
||||
"focused smoke: `/trading/broker-state` 与 `/trading/market-state` 返回 broker state",
|
||||
],
|
||||
"P0-3": [
|
||||
"建立 `TradingRuntimeScheduler`",
|
||||
"建立 `TradingRuntimeSupervisor`",
|
||||
"提供 runtime health API",
|
||||
"前端展示 runtime status",
|
||||
"focused smoke: runtime 状态 API / workbench runtime status 可见",
|
||||
],
|
||||
"P0-4": [
|
||||
"建立 `DecisionPacketService`",
|
||||
"输出统一决策包字段",
|
||||
"接入主动提醒通道",
|
||||
"workbench 展示决策卡",
|
||||
],
|
||||
"P0-5": [
|
||||
"建立 execution 与真实 order/fill 映射",
|
||||
"真实持仓变化触发 journal entry",
|
||||
"增强真实交易归因与周报闭环",
|
||||
],
|
||||
"P1-1": [
|
||||
"建立 symbol / session / setup 表现统计",
|
||||
"增加辅助可信度评分与白名单",
|
||||
],
|
||||
"P1-2": [
|
||||
"补齐 workbench 快捷筛选、session 视图、alert history、operator notes",
|
||||
"支持一键生成盘后 review packet",
|
||||
],
|
||||
"P1-3": [
|
||||
"校准真实 order schema",
|
||||
"补全 bracket / OCO reconciliation 语义",
|
||||
"建立 live readiness checklist 与 operator confirmation protocol",
|
||||
],
|
||||
}
|
||||
DEFAULT_VALIDATION_COMMANDS = ["npm run lint", "npm run build", "npm run visual:snapshots"]
|
||||
BLOCKED_MARKERS = [
|
||||
"缺凭据",
|
||||
"权限不足",
|
||||
"不可逆",
|
||||
"网络/供应商限制",
|
||||
"外部阻塞",
|
||||
]
|
||||
|
||||
|
||||
def run_until_blocked(env=None, runner=None, sleeper=None) -> dict:
|
||||
env = dict(os.environ if env is None else env)
|
||||
runner = runner or subprocess.run
|
||||
output_json = _get_bool(env, "RUN_UNTIL_BLOCKED_OUTPUT_JSON", False)
|
||||
max_rounds = max(_get_int(env, "RUN_UNTIL_BLOCKED_MAX_ROUNDS", 8), 1)
|
||||
max_idle_rounds = max(_get_int(env, "RUN_UNTIL_BLOCKED_MAX_IDLE_ROUNDS", 1), 0)
|
||||
|
||||
completed_stages = parse_done_stages(TASKS_PATH.read_text(encoding="utf-8"))
|
||||
current_stage = detect_current_stage(SESSION_PATH.read_text(encoding="utf-8"), completed_stages)
|
||||
idle_rounds = 0
|
||||
rounds = []
|
||||
blocked_reason = None
|
||||
started_ts = datetime.now(timezone.utc).isoformat()
|
||||
write_runner_status(
|
||||
{
|
||||
"state": "running",
|
||||
"started_ts": started_ts,
|
||||
"heartbeat_ts": started_ts,
|
||||
"current_stage": current_stage,
|
||||
"completed_stages": completed_stages,
|
||||
"blocked_reason": None,
|
||||
"rounds_completed": 0,
|
||||
}
|
||||
)
|
||||
|
||||
try:
|
||||
while current_stage is not None and len(rounds) < max_rounds:
|
||||
write_runner_status(
|
||||
{
|
||||
"state": "running",
|
||||
"started_ts": started_ts,
|
||||
"heartbeat_ts": datetime.now(timezone.utc).isoformat(),
|
||||
"current_stage": current_stage,
|
||||
"completed_stages": completed_stages,
|
||||
"blocked_reason": blocked_reason,
|
||||
"rounds_completed": len(rounds),
|
||||
}
|
||||
)
|
||||
round_env = dict(env)
|
||||
round_env["RUN_UNTIL_BLOCKED_STAGE"] = current_stage
|
||||
round_env["RUN_UNTIL_BLOCKED_STAGE_TITLE"] = STAGE_TITLES[current_stage]
|
||||
summary = run_trading_delivery_rehearsal(env=round_env, runner=runner, sleeper=sleeper)
|
||||
round_result = {
|
||||
"round": len(rounds) + 1,
|
||||
"stage": current_stage,
|
||||
"stage_title": STAGE_TITLES[current_stage],
|
||||
"continuation_decision": summary.get("continuation_decision", "continue"),
|
||||
"mode": summary.get("mode"),
|
||||
"passed": summary.get("passed", False),
|
||||
"blockers": list(summary.get("blockers") or []),
|
||||
"promotion_decision": summary.get("promotion_decision"),
|
||||
}
|
||||
rounds.append(round_result)
|
||||
|
||||
if summary.get("continuation_decision") == "advance":
|
||||
completed_stages.append(current_stage)
|
||||
next_stage = get_next_stage(current_stage, completed_stages)
|
||||
write_anchor_files(
|
||||
completed_stages=completed_stages,
|
||||
current_stage=next_stage,
|
||||
last_result=summary,
|
||||
)
|
||||
current_stage = next_stage
|
||||
idle_rounds = 0
|
||||
continue
|
||||
|
||||
blocked_reason = first_blocked_reason(summary)
|
||||
if blocked_reason is not None or summary.get("continuation_decision") == "blocked":
|
||||
write_anchor_files(
|
||||
completed_stages=completed_stages,
|
||||
current_stage=current_stage,
|
||||
last_result=summary,
|
||||
blocked_reason=blocked_reason or ", ".join(summary.get("blockers") or []) or summary.get("mode") or "blocked",
|
||||
)
|
||||
break
|
||||
|
||||
idle_rounds += 1
|
||||
write_anchor_files(
|
||||
completed_stages=completed_stages,
|
||||
current_stage=current_stage,
|
||||
last_result=summary,
|
||||
blocked_reason=None,
|
||||
)
|
||||
if idle_rounds > max_idle_rounds:
|
||||
blocked_reason = f"idle_round_limit_exceeded:{summary.get('mode') or 'unknown'}"
|
||||
break
|
||||
|
||||
final_stage = current_stage
|
||||
summary = {
|
||||
"started_ts": started_ts,
|
||||
"finished_ts": datetime.now(timezone.utc).isoformat(),
|
||||
"rounds": rounds,
|
||||
"completed_stages": completed_stages,
|
||||
"current_stage": final_stage,
|
||||
"current_stage_title": STAGE_TITLES.get(final_stage),
|
||||
"blocked_reason": blocked_reason,
|
||||
"status": "completed" if final_stage is None and blocked_reason is None else "blocked" if blocked_reason else "stopped",
|
||||
}
|
||||
write_runner_status(
|
||||
{
|
||||
"state": summary["status"],
|
||||
"started_ts": started_ts,
|
||||
"heartbeat_ts": datetime.now(timezone.utc).isoformat(),
|
||||
"current_stage": final_stage,
|
||||
"completed_stages": completed_stages,
|
||||
"blocked_reason": blocked_reason,
|
||||
"rounds_completed": len(rounds),
|
||||
}
|
||||
)
|
||||
except Exception:
|
||||
write_runner_status(
|
||||
{
|
||||
"state": "failed",
|
||||
"started_ts": started_ts,
|
||||
"heartbeat_ts": datetime.now(timezone.utc).isoformat(),
|
||||
"current_stage": current_stage,
|
||||
"completed_stages": completed_stages,
|
||||
"blocked_reason": blocked_reason or "runner_exception",
|
||||
"rounds_completed": len(rounds),
|
||||
}
|
||||
)
|
||||
raise
|
||||
|
||||
if output_json:
|
||||
print(json.dumps(summary, ensure_ascii=False, sort_keys=True))
|
||||
else:
|
||||
print(f"Run-until-blocked status: {summary['status']}")
|
||||
print(f"Completed stages: {', '.join(completed_stages) if completed_stages else 'none'}")
|
||||
if final_stage:
|
||||
print(f"Current stage: {final_stage} {STAGE_TITLES[final_stage]}")
|
||||
if blocked_reason:
|
||||
print(f"Blocked reason: {blocked_reason}")
|
||||
return summary
|
||||
|
||||
|
||||
def run_supervised_until_stopped(env=None, runner=None, sleeper=None) -> dict:
|
||||
env = dict(os.environ if env is None else env)
|
||||
sleeper = sleeper or time.sleep
|
||||
loop_sleep_seconds = max(_get_int(env, "RUN_UNTIL_BLOCKED_LOOP_SLEEP_SECONDS", 30), 1)
|
||||
max_supervisor_cycles = max(_get_int(env, "RUN_UNTIL_BLOCKED_SUPERVISOR_CYCLES", 0), 0)
|
||||
cycles = 0
|
||||
last_summary = None
|
||||
resume_fingerprint = compute_resume_fingerprint()
|
||||
|
||||
while True:
|
||||
last_summary = run_until_blocked(env=env, runner=runner, sleeper=sleeper)
|
||||
cycles += 1
|
||||
if last_summary["status"] == "completed":
|
||||
return last_summary
|
||||
|
||||
supervisor_state = classify_supervisor_state(last_summary)
|
||||
write_runner_status(
|
||||
{
|
||||
"state": supervisor_state,
|
||||
"started_ts": last_summary["started_ts"],
|
||||
"heartbeat_ts": datetime.now(timezone.utc).isoformat(),
|
||||
"current_stage": last_summary.get("current_stage"),
|
||||
"completed_stages": last_summary.get("completed_stages") or [],
|
||||
"blocked_reason": last_summary.get("blocked_reason"),
|
||||
"rounds_completed": len(last_summary.get("rounds") or []),
|
||||
"sleep_seconds": loop_sleep_seconds,
|
||||
"supervisor_cycles": cycles,
|
||||
"resume_fingerprint": resume_fingerprint,
|
||||
}
|
||||
)
|
||||
if max_supervisor_cycles and cycles >= max_supervisor_cycles:
|
||||
return last_summary
|
||||
if supervisor_state in {"paused", "needs_fix"}:
|
||||
if wait_for_resume_condition(
|
||||
previous_fingerprint=resume_fingerprint,
|
||||
sleeper=sleeper,
|
||||
loop_sleep_seconds=loop_sleep_seconds,
|
||||
):
|
||||
resume_fingerprint = compute_resume_fingerprint()
|
||||
continue
|
||||
return last_summary
|
||||
sleeper(loop_sleep_seconds)
|
||||
|
||||
|
||||
def classify_supervisor_state(summary: dict) -> str:
|
||||
blocked_reason = str(summary.get("blocked_reason") or "")
|
||||
if summary.get("status") == "completed":
|
||||
return "completed"
|
||||
if blocked_reason.startswith("autonomous_mode:pipeline_failed"):
|
||||
return "needs_fix"
|
||||
if blocked_reason.startswith("autonomous_mode:offline_checks_failed"):
|
||||
return "needs_fix"
|
||||
if blocked_reason.startswith("autonomous_mode:migration_failed"):
|
||||
return "needs_fix"
|
||||
if blocked_reason.startswith("autonomous_mode:backfill_failed"):
|
||||
return "paused"
|
||||
if blocked_reason.startswith("autonomous_mode:waiting_for_database"):
|
||||
return "sleeping"
|
||||
if blocked_reason:
|
||||
return "paused"
|
||||
return "sleeping"
|
||||
|
||||
|
||||
def wait_for_resume_condition(*, previous_fingerprint: str, sleeper, loop_sleep_seconds: int) -> bool:
|
||||
current_fingerprint = compute_resume_fingerprint()
|
||||
if current_fingerprint != previous_fingerprint:
|
||||
return True
|
||||
sleeper(loop_sleep_seconds)
|
||||
return compute_resume_fingerprint() != previous_fingerprint
|
||||
|
||||
|
||||
def compute_resume_fingerprint() -> str:
|
||||
entries = []
|
||||
for path in RUNNER_RESUME_WATCH_PATHS:
|
||||
if not path.exists():
|
||||
entries.append(f"{path}:missing")
|
||||
continue
|
||||
if path.is_file():
|
||||
stat = path.stat()
|
||||
entries.append(f"{path}:{stat.st_mtime_ns}:{stat.st_size}")
|
||||
continue
|
||||
for child in sorted(path.rglob("*")):
|
||||
if not child.is_file():
|
||||
continue
|
||||
stat = child.stat()
|
||||
entries.append(f"{child}:{stat.st_mtime_ns}:{stat.st_size}")
|
||||
return "|".join(entries)
|
||||
|
||||
|
||||
def write_runner_status(payload: dict) -> None:
|
||||
RUNNER_STATUS_PATH.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
|
||||
|
||||
def detect_current_stage(session_text: str, completed_stages: list[str]) -> Optional[str]:
|
||||
exact_next_step = re.search(r"###\s+`(P\d-\d)`", session_text)
|
||||
if exact_next_step and exact_next_step.group(1) in STAGE_SEQUENCE:
|
||||
return exact_next_step.group(1)
|
||||
for stage in STAGE_SEQUENCE:
|
||||
if stage not in completed_stages:
|
||||
return stage
|
||||
return None
|
||||
|
||||
|
||||
def get_next_stage(current_stage: str, completed_stages: list[str]) -> Optional[str]:
|
||||
for stage in STAGE_SEQUENCE:
|
||||
if stage not in completed_stages:
|
||||
return stage
|
||||
return None
|
||||
|
||||
|
||||
def parse_done_stages(tasks_text: str) -> list[str]:
|
||||
return re.findall(r"- \[x\] `(P\d-\d)`", tasks_text)
|
||||
|
||||
|
||||
def first_blocked_reason(summary: dict) -> Optional[str]:
|
||||
blockers = [str(item) for item in summary.get("blockers") or []]
|
||||
for blocker in blockers:
|
||||
if any(marker in blocker for marker in BLOCKED_MARKERS):
|
||||
return blocker
|
||||
if summary.get("continuation_decision") == "blocked":
|
||||
return blockers[0] if blockers else summary.get("mode")
|
||||
return None
|
||||
|
||||
|
||||
def write_anchor_files(*, completed_stages: list[str], current_stage: Optional[str], last_result: dict, blocked_reason: Optional[str] = None) -> None:
|
||||
SESSION_PATH.write_text(render_session(completed_stages, current_stage, last_result, blocked_reason), encoding="utf-8")
|
||||
TASKS_PATH.write_text(render_tasks(completed_stages, current_stage, blocked_reason), encoding="utf-8")
|
||||
|
||||
|
||||
def render_session(completed_stages: list[str], current_stage: Optional[str], last_result: dict, blocked_reason: Optional[str]) -> str:
|
||||
completed_lines = [f"- 已完成 `{stage}`:{STAGE_TITLES[stage]}" for stage in completed_stages] or ["- 本轮尚无新增完成阶段"]
|
||||
next_step_block = render_next_step_block(current_stage)
|
||||
risk_lines = [
|
||||
"- 当前市场真值链路依赖本机 `192.168.3.73:10808` 持续可达;如本机关机或代理退出,远端将退化",
|
||||
"- 真实账户只读同步下一阶段可能需要额外凭据或账号权限;若现有环境缺失,将构成唯一允许的用户介入点",
|
||||
]
|
||||
if blocked_reason:
|
||||
risk_lines.append(f"- 当前自动化已阻塞:{blocked_reason}")
|
||||
return "\n".join(
|
||||
[
|
||||
"# ai-exchange Session",
|
||||
"",
|
||||
"## Current Goal",
|
||||
"",
|
||||
"从现在开始按全自动模式持续推进交易模块主线,不再等待阶段确认,直到完成 `docs/AI_ICT_实盘辅助决策系统开发方案_v2.md` 中剩余开发目标。",
|
||||
"",
|
||||
"## Current Branch",
|
||||
"",
|
||||
"`main`",
|
||||
"",
|
||||
"## Last Updated",
|
||||
"",
|
||||
f"{datetime.now(timezone.utc).strftime('%Y-%m-%d UTC')}",
|
||||
"",
|
||||
"## Completed In Last Session",
|
||||
"",
|
||||
*completed_lines,
|
||||
"",
|
||||
"## Current Working State",
|
||||
"",
|
||||
"- 当前主线执行协议已固化到:",
|
||||
" - `docs/AI_ICT_全自动开发执行流程_v1.md`",
|
||||
"- 当前阶段队列来源于:",
|
||||
" - `docs/AI_ICT_实盘辅助决策系统开发方案_v2.md`",
|
||||
"- 默认不再等待用户中途确认",
|
||||
"- 每阶段必须完成:",
|
||||
" - 实现",
|
||||
" - focused tests",
|
||||
" - `npm run lint`",
|
||||
" - `npm run build`",
|
||||
" - `npm run visual:snapshots`",
|
||||
" - Docker 部署",
|
||||
" - 远端 smoke",
|
||||
" - 状态回写",
|
||||
"",
|
||||
"## Exact Next Step",
|
||||
"",
|
||||
next_step_block,
|
||||
"",
|
||||
"## Validation Commands",
|
||||
"",
|
||||
"```bash",
|
||||
*DEFAULT_VALIDATION_COMMANDS,
|
||||
"```",
|
||||
"",
|
||||
"## Risks Or Open Questions",
|
||||
"",
|
||||
*risk_lines,
|
||||
"",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def render_next_step_block(current_stage: Optional[str]) -> str:
|
||||
if current_stage is None:
|
||||
return "所有阶段已完成,无下一阶段。"
|
||||
goal_lines = STAGE_GOALS.get(current_stage, [])
|
||||
rendered_goals = "\n".join(f"- {line}" for line in goal_lines)
|
||||
return f"直接进入:\n\n### `{current_stage}` {STAGE_TITLES[current_stage]}\n\n目标:\n\n{rendered_goals}"
|
||||
|
||||
|
||||
def render_tasks(completed_stages: list[str], current_stage: Optional[str], blocked_reason: Optional[str]) -> str:
|
||||
in_progress_lines = [f"- [ ] `{current_stage}` {STAGE_TITLES[current_stage]}"] if current_stage else ["- [x] 所有阶段已完成"]
|
||||
next_up_lines = [
|
||||
f"- [ ] `{stage}` {STAGE_TITLES[stage]}"
|
||||
for stage in STAGE_SEQUENCE
|
||||
if stage != current_stage and stage not in completed_stages
|
||||
] or ["- [x] 当前无后续阶段"]
|
||||
blocked_lines = [f"- [ ] {blocked_reason}"] if blocked_reason else ["- [ ] 当前无阻塞;如缺凭据或外部权限,再转入此区"]
|
||||
done_lines = [f"- [x] `{stage}` {STAGE_TITLES[stage]}" for stage in completed_stages] or ["- [ ] 当前尚无完成阶段"]
|
||||
return "\n".join(
|
||||
[
|
||||
"# ai-exchange Tasks",
|
||||
"",
|
||||
"## Execution Mode",
|
||||
"",
|
||||
"- [x] 全自动模式已启用",
|
||||
"- [x] 默认不等待阶段确认",
|
||||
"- [x] 每阶段必须完成实现、验证、部署、smoke、状态回写",
|
||||
"",
|
||||
"## In Progress",
|
||||
"",
|
||||
*in_progress_lines,
|
||||
"",
|
||||
"## Next Up",
|
||||
"",
|
||||
*next_up_lines,
|
||||
"",
|
||||
"## Backlog",
|
||||
"",
|
||||
"- [ ] 长期目标:在不默认开启真实自动下单的前提下,完成 `Real-time Assisted Trading V1`",
|
||||
"",
|
||||
"## Blocked",
|
||||
"",
|
||||
*blocked_lines,
|
||||
"",
|
||||
"## Done",
|
||||
"",
|
||||
*done_lines,
|
||||
"",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def _get_bool(env, key: str, default: bool) -> bool:
|
||||
value = env.get(key)
|
||||
if value is None or value == "":
|
||||
return default
|
||||
return str(value).strip().lower() in {"1", "true", "yes", "on"}
|
||||
|
||||
|
||||
def _get_int(env, key: str, default: int) -> int:
|
||||
value = env.get(key)
|
||||
if value is None or value == "":
|
||||
return default
|
||||
return int(value)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
env = dict(os.environ)
|
||||
summary = run_supervised_until_stopped(env=env) if _get_bool(env, "RUN_UNTIL_BLOCKED_SUPERVISOR", False) else run_until_blocked(env=env)
|
||||
return 0 if summary["status"] == "completed" else 2 if summary["status"] == "blocked" else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user