@hupan56/wlkj 2.7.12 → 3.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (253) hide show
  1. package/bin/cli.js +1435 -1107
  2. package/package.json +29 -29
  3. package/templates/qoder/agents/insight-planning.md +1 -1
  4. package/templates/qoder/agents/insight-research.md +28 -15
  5. package/templates/qoder/commands/optional/wl-insight.md +161 -161
  6. package/templates/qoder/commands/optional/wl-report.md +20 -39
  7. package/templates/qoder/commands/optional/wl-status.md +4 -4
  8. package/templates/qoder/commands/wl-code.md +2 -2
  9. package/templates/qoder/commands/wl-design.md +6 -6
  10. package/templates/qoder/commands/wl-init.md +3 -3
  11. package/templates/qoder/commands/wl-prd.md +26 -13
  12. package/templates/qoder/commands/wl-req.md +43 -0
  13. package/templates/qoder/commands/wl-search.md +78 -47
  14. package/templates/qoder/commands/wl-task.md +343 -40
  15. package/templates/qoder/commands/wl-test.md +43 -17
  16. package/templates/qoder/config.yaml +52 -14
  17. package/templates/qoder/hooks/post-tool-use.py +181 -0
  18. package/templates/qoder/hooks/session-start.py +94 -23
  19. package/templates/qoder/hooks/stop-eval.py +207 -0
  20. package/templates/qoder/hooks/user-prompt-submit.py +139 -0
  21. package/templates/qoder/rules/wl-pipeline.md +85 -49
  22. package/templates/qoder/scripts/README.md +102 -0
  23. package/templates/qoder/scripts/capability/__init__.py +26 -0
  24. package/templates/qoder/scripts/capability/__main__.py +72 -0
  25. package/templates/qoder/scripts/capability/adapters/__init__.py +29 -0
  26. package/templates/qoder/scripts/capability/adapters/cli.py +316 -0
  27. package/templates/qoder/scripts/capability/adapters/mcp.py +334 -0
  28. package/templates/qoder/scripts/capability/adapters/qw.py +271 -0
  29. package/templates/qoder/scripts/capability/caps/__init__.py +27 -0
  30. package/templates/qoder/scripts/capability/caps/context.py +36 -0
  31. package/templates/qoder/scripts/capability/caps/cron.py +50 -0
  32. package/templates/qoder/scripts/capability/caps/identity.py +43 -0
  33. package/templates/qoder/scripts/capability/caps/memory.py +71 -0
  34. package/templates/qoder/scripts/capability/caps/notify.py +41 -0
  35. package/templates/qoder/scripts/capability/caps/present.py +48 -0
  36. package/templates/qoder/scripts/capability/caps/repo.py +31 -0
  37. package/templates/qoder/scripts/capability/caps/sandbox.py +43 -0
  38. package/templates/qoder/scripts/capability/chain.py +92 -0
  39. package/templates/qoder/scripts/capability/memory_chain.py +41 -0
  40. package/templates/qoder/scripts/capability/registry.py +246 -0
  41. package/templates/qoder/scripts/capability/registry_mcp.py +250 -0
  42. package/templates/qoder/scripts/capability/smoke_test.py +211 -0
  43. package/templates/qoder/scripts/capability/smoke_test_report.json +94 -0
  44. package/templates/qoder/scripts/deployment/setup/__init__.py +11 -0
  45. package/templates/qoder/scripts/deployment/setup/carriers.py +669 -0
  46. package/templates/qoder/scripts/deployment/setup/carriers_verify.py +148 -0
  47. package/templates/qoder/scripts/{init_doctor.py → deployment/setup/init_doctor.py} +100 -39
  48. package/templates/qoder/scripts/{install_qoderwork.py → deployment/setup/install_qoderwork.py} +48 -30
  49. package/templates/qoder/scripts/{platform_doctor.py → deployment/setup/platform_doctor.py} +271 -259
  50. package/templates/qoder/scripts/{repo_root.py → deployment/setup/repo_root.py} +9 -2
  51. package/templates/qoder/scripts/{setup.py → deployment/setup/setup.py} +143 -14
  52. package/templates/qoder/scripts/{setup_lanhu.py → deployment/setup/setup_lanhu.py} +979 -963
  53. package/templates/qoder/scripts/domain/design/__init__.py +0 -0
  54. package/templates/qoder/scripts/{fill_prototype.py → domain/design/fill_prototype.py} +17 -5
  55. package/templates/qoder/scripts/{gen_design_doc.py → domain/design/gen_design_doc.py} +406 -394
  56. package/templates/qoder/scripts/domain/kg/__init__.py +11 -0
  57. package/templates/qoder/scripts/domain/kg/build/__init__.py +0 -0
  58. package/templates/qoder/scripts/domain/kg/build/build_entity_registry.py +201 -0
  59. package/templates/qoder/scripts/domain/kg/build/build_relations.py +132 -0
  60. package/templates/qoder/scripts/{build_style_index.py → domain/kg/build/build_style_index.py} +50 -15
  61. package/templates/qoder/scripts/domain/kg/build/build_workflows.py +149 -0
  62. package/templates/qoder/scripts/{kg_build.py → domain/kg/build/kg_build.py} +685 -612
  63. package/templates/qoder/scripts/{kg_build_db.py → domain/kg/build/kg_build_db.py} +338 -327
  64. package/templates/qoder/scripts/{kg_incremental.py → domain/kg/build/kg_incremental.py} +425 -393
  65. package/templates/qoder/scripts/{learn_aggregate.py → domain/kg/build/learn_aggregate.py} +212 -201
  66. package/templates/qoder/scripts/domain/kg/extract/__init__.py +0 -0
  67. package/templates/qoder/scripts/{common → domain/kg/extract}/extract.py +430 -419
  68. package/templates/qoder/scripts/domain/kg/extract/test_extract.py +115 -0
  69. package/templates/qoder/scripts/{common → domain/kg/extract}/ts_extract.py +614 -536
  70. package/templates/qoder/scripts/domain/kg/graph/__init__.py +0 -0
  71. package/templates/qoder/scripts/{kg_link_db.py → domain/kg/graph/kg_link_db.py} +235 -224
  72. package/templates/qoder/scripts/domain/kg/graph/kg_semantic.py +683 -0
  73. package/templates/qoder/scripts/{kg.py → domain/kg/kg.py} +871 -708
  74. package/templates/qoder/scripts/domain/kg/kg_capabilities.py +182 -0
  75. package/templates/qoder/scripts/domain/kg/search/__init__.py +0 -0
  76. package/templates/qoder/scripts/{context_pack.py → domain/kg/search/context_pack.py} +45 -17
  77. package/templates/qoder/scripts/{enrich_prompt.py → domain/kg/search/enrich_prompt.py} +238 -226
  78. package/templates/qoder/scripts/domain/kg/search/prefetch.py +384 -0
  79. package/templates/qoder/scripts/{common → domain/kg/search}/search_engine.py +207 -205
  80. package/templates/qoder/scripts/{search_index.py → domain/kg/search/search_index.py} +210 -88
  81. package/templates/qoder/scripts/domain/kg/server/__init__.py +0 -0
  82. package/templates/qoder/scripts/domain/kg/server/kgd.py +549 -0
  83. package/templates/qoder/scripts/domain/kg/server/perf_bench.py +197 -0
  84. package/templates/qoder/scripts/domain/kg/storage/__init__.py +0 -0
  85. package/templates/qoder/scripts/{kg_duckdb.py → domain/kg/storage/kg_duckdb.py} +70 -45
  86. package/templates/qoder/scripts/domain/learning/__init__.py +0 -0
  87. package/templates/qoder/scripts/{learn.py → domain/learning/learn.py} +157 -146
  88. package/templates/qoder/scripts/domain/report/__init__.py +11 -0
  89. package/templates/qoder/scripts/{add_session.py → domain/report/add_session.py} +256 -244
  90. package/templates/qoder/scripts/{export.py → domain/report/export.py} +72 -4
  91. package/templates/qoder/scripts/{report.py → domain/report/report.py} +292 -281
  92. package/templates/qoder/scripts/domain/report/report_snapshot.py +261 -0
  93. package/templates/qoder/scripts/domain/report/role.py +39 -0
  94. package/templates/qoder/scripts/{status.py → domain/report/status.py} +737 -628
  95. package/templates/qoder/scripts/domain/report/status_snapshot.py +191 -0
  96. package/templates/qoder/scripts/domain/requirement/__init__.py +0 -0
  97. package/templates/qoder/scripts/{archive_prd.py → domain/requirement/archive_prd.py} +389 -377
  98. package/templates/qoder/scripts/domain/requirement/req.py +228 -0
  99. package/templates/qoder/scripts/domain/task/__init__.py +11 -0
  100. package/templates/qoder/scripts/{git_sync.py → domain/task/git_sync.py} +90 -50
  101. package/templates/qoder/scripts/{syncgate.py → domain/task/syncgate.py} +18 -7
  102. package/templates/qoder/scripts/domain/task/task.py +229 -0
  103. package/templates/qoder/scripts/domain/task/task_lifecycle.py +606 -0
  104. package/templates/qoder/scripts/domain/task/task_query.py +162 -0
  105. package/templates/qoder/scripts/domain/task/task_relations.py +425 -0
  106. package/templates/qoder/scripts/{team_sync.py → domain/task/team_sync.py} +101 -23
  107. package/templates/qoder/scripts/domain/task/zentao_sync.py +370 -0
  108. package/templates/qoder/scripts/foundation/__init__.py +0 -0
  109. package/templates/qoder/scripts/foundation/bootstrap.py +145 -0
  110. package/templates/qoder/scripts/foundation/core/__init__.py +0 -0
  111. package/templates/qoder/scripts/foundation/core/cmd_registry.py +112 -0
  112. package/templates/qoder/scripts/foundation/core/config.py +187 -0
  113. package/templates/qoder/scripts/{common → foundation/core}/paths.py +706 -400
  114. package/templates/qoder/scripts/foundation/data/__init__.py +0 -0
  115. package/templates/qoder/scripts/foundation/data/contract.py +317 -0
  116. package/templates/qoder/scripts/{common → foundation/data}/events.py +2 -2
  117. package/templates/qoder/scripts/foundation/data/result.py +223 -0
  118. package/templates/qoder/scripts/{common → foundation/data}/task_utils.py +427 -392
  119. package/templates/qoder/scripts/foundation/identity/__init__.py +0 -0
  120. package/templates/qoder/scripts/foundation/identity/check_publish.py +103 -0
  121. package/templates/qoder/scripts/{common → foundation/identity}/developer.py +239 -238
  122. package/templates/qoder/scripts/foundation/identity/guard.py +159 -0
  123. package/templates/qoder/scripts/{common → foundation/identity}/identity.py +132 -9
  124. package/templates/qoder/scripts/foundation/identity/roles.py +60 -0
  125. package/templates/qoder/scripts/foundation/integrations/__init__.py +0 -0
  126. package/templates/qoder/scripts/{common → foundation/integrations}/active_task.py +30 -15
  127. package/templates/qoder/scripts/{common → foundation/integrations}/feishu.py +11 -10
  128. package/templates/qoder/scripts/{common → foundation/integrations}/terms.py +3 -3
  129. package/templates/qoder/scripts/foundation/integrations/zentao_client.py +220 -0
  130. package/templates/qoder/scripts/foundation/io/__init__.py +0 -0
  131. package/templates/qoder/scripts/{common → foundation/io}/filelock.py +1 -1
  132. package/templates/qoder/scripts/{common → foundation/io}/reqid.py +1 -1
  133. package/templates/qoder/scripts/foundation/protocol/__init__.py +0 -0
  134. package/templates/qoder/scripts/foundation/protocol/duckdb_conn.py +39 -0
  135. package/templates/qoder/scripts/foundation/protocol/mcp_base.py +268 -0
  136. package/templates/qoder/scripts/foundation/utils/__init__.py +0 -0
  137. package/templates/qoder/scripts/{common → foundation/utils}/eval_api.py +12 -1
  138. package/templates/qoder/scripts/{common → foundation/utils}/pip_install.py +23 -6
  139. package/templates/qoder/scripts/{common → foundation/utils}/platform_guard.py +1 -1
  140. package/templates/qoder/scripts/orchestration/__init__.py +0 -0
  141. package/templates/qoder/scripts/orchestration/wlkj.py +185 -0
  142. package/templates/qoder/scripts/protocol/__init__.py +0 -0
  143. package/templates/qoder/scripts/protocol/browser/README.md +207 -0
  144. package/templates/qoder/scripts/protocol/browser/SKILL.md +265 -0
  145. package/templates/qoder/scripts/protocol/browser/config.env +9 -0
  146. package/templates/qoder/scripts/protocol/browser/references/cdp-api.md +110 -0
  147. package/templates/qoder/scripts/protocol/browser/references/migration-2.5.3.md +72 -0
  148. package/templates/qoder/scripts/protocol/browser/references/site-patterns/.gitkeep +0 -0
  149. package/templates/qoder/scripts/protocol/browser/scripts/browser-discovery.mjs +138 -0
  150. package/templates/qoder/scripts/protocol/browser/scripts/cdp-proxy.mjs +672 -0
  151. package/templates/qoder/scripts/protocol/browser/scripts/check-deps.mjs +206 -0
  152. package/templates/qoder/scripts/protocol/browser/scripts/find-url.mjs +253 -0
  153. package/templates/qoder/scripts/protocol/browser/scripts/match-site.mjs +46 -0
  154. package/templates/qoder/scripts/protocol/browser/templates/config.env.template +9 -0
  155. package/templates/qoder/scripts/protocol/mcp/__init__.py +11 -0
  156. package/templates/qoder/scripts/protocol/mcp/kg_mcp_server.py +478 -0
  157. package/templates/qoder/scripts/{lanhu_stdio_wrapper.py → protocol/mcp/lanhu_stdio_wrapper.py} +131 -119
  158. package/templates/qoder/scripts/protocol/mcp/mcp_doctor.py +548 -0
  159. package/templates/qoder/scripts/protocol/mcp/mcp_launcher.py +600 -0
  160. package/templates/qoder/scripts/protocol/mcp/mysql_mcp_server.py +461 -0
  161. package/templates/qoder/scripts/protocol/mcp/zentao_mcp_server.py +1920 -0
  162. package/templates/qoder/scripts/protocol/transports/__init__.py +56 -0
  163. package/templates/qoder/scripts/protocol/transports/base.py +87 -0
  164. package/templates/qoder/scripts/protocol/transports/cli.py +132 -0
  165. package/templates/qoder/scripts/protocol/transports/http.py +141 -0
  166. package/templates/qoder/scripts/protocol/transports/stdio.py +293 -0
  167. package/templates/qoder/scripts/run_weekly_update.bat +27 -18
  168. package/templates/qoder/scripts/run_weekly_update.sh +22 -13
  169. package/templates/qoder/scripts/validation/__init__.py +0 -0
  170. package/templates/qoder/scripts/validation/eval/__init__.py +0 -0
  171. package/templates/qoder/scripts/validation/eval/perf-report-2026-06-24.md +139 -0
  172. package/templates/qoder/scripts/validation/eval/qwork_harness.py +617 -0
  173. package/templates/qoder/scripts/validation/eval/report-commands.md +220 -0
  174. package/templates/qoder/scripts/validation/eval/transcript_timing.py +386 -0
  175. package/templates/qoder/scripts/validation/metrics/__init__.py +0 -0
  176. package/templates/qoder/scripts/{eval_prd.py → validation/metrics/eval_prd.py} +84 -15
  177. package/templates/qoder/scripts/validation/metrics/usability_score.py +750 -0
  178. package/templates/qoder/scripts/validation/test/__init__.py +11 -0
  179. package/templates/qoder/scripts/validation/test/assertion_gen.py +551 -0
  180. package/templates/qoder/scripts/{autotest.py → validation/test/autotest.py} +1234 -1751
  181. package/templates/qoder/scripts/validation/test/autotest_auth.py +109 -0
  182. package/templates/qoder/scripts/{autotest_batch.py → validation/test/autotest_batch.py} +255 -224
  183. package/templates/qoder/scripts/validation/test/autotest_data.py +680 -0
  184. package/templates/qoder/scripts/validation/test/autotest_opencli.py +646 -0
  185. package/templates/qoder/scripts/{autotest_run.py → validation/test/autotest_run.py} +323 -297
  186. package/templates/qoder/scripts/validation/test/autotest_webaccess.py +571 -0
  187. package/templates/qoder/scripts/{benchmark.py → validation/test/benchmark.py} +17 -5
  188. package/templates/qoder/scripts/{kg_auto_login.py → validation/test/kg_auto_login.py} +208 -196
  189. package/templates/qoder/scripts/{kg_test_runner.py → validation/test/kg_test_runner.py} +13 -2
  190. package/templates/qoder/scripts/{page_probe.py → validation/test/page_probe.py} +470 -459
  191. package/templates/qoder/scripts/validation/test/smoke_all_commands.py +170 -0
  192. package/templates/qoder/settings.json +23 -2
  193. package/templates/qoder/skills/design-import/SKILL.md +235 -226
  194. package/templates/qoder/skills/design-review/SKILL.md +91 -82
  195. package/templates/qoder/skills/prd-generator/SKILL.md +38 -24
  196. package/templates/qoder/skills/prd-review/SKILL.md +8 -6
  197. package/templates/qoder/skills/prototype-generator/SKILL.md +296 -247
  198. package/templates/qoder/skills/spec-coder/SKILL.md +24 -15
  199. package/templates/qoder/skills/spec-generator/SKILL.md +24 -12
  200. package/templates/qoder/skills/test-generator/SKILL.md +9 -7
  201. package/templates/qoder/skills/wl-code/SKILL.md +25 -13
  202. package/templates/qoder/skills/wl-commit/SKILL.md +10 -9
  203. package/templates/qoder/skills/wl-design/SKILL.md +7 -5
  204. package/templates/qoder/skills/wl-init/SKILL.md +8 -8
  205. package/templates/qoder/skills/wl-insight/SKILL.md +25 -12
  206. package/templates/qoder/skills/wl-prd-full/SKILL.md +59 -8
  207. package/templates/qoder/skills/wl-prd-quick/SKILL.md +7 -7
  208. package/templates/qoder/skills/wl-prd-review/SKILL.md +18 -6
  209. package/templates/qoder/skills/wl-report/SKILL.md +27 -25
  210. package/templates/qoder/skills/wl-search/SKILL.md +151 -80
  211. package/templates/qoder/skills/wl-spec/SKILL.md +9 -7
  212. package/templates/qoder/skills/wl-status/SKILL.md +50 -18
  213. package/templates/qoder/skills/wl-task/SKILL.md +94 -12
  214. package/templates/qoder/skills/wl-test/SKILL.md +154 -45
  215. package/templates/qoder/templates/prd-full-template.md +7 -0
  216. package/templates/root/AGENTS.md +275 -237
  217. package/templates/root/requirements.txt +3 -0
  218. package/templates/root//344/275/277/347/224/250/350/257/264/346/230/216.md +3 -3
  219. package/templates/root//346/226/260/346/211/213/346/214/207/345/215/227.md +2 -2
  220. package/templates/qoder/hooks/inject-workflow-state.py +0 -169
  221. package/templates/qoder/scripts/__pycache__/check_mcp_launch.cpython-39.pyc +0 -0
  222. package/templates/qoder/scripts/__pycache__/install_qoderwork.cpython-39.pyc +0 -0
  223. package/templates/qoder/scripts/__pycache__/mcp_launcher.cpython-39.pyc +0 -0
  224. package/templates/qoder/scripts/__pycache__/platform_doctor.cpython-39.pyc +0 -0
  225. package/templates/qoder/scripts/check_carriers.py +0 -238
  226. package/templates/qoder/scripts/check_mcp.py +0 -298
  227. package/templates/qoder/scripts/check_mcp_launch.py +0 -183
  228. package/templates/qoder/scripts/check_qoderwork_consistency.py +0 -166
  229. package/templates/qoder/scripts/collect_prds.py +0 -31
  230. package/templates/qoder/scripts/common/mentions.py +0 -134
  231. package/templates/qoder/scripts/common/utf8.py +0 -38
  232. package/templates/qoder/scripts/extract_api_params.py +0 -246
  233. package/templates/qoder/scripts/extract_routes.py +0 -54
  234. package/templates/qoder/scripts/extract_routes_tree.py +0 -78
  235. package/templates/qoder/scripts/handoff.py +0 -22
  236. package/templates/qoder/scripts/init_developer.py +0 -76
  237. package/templates/qoder/scripts/kg_mcp_server.py +0 -801
  238. package/templates/qoder/scripts/kg_semantic.py +0 -150
  239. package/templates/qoder/scripts/mcp_launcher.py +0 -414
  240. package/templates/qoder/scripts/mysql_mcp_server.py +0 -396
  241. package/templates/qoder/scripts/parse_prds.py +0 -33
  242. package/templates/qoder/scripts/role.py +0 -51
  243. package/templates/qoder/scripts/sync_carriers.py +0 -259
  244. package/templates/qoder/scripts/task.py +0 -1261
  245. package/templates/qoder/scripts/workspace_init.py +0 -102
  246. package/templates/qoder/scripts/zentao_mcp_server.py +0 -424
  247. package/templates/qoder/skills/prompt-enrich/SKILL.md +0 -90
  248. /package/templates/qoder/scripts/{common → deployment}/__init__.py +0 -0
  249. /package/templates/qoder/{skills/prototype-generator/SKILL.md.zcode-79180-2af4721f-f9a6-412c-88db-c0af680d211b.tmp → scripts/domain/__init__.py} +0 -0
  250. /package/templates/qoder/scripts/{common → domain/kg/graph}/graph_traverse.py +0 -0
  251. /package/templates/qoder/scripts/{common → domain/kg/server}/repowiki.py +0 -0
  252. /package/templates/qoder/scripts/{common → foundation/io}/atomicio.py +0 -0
  253. /package/templates/qoder/scripts/{secure-ls.js → validation/test/secure-ls.js} +0 -0
@@ -1,628 +1,737 @@
1
- #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
- """
4
- status.py - 项目状态计算引擎 (支撑 /wl-status 命令)
5
-
6
- /wl-status markdown 指令由 AI 执行, 但数据计算 (健康分/周期时间/阻塞图/
7
- 截止日期) 由本脚本提供, 保证数据准确。
8
-
9
- Usage:
10
- python status.py # 全景: current + roadmap + health
11
- python status.py health # 只算健康分
12
- python status.py cycle # 只算周期时间 (从 stage_ts)
13
- python status.py deadlines [--days 7] # 未来 N 天的截止任务
14
- python status.py blocked # 被阻塞的任务图
15
- """
16
-
17
- import argparse
18
- import json
19
- import os
20
- import sys
21
- from datetime import datetime, date, timedelta
22
- from pathlib import Path
23
-
24
- try:
25
- sys.stdout.reconfigure(encoding='utf-8', errors='replace')
26
- except (AttributeError, TypeError, OSError):
27
- pass
28
-
29
- THIS_DIR = os.path.dirname(os.path.abspath(__file__))
30
- sys.path.insert(0, THIS_DIR)
31
- from common.paths import get_repo_root, get_developer, get_tasks_dir, MEMBERS_DIR
32
- from common.task_utils import load_task_json
33
- from common.atomicio import safe_read_json
34
-
35
- BASE = get_repo_root()
36
- INDEX_DIR = BASE / 'data' / 'index'
37
-
38
-
39
- def load_all_tasks():
40
- """加载所有活跃任务 (workspace/tasks/)。"""
41
- tasks_dir = get_tasks_dir(BASE)
42
- if not tasks_dir.is_dir():
43
- return {}
44
- out = {}
45
- for d in tasks_dir.iterdir():
46
- if d.is_dir():
47
- data = load_task_json(d)
48
- if data:
49
- out[d.name] = data
50
- return out
51
-
52
-
53
- # ============================================================
54
- # 周期时间分析 (B4: cycle time)
55
- # ============================================================
56
-
57
- def compute_cycle_times(tasks):
58
- """从 stage_ts 算各阶段耗时。
59
-
60
- Returns:
61
- {
62
- "avg_total_hours": float, # created -> completed 平均
63
- "avg_dev_hours": float, # started -> completed 平均 (纯开发)
64
- "samples": int, # 有完整时间戳的任务数
65
- "by_task": [{name, total_h, dev_h, ...}]
66
- }
67
- """
68
- samples = []
69
- for name, data in tasks.items():
70
- if data.get("status") != "completed":
71
- continue
72
- ts = data.get("stage_ts") or {}
73
- created = ts.get("created")
74
- started = ts.get("started")
75
- completed = ts.get("completed")
76
- if not created or not completed:
77
- continue
78
- try:
79
- t_created = datetime.fromisoformat(created)
80
- t_completed = datetime.fromisoformat(completed)
81
- total_h = (t_completed - t_created).total_seconds() / 3600
82
- dev_h = None
83
- if started:
84
- t_started = datetime.fromisoformat(started)
85
- dev_h = (t_completed - t_started).total_seconds() / 3600
86
- samples.append({
87
- "name": name,
88
- "title": data.get("title", name),
89
- "total_hours": round(total_h, 1),
90
- "dev_hours": round(dev_h, 1) if dev_h is not None else None,
91
- })
92
- except (ValueError, TypeError):
93
- continue
94
-
95
- if not samples:
96
- return {"avg_total_hours": 0, "avg_dev_hours": 0, "samples": 0, "by_task": []}
97
-
98
- avg_total = sum(s["total_hours"] for s in samples) / len(samples)
99
- dev_samples = [s for s in samples if s["dev_hours"] is not None]
100
- avg_dev = sum(s["dev_hours"] for s in dev_samples) / len(dev_samples) if dev_samples else 0
101
-
102
- return {
103
- "avg_total_hours": round(avg_total, 1),
104
- "avg_dev_hours": round(avg_dev, 1),
105
- "samples": len(samples),
106
- "by_task": sorted(samples, key=lambda x: -x["total_hours"])[:10],
107
- }
108
-
109
-
110
- # ============================================================
111
- # 截止日期提醒 (B4: deadlines)
112
- # ============================================================
113
-
114
- def compute_deadlines(tasks, days=7):
115
- """未来 N 天内到期的任务 + 已逾期任务。
116
-
117
- Returns:
118
- {
119
- "overdue": [...], # 已逾期未完成
120
- "due_soon": [...], # 未来 days 天内到期
121
- "no_date_count": int, # 无截止日期的活跃任务数
122
- }
123
- """
124
- today = date.today()
125
- horizon = today + timedelta(days=days)
126
- overdue = []
127
- due_soon = []
128
- no_date = 0
129
-
130
- for name, data in tasks.items():
131
- if data.get("status") == "completed":
132
- continue
133
- due = data.get("due_date")
134
- if not due:
135
- no_date += 1
136
- continue
137
- try:
138
- due_d = date.fromisoformat(due)
139
- except ValueError:
140
- continue
141
- item = {
142
- "name": name,
143
- "title": data.get("title", name),
144
- "due_date": due,
145
- "assignee": data.get("assignee", "?"),
146
- "priority": data.get("priority", "?"),
147
- "days_left": (due_d - today).days,
148
- }
149
- if due_d < today:
150
- overdue.append(item)
151
- elif due_d <= horizon:
152
- due_soon.append(item)
153
-
154
- overdue.sort(key=lambda x: x["days_left"])
155
- due_soon.sort(key=lambda x: x["days_left"])
156
- return {
157
- "overdue": overdue,
158
- "due_soon": due_soon,
159
- "no_date_count": no_date,
160
- }
161
-
162
-
163
- # ============================================================
164
- # 阻塞图 (B4: blocked)
165
- # ============================================================
166
-
167
- def compute_blocked_graph(tasks):
168
- """分析任务间的阻塞关系。
169
-
170
- Returns:
171
- {
172
- "blocked_tasks": [{name, blocked_by: [...open...], title}],
173
- "blocking_count": {blocker_name: count}, # 谁阻塞了最多任务
174
- }
175
- """
176
- blocked = []
177
- blocking_count = {}
178
- for name, data in tasks.items():
179
- if data.get("status") == "completed":
180
- continue
181
- blocked_by = data.get("blocked_by") or []
182
- open_blocks = []
183
- for dep in blocked_by:
184
- dep_data = tasks.get(dep)
185
- if not dep_data or dep_data.get("status") != "completed":
186
- open_blocks.append(dep)
187
- blocking_count[dep] = blocking_count.get(dep, 0) + 1
188
- if open_blocks:
189
- blocked.append({
190
- "name": name,
191
- "title": data.get("title", name),
192
- "blocked_by": open_blocks,
193
- "assignee": data.get("assignee", "?"),
194
- })
195
- return {
196
- "blocked_tasks": blocked,
197
- "blocking_count": dict(sorted(blocking_count.items(), key=lambda x: -x[1])[:5]),
198
- }
199
-
200
-
201
- # ============================================================
202
- # 学习数据采集 (扫全部 dev 的 journal, 修旧版路径 bug)
203
- # ============================================================
204
-
205
- def _count_all_feedback():
206
- """统计全团队 feedback 事件总数 (扫 workspace/members/*/journal/feedback.jsonl)。
207
-
208
- 旧版读 .qoder/learning/feedback.jsonl (已废弃路径, 文件不存在) 0 → 学习度恒 2.0。
209
- learn.py 早已迁移到 workspace/members/{dev}/journal/, 这里跟上。
210
- """
211
- if not MEMBERS_DIR.is_dir():
212
- return 0
213
- total = 0
214
- for dev_dir in MEMBERS_DIR.iterdir():
215
- if not dev_dir.is_dir():
216
- continue
217
- fb = dev_dir / 'journal' / 'feedback.jsonl'
218
- if fb.is_file():
219
- try:
220
- with open(fb, encoding='utf-8') as f:
221
- total += sum(1 for line in f if line.strip())
222
- except Exception:
223
- continue
224
- return total
225
-
226
-
227
- # ============================================================
228
- # 健康分 (综合)
229
- # ============================================================
230
-
231
- def compute_health(tasks):
232
- """加权健康分 (满分 5)
233
-
234
- 维度: EVA 合格率 / 索引新鲜度 / 按时交付 / 团队同步 / 流水线 / 学习
235
- """
236
- scores = {}
237
-
238
- # 1. EVA 合格率 (25%)
239
- # eval-history jsonl (每行一个 JSON), 不能用 safe_read_json
240
- eval_path = BASE / '.qoder' / 'learning' / 'eval-history.jsonl'
241
- if eval_path.is_file():
242
- records = []
243
- try:
244
- with open(eval_path, encoding='utf-8') as f:
245
- for line in f:
246
- line = line.strip()
247
- if line:
248
- records.append(json.loads(line))
249
- recent = records[-10:]
250
- if recent:
251
- passed = sum(1 for r in recent if r.get('passed'))
252
- scores['eva'] = (passed / len(recent)) * 5
253
- else:
254
- scores['eva'] = 3.0 # 无数据, 中性
255
- except Exception:
256
- scores['eva'] = 3.0
257
- else:
258
- scores['eva'] = 3.0
259
-
260
- # 2. 索引新鲜度 (20%)
261
- meta = safe_read_json(INDEX_DIR / '.index-meta.json', default={}) or {}
262
- last_sync = meta.get('last_sync', '')
263
- try:
264
- ts = str(last_sync).replace('T', ' ').split('.')[0].strip()
265
- sync_dt = datetime.strptime(ts, '%Y-%m-%d %H:%M')
266
- age_days = (datetime.now() - sync_dt).days
267
- if age_days <= 7:
268
- scores['index'] = 5.0
269
- elif age_days <= 14:
270
- scores['index'] = 3.0
271
- else:
272
- scores['index'] = 1.0
273
- except Exception:
274
- scores['index'] = 2.0
275
-
276
- # 3. 按时交付 (20%) - 从 deadlines 算逾期率
277
- deadlines = compute_deadlines(tasks, days=0)
278
- total_with_due = len(deadlines['overdue']) + len(deadlines['due_soon']) + 1
279
- overdue_rate = len(deadlines['overdue']) / total_with_due if total_with_due else 0
280
- scores['on_time'] = max(0, 5 - overdue_rate * 10)
281
-
282
- # 4. 团队同步 (15%) - 简化: ahead 提交 = 有未同步
283
- scores['sync'] = 4.0 # 默认良好 (详细检查由 team_sync status 做)
284
-
285
- # 5. 流水线流转 (10%) - 任务在各阶段分布
286
- statuses = {}
287
- for data in tasks.values():
288
- s = data.get('status', '?')
289
- statuses[s] = statuses.get(s, 0) + 1
290
- # 有 in_progress 且不全卡 planning = 健康
291
- if statuses.get('in_progress', 0) > 0:
292
- scores['pipeline'] = 4.0
293
- elif statuses.get('planning', 0) > 0:
294
- scores['pipeline'] = 3.0
295
- else:
296
- scores['pipeline'] = 2.0
297
-
298
- # 6. 学习 (10%)
299
- # bug: 旧代码读 .qoder/learning/feedback.jsonl (旧路径, 已废弃),
300
- # learn.py 实际写 workspace/members/{dev}/journal/feedback.jsonl。
301
- # 迁移了写路径没迁移读路径 → 学习度恒为 2.0。改为扫全部 dev 的 journal。
302
- fb_count = _count_all_feedback()
303
- scores['learning'] = min(5.0, 2.0 + fb_count * 0.1) if fb_count else 2.0
304
-
305
- # 加权
306
- weights = {
307
- 'eva': 0.25, 'index': 0.20, 'on_time': 0.20,
308
- 'sync': 0.15, 'pipeline': 0.10, 'learning': 0.10,
309
- }
310
- total = sum(scores[k] * weights[k] for k in weights)
311
- return {
312
- 'total': round(total, 2),
313
- 'scores': scores,
314
- 'weights': weights,
315
- 'verdict': '健康' if total >= 4 else ('有风险' if total >= 3 else '需关注'),
316
- }
317
-
318
-
319
- # ============================================================
320
- # 流水线自检 (测工具本身, 不测产出) —— 可观测性闭环
321
- # ============================================================
322
-
323
- def _scan_skill_instrumentation():
324
- """扫 .qoder/skills/*/SKILL.md, 统计有多少 skill 调了 learn.py record (已埋点)
325
- 返回 (已埋点skill列表, 全部skill列表)。
326
- """
327
- skills_dir = BASE / '.qoder' / 'skills'
328
- if not skills_dir.is_dir():
329
- return [], []
330
- all_skills = []
331
- instrumented = []
332
- for d in sorted(skills_dir.iterdir()):
333
- if not d.is_dir():
334
- continue
335
- skill_md = d / 'SKILL.md'
336
- if not skill_md.is_file():
337
- continue
338
- all_skills.append(d.name)
339
- try:
340
- text = skill_md.read_text(encoding='utf-8', errors='ignore')
341
- except Exception:
342
- continue
343
- if 'learn.py' in text and 'record' in text:
344
- instrumented.append(d.name)
345
- return instrumented, all_skills
346
-
347
-
348
- def _analyze_eva_rounds():
349
- """从 eval-history.jsonl 分析 EVA 修正轮数。
350
- 返回 {samples, rework_count, avg_rounds}。
351
- rework = 连续 passed=false 后出现 passed=true 的序列。
352
- """
353
- eval_path = BASE / '.qoder' / 'learning' / 'eval-history.jsonl'
354
- if not eval_path.is_file():
355
- return {'samples': 0, 'rework_count': 0, 'avg_rounds': 0}
356
- records = []
357
- try:
358
- with open(eval_path, encoding='utf-8') as f:
359
- for line in f:
360
- line = line.strip()
361
- if line:
362
- try:
363
- records.append(json.loads(line))
364
- except Exception:
365
- continue
366
- except Exception:
367
- return {'samples': 0, 'rework_count': 0, 'avg_rounds': 0}
368
-
369
- recent = records[-20:]
370
- rework = 0
371
- total_fail_before_pass = 0
372
- fails_in_run = 0
373
- for r in recent:
374
- if r.get('passed'):
375
- if fails_in_run > 0:
376
- rework += 1
377
- total_fail_before_pass += fails_in_run
378
- fails_in_run = 0
379
- else:
380
- fails_in_run += 1
381
- avg_rounds = (total_fail_before_pass / rework + 1) if rework else 0
382
- return {
383
- 'samples': len(recent),
384
- 'rework_count': rework,
385
- 'avg_rounds': round(avg_rounds, 1),
386
- }
387
-
388
-
389
- def _count_rule_violations():
390
- """统计规则违规事件 (没问平台就调脚本)
391
- 扫全部 dev 的 journal 里 event=rule_violation 的条数。
392
- """
393
- if not MEMBERS_DIR.is_dir():
394
- return 0
395
- count = 0
396
- for dev_dir in MEMBERS_DIR.iterdir():
397
- if not dev_dir.is_dir():
398
- continue
399
- fb = dev_dir / 'journal' / 'feedback.jsonl'
400
- if not fb.is_file():
401
- continue
402
- try:
403
- with open(fb, encoding='utf-8') as f:
404
- for line in f:
405
- line = line.strip()
406
- if not line:
407
- continue
408
- try:
409
- if json.loads(line).get('event') == 'rule_violation':
410
- count += 1
411
- except Exception:
412
- continue
413
- except Exception:
414
- continue
415
- return count
416
-
417
-
418
- def compute_pipeline_self():
419
- """流水线自检 —— 4 个指标全从已有数据派生, 不增加 PM 操作。
420
-
421
- 返回:
422
- instrumentation: {done, total, missing} 埋点覆盖率
423
- eva: {samples, rework_count, avg_rounds} EVA 修正轮数
424
- violations: int 规则违规次数
425
- verdict: str 一句话结论
426
- """
427
- done, total = _scan_skill_instrumentation()
428
- missing = [s for s in total if s not in done]
429
- eva = _analyze_eva_rounds()
430
- violations = _count_rule_violations()
431
-
432
- parts = []
433
- cov_pct = (len(done) / len(total) * 100) if total else 0
434
- if cov_pct < 50:
435
- parts.append('埋点覆盖偏低')
436
- if eva['rework_count'] > 0 and eva['avg_rounds'] > 2:
437
- parts.append('EVA 修正轮数偏高')
438
- if violations > 0:
439
- parts.append('存在规则违规')
440
- verdict = ';'.join(parts) if parts else '正常'
441
-
442
- return {
443
- 'instrumentation': {'done': len(done), 'total': len(total),
444
- 'missing': missing, 'pct': round(cov_pct)},
445
- 'eva': eva,
446
- 'violations': violations,
447
- 'verdict': verdict,
448
- }
449
-
450
-
451
- # ============================================================
452
- # 角色引导 (该角色该用哪些命令 —— 软引导, 帮新人快速上手)
453
- # ============================================================
454
-
455
- def compute_role_guide():
456
- """读当前角色 + config.yaml 的 commands 映射, 返回该角色的建议命令。
457
- 软引导: 不拦截任何命令, 只是把"你该用什么"摆出来。
458
- """
459
- # 当前角色 (role.py 的 get_role 逻辑)
460
- role = None
461
- try:
462
- sys.path.insert(0, str(BASE / '.qoder' / 'scripts'))
463
- from role import get_role
464
- role = get_role()
465
- except Exception:
466
- pass
467
- if not role:
468
- role = 'pm' # 默认
469
-
470
- # 读 config.yaml 的 commands 映射
471
- commands = []
472
- role_name = role
473
- try:
474
- import yaml
475
- cfg_path = BASE / '.qoder' / 'config.yaml'
476
- if cfg_path.is_file():
477
- with open(cfg_path, encoding='utf-8') as f:
478
- cfg = yaml.safe_load(f) or {}
479
- roles = cfg.get('roles', {})
480
- rinfo = roles.get(role, {})
481
- commands = rinfo.get('commands', [])
482
- role_name = rinfo.get('name', role)
483
- except Exception:
484
- pass
485
-
486
- # 校验建议命令是否真实存在 (command 文件在不在)
487
- available = []
488
- missing = []
489
- cmds_dir = BASE / '.qoder' / 'commands'
490
- for cmd in commands:
491
- exists = (cmds_dir / (cmd + '.md')).is_file() or \
492
- (cmds_dir / 'optional' / (cmd + '.md')).is_file()
493
- (available if exists else missing).append(cmd)
494
-
495
- return {
496
- 'role': role,
497
- 'role_name': role_name,
498
- 'suggested': commands,
499
- 'available': available,
500
- 'missing': missing,
501
- }
502
-
503
-
504
- # ============================================================
505
- # 渲染
506
- # ============================================================
507
-
508
- def render_full():
509
- tasks = load_all_tasks()
510
- print('=' * 50)
511
- print('项目状态总览')
512
- print('=' * 50)
513
- print(f'\n活跃任务: {len(tasks)} 个')
514
- dev = get_developer(BASE)
515
- if dev:
516
- my_tasks = [n for n, d in tasks.items() if dev in (d.get('assignee'), d.get('creator'))]
517
- print(f'我的任务: {len(my_tasks)} 个 (开发者: {dev})')
518
-
519
- # 角色引导 (该角色该用哪些命令)
520
- rg = compute_role_guide()
521
- print(f'当前角色: {rg["role_name"]} ({rg["role"]})')
522
- if rg['suggested']:
523
- # 标注哪些命令当前可用
524
- avail = rg['available']
525
- print(f' 建议命令 ({len(avail)}/{len(rg["suggested"])} 可用): '
526
- + ' '.join('/' + c for c in avail))
527
- if rg['missing']:
528
- print(f' ⚠ 建议但未安装: {", ".join(rg["missing"])}')
529
-
530
- # 健康分
531
- print('\n--- 健康度 ---')
532
- health = compute_health(tasks)
533
- print(f"总分: {health['total']}/5 ({health['verdict']})")
534
- for k, v in health['scores'].items():
535
- w = health['weights'][k]
536
- print(f" {k:12} {v:.1f} × {w:.0%}")
537
-
538
- # 阻塞
539
- print('\n--- 阻塞图 ---')
540
- bg = compute_blocked_graph(tasks)
541
- if bg['blocked_tasks']:
542
- print(f"被阻塞的任务: {len(bg['blocked_tasks'])} 个")
543
- for bt in bg['blocked_tasks'][:10]:
544
- print(f" {bt['name']} ({bt['assignee']}) <- {', '.join(bt['blocked_by'])}")
545
- if bg['blocking_count']:
546
- print(f"阻塞最多的: {bg['blocking_count']}")
547
- else:
548
- print('无被阻塞的任务')
549
-
550
- # 截止日期
551
- print('\n--- 截止日期 (未来 7 天) ---')
552
- dl = compute_deadlines(tasks, days=7)
553
- if dl['overdue']:
554
- print(f"⚠️ 已逾期: {len(dl['overdue'])} 个")
555
- for item in dl['overdue'][:5]:
556
- print(f" {item['name']} ({item['days_left']}天前) [{item['priority']}] {item['title']}")
557
- if dl['due_soon']:
558
- print(f"即将到期: {len(dl['due_soon'])} 个")
559
- for item in dl['due_soon'][:5]:
560
- print(f" {item['name']} (还剩{item['days_left']}天) [{item['priority']}] {item['title']}")
561
- if not dl['overdue'] and not dl['due_soon']:
562
- print('未来 7 天无截止任务')
563
- print(f"无截止日期的活跃任务: {dl['no_date_count']} 个")
564
-
565
- # 周期时间
566
- print('\n--- 周期时间 ---')
567
- ct = compute_cycle_times(tasks)
568
- if ct['samples'] > 0:
569
- print(f"已完成样本: {ct['samples']} 个")
570
- print(f"平均总周期: {ct['avg_total_hours']} 小时 ({ct['avg_total_hours']/24:.1f} 天)")
571
- print(f"平均开发时长: {ct['avg_dev_hours']} 小时")
572
- if ct['by_task']:
573
- print("最慢的 5 个:")
574
- for s in ct['by_task'][:5]:
575
- print(f" {s['name']}: {s['total_hours']}h (开发 {s['dev_hours']}h)")
576
- else:
577
- print('暂无已完成任务的时间戳数据 (stage_ts B3 后才有)')
578
-
579
- # 流水线自检 (测工具本身)
580
- print('\n--- 流水线自检 (测工具本身) ---')
581
- ps = compute_pipeline_self()
582
- inst = ps['instrumentation']
583
- cov_mark = '✓' if inst['pct'] >= 50 else '⚠'
584
- print(f" 埋点覆盖: {inst['done']}/{inst['total']} skill ({inst['pct']}%) {cov_mark}")
585
- if inst['missing']:
586
- # 只列前 5 个, 避免刷屏
587
- shown = inst['missing'][:5]
588
- more = '' if len(inst['missing']) <= 5 else f" 等{len(inst['missing'])}个"
589
- print(f" 缺埋点: {', '.join(shown)}{more}")
590
- eva = ps['eva']
591
- if eva['samples'] > 0:
592
- eva_mark = '✓' if eva['avg_rounds'] <= 2 else '⚠'
593
- print(f" EVA 修正: 近{eva['samples']}次 {eva['rework_count']}次打回, "
594
- f"平均{eva['avg_rounds']}轮过 {eva_mark}")
595
- else:
596
- print(' EVA 修正: 暂无评估历史')
597
- v_mark = '✓' if ps['violations'] == 0 else '⚠'
598
- print(f" 规则违规: {ps['violations']} {v_mark}")
599
- print(f" 自检结论: {ps['verdict']}")
600
-
601
-
602
- def main():
603
- parser = argparse.ArgumentParser(description='项目状态计算')
604
- parser.add_argument('scope', nargs='?', default='all',
605
- choices=['all', 'health', 'cycle', 'deadlines', 'blocked'])
606
- parser.add_argument('--days', type=int, default=7, help='截止日期 horizon 天数')
607
- args = parser.parse_args()
608
-
609
- tasks = load_all_tasks()
610
-
611
- if args.scope == 'health':
612
- h = compute_health(tasks)
613
- print(json.dumps(h, indent=2, ensure_ascii=False))
614
- elif args.scope == 'cycle':
615
- c = compute_cycle_times(tasks)
616
- print(json.dumps(c, indent=2, ensure_ascii=False))
617
- elif args.scope == 'deadlines':
618
- d = compute_deadlines(tasks, args.days)
619
- print(json.dumps(d, indent=2, ensure_ascii=False))
620
- elif args.scope == 'blocked':
621
- b = compute_blocked_graph(tasks)
622
- print(json.dumps(b, indent=2, ensure_ascii=False))
623
- else:
624
- render_full()
625
-
626
-
627
- if __name__ == '__main__':
628
- main()
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # v3.0 路径自举: 引导到 common/bootstrap, 统一 sys.path 逻辑
4
+ import os as _o, sys as _s
5
+ _f = _o.path.abspath(__file__)
6
+ for _ in range(10):
7
+ _f = _o.path.dirname(_f)
8
+ _cp = _o.path.join(_f, 'foundation')
9
+ if _o.path.isfile(_o.path.join(_cp, 'bootstrap.py')):
10
+ break
11
+ if _cp not in _s.path: _s.path.insert(0, _cp)
12
+ from bootstrap import setup; setup()
13
+
14
+ """
15
+ status.py - 项目状态计算引擎 (支撑 /wl-status 命令)
16
+
17
+ /wl-status 的 markdown 指令由 AI 执行, 但数据计算 (健康分/周期时间/阻塞图/
18
+ 截止日期) 由本脚本提供, 保证数据准确。
19
+
20
+ Usage:
21
+ python status.py # 全景: current + roadmap + health
22
+ python status.py health # 只算健康分
23
+ python status.py cycle # 只算周期时间 (从 stage_ts)
24
+ python status.py deadlines [--days 7] # 未来 N 天的截止任务
25
+ python status.py blocked # 被阻塞的任务图
26
+ """
27
+
28
+ import argparse
29
+ import json
30
+ import os
31
+ import sys
32
+ from datetime import datetime, date, timedelta
33
+ from pathlib import Path
34
+
35
+ try:
36
+ sys.stdout.reconfigure(encoding='utf-8', errors='replace')
37
+ except (AttributeError, TypeError, OSError):
38
+ pass
39
+
40
+ THIS_DIR = os.path.dirname(os.path.abspath(__file__))
41
+ sys.path.insert(0, THIS_DIR)
42
+ from foundation.core.paths import get_repo_root, get_developer, get_tasks_dir, MEMBERS_DIR, EVAL_HISTORY_PATH
43
+ from foundation.data.task_utils import load_task_json
44
+ from foundation.io.atomicio import safe_read_json
45
+
46
+ BASE = get_repo_root()
47
+ INDEX_DIR = BASE / 'data' / 'index'
48
+
49
+
50
+ def load_all_tasks():
51
+ """加载所有活跃任务 (workspace/tasks/)。"""
52
+ tasks_dir = get_tasks_dir(BASE)
53
+ if not tasks_dir.is_dir():
54
+ return {}
55
+ out = {}
56
+ for d in tasks_dir.iterdir():
57
+ if d.is_dir():
58
+ data = load_task_json(d)
59
+ if data:
60
+ out[d.name] = data
61
+ return out
62
+
63
+
64
+ # ============================================================
65
+ # 周期时间分析 (B4: cycle time)
66
+ # ============================================================
67
+
68
+ def compute_cycle_times(tasks):
69
+ """从 stage_ts 算各阶段耗时。
70
+
71
+ Returns:
72
+ {
73
+ "avg_total_hours": float, # created -> completed 平均
74
+ "avg_dev_hours": float, # started -> completed 平均 (纯开发)
75
+ "samples": int, # 有完整时间戳的任务数
76
+ "by_task": [{name, total_h, dev_h, ...}]
77
+ }
78
+ """
79
+ samples = []
80
+ for name, data in tasks.items():
81
+ if data.get("status") != "completed":
82
+ continue
83
+ ts = data.get("stage_ts") or {}
84
+ created = ts.get("created")
85
+ started = ts.get("started")
86
+ completed = ts.get("completed")
87
+ if not created or not completed:
88
+ continue
89
+ try:
90
+ t_created = datetime.fromisoformat(created)
91
+ t_completed = datetime.fromisoformat(completed)
92
+ total_h = (t_completed - t_created).total_seconds() / 3600
93
+ dev_h = None
94
+ if started:
95
+ t_started = datetime.fromisoformat(started)
96
+ dev_h = (t_completed - t_started).total_seconds() / 3600
97
+ samples.append({
98
+ "name": name,
99
+ "title": data.get("title", name),
100
+ "total_hours": round(total_h, 1),
101
+ "dev_hours": round(dev_h, 1) if dev_h is not None else None,
102
+ })
103
+ except (ValueError, TypeError):
104
+ continue
105
+
106
+ if not samples:
107
+ return {"avg_total_hours": 0, "avg_dev_hours": 0, "samples": 0, "by_task": []}
108
+
109
+ avg_total = sum(s["total_hours"] for s in samples) / len(samples)
110
+ dev_samples = [s for s in samples if s["dev_hours"] is not None]
111
+ avg_dev = sum(s["dev_hours"] for s in dev_samples) / len(dev_samples) if dev_samples else 0
112
+
113
+ return {
114
+ "avg_total_hours": round(avg_total, 1),
115
+ "avg_dev_hours": round(avg_dev, 1),
116
+ "samples": len(samples),
117
+ "by_task": sorted(samples, key=lambda x: -x["total_hours"])[:10],
118
+ }
119
+
120
+
121
+ # ============================================================
122
+ # 截止日期提醒 (B4: deadlines)
123
+ # ============================================================
124
+
125
+ def compute_deadlines(tasks, days=7):
126
+ """未来 N 天内到期的任务 + 已逾期任务。
127
+
128
+ Returns:
129
+ {
130
+ "overdue": [...], # 已逾期未完成
131
+ "due_soon": [...], # 未来 days 天内到期
132
+ "no_date_count": int, # 无截止日期的活跃任务数
133
+ }
134
+ """
135
+ today = date.today()
136
+ horizon = today + timedelta(days=days)
137
+ overdue = []
138
+ due_soon = []
139
+ no_date = 0
140
+
141
+ for name, data in tasks.items():
142
+ if data.get("status") == "completed":
143
+ continue
144
+ due = data.get("due_date")
145
+ if not due:
146
+ no_date += 1
147
+ continue
148
+ try:
149
+ due_d = date.fromisoformat(due)
150
+ except ValueError:
151
+ continue
152
+ item = {
153
+ "name": name,
154
+ "title": data.get("title", name),
155
+ "due_date": due,
156
+ "assignee": data.get("assignee", "?"),
157
+ "priority": data.get("priority", "?"),
158
+ "days_left": (due_d - today).days,
159
+ }
160
+ if due_d < today:
161
+ overdue.append(item)
162
+ elif due_d <= horizon:
163
+ due_soon.append(item)
164
+
165
+ overdue.sort(key=lambda x: x["days_left"])
166
+ due_soon.sort(key=lambda x: x["days_left"])
167
+ return {
168
+ "overdue": overdue,
169
+ "due_soon": due_soon,
170
+ "no_date_count": no_date,
171
+ }
172
+
173
+
174
+ # ============================================================
175
+ # 阻塞图 (B4: blocked)
176
+ # ============================================================
177
+
178
+ def compute_blocked_graph(tasks):
179
+ """分析任务间的阻塞关系。
180
+
181
+ Returns:
182
+ {
183
+ "blocked_tasks": [{name, blocked_by: [...open...], title}],
184
+ "blocking_count": {blocker_name: count}, # 谁阻塞了最多任务
185
+ }
186
+ """
187
+ blocked = []
188
+ blocking_count = {}
189
+ for name, data in tasks.items():
190
+ if data.get("status") == "completed":
191
+ continue
192
+ blocked_by = data.get("blocked_by") or []
193
+ open_blocks = []
194
+ for dep in blocked_by:
195
+ dep_data = tasks.get(dep)
196
+ if not dep_data or dep_data.get("status") != "completed":
197
+ open_blocks.append(dep)
198
+ blocking_count[dep] = blocking_count.get(dep, 0) + 1
199
+ if open_blocks:
200
+ blocked.append({
201
+ "name": name,
202
+ "title": data.get("title", name),
203
+ "blocked_by": open_blocks,
204
+ "assignee": data.get("assignee", "?"),
205
+ })
206
+ return {
207
+ "blocked_tasks": blocked,
208
+ "blocking_count": dict(sorted(blocking_count.items(), key=lambda x: -x[1])[:5]),
209
+ }
210
+
211
+
212
+ # ============================================================
213
+ # 学习数据采集 (扫全部 dev 的 journal, 修旧版路径 bug)
214
+ # ============================================================
215
+
216
+ def _count_all_feedback():
217
+ """统计全团队 feedback 事件总数 (扫 workspace/members/*/journal/feedback.jsonl)。
218
+
219
+ 旧版读 data/learning/feedback.jsonl (已废弃路径, 文件不存在) → 恒 0 → 学习度恒 2.0。
220
+ learn.py 早已迁移到 workspace/members/{dev}/journal/, 这里跟上。
221
+ """
222
+ if not MEMBERS_DIR.is_dir():
223
+ return 0
224
+ total = 0
225
+ for dev_dir in MEMBERS_DIR.iterdir():
226
+ if not dev_dir.is_dir():
227
+ continue
228
+ fb = dev_dir / 'journal' / 'feedback.jsonl'
229
+ if fb.is_file():
230
+ try:
231
+ with open(fb, encoding='utf-8') as f:
232
+ total += sum(1 for line in f if line.strip())
233
+ except Exception:
234
+ continue
235
+ return total
236
+
237
+
238
+ def _compute_adoption_score():
239
+ """工作流采纳度 (0-5), compute_health adoption 维度用。
240
+
241
+ usability_score.py 的 adoption_score 同源逻辑 (避免 import 验证脚本产生环依赖, 内联):
242
+ 取能算的自动采纳指标 (U2一次过审/U3返工/U4违规/U6活跃), 按达标率映射 0-5。
243
+ 无数据 → 中性 2.5。
244
+ """
245
+ # U2 一次过审率 + U3 返工 (从 eval-history)
246
+ records = []
247
+ if EVAL_HISTORY_PATH.is_file():
248
+ try:
249
+ with open(EVAL_HISTORY_PATH, encoding='utf-8') as f:
250
+ for line in f:
251
+ line = line.strip()
252
+ if line:
253
+ records.append(json.loads(line))
254
+ except Exception:
255
+ pass
256
+
257
+ scored = [] # 每项 1.0(绿)/0.5(黄)/0(红)
258
+
259
+ # U2: 首轮过审率
260
+ by_prd = {}
261
+ for r in records:
262
+ prd = r.get('prd')
263
+ if not prd:
264
+ continue
265
+ t = r.get('time', '')
266
+ if prd not in by_prd or (t and t < by_prd[prd][0]):
267
+ by_prd[prd] = (t, r)
268
+ if by_prd:
269
+ first_pass = sum(1 for (t, row) in by_prd.values() if row.get('passed'))
270
+ u2_rate = 100.0 * first_pass / len(by_prd)
271
+ scored.append(1.0 if u2_rate >= 60 else (0.5 if u2_rate >= 30 else 0.0))
272
+
273
+ # U3: 平均返工轮数 (越低越好)
274
+ if by_prd:
275
+ per_prd = {}
276
+ for r in records:
277
+ if r.get('prd'):
278
+ per_prd.setdefault(r['prd'], []).append(r)
279
+ total_evals = sum(len(v) for v in per_prd.values())
280
+ avg_rework = total_evals / len(per_prd) - 1.0
281
+ avg_rework = max(avg_rework, 0.0)
282
+ scored.append(1.0 if avg_rework <= 1.0 else (0.5 if avg_rework <= 2.0 else 0.0))
283
+
284
+ # U4 规则违反率 + U6 活跃 (从 feedback)
285
+ violations = 0
286
+ prds_gen = 0
287
+ active_devs = set()
288
+ now = datetime.now()
289
+ week_ago = now - timedelta(days=7)
290
+ if MEMBERS_DIR.is_dir():
291
+ for dev_dir in MEMBERS_DIR.iterdir():
292
+ if not dev_dir.is_dir():
293
+ continue
294
+ fb = dev_dir / 'journal' / 'feedback.jsonl'
295
+ if not fb.is_file():
296
+ continue
297
+ try:
298
+ with open(fb, encoding='utf-8') as f:
299
+ for line in f:
300
+ line = line.strip()
301
+ if not line:
302
+ continue
303
+ e = json.loads(line)
304
+ if e.get('event') == 'rule_violation':
305
+ violations += 1
306
+ if e.get('event') == 'prd_accepted':
307
+ prds_gen += 1
308
+ # U6: 近7天有事件的 dev
309
+ ts = e.get('ts', '')
310
+ try:
311
+ t = datetime.fromisoformat(str(ts).split('.')[0])
312
+ if t >= week_ago:
313
+ active_devs.add(e.get('dev'))
314
+ except (ValueError, TypeError):
315
+ pass
316
+ except Exception:
317
+ continue
318
+ # U4
319
+ if prds_gen:
320
+ vrate = 100.0 * violations / prds_gen
321
+ scored.append(1.0 if vrate <= 10 else (0.5 if vrate <= 25 else 0.0))
322
+ # U6
323
+ n_devs = len(active_devs)
324
+ scored.append(1.0 if n_devs >= 2 else (0.5 if n_devs == 1 else 0.0))
325
+
326
+ if not scored:
327
+ return 2.5 # 无数据, 中性
328
+ # 达标率 → 0-5 (1.0全绿=5, 全红=0)
329
+ return round(5.0 * sum(scored) / len(scored), 2)
330
+
331
+
332
+ # ============================================================
333
+ # 健康分 (综合)
334
+ # ============================================================
335
+
336
+ def compute_health(tasks):
337
+ """加权健康分 (满分 5)。
338
+
339
+ 维度: EVA 合格率 / 索引新鲜度 / 按时交付 / 团队同步 / 流水线 / 学习
340
+ """
341
+ scores = {}
342
+
343
+ # 1. EVA 合格率 (25%)
344
+ # eval-history 是 jsonl (每行一个 JSON), 不能用 safe_read_json
345
+ eval_path = EVAL_HISTORY_PATH
346
+ if eval_path.is_file():
347
+ records = []
348
+ try:
349
+ with open(eval_path, encoding='utf-8') as f:
350
+ for line in f:
351
+ line = line.strip()
352
+ if line:
353
+ records.append(json.loads(line))
354
+ recent = records[-10:]
355
+ if recent:
356
+ passed = sum(1 for r in recent if r.get('passed'))
357
+ scores['eva'] = (passed / len(recent)) * 5
358
+ else:
359
+ scores['eva'] = 3.0 # 无数据, 中性
360
+ except Exception:
361
+ scores['eva'] = 3.0
362
+ else:
363
+ scores['eva'] = 3.0
364
+
365
+ # 2. 索引新鲜度 (20%)
366
+ meta = safe_read_json(INDEX_DIR / '.index-meta.json', default={}) or {}
367
+ last_sync = meta.get('last_sync', '')
368
+ try:
369
+ ts = str(last_sync).replace('T', ' ').split('.')[0].strip()
370
+ sync_dt = datetime.strptime(ts, '%Y-%m-%d %H:%M')
371
+ age_days = (datetime.now() - sync_dt).days
372
+ if age_days <= 7:
373
+ scores['index'] = 5.0
374
+ elif age_days <= 14:
375
+ scores['index'] = 3.0
376
+ else:
377
+ scores['index'] = 1.0
378
+ except Exception:
379
+ scores['index'] = 2.0
380
+
381
+ # 3. 按时交付 (20%) - deadlines 算逾期率
382
+ deadlines = compute_deadlines(tasks, days=0)
383
+ total_with_due = len(deadlines['overdue']) + len(deadlines['due_soon']) + 1
384
+ overdue_rate = len(deadlines['overdue']) / total_with_due if total_with_due else 0
385
+ scores['on_time'] = max(0, 5 - overdue_rate * 10)
386
+
387
+ # 4. 团队同步 (15%) - 简化: 有 ahead 提交 = 有未同步
388
+ scores['sync'] = 4.0 # 默认良好 (详细检查由 team_sync status 做)
389
+
390
+ # 5. 流水线流转 (10%) - 任务在各阶段分布
391
+ statuses = {}
392
+ for data in tasks.values():
393
+ s = data.get('status', '?')
394
+ statuses[s] = statuses.get(s, 0) + 1
395
+ # 有 in_progress 且不全卡 planning = 健康
396
+ if statuses.get('in_progress', 0) > 0:
397
+ scores['pipeline'] = 4.0
398
+ elif statuses.get('planning', 0) > 0:
399
+ scores['pipeline'] = 3.0
400
+ else:
401
+ scores['pipeline'] = 2.0
402
+
403
+ # 6. 学习 (5%)
404
+ # bug: 旧代码读 data/learning/feedback.jsonl (旧路径, 已废弃),
405
+ # learn.py 实际写 workspace/members/{dev}/journal/feedback.jsonl。
406
+ # 迁移了写路径没迁移读路径 → 学习度恒为 2.0。改为扫全部 dev 的 journal。
407
+ fb_count = _count_all_feedback()
408
+ scores['learning'] = min(5.0, 2.0 + fb_count * 0.1) if fb_count else 2.0
409
+
410
+ # 7. 工作流采纳 (10%) — 与 EVA/学习并列, 回答"用不用得起来"
411
+ # 复用 usability_score.py 的采纳分逻辑 (不 import 验证脚本, 内联等价计算避免环依赖)
412
+ scores['adoption'] = _compute_adoption_score()
413
+
414
+ # 加权
415
+ weights = {
416
+ 'eva': 0.25, 'index': 0.20, 'on_time': 0.20,
417
+ 'sync': 0.15, 'pipeline': 0.05, 'learning': 0.05, 'adoption': 0.10,
418
+ }
419
+ total = sum(scores[k] * weights[k] for k in weights)
420
+ return {
421
+ 'total': round(total, 2),
422
+ 'scores': scores,
423
+ 'weights': weights,
424
+ 'verdict': '健康' if total >= 4 else ('有风险' if total >= 3 else '需关注'),
425
+ }
426
+
427
+
428
+ # ============================================================
429
+ # 流水线自检 (测工具本身, 不测产出) —— 可观测性闭环
430
+ # ============================================================
431
+
432
+ def _scan_skill_instrumentation():
433
+ """扫 .qoder/skills/*/SKILL.md, 统计有多少 skill 调了 learn.py record (已埋点)。
434
+ 返回 (已埋点skill列表, 全部skill列表)。
435
+ """
436
+ skills_dir = BASE / '.qoder' / 'skills'
437
+ if not skills_dir.is_dir():
438
+ return [], []
439
+ all_skills = []
440
+ instrumented = []
441
+ for d in sorted(skills_dir.iterdir()):
442
+ if not d.is_dir():
443
+ continue
444
+ skill_md = d / 'SKILL.md'
445
+ if not skill_md.is_file():
446
+ continue
447
+ all_skills.append(d.name)
448
+ try:
449
+ text = skill_md.read_text(encoding='utf-8', errors='ignore')
450
+ except Exception:
451
+ continue
452
+ if 'learn.py' in text and 'record' in text:
453
+ instrumented.append(d.name)
454
+ return instrumented, all_skills
455
+
456
+
457
+ def _analyze_eva_rounds():
458
+ """从 eval-history.jsonl 分析 EVA 修正轮数。
459
+ 返回 {samples, rework_count, avg_rounds}。
460
+ rework = 连续 passed=false 后出现 passed=true 的序列。
461
+ """
462
+ eval_path = EVAL_HISTORY_PATH
463
+ if not eval_path.is_file():
464
+ return {'samples': 0, 'rework_count': 0, 'avg_rounds': 0}
465
+ records = []
466
+ try:
467
+ with open(eval_path, encoding='utf-8') as f:
468
+ for line in f:
469
+ line = line.strip()
470
+ if line:
471
+ try:
472
+ records.append(json.loads(line))
473
+ except Exception:
474
+ continue
475
+ except Exception:
476
+ return {'samples': 0, 'rework_count': 0, 'avg_rounds': 0}
477
+
478
+ recent = records[-20:]
479
+ rework = 0
480
+ total_fail_before_pass = 0
481
+ fails_in_run = 0
482
+ for r in recent:
483
+ if r.get('passed'):
484
+ if fails_in_run > 0:
485
+ rework += 1
486
+ total_fail_before_pass += fails_in_run
487
+ fails_in_run = 0
488
+ else:
489
+ fails_in_run += 1
490
+ avg_rounds = (total_fail_before_pass / rework + 1) if rework else 0
491
+ return {
492
+ 'samples': len(recent),
493
+ 'rework_count': rework,
494
+ 'avg_rounds': round(avg_rounds, 1),
495
+ }
496
+
497
+
498
+ def _count_rule_violations():
499
+ """统计规则违规事件 (没问平台就调脚本)。
500
+ 扫全部 dev 的 journal 里 event=rule_violation 的条数。
501
+ """
502
+ if not MEMBERS_DIR.is_dir():
503
+ return 0
504
+ count = 0
505
+ for dev_dir in MEMBERS_DIR.iterdir():
506
+ if not dev_dir.is_dir():
507
+ continue
508
+ fb = dev_dir / 'journal' / 'feedback.jsonl'
509
+ if not fb.is_file():
510
+ continue
511
+ try:
512
+ with open(fb, encoding='utf-8') as f:
513
+ for line in f:
514
+ line = line.strip()
515
+ if not line:
516
+ continue
517
+ try:
518
+ if json.loads(line).get('event') == 'rule_violation':
519
+ count += 1
520
+ except Exception:
521
+ continue
522
+ except Exception:
523
+ continue
524
+ return count
525
+
526
+
527
+ def compute_pipeline_self():
528
+ """流水线自检 —— 4 个指标全从已有数据派生, 不增加 PM 操作。
529
+
530
+ 返回:
531
+ instrumentation: {done, total, missing} 埋点覆盖率
532
+ eva: {samples, rework_count, avg_rounds} EVA 修正轮数
533
+ violations: int 规则违规次数
534
+ verdict: str 一句话结论
535
+ """
536
+ done, total = _scan_skill_instrumentation()
537
+ missing = [s for s in total if s not in done]
538
+ eva = _analyze_eva_rounds()
539
+ violations = _count_rule_violations()
540
+
541
+ parts = []
542
+ cov_pct = (len(done) / len(total) * 100) if total else 0
543
+ if cov_pct < 50:
544
+ parts.append('埋点覆盖偏低')
545
+ if eva['rework_count'] > 0 and eva['avg_rounds'] > 2:
546
+ parts.append('EVA 修正轮数偏高')
547
+ if violations > 0:
548
+ parts.append('存在规则违规')
549
+ verdict = ';'.join(parts) if parts else '正常'
550
+
551
+ return {
552
+ 'instrumentation': {'done': len(done), 'total': len(total),
553
+ 'missing': missing, 'pct': round(cov_pct)},
554
+ 'eva': eva,
555
+ 'violations': violations,
556
+ 'verdict': verdict,
557
+ }
558
+
559
+
560
+ # ============================================================
561
+ # 角色引导 (该角色该用哪些命令 —— 软引导, 帮新人快速上手)
562
+ # ============================================================
563
+
564
+ def compute_role_guide():
565
+ """读当前角色 + config.yaml 的 commands 映射, 返回该角色的建议命令。
566
+ 软引导: 不拦截任何命令, 只是把"你该用什么"摆出来。
567
+ """
568
+ # 当前角色 (v3.0: 角色能力已并入 common/identity.py)
569
+ role = None
570
+ try:
571
+ sys.path.insert(0, str(BASE / '.qoder' / 'scripts'))
572
+ from foundation.identity.identity import get_role
573
+ role = get_role()
574
+ except Exception:
575
+ pass
576
+ if not role:
577
+ role = 'pm' # 默认
578
+
579
+ # config.yaml 的 commands 映射
580
+ commands = []
581
+ role_name = role
582
+ try:
583
+ import yaml
584
+ cfg_path = BASE / '.qoder' / 'config.yaml'
585
+ if cfg_path.is_file():
586
+ with open(cfg_path, encoding='utf-8') as f:
587
+ cfg = yaml.safe_load(f) or {}
588
+ roles = cfg.get('roles', {})
589
+ rinfo = roles.get(role, {})
590
+ commands = rinfo.get('commands', [])
591
+ role_name = rinfo.get('name', role)
592
+ except Exception:
593
+ pass
594
+
595
+ # 校验建议命令是否真实存在 (command 文件在不在)
596
+ available = []
597
+ missing = []
598
+ cmds_dir = BASE / '.qoder' / 'commands'
599
+ for cmd in commands:
600
+ exists = (cmds_dir / (cmd + '.md')).is_file() or \
601
+ (cmds_dir / 'optional' / (cmd + '.md')).is_file()
602
+ (available if exists else missing).append(cmd)
603
+
604
+ return {
605
+ 'role': role,
606
+ 'role_name': role_name,
607
+ 'suggested': commands,
608
+ 'available': available,
609
+ 'missing': missing,
610
+ }
611
+
612
+
613
+ # ============================================================
614
+ # 渲染
615
+ # ============================================================
616
+
617
+ def render_full():
618
+ tasks = load_all_tasks()
619
+ print('=' * 50)
620
+ print('项目状态总览')
621
+ print('=' * 50)
622
+ print(f'\n活跃任务: {len(tasks)} 个')
623
+ dev = get_developer(BASE)
624
+ if dev:
625
+ my_tasks = [n for n, d in tasks.items() if dev in (d.get('assignee'), d.get('creator'))]
626
+ print(f'我的任务: {len(my_tasks)} 个 (开发者: {dev})')
627
+
628
+ # 角色引导 (该角色该用哪些命令)
629
+ rg = compute_role_guide()
630
+ print(f'当前角色: {rg["role_name"]} ({rg["role"]})')
631
+ if rg['suggested']:
632
+ # 标注哪些命令当前可用
633
+ avail = rg['available']
634
+ print(f' 建议命令 ({len(avail)}/{len(rg["suggested"])} 可用): '
635
+ + ' '.join('/' + c for c in avail))
636
+ if rg['missing']:
637
+ print(f' ⚠ 建议但未安装: {", ".join(rg["missing"])}')
638
+
639
+ # 健康分
640
+ print('\n--- 健康度 ---')
641
+ health = compute_health(tasks)
642
+ print(f"总分: {health['total']}/5 ({health['verdict']})")
643
+ for k, v in health['scores'].items():
644
+ w = health['weights'][k]
645
+ print(f" {k:12} {v:.1f} × {w:.0%}")
646
+
647
+ # 阻塞
648
+ print('\n--- 阻塞图 ---')
649
+ bg = compute_blocked_graph(tasks)
650
+ if bg['blocked_tasks']:
651
+ print(f"被阻塞的任务: {len(bg['blocked_tasks'])} 个")
652
+ for bt in bg['blocked_tasks'][:10]:
653
+ print(f" {bt['name']} ({bt['assignee']}) <- {', '.join(bt['blocked_by'])}")
654
+ if bg['blocking_count']:
655
+ print(f"阻塞最多的: {bg['blocking_count']}")
656
+ else:
657
+ print('无被阻塞的任务')
658
+
659
+ # 截止日期
660
+ print('\n--- 截止日期 (未来 7 天) ---')
661
+ dl = compute_deadlines(tasks, days=7)
662
+ if dl['overdue']:
663
+ print(f"⚠️ 已逾期: {len(dl['overdue'])} 个")
664
+ for item in dl['overdue'][:5]:
665
+ print(f" {item['name']} ({item['days_left']}天前) [{item['priority']}] {item['title']}")
666
+ if dl['due_soon']:
667
+ print(f"即将到期: {len(dl['due_soon'])} 个")
668
+ for item in dl['due_soon'][:5]:
669
+ print(f" {item['name']} (还剩{item['days_left']}天) [{item['priority']}] {item['title']}")
670
+ if not dl['overdue'] and not dl['due_soon']:
671
+ print('未来 7 天无截止任务')
672
+ print(f"无截止日期的活跃任务: {dl['no_date_count']} 个")
673
+
674
+ # 周期时间
675
+ print('\n--- 周期时间 ---')
676
+ ct = compute_cycle_times(tasks)
677
+ if ct['samples'] > 0:
678
+ print(f"已完成样本: {ct['samples']} 个")
679
+ print(f"平均总周期: {ct['avg_total_hours']} 小时 ({ct['avg_total_hours']/24:.1f} 天)")
680
+ print(f"平均开发时长: {ct['avg_dev_hours']} 小时")
681
+ if ct['by_task']:
682
+ print("最慢的 5 个:")
683
+ for s in ct['by_task'][:5]:
684
+ print(f" {s['name']}: {s['total_hours']}h (开发 {s['dev_hours']}h)")
685
+ else:
686
+ print('暂无已完成任务的时间戳数据 (stage_ts 在 B3 后才有)')
687
+
688
+ # 流水线自检 (测工具本身)
689
+ print('\n--- 流水线自检 (测工具本身) ---')
690
+ ps = compute_pipeline_self()
691
+ inst = ps['instrumentation']
692
+ cov_mark = '✓' if inst['pct'] >= 50 else '⚠'
693
+ print(f" 埋点覆盖: {inst['done']}/{inst['total']} skill ({inst['pct']}%) {cov_mark}")
694
+ if inst['missing']:
695
+ # 只列前 5 个, 避免刷屏
696
+ shown = inst['missing'][:5]
697
+ more = '' if len(inst['missing']) <= 5 else f" 等{len(inst['missing'])}个"
698
+ print(f" 缺埋点: {', '.join(shown)}{more}")
699
+ eva = ps['eva']
700
+ if eva['samples'] > 0:
701
+ eva_mark = '✓' if eva['avg_rounds'] <= 2 else '⚠'
702
+ print(f" EVA 修正: 近{eva['samples']}次 {eva['rework_count']}次打回, "
703
+ f"平均{eva['avg_rounds']}轮过 {eva_mark}")
704
+ else:
705
+ print(' EVA 修正: 暂无评估历史')
706
+ v_mark = '✓' if ps['violations'] == 0 else '⚠'
707
+ print(f" 规则违规: {ps['violations']} 次 {v_mark}")
708
+ print(f" 自检结论: {ps['verdict']}")
709
+
710
+
711
+ def main():
712
+ parser = argparse.ArgumentParser(description='项目状态计算')
713
+ parser.add_argument('scope', nargs='?', default='all',
714
+ choices=['all', 'health', 'cycle', 'deadlines', 'blocked'])
715
+ parser.add_argument('--days', type=int, default=7, help='截止日期 horizon 天数')
716
+ args = parser.parse_args()
717
+
718
+ tasks = load_all_tasks()
719
+
720
+ if args.scope == 'health':
721
+ h = compute_health(tasks)
722
+ print(json.dumps(h, indent=2, ensure_ascii=False))
723
+ elif args.scope == 'cycle':
724
+ c = compute_cycle_times(tasks)
725
+ print(json.dumps(c, indent=2, ensure_ascii=False))
726
+ elif args.scope == 'deadlines':
727
+ d = compute_deadlines(tasks, args.days)
728
+ print(json.dumps(d, indent=2, ensure_ascii=False))
729
+ elif args.scope == 'blocked':
730
+ b = compute_blocked_graph(tasks)
731
+ print(json.dumps(b, indent=2, ensure_ascii=False))
732
+ else:
733
+ render_full()
734
+
735
+
736
+ if __name__ == '__main__':
737
+ main()