#!/usr/bin/env python3 """只读查看 Blender → Web Blender UI 对标执行状态与最新功能交接。 不会启动执行器、构建、部署或运行功能验证;候选源码对应关系仍明确标记为未知。 """ from __future__ import annotations import argparse from datetime import datetime, timezone import json import math from pathlib import Path import re import sys import time from wbv2_gap_progress import resolve_gap ROOT = Path(__file__).resolve().parents[1] FOCUS_NAMES = {'editors': '桌面与编辑器', 'gpu': '原生绘制与计算', 'bpy': 'Python/操作入口'} STATE_NAMES = { 'running': '执行中', 'starting': '正在启动工作轮次', 'handed_off': '已交接,准备继续', 'recovering': '恢复中', 'waiting_retry': '等待重试', 'reloading': '安全交接后加载新执行逻辑', 'stopped': '已停止', 'awaiting_user_verification': '执行器报告实现已交接,等待用户使用', } WORK_NAMES = { 'reading_sources': '定位原生源码', 'source_changes_observed': '已有源码改动', 'executing_command': '执行实现命令', 'waiting_executor': '等待执行器动作', 'waiting_provider_retry': '服务退避后重试', 'waiting_business_retry': '业务轮次重试等待', } def obj(value): return value if isinstance(value, dict) else {} def rows(value): return [item for item in value if isinstance(item, dict)] if isinstance(value, list) else [] def number(value): return value if isinstance(value, (int, float)) and not isinstance(value, bool) and math.isfinite(value) else None def read_record(path, notes, required=False): try: # Only small state/manifest/result files; never load full inventory or logs. with path.open(encoding='utf-8') as stream: raw = stream.read(2 * 1024 * 1024 + 1) if len(raw) > 2 * 1024 * 1024: raise ValueError('超过进度记录大小上限') data = json.loads(raw) if not isinstance(data, dict): raise ValueError('记录不是 JSON 对象') return data except FileNotFoundError: if required: notes.append(f'尚无记录:{path.relative_to(ROOT)}') except (OSError, ValueError) as error: notes.append(f'暂时无法读取 {path.relative_to(ROOT)}:{error}') return {} def timestamp(value): try: result = datetime.fromisoformat(value.replace('Z', '+00:00')) return result.timestamp() if result.tzinfo else None except (AttributeError, TypeError, ValueError, OverflowError): return None def local_time(value): stamp = timestamp(value) return datetime.fromtimestamp(stamp).astimezone().strftime('%m-%d %H:%M:%S %Z') if stamp is not None else '未知' def duration(seconds): seconds = number(seconds) if seconds is None: return '未知' value = max(0, int(seconds)) if value >= 3600: return f'{value // 3600}小时{value % 3600 // 60}分' return f'{value // 60}分{value % 60}秒' if value >= 60 else f'{value}秒' def cycle_from_path(value): """Extract a cycle number from a candidate/report path when available.""" match = re.search(r'(?:cycle-|through-|candidate-)(\d+)', str(value or '')) return int(match.group(1)) if match else None def cadence_label(cadence): average = number(cadence.get('averageSeconds')) if average is None: return '暂无足够交接样本' return f"最近 {cadence.get('sampleCount', 0)} 轮平均 {duration(average)},上一轮 {duration(cadence.get('lastSeconds'))}" def assigned_ui_module(delivery_stage): """Read the module explicitly named by the current delivery assignment.""" assignment = obj(delivery_stage).get('assignment') modules = re.findall(r'UI-[A-Z]+-\d{3}', assignment or '') return modules[0] if len(set(modules)) == 1 else None def process_status(pid, expected_identity): if not isinstance(pid, int) or isinstance(pid, bool) or pid <= 0: return 'not_recorded' try: fields = Path(f'/proc/{pid}/stat').read_text().rsplit(')', 1)[1].split() if fields[0] in {'Z', 'X'}: return 'exited' if expected_identity is None: return 'identity_unknown' return 'alive' if fields[19] == str(expected_identity) else 'pid_reused' except FileNotFoundError: return 'exited' if Path('/proc').exists() else 'unknown' except (OSError, IndexError): return 'unknown' def snapshot(recent_limit=3): notes = [] now = time.time() try: queue = (ROOT / 'docs/EXECUTION_QUEUE.md').read_text(encoding='utf-8') except OSError as error: queue = '' notes.append(f'执行队列无法读取:{error}') match = re.search(r'\| 当前任务 \| `([A-Z0-9_-]+)`', queue) task = match.group(1) if match else None data = {'observedAt': datetime.now(timezone.utc).isoformat(), 'task': task, 'notes': notes, 'basis': '既有实现交接;只读查询,未执行任何功能验证'} if not task: notes.append('未找到当前任务指针,不能判断完成情况。') return data base = ROOT / 'tests/golden' / task state = read_record(base / 'unattended-implementation/state.json', notes, True) if state and state.get('task') != task: notes.append('执行状态所属任务与队列不一致,未将旧状态计入当前进度。') state = {} delivery = read_record(base / 'desktop-delivery-manifest.json', notes, True) for record in (delivery,): if record and record.get('task') != task: notes.append('计划所属任务不一致,忽略该计划。') record.clear() first_group = obj(delivery.get('firstGroupExecution')) gap_policy = obj(delivery.get('gapScheduling')) cadence_policy = obj(first_group.get('candidateCadence')) refresh_gap = cadence_policy.get('sourceIncrementGap') or gap_policy.get('candidateRefreshSourceGap') or 0 first_group_view = { 'stageId': first_group.get('stageId') or 'main-desktop', 'status': first_group.get('status') or obj(delivery.get('stages')[0] if rows(delivery.get('stages')) else {}).get('status', 'in_progress'), 'scope': first_group.get('scope') or 'DW01-DW03 concrete operator workflows', 'planPath': first_group.get('planPath') or gap_policy.get('firstGroupScopeReport'), 'currentWave': first_group.get('currentWave') or 'unspecified', 'nextWaves': first_group.get('nextWaves') or [], 'coherentFocusTurns': cadence_policy.get('coherentFocusTurns') or gap_policy.get('coherentFocusTurns') or 0, 'candidateRefreshSourceGap': refresh_gap, 'buildPolicy': first_group.get('buildPolicy') or {'mode': 'unknown'}, } current = obj(state.get('deliveryStage')) progress = obj(state.get('deliveryStages')) stages = [] for stage in rows(delivery.get('stages')): stage_id = stage.get('id') recorded = obj(obj(progress.get(stage_id)).get('focuses')) order = stage.get('focusOrder') if isinstance(stage.get('focusOrder'), list) else [] order = [name for name in order if isinstance(name, str)] done = bool(order) and all(obj(recorded.get(name)).get('reportedImplemented') is True for name in order) status = 'reported_implemented_unverified' if done else ( 'in_progress' if recorded or current.get('id') == stage_id else 'no_stage_completion_record') stages.append({'id': stage_id, 'title': stage.get('title') or stage_id, 'status': status, 'current': current.get('id') == stage_id, 'focusesReportedImplemented': sum(obj(recorded.get(name)).get('reportedImplemented') is True for name in order), 'focusCount': len(order)}) supervisor = process_status(state.get('pid'), state.get('identity')) worker = process_status(state.get('childPid'), state.get('childIdentity')) updated = timestamp(state.get('updatedAt')) age = max(0, now - updated) if updated is not None else None recorded_state = state.get('state', 'not_started') health = 'normal' if not state: health = 'unknown' elif supervisor in {'exited', 'pid_reused', 'not_recorded'}: health = 'stopped' if recorded_state in {'stopped', 'awaiting_user_verification'} else 'process_missing' elif supervisor != 'alive': health = 'process_unknown' elif age is None or age > 30: health = 'heartbeat_stale' elif recorded_state == 'running' and worker == 'exited': health = 'worker_exited_pending_handoff' provider = obj(state.get('provider')) retry = number(provider.get('retryAt')) issues = [item for item in obj(obj(state.get('efficiency')).get('issues')).values() if isinstance(item, dict) and item.get('status') == 'open'] focus_records = obj(obj(progress.get(current.get('id'))).get('focuses')) current_plan = next((item for item in rows(delivery.get('stages')) if item.get('id') == current.get('id')), {}) concrete_gaps = {} findings = obj(obj(state.get('efficiency')).get('issues')) for name, seeds in obj(current_plan.get('gapItems')).items(): concrete_gaps[name] = [] focus_delivery = obj(obj(obj(progress.get(current.get('id'))).get('focuses')).get(name)) for seed in rows(seeds): key = f"{current.get('id')}:{name}:{seed.get('id')}" finding = obj(findings.get(key)) item = resolve_gap(seed, finding, focus_delivery) handoff = obj(seed.get('implementationHandoff')) # Supersede only explicitly named old residuals, below the recorded # cycle boundary. A later worker observation always remains visible. boundary = number(handoff.get('throughCycle')) latest_cycle = max(number(finding.get('cycle')) or 0, number(focus_delivery.get('cycle')) or 0) covered = set(handoff.get('supersededRoutes') or []) if handoff.get('reportPath') and boundary is not None and latest_cycle <= boundary: previous = item.get('remainingSubroutes') or [] remaining = [route for route in previous if route not in covered] if covered and previous and len(remaining) < len(previous): remaining.extend(handoff.get('remainingRoutes') or []) item['remainingSubroutes'] = list(dict.fromkeys(remaining)) item['completedSubroutes'] = list(dict.fromkeys( item.get('completedSubroutes', []) + handoff.get('completedRoutes', []))) item['status'] = 'open' if remaining else 'implemented_unverified' item['statusSource'] = 'explicit_implementation_handoff' item['latestFinding'] = { 'cycle': None, 'reason': '\n'.join(item['remainingSubroutes']), 'action': handoff.get('nextAction'), 'sourcePaths': handoff.get('sourcePaths', []), } item['handoffReportPath'] = handoff['reportPath'] handoff_cycles = [value for value in (handoff.get('throughCycles') or []) if isinstance(value, int) and not isinstance(value, bool)] handoff_boundary = max(handoff_cycles) if handoff_cycles else None if (handoff.get('reportPath') and handoff.get('candidatePath') and handoff_boundary is not None and latest_cycle <= handoff_boundary): residuals = list(dict.fromkeys(handoff.get('remainingRoutes') or [])) item['completedSubroutes'] = list(dict.fromkeys( item.get('completedSubroutes', []) + (handoff.get('completedRoutes') or []))) item['remainingSubroutes'] = residuals item['missing'] = '\n'.join(residuals) item['nextAction'] = handoff.get('nextAction', item.get('nextAction', '')) item['status'] = 'open' if residuals else 'implemented_unverified' item['statusSource'] = 'explicit_candidate_implementation_handoff' item['latestFinding'] = { 'cycle': handoff_boundary, 'reason': item.get('missing'), 'action': item.get('nextAction'), 'sourcePaths': handoff.get('sourcePaths', []), } item['handoffReportPath'] = handoff['reportPath'] concrete_gaps[name].append(item) focuses = [] for name in dict.fromkeys([*obj(state.get('focusHandoffs')), *focus_records]): record = obj(focus_records.get(name)) branch = obj(obj(state.get('focusHandoffs')).get(name)) next_retry = number(branch.get('retryAt')) circuit_deadline = number(branch.get('circuitOpenUntil')) circuit_seconds = (max(0, int(circuit_deadline - now)) if circuit_deadline is not None and branch.get('circuitStageId') == current.get('id') else 0) focuses.append({'id': name, 'title': FOCUS_NAMES.get(name, name), 'cycle': record.get('cycle'), 'deferredPathSeconds': circuit_seconds, 'deferredPathId': branch.get('stalledScope'), 'completedRoutes': record.get('completedRoutes', []), 'remainingRoutes': record.get('remainingRoutes', []), 'reportPath': record.get('reportPath'), 'blockingRoute': record.get('blockingRoute'), 'nextAction': record.get('nextAction'), 'repeatedBlockerRounds': record.get('sameBusinessBlockerRounds', 0), 'noProgressRounds': branch.get('noProgressRounds', 0), 'retrySeconds': max(0, int(next_retry - now)) if next_retry is not None else 0}) source_updates = [] for stage in rows(delivery.get('stages')): for seeds in obj(stage.get('gapItems')).values(): for seed in rows(seeds): handoff = obj(seed.get('implementationHandoff')) if handoff.get('reportPath'): source_updates.append({**handoff, 'gapId': seed.get('id'), 'stageId': stage.get('id')}) source_updates.sort(key=lambda item: timestamp(item.get('recordedAt')) or 0, reverse=True) recent = [] cycle = state.get('cycle') current_attempt = {} if isinstance(cycle, int) and not isinstance(cycle, bool): current_attempt = read_record(base / 'unattended-implementation' / f'cycle-{cycle:06d}' / 'attempt.json', notes) if (current_attempt.get('cycle') != cycle or current_attempt.get('focus') != state.get('focus') or current_attempt.get('deliveryStage') != current.get('id')): current_attempt = {} # Read only the requested recent reports, within a 24-cycle window. for index in range(cycle, max(0, cycle - 24), -1): if len(recent) >= recent_limit: break directory = base / 'unattended-implementation' / f'cycle-{index:06d}' result_path = directory / 'result.json' result = read_record(result_path, notes) if not result: continue process = read_record(directory / 'process.json', notes) recent.append({'cycle': index, 'status': result.get('status'), 'businessEntry': result.get('businessEntry'), 'summary': result.get('summary'), 'elapsedSeconds': process.get('elapsedSeconds'), 'exitCode': process.get('exitCode'), 'reason': process.get('reason'), 'changedProductionFiles': process.get('changedProductionFiles', []), 'processRecorded': bool(process), 'buildStatus': obj(result.get('build')).get('status'), 'reportPath': str(result_path.relative_to(ROOT))}) # A report mtime interval includes pauses, retries and skipped cycles. It is # not execution speed. Expose only explicitly recorded process durations. measured = [item['elapsedSeconds'] for item in recent if number(item.get('elapsedSeconds')) is not None] cadence = {'sampleCount': len(measured), 'averageSeconds': None, 'lastSeconds': measured[0] if measured else None, 'basis': 'process.json elapsedSeconds; no throughput estimate'} build = obj(state.get('sharedBuild')) # Only the current scheduler's explicit candidate is an application handoff. # A directory's mtime cannot associate an arbitrary candidate with this task. candidate = None recorded_candidate = obj(state.get('applicationCandidate')) preferred = recorded_candidate.get('path') preferred_path = (ROOT / preferred).resolve() if isinstance(preferred, str) and preferred else None chosen = None if preferred_path and preferred_path.is_file(): try: chosen = (preferred_path.stat().st_mtime, preferred_path, 'state.applicationCandidate') except OSError: chosen = None if preferred and chosen is None: notes.append(f'当前候选记录不可读:{preferred};未用其它目录替代。') if chosen: modified, path, selection_basis = chosen record = read_record(path, notes) if record: latest_impl = obj(state.get('latestImplementation')) latest_at = timestamp(latest_impl.get('at')) latest_cycle = latest_impl.get('cycle') if isinstance(latest_impl.get('cycle'), int) else cycle candidate_cycle = (recorded_candidate.get('cycle') if selection_basis == 'state.applicationCandidate' and isinstance(recorded_candidate.get('cycle'), int) else cycle_from_path(path)) cycle_gap = max(0, latest_cycle - candidate_cycle) if isinstance(latest_cycle, int) and isinstance(candidate_cycle, int) else None source_gap = None count_basis = 'not_recorded' pending = state.get('pendingSourceCycles') if selection_basis == 'state.applicationCandidate' and isinstance(pending, list): pending = sorted({n for n in pending if isinstance(n, int) and not isinstance(n, bool)}) source_gap = len(pending) count_basis = 'recorded_source_change_rounds' candidate = {'path': str(path.relative_to(ROOT)), 'status': record.get('status'), 'scopeComplete': record.get('scopeComplete'), 'validation': record.get('validation'), 'deployment': record.get('deployment'), 'recordedAt': datetime.fromtimestamp(modified, timezone.utc).isoformat(), 'candidateCycle': candidate_cycle, 'latestImplementationCycle': latest_cycle, 'sourceGap': source_gap, 'pendingSourceCycles': pending if isinstance(pending, list) else None, 'pendingHistoryIncomplete': bool(state.get('pendingSourceHistoryIncomplete')), 'externalIntegrationUnconfirmed': [item for item in source_updates if not item.get('candidatePath')], 'cycleGap': cycle_gap, 'countBasis': count_basis, 'refreshGap': refresh_gap, 'refreshDue': bool(source_gap is not None and refresh_gap and source_gap >= refresh_gap) or (selection_basis == 'state.applicationCandidate' and bool(state.get('pendingSourceHistoryIncomplete'))), 'selectionBasis': selection_basis, 'newerImplementationHandoff': ((source_gap > 0) if source_gap is not None else (latest_at is not None and latest_at > modified)), 'sourceParity': 'unknown_not_checked', 'taskAssociation': 'candidate does not declare task; local directory reference only'} current_focus = next((focus for focus in focuses if focus.get('id') == state.get('focus')), None) source_idle = number(state.get('sourceIdleSeconds')) last_source_age = number(state.get('lastSourceChangeAgeSeconds')) alerts = [] if health != 'normal': alerts.append(health) if provider.get('reconnecting') or (number(provider.get('consecutiveFailures')) or 0) > 0: alerts.append('provider_retry_or_reconnect') if runner_active := (state.get('activeArtifactCommandCount') or 0): alerts.append('artifact_command_active') if candidate and candidate.get('refreshDue'): alerts.append('candidate_refresh_due') if (source_idle is not None and source_idle > 180 and state.get('activity') not in {'command_execution', 'artifact_generation'}): alerts.append('source_idle_over_3m') if last_source_age is not None and last_source_age > 600: alerts.append('source_progress_stale') if current_focus and ((number(current_focus.get('noProgressRounds')) or 0) >= 2 or (number(current_focus.get('repeatedBlockerRounds')) or 0) >= 2): alerts.append('focus_repeated_no_progress') done_count = sum(stage['status'] == 'reported_implemented_unverified' for stage in stages) ui_plan = obj(delivery.get('uiParityExecution')) ui_modules = [] ownership = obj(ui_plan.get('moduleGapOwnership')) for module in rows(delivery.get('uiParityModules')): handoff = obj(module.get('progressHandoff')) latest = obj(handoff.get('latestImplementation')) if latest.get('reportPath'): source_updates.append({**latest, 'moduleId': module.get('id'), 'stageId': current.get('id'), 'summary': latest.get('completedRoutes', [''])[0]}) focus = module.get('focus') items = concrete_gaps.get(focus, []) gap_ids = ownership.get(module.get('id'), module.get('gapIds', [])) or [] selected = [item for item in items if item.get('id') in set(gap_ids)] handoff = obj(module.get('progressHandoff')) counts = { 'implemented_unverified': sum(item.get('status') == 'implemented_unverified' for item in selected), 'locate': sum(item.get('phase') == 'locate' and item.get('status') != 'implemented_unverified' for item in selected), 'open': sum(item.get('status') != 'implemented_unverified' and item.get('phase') != 'locate' for item in selected), } if not selected: if handoff: status = handoff.get('status', '已登记,状态未完成') counts = { 'implemented_unverified': len(handoff.get('completedRoutes') or []), 'locate': 1 if handoff.get('phase') == 'locate' else 0, 'open': len(handoff.get('remainingRoutes') or []), } else: status = '尚无模块专属交接' elif counts['open'] == 0 and counts['locate'] == 0: status = '登记子项已补漏,整模块未确认' elif counts['implemented_unverified'] or counts['locate']: status = '部分已补漏/定位中' else: status = '待处理' related = obj(focus_records.get(focus)) ui_modules.append({**module, 'status': status, 'counts': counts, 'gaps': selected, 'progressHandoff': handoff or None, 'scopeComplete': False, 'visualParity': '未有样式对标交接', 'relatedHandoffCycle': related.get('cycle') if selected else None, 'relatedHandoffPath': related.get('reportPath') if selected else None, 'gapStatuses': [{'id': item.get('id'), 'phase': item.get('phase', 'implement'), 'status': item.get('status'), 'entry': item.get('entry')} for item in selected]}) source_updates.sort(key=lambda item: timestamp(item.get('recordedAt')) or 0, reverse=True) current_module = current.get('moduleId') or assigned_ui_module(current) concrete_open_count = sum( 1 for items in concrete_gaps.values() for item in items if item.get('status') != 'implemented_unverified' or item.get('remainingSubroutes') ) ui_parity = {**ui_plan, 'modules': ui_modules, # Focus names are implementation roles, not unique UI modules. 'currentModule': current_module, 'currentModuleSource': ('deliveryStage.assignment' if not current.get('moduleId') and current_module else 'state.moduleId'), 'associatedModules': [module.get('id') for module in ui_modules if module.get('focus') == state.get('focus') and module.get('gaps')], 'routeFields': ui_plan.get('routeFields', []), 'validation': 'NOT_RUN', 'acceptance': 'NOT_CLAIMED'} data.update({'runner': {'recordedState': recorded_state, 'health': health, 'pid': state.get('pid'), 'supervisor': supervisor, 'childPid': state.get('childPid'), 'worker': worker, 'cycle': cycle, 'focus': state.get('focus'), 'workState': state.get('workState'), 'startedAt': current_attempt.get('startedAt'), 'assignment': current.get('assignment'), 'executionMode': state.get('executionMode'), 'sourceProgressDeadlineSeconds': state.get('sourceProgressDeadlineSeconds'), 'activity': state.get('activity'), 'noProgressCycles': state.get('noProgressCycles', 0), 'elapsedSeconds': state.get('elapsedSeconds'), 'lastActionAgeSeconds': state.get('lastActionAgeSeconds'), 'activeCommandCount': state.get('activeCommandCount', 0), 'observedProductionFiles': state.get('observedProductionFiles', []), 'sourceIdleSeconds': state.get('sourceIdleSeconds'), 'sourceReadCommands': state.get('sourceReadCommands'), 'sourceReadCommandBudget': state.get('sourceReadCommandBudget'), 'implementationProgressDeadlineSeconds': state.get('implementationProgressDeadlineSeconds'), 'lastSourceChangeAgeSeconds': state.get('lastSourceChangeAgeSeconds'), 'workflowContinuation': state.get('workflowContinuation'), 'updatedAt': state.get('updatedAt'), 'heartbeatAgeSeconds': age, 'reason': state.get('reason'), 'activeArtifactCommands': state.get('activeArtifactCommandCount', 0)}, 'integrationUrgent': state.get('integrationUrgent'), 'progress': {'stagesReportedImplemented': done_count, 'stageTotal': len(stages), 'allStagesReportedImplemented': bool(stages) and done_count == len(stages), 'featureCompletionPercent': None, 'stages': stages, 'currentStage': current.get('id'), 'focuses': focuses, 'concreteGaps': concrete_gaps, 'openRecordedIssueCount': len(issues), 'openConcreteGapCount': concrete_open_count, 'scope': '仅统计当前 manifest 固定缺口与最新交接,不代表全部功能比例'}, 'provider': {'error': provider.get('errorKind'), 'consecutiveFailures': provider.get('consecutiveFailures', 0), 'retrySeconds': max(0, int(retry - now)) if retry is not None else 0, 'reconnecting': provider.get('reconnecting', False)}, 'firstGroup': first_group_view, 'latestImplementation': obj(state.get('latestImplementation')), 'recent': recent, 'uiParity': ui_parity, 'cadence': cadence, 'alerts': alerts, 'sourceUpdates': source_updates, 'build': {key: build.get(key) for key in ['cycle', 'status', 'logPath', 'artifactPaths', 'blockingSymbols']}, 'candidate': candidate, 'validation': state.get('validation', 'UNKNOWN'), 'acceptance': state.get('acceptance', 'UNKNOWN')}) return data HEALTH_NAMES = { 'normal': '进程存活、心跳更新(不代表功能完成)', 'unknown': '缺少运行记录', 'stopped': '执行器已停止', 'process_missing': '执行器退出或 PID 已复用', 'process_unknown': '无法确认进程身份', 'heartbeat_stale': '心跳过期;下方活动为旧记录', 'worker_exited_pending_handoff': '工作进程已退出,等待交接', } def lines(value): return list(dict.fromkeys(line.strip() for line in str(value or '').splitlines() if line.strip())) def display(data, details=False): print(f"桌面 Blender UI → Web Blender UI | {local_time(data['observedAt'])}") print(f"任务:{data.get('task') or '未知'};实现状态:尚未完整交付") runner = data.get('runner') or {} if not runner: for note in data.get('notes', []): print(f'提示:{note}') return health = runner.get('health') print(f"执行:{STATE_NAMES.get(runner.get('recordedState'), runner.get('recordedState'))};" f"{HEALTH_NAMES.get(health, health)}") print(f"第 {runner.get('cycle')} 轮 | {FOCUS_NAMES.get(runner.get('focus'), runner.get('focus'))} | " f"{WORK_NAMES.get(runner.get('workState'), runner.get('workState') or '活动未记录')}") print(f"本轮记录用时 {duration(runner.get('elapsedSeconds'))};最近动作距心跳 {duration(runner.get('lastActionAgeSeconds'))};" f"心跳距今 {duration(runner.get('heartbeatAgeSeconds'))}") print(f"活动命令 {runner.get('activeCommandCount', 0)};构建/打包 {runner.get('activeArtifactCommands', 0)};" f"本轮观察到生产文件修改 {len(runner.get('observedProductionFiles') or [])} 个") if runner.get('sourceIdleSeconds') is not None: print(f"未观察到源码修改 {duration(runner['sourceIdleSeconds'])}(截至心跳,不单独判为空转)") if runner.get('sourceReadCommands') is not None: print(f"读取观察 {runner.get('sourceReadCommands')} 次;阈值 {runner.get('sourceReadCommandBudget')} 次且无进展计时达 " f"{duration(runner.get('implementationProgressDeadlineSeconds'))} 后,在命令结束处收束;读取数不是硬上限") if runner.get('executionMode'): print('本轮工作:' + {'INTEGRATE_PENDING': '先组合已有源码并交接候选', 'IMPLEMENT_FAMILY': '实现同一业务族的具体缺口'}.get( runner['executionMode'], runner['executionMode'])) if details: print(f"进程:{runner.get('pid')} [{runner.get('supervisor')}];工作进程:" f"{runner.get('childPid')} [{runner.get('worker')}];记录时间 {local_time(runner.get('updatedAt'))}") if runner.get('startedAt'): print(f"本轮开始:{local_time(runner['startedAt'])}") for path in runner.get('observedProductionFiles') or []: print(f" 本轮改动:{path}") if runner.get('reason'): print(f"执行原因:{runner['reason']}") urgent = data.get('integrationUrgent') if urgent: reason = {'coherent_batch_ready': '业务批次源码已闭合', 'source_batch_limit': '待组合批次数到限', 'pending_delivery_deferred': '待交付跨轮积压', 'source_history_needs_reconciliation': '部分源码交接待对齐'}.get( urgent.get('reason'), '候选刷新到期') print(f"集成优先:{reason};待组合源码轮次 {urgent.get('pendingSourceCycles', [])};先生成匹配候选") ui = data.get('uiParity') or {} module_by_id = {module.get('id'): module for module in ui.get('modules', [])} module_id = ui.get('currentModule') if module_id: print(f"当前模块:{module_id} {module_by_id.get(module_id, {}).get('title', '')}(当前任务指派)") else: print('当前精确模块未记录;不由执行方向推断模块完成度。') progress = data.get('progress') or {} focus = next((item for item in progress.get('focuses', []) if item.get('id') == runner.get('focus')), {}) if focus.get('nextAction'): print(f"本批接续(第 {focus.get('cycle')} 轮交接):{focus['nextAction']}") elif runner.get('assignment'): print(f"任务指派:{runner['assignment']}") if details and runner.get('assignment') and focus.get('nextAction'): print(f"任务指派:{runner['assignment']}") provider = data.get('provider') or {} if provider.get('retrySeconds') or provider.get('reconnecting') or provider.get('consecutiveFailures'): print(f"服务等待:退避 {duration(provider.get('retrySeconds'))};重连 {'是' if provider.get('reconnecting') else '否'};" f"连续失败 {provider.get('consecutiveFailures', 0)};分类 {provider.get('error') or '未记录'}") for item in progress.get('focuses', []): if item.get('deferredPathSeconds') or item.get('retrySeconds'): print(f"路径暂缓:{item.get('title')} / {item.get('deferredPathId') or '未记录路径'};" f"暂缓剩余 {duration(item.get('deferredPathSeconds', 0))},重试剩余 {duration(item.get('retrySeconds', 0))}") if (number(focus.get('noProgressRounds')) or 0) >= 2 or (number(focus.get('repeatedBlockerRounds')) or 0) >= 2: print(f"推进警示:当前方向连续无源码进展 {focus.get('noProgressRounds', 0)} 轮;" f"重复阻塞 {focus.get('repeatedBlockerRounds', 0)} 轮。") print('\n最新交接(源码实现报告,未验证):') latest = data.get('latestImplementation') or {} if latest: stamp = timestamp(latest.get('at')) age = (timestamp(data.get('observedAt')) or 0) - stamp if stamp is not None else None print(f" 最近调度器源码交接:第 {latest.get('cycle')} 轮,{local_time(latest.get('at'))},距今 {duration(age)}") print(f" {latest.get('businessEntry') or latest.get('workflow') or '未提供业务入口'}") else: print(' 尚无调度器源码交接记录。') for item in data.get('recent', []): measured = duration(item.get('elapsedSeconds')) if item.get('processRecorded') and (item.get('exitCode') != 0 or item.get('reason')): label = f"退出 {item.get('exitCode')} / {item.get('reason') or '原因未记录'};报告未视为完成" else: label = '已写交接' if item.get('processRecorded') else '仅结果记录,进程交接未记录' print(f" [{item['cycle']}] {item.get('businessEntry') or item.get('summary') or '未提供业务入口'};{label};工作进程用时 {measured}") if details: print(f" {item.get('summary') or '未提供摘要'}") print(f" 记录:{item.get('reportPath')}") for item in data.get('sourceUpdates', []): print(f" [根会话补缺 {local_time(item.get('recordedAt'))}] {item.get('summary')}") if item.get('candidatePath'): print(f" 候选:{item['candidatePath']};部署:{item.get('deployment', 'not_performed')};验证:{item.get('validation', 'NOT_RUN')}") if details: print(f" 记录:{item.get('reportPath')}") print('\n最新记录余项(不是全功能缺口总表):') displayed = False # Show the active batch first. Completed fixed IDs remain in JSON/details; # an old fixed inventory is not the current executable work list. focuses = sorted(progress.get('focuses', []), key=lambda item: item.get('id') != runner.get('focus')) for item in focuses: gaps = progress.get('concreteGaps', {}).get(item.get('id'), []) open_gaps = [gap for gap in gaps if gap.get('status') != 'implemented_unverified' or gap.get('remainingSubroutes')] known_routes = [] for gap in open_gaps: finding = obj(gap.get('latestFinding')) residuals = gap.get('remainingSubroutes') or lines(finding.get('reason') or gap.get('missing')) known_routes.extend(residuals) displayed = True origin = f"第 {finding['cycle']} 轮" if finding.get('cycle') is not None else gap.get('statusSource', 'manifest') print(f" [{gap.get('id')}] {gap.get('entry')}(来源:{origin})") for residual in residuals: for line in lines(residual): print(f" {line}") if details: print(f" 下一步:{finding.get('action') or gap.get('nextAction') or '未记录'}") if gap.get('handoffReportPath'): print(f" 交接:{gap['handoffReportPath']}") # Native drawing/region work often uses DW IDs outside the old 7-item # inventory. Keep these current residuals instead of showing only G01. for route in item.get('remainingRoutes') or []: if route in known_routes: continue if any(gap.get('id') and gap['id'] in route for gap in gaps): continue displayed = True print(f" [{item.get('title')},第 {item.get('cycle')} 轮] {route}") if details: for gap in gaps: if gap not in open_gaps: print(f" [{gap.get('id')}] 已报告实现、未验证:{gap.get('entry')}") if not displayed: print(' 当前交接未列明确余项;不能据此认定整条 UI 主线完成。') no_handoff = [module for module in ui.get('modules', []) if not module.get('gaps') and not module.get('progressHandoff')] if no_handoff: print(' 尚无模块专属进度记录:' + '、'.join(module['id'] for module in no_handoff) + '(状态未知,不显示零进度)') if details: for module in ui.get('modules', []): print(f" 模块 {module.get('id')} / {module.get('title')}:{module.get('status')};完整样式与入口对标未确认") candidate = data.get('candidate') print('\n应用候选与源码集成:') if candidate: print(f" 调度器指定候选:{candidate.get('path')}") pending = candidate.get('pendingSourceCycles') if pending is None: print(' 待组合源码批次:未记录;不使用轮次差估算。') else: cycle_list = '、'.join(map(str, pending)) or '无' print(f" 调度器待组合 {len(pending)} 批:{cycle_list}") if candidate.get('pendingHistoryIncomplete'): print(' 待组合历史不完整,不能认定候选已包含所有源码。') external = candidate.get('externalIntegrationUnconfirmed') or [] if external: print(' 另有根会话补缺未记录候选归属:' + '、'.join(item.get('id', '未知') for item in external)) if candidate.get('refreshDue'): print(' 已达到记录中的候选刷新条件。') print(f" 部署记录:{candidate.get('deployment') or '未知'};源码包含关系及运行结果未验证。") else: print(' 当前任务尚无可读取候选记录。') build = data.get('build') or {} if details or build.get('blockingSymbols') or build.get('status') in {'failed', 'blocked'}: print(f" 最近构件交接:第 {build.get('cycle', '—')} 轮 / {build.get('status') or '未记录'}") if build.get('logPath'): print(f" 构件日志:{build['logPath']}") for symbol in (build.get('blockingSymbols') or [])[:8]: print(f" 构建卡点:{symbol}") print(f"验证:{data.get('validation')};验收:{data.get('acceptance')}。轮次、心跳和构建不计作功能完成。") for note in data.get('notes', []): print(f'提示:{note}') def interval_value(value): interval = float(value) if not math.isfinite(interval) or interval < 1: raise argparse.ArgumentTypeError('刷新间隔必须是至少 1 秒的有限数字') return interval def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--details', action='store_true', help='展开源码/进程/交接/已实现条目;默认只显示当前执行与实际余项') parser.add_argument('--json', action='store_true', help='输出 JSON;配合 --watch 时每行一个快照') parser.add_argument('--watch', action='store_true', help='持续刷新,Ctrl+C 退出') parser.add_argument('--interval', type=interval_value, default=5, metavar='秒', help='刷新间隔,默认 5 秒') parser.add_argument('--recent', type=int, choices=range(0, 11), default=3, metavar='0..10', help='显示最近交接条数,默认 3,最多回看 24 轮') args = parser.parse_args() try: while True: data = snapshot(args.recent) if args.json: print(json.dumps(data, ensure_ascii=False, indent=None if args.watch else 2), flush=True) else: if args.watch and sys.stdout.isatty(): print('\033[2J\033[H', end='') display(data, details=args.details) sys.stdout.flush() if not args.watch: return 0 if data.get('task') else 1 time.sleep(args.interval) except (KeyboardInterrupt, BrokenPipeError): return 0 if __name__ == '__main__': raise SystemExit(main())