@hupan56/wlkj 3.0.0 → 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.
- package/bin/cli.js +1435 -1373
- package/package.json +1 -1
- package/templates/qoder/commands/optional/wl-insight.md +6 -4
- package/templates/qoder/commands/optional/wl-report.md +20 -39
- package/templates/qoder/commands/optional/wl-status.md +4 -4
- package/templates/qoder/commands/wl-code.md +2 -2
- package/templates/qoder/commands/wl-design.md +5 -5
- package/templates/qoder/commands/wl-init.md +3 -3
- package/templates/qoder/commands/wl-prd.md +26 -13
- package/templates/qoder/commands/wl-req.md +3 -3
- package/templates/qoder/commands/wl-search.md +78 -47
- package/templates/qoder/commands/wl-task.md +343 -40
- package/templates/qoder/commands/wl-test.md +10 -10
- package/templates/qoder/config.yaml +35 -13
- package/templates/qoder/hooks/post-tool-use.py +181 -0
- package/templates/qoder/hooks/session-start.py +82 -1
- package/templates/qoder/hooks/stop-eval.py +207 -0
- package/templates/qoder/hooks/user-prompt-submit.py +139 -0
- package/templates/qoder/rules/wl-pipeline.md +70 -8
- package/templates/qoder/scripts/README.md +78 -115
- package/templates/qoder/scripts/capability/__init__.py +26 -0
- package/templates/qoder/scripts/capability/__main__.py +72 -0
- package/templates/qoder/scripts/capability/adapters/__init__.py +29 -0
- package/templates/qoder/scripts/capability/adapters/cli.py +316 -0
- package/templates/qoder/scripts/capability/adapters/mcp.py +334 -0
- package/templates/qoder/scripts/capability/adapters/qw.py +271 -0
- package/templates/qoder/scripts/capability/caps/__init__.py +27 -0
- package/templates/qoder/scripts/capability/caps/context.py +36 -0
- package/templates/qoder/scripts/capability/caps/cron.py +50 -0
- package/templates/qoder/scripts/capability/caps/identity.py +43 -0
- package/templates/qoder/scripts/capability/caps/memory.py +71 -0
- package/templates/qoder/scripts/capability/caps/notify.py +41 -0
- package/templates/qoder/scripts/capability/caps/present.py +48 -0
- package/templates/qoder/scripts/capability/caps/repo.py +31 -0
- package/templates/qoder/scripts/capability/caps/sandbox.py +43 -0
- package/templates/qoder/scripts/capability/chain.py +92 -0
- package/templates/qoder/scripts/capability/memory_chain.py +41 -0
- package/templates/qoder/scripts/capability/registry.py +246 -0
- package/templates/qoder/scripts/capability/registry_mcp.py +250 -0
- package/templates/qoder/scripts/capability/smoke_test.py +211 -0
- package/templates/qoder/scripts/capability/smoke_test_report.json +94 -0
- package/templates/qoder/scripts/{report → deployment/setup}/__init__.py +11 -11
- package/templates/qoder/scripts/{setup → deployment/setup}/carriers.py +33 -26
- package/templates/qoder/scripts/deployment/setup/carriers_verify.py +148 -0
- package/templates/qoder/scripts/{setup → deployment/setup}/init_doctor.py +40 -16
- package/templates/qoder/scripts/{setup → deployment/setup}/install_qoderwork.py +20 -12
- package/templates/qoder/scripts/{setup → deployment/setup}/platform_doctor.py +8 -2
- package/templates/qoder/scripts/{setup → deployment/setup}/repo_root.py +113 -112
- package/templates/qoder/scripts/{setup → deployment/setup}/setup.py +31 -11
- package/templates/qoder/scripts/{setup → deployment/setup}/setup_lanhu.py +13 -7
- package/templates/qoder/scripts/domain/__init__.py +0 -0
- package/templates/qoder/scripts/domain/design/__init__.py +0 -0
- package/templates/qoder/scripts/{report → domain/design}/fill_prototype.py +12 -6
- package/templates/qoder/scripts/{report → domain/design}/gen_design_doc.py +8 -2
- package/templates/qoder/scripts/{setup → domain/kg}/__init__.py +11 -11
- package/templates/qoder/scripts/domain/kg/build/__init__.py +0 -0
- package/templates/qoder/scripts/{kg → domain/kg/build}/build_entity_registry.py +7 -2
- package/templates/qoder/scripts/{kg → domain/kg/build}/build_relations.py +7 -2
- package/templates/qoder/scripts/{kg → domain/kg/build}/build_style_index.py +13 -7
- package/templates/qoder/scripts/{kg → domain/kg/build}/build_workflows.py +7 -2
- package/templates/qoder/scripts/{kg → domain/kg/build}/kg_build.py +76 -9
- package/templates/qoder/scripts/{kg → domain/kg/build}/kg_build_db.py +7 -2
- package/templates/qoder/scripts/{kg → domain/kg/build}/kg_incremental.py +11 -6
- package/templates/qoder/scripts/{report → domain/kg/build}/learn_aggregate.py +9 -4
- package/templates/qoder/scripts/domain/kg/extract/__init__.py +0 -0
- package/templates/qoder/scripts/{common → domain/kg/extract}/extract.py +430 -419
- package/templates/qoder/scripts/{common → domain/kg/extract}/test_extract.py +115 -115
- package/templates/qoder/scripts/{common → domain/kg/extract}/ts_extract.py +614 -536
- package/templates/qoder/scripts/domain/kg/graph/__init__.py +0 -0
- package/templates/qoder/scripts/{kg → domain/kg/graph}/kg_link_db.py +7 -2
- package/templates/qoder/scripts/domain/kg/graph/kg_semantic.py +683 -0
- package/templates/qoder/scripts/{kg → domain/kg}/kg.py +263 -30
- package/templates/qoder/scripts/{common → domain/kg}/kg_capabilities.py +182 -182
- package/templates/qoder/scripts/domain/kg/search/__init__.py +0 -0
- package/templates/qoder/scripts/{kg → domain/kg/search}/context_pack.py +30 -9
- package/templates/qoder/scripts/{kg → domain/kg/search}/enrich_prompt.py +10 -4
- package/templates/qoder/scripts/{kg → domain/kg/search}/prefetch.py +33 -8
- package/templates/qoder/scripts/{common → domain/kg/search}/search_engine.py +207 -205
- package/templates/qoder/scripts/{kg → domain/kg/search}/search_index.py +143 -77
- package/templates/qoder/scripts/domain/kg/server/__init__.py +0 -0
- package/templates/qoder/scripts/domain/kg/server/kgd.py +549 -0
- package/templates/qoder/scripts/domain/kg/server/perf_bench.py +197 -0
- package/templates/qoder/scripts/domain/kg/storage/__init__.py +0 -0
- package/templates/qoder/scripts/{kg → domain/kg/storage}/kg_duckdb.py +38 -14
- package/templates/qoder/scripts/domain/learning/__init__.py +0 -0
- package/templates/qoder/scripts/{report → domain/learning}/learn.py +8 -3
- package/templates/qoder/scripts/{mcp → domain/report}/__init__.py +11 -11
- package/templates/qoder/scripts/{report → domain/report}/add_session.py +9 -3
- package/templates/qoder/scripts/{report → domain/report}/export.py +10 -5
- package/templates/qoder/scripts/{report → domain/report}/report.py +9 -4
- package/templates/qoder/scripts/domain/report/report_snapshot.py +261 -0
- package/templates/qoder/scripts/{report → domain/report}/role.py +10 -4
- package/templates/qoder/scripts/{report → domain/report}/status.py +112 -9
- package/templates/qoder/scripts/domain/report/status_snapshot.py +191 -0
- package/templates/qoder/scripts/domain/requirement/__init__.py +0 -0
- package/templates/qoder/scripts/{report → domain/requirement}/archive_prd.py +8 -2
- package/templates/qoder/scripts/{report → domain/requirement}/req.py +228 -222
- package/templates/qoder/scripts/{kg → domain/task}/__init__.py +11 -11
- package/templates/qoder/scripts/{task → domain/task}/git_sync.py +40 -21
- package/templates/qoder/scripts/{task → domain/task}/syncgate.py +13 -8
- package/templates/qoder/scripts/{task → domain/task}/task.py +13 -5
- package/templates/qoder/scripts/{task → domain/task}/task_lifecycle.py +606 -596
- package/templates/qoder/scripts/{task → domain/task}/task_query.py +162 -161
- package/templates/qoder/scripts/{task → domain/task}/task_relations.py +425 -424
- package/templates/qoder/scripts/{task → domain/task}/team_sync.py +9 -4
- package/templates/qoder/scripts/domain/task/zentao_sync.py +370 -0
- package/templates/qoder/scripts/foundation/__init__.py +0 -0
- package/templates/qoder/scripts/{common → foundation}/bootstrap.py +10 -10
- package/templates/qoder/scripts/foundation/core/__init__.py +0 -0
- package/templates/qoder/scripts/{common → foundation/core}/cmd_registry.py +1 -1
- package/templates/qoder/scripts/{common → foundation/core}/config.py +1 -1
- package/templates/qoder/scripts/{common → foundation/core}/paths.py +706 -586
- package/templates/qoder/scripts/foundation/data/__init__.py +0 -0
- package/templates/qoder/scripts/{common → foundation/data}/contract.py +2 -2
- package/templates/qoder/scripts/{common → foundation/data}/events.py +2 -2
- package/templates/qoder/scripts/{common → foundation/data}/result.py +1 -1
- package/templates/qoder/scripts/{common → foundation/data}/task_utils.py +427 -404
- package/templates/qoder/scripts/foundation/identity/__init__.py +0 -0
- package/templates/qoder/scripts/{common → foundation/identity}/check_publish.py +103 -98
- package/templates/qoder/scripts/{common → foundation/identity}/developer.py +239 -239
- package/templates/qoder/scripts/{common → foundation/identity}/guard.py +2 -2
- package/templates/qoder/scripts/{common → foundation/identity}/identity.py +14 -10
- package/templates/qoder/scripts/{common → foundation/identity}/roles.py +1 -1
- package/templates/qoder/scripts/foundation/integrations/__init__.py +0 -0
- package/templates/qoder/scripts/{common → foundation/integrations}/active_task.py +30 -15
- package/templates/qoder/scripts/{common → foundation/integrations}/feishu.py +2 -2
- package/templates/qoder/scripts/{common → foundation/integrations}/terms.py +3 -3
- package/templates/qoder/scripts/foundation/integrations/zentao_client.py +220 -0
- package/templates/qoder/scripts/foundation/io/__init__.py +0 -0
- package/templates/qoder/scripts/{common → foundation/io}/filelock.py +1 -1
- package/templates/qoder/scripts/{common → foundation/io}/reqid.py +1 -1
- package/templates/qoder/scripts/foundation/protocol/__init__.py +0 -0
- package/templates/qoder/scripts/foundation/protocol/duckdb_conn.py +39 -0
- package/templates/qoder/scripts/{common → foundation/protocol}/mcp_base.py +1 -1
- package/templates/qoder/scripts/foundation/utils/__init__.py +0 -0
- package/templates/qoder/scripts/{common → foundation/utils}/eval_api.py +12 -1
- package/templates/qoder/scripts/{common → foundation/utils}/pip_install.py +23 -6
- package/templates/qoder/scripts/{common → foundation/utils}/platform_guard.py +1 -1
- package/templates/qoder/scripts/orchestration/__init__.py +0 -0
- package/templates/qoder/scripts/orchestration/wlkj.py +185 -0
- package/templates/qoder/scripts/protocol/__init__.py +0 -0
- package/templates/qoder/scripts/protocol/browser/README.md +207 -0
- package/templates/qoder/scripts/protocol/browser/SKILL.md +265 -0
- package/templates/qoder/scripts/protocol/browser/config.env +9 -0
- package/templates/qoder/scripts/protocol/browser/references/cdp-api.md +110 -0
- package/templates/qoder/scripts/protocol/browser/references/migration-2.5.3.md +72 -0
- package/templates/qoder/scripts/protocol/browser/references/site-patterns/.gitkeep +0 -0
- package/templates/qoder/scripts/protocol/browser/scripts/browser-discovery.mjs +138 -0
- package/templates/qoder/scripts/protocol/browser/scripts/cdp-proxy.mjs +672 -0
- package/templates/qoder/scripts/protocol/browser/scripts/check-deps.mjs +206 -0
- package/templates/qoder/scripts/protocol/browser/scripts/find-url.mjs +253 -0
- package/templates/qoder/scripts/protocol/browser/scripts/match-site.mjs +46 -0
- package/templates/qoder/scripts/protocol/browser/templates/config.env.template +9 -0
- package/templates/qoder/scripts/protocol/mcp/__init__.py +11 -0
- package/templates/qoder/scripts/protocol/mcp/kg_mcp_server.py +478 -0
- package/templates/qoder/scripts/{mcp → protocol/mcp}/lanhu_stdio_wrapper.py +11 -5
- package/templates/qoder/scripts/{mcp → protocol/mcp}/mcp_doctor.py +47 -14
- package/templates/qoder/scripts/protocol/mcp/mcp_launcher.py +600 -0
- package/templates/qoder/scripts/{mcp → protocol/mcp}/mysql_mcp_server.py +181 -67
- package/templates/qoder/scripts/protocol/mcp/zentao_mcp_server.py +1920 -0
- package/templates/qoder/scripts/protocol/transports/__init__.py +56 -0
- package/templates/qoder/scripts/protocol/transports/base.py +87 -0
- package/templates/qoder/scripts/protocol/transports/cli.py +132 -0
- package/templates/qoder/scripts/protocol/transports/http.py +141 -0
- package/templates/qoder/scripts/protocol/transports/stdio.py +293 -0
- package/templates/qoder/scripts/run_weekly_update.bat +27 -18
- package/templates/qoder/scripts/run_weekly_update.sh +22 -13
- package/templates/qoder/scripts/validation/__init__.py +0 -0
- package/templates/qoder/scripts/validation/eval/__init__.py +0 -0
- package/templates/qoder/scripts/validation/eval/perf-report-2026-06-24.md +139 -0
- package/templates/qoder/scripts/validation/eval/qwork_harness.py +617 -0
- package/templates/qoder/scripts/validation/eval/report-commands.md +220 -0
- package/templates/qoder/scripts/validation/eval/transcript_timing.py +386 -0
- package/templates/qoder/scripts/validation/metrics/__init__.py +0 -0
- package/templates/qoder/scripts/{report → validation/metrics}/eval_prd.py +17 -10
- package/templates/qoder/scripts/validation/metrics/usability_score.py +750 -0
- package/templates/qoder/scripts/validation/test/__init__.py +11 -0
- package/templates/qoder/scripts/validation/test/assertion_gen.py +551 -0
- package/templates/qoder/scripts/{test → validation/test}/autotest.py +64 -4
- package/templates/qoder/scripts/{common → validation/test}/autotest_auth.py +1 -1
- package/templates/qoder/scripts/{test → validation/test}/autotest_batch.py +16 -3
- package/templates/qoder/scripts/{test → validation/test}/autotest_data.py +17 -12
- package/templates/qoder/scripts/validation/test/autotest_opencli.py +646 -0
- package/templates/qoder/scripts/{test → validation/test}/autotest_run.py +17 -3
- package/templates/qoder/scripts/validation/test/autotest_webaccess.py +571 -0
- package/templates/qoder/scripts/{test → validation/test}/benchmark.py +12 -6
- package/templates/qoder/scripts/{test → validation/test}/kg_auto_login.py +8 -2
- package/templates/qoder/scripts/{test → validation/test}/kg_test_runner.py +7 -2
- package/templates/qoder/scripts/{test → validation/test}/page_probe.py +8 -3
- package/templates/qoder/scripts/validation/test/smoke_all_commands.py +170 -0
- package/templates/qoder/settings.json +30 -0
- package/templates/qoder/skills/design-import/SKILL.md +22 -13
- package/templates/qoder/skills/design-review/SKILL.md +19 -10
- package/templates/qoder/skills/prd-generator/SKILL.md +22 -18
- package/templates/qoder/skills/prd-review/SKILL.md +6 -4
- package/templates/qoder/skills/prototype-generator/SKILL.md +63 -14
- package/templates/qoder/skills/spec-coder/SKILL.md +22 -13
- package/templates/qoder/skills/spec-generator/SKILL.md +22 -10
- package/templates/qoder/skills/test-generator/SKILL.md +7 -5
- package/templates/qoder/skills/wl-code/SKILL.md +23 -11
- package/templates/qoder/skills/wl-commit/SKILL.md +9 -8
- package/templates/qoder/skills/wl-design/SKILL.md +5 -3
- package/templates/qoder/skills/wl-init/SKILL.md +6 -6
- package/templates/qoder/skills/wl-insight/SKILL.md +23 -10
- package/templates/qoder/skills/wl-prd-full/SKILL.md +57 -6
- package/templates/qoder/skills/wl-prd-quick/SKILL.md +3 -3
- package/templates/qoder/skills/wl-prd-review/SKILL.md +16 -4
- package/templates/qoder/skills/wl-report/SKILL.md +25 -23
- package/templates/qoder/skills/wl-search/SKILL.md +149 -78
- package/templates/qoder/skills/wl-spec/SKILL.md +7 -5
- package/templates/qoder/skills/wl-status/SKILL.md +48 -16
- package/templates/qoder/skills/wl-task/SKILL.md +92 -10
- package/templates/qoder/skills/wl-test/SKILL.md +75 -29
- package/templates/qoder/templates/prd-full-template.md +7 -0
- package/templates/root/AGENTS.md +275 -236
- package/templates/root/requirements.txt +3 -0
- package/templates/root//344/275/277/347/224/250/350/257/264/346/230/216.md +3 -3
- package/templates/root//346/226/260/346/211/213/346/214/207/345/215/227.md +2 -2
- package/templates/.qoder/.runtime/ctx-cache-5660152f1d6dd819.md +0 -23
- package/templates/.qoder/.runtime/ctx-cache-afdce0dac06b25b0.md +0 -23
- package/templates/.qoder/.runtime/search-cache-eae7644e7b122f35.txt +0 -1
- package/templates/.qoder/learning/eval-history.jsonl +0 -28
- package/templates/data/index/wiki-index.json +0 -8
- package/templates/qoder/nul +0 -4
- package/templates/qoder/scripts/kg/extract.py +0 -398
- package/templates/qoder/scripts/kg/kg_semantic.py +0 -156
- package/templates/qoder/scripts/mcp/kg_mcp_server.py +0 -606
- package/templates/qoder/scripts/mcp/mcp_launcher.py +0 -442
- package/templates/qoder/scripts/mcp/zentao_mcp_server.py +0 -384
- package/templates/qoder/scripts/task/__init__.py +0 -11
- package/templates/qoder/scripts/test/__init__.py +0 -11
- package/templates/qoder/scripts/wlkj.py +0 -116
- /package/templates/qoder/scripts/{common → deployment}/__init__.py +0 -0
- /package/templates/qoder/scripts/{common → domain/kg/graph}/graph_traverse.py +0 -0
- /package/templates/qoder/scripts/{common → domain/kg/server}/repowiki.py +0 -0
- /package/templates/qoder/scripts/{common → foundation/io}/atomicio.py +0 -0
- /package/templates/qoder/scripts/{test → validation/test}/secure-ls.js +0 -0
|
@@ -11,7 +11,7 @@ roles:
|
|
|
11
11
|
pm:
|
|
12
12
|
name: 产品经理
|
|
13
13
|
permissions: [create_task, write_prd, approve_prd, review_spec]
|
|
14
|
-
commands: [wl-init, wl-insight, wl-prd
|
|
14
|
+
commands: [wl-init, wl-insight, wl-prd, wl-design, wl-task, wl-search, wl-status, wl-report]
|
|
15
15
|
can_produce: [prd, prototype]
|
|
16
16
|
can_consume: ["*"] # PM 需读代码做现状分析
|
|
17
17
|
design:
|
|
@@ -48,20 +48,42 @@ pipeline:
|
|
|
48
48
|
auto_spec_on_prd_push: true
|
|
49
49
|
req_id_start: 1
|
|
50
50
|
|
|
51
|
-
# ── MySQL 只读 MCP (
|
|
51
|
+
# ── MySQL 只读 MCP (团队共享, 多环境) ──
|
|
52
|
+
# AI 查数据库时会先问"查哪个环境",用这里的标签让用户选。
|
|
53
|
+
# 标签名自定义 (test/pre/prod 或 qa/staging...),对应 environments 下的 key。
|
|
54
|
+
# 没配 environments (旧扁平结构) 时兼容:当作只有 default_env 一个环境。
|
|
52
55
|
mysql:
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
default_env: test # AI 不指定 / 用户不选时的默认环境
|
|
57
|
+
environments:
|
|
58
|
+
test: # 测试环境
|
|
59
|
+
host: 10.54.6.9
|
|
60
|
+
port: 3306
|
|
61
|
+
user: fywl_ics_test
|
|
62
|
+
password: Fywl_ics_test56476
|
|
63
|
+
env_name: "QAS 测试环境" # 显示标签,会随查询结果返回提醒 AI 当前在哪个环境
|
|
64
|
+
pre: # 预生产环境 (按需填, 不用可整段删)
|
|
65
|
+
host: 10.54.6.10
|
|
66
|
+
port: 3306
|
|
67
|
+
user: fywl_ics_pre_ro
|
|
68
|
+
password: ""
|
|
69
|
+
env_name: "预生产环境"
|
|
70
|
+
prod: # 生产环境 (⚠️ 只读账号! 配了才能查, 查时 AI 会标红提醒)
|
|
71
|
+
host: fywl-mysql.rwlb.rds.aliyuncs.com
|
|
72
|
+
port: 3306
|
|
73
|
+
user: queryuser
|
|
74
|
+
password: "query2025_ics@infore"
|
|
75
|
+
env_name: "生产环境"
|
|
58
76
|
|
|
59
|
-
# ── 禅道 MCP (
|
|
60
|
-
#
|
|
77
|
+
# ── 禅道 MCP (自建内网) ──
|
|
78
|
+
# v3.1.4: 禅道是【每人一个账号】(非团队共享)。
|
|
79
|
+
# - url (服务地址): 团队共享, 留这里。
|
|
80
|
+
# - user/password (个人凭据): 不放这里 (进 git 会泄露)。
|
|
81
|
+
# 每人各自填 workspace/members/{我}/.private/secrets/zentao.env:
|
|
82
|
+
# ZENTAO_USER=<你的禅道账号>
|
|
83
|
+
# ZENTAO_PASSWORD=<你的禅道密码>
|
|
84
|
+
# - 账号名映射: member.json 的 zentao_account (进 git, 用于归属过滤)。
|
|
61
85
|
zentao:
|
|
62
86
|
url: http://10.89.2.91/zentao
|
|
63
|
-
user: hupan
|
|
64
|
-
password: Aa123456
|
|
65
87
|
|
|
66
88
|
# ── Platforms (知识图谱/搜索的平台映射, 换项目改这里即可) ──
|
|
67
89
|
# search_index.py --platform <key或alias> 会过滤到对应 project 目录
|
|
@@ -105,14 +127,14 @@ team_remote:
|
|
|
105
127
|
|
|
106
128
|
# ── Git Sync (Weekly) ──
|
|
107
129
|
# 每个项目在 data/code/ 下有自己的 git 仓库
|
|
108
|
-
# 运行: python .qoder/scripts/
|
|
130
|
+
# 运行: python .qoder/scripts/orchestration/wlkj.py git-sync
|
|
109
131
|
# 或设 cron: 每周五 18:00
|
|
110
132
|
#
|
|
111
133
|
# ★ 源码仓库配置 (重要) ★
|
|
112
134
|
# 把下面的 url 改成你团队的 git 地址 (Gitea/GitLab/GitHub 均可)。
|
|
113
135
|
# branch 改成你的主分支。
|
|
114
136
|
# 不知道填什么? 注释掉整个 projects 段, 手动把代码放到 data/code/<项目名>/ 即可。
|
|
115
|
-
# 填完后跑: python .qoder/scripts/
|
|
137
|
+
# 填完后跑: python .qoder/scripts/orchestration/wlkj.py doctor --fix 会自动克隆+建索引。
|
|
116
138
|
git_sync:
|
|
117
139
|
schedule: "0 18 * * 5"
|
|
118
140
|
projects:
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""post-tool-use.py - PostToolUse hook: 工具执行后自动埋点 (确定性, 治漏斗断链)
|
|
4
|
+
|
|
5
|
+
吸收宿主确定性 hook 能力: 把"埋点"从"靠 AI 手动调 learn record"(老忘 → F2 漏斗数据残缺)
|
|
6
|
+
升级为"宿主在工具执行后自动埋点"。AI 想忘都忘不了。
|
|
7
|
+
|
|
8
|
+
触发场景 (只埋这几类, 不滥用):
|
|
9
|
+
- Write/Edit 落到 **/prd*.md 或 **/PRD*.md → 记 prd_written
|
|
10
|
+
- Write/Edit/MultiEdit 落到 **/task.json → 记 task_created
|
|
11
|
+
- Bash 跑了 git commit / team_sync push → 记 commit_done (兜底, 主路径在 team_sync.py)
|
|
12
|
+
|
|
13
|
+
铁律:
|
|
14
|
+
- 永远 exit 0 (埋点绝不阻断工作流)
|
|
15
|
+
- 失败 try/except 静默 (hook 崩溃绝不阻塞会话)
|
|
16
|
+
- 幂等: 同一文件短时间内重复埋点靠 feedback.jsonl append 即可 (统计层去重)
|
|
17
|
+
|
|
18
|
+
阻断契约: exit 0 放行 (本 hook 不阻断)。
|
|
19
|
+
stdin 字段名官方未公开, 兼容 tool_name/tool_input 等多种写法。
|
|
20
|
+
"""
|
|
21
|
+
import json
|
|
22
|
+
import os
|
|
23
|
+
import re
|
|
24
|
+
import sys
|
|
25
|
+
from datetime import datetime
|
|
26
|
+
|
|
27
|
+
if sys.platform == 'win32':
|
|
28
|
+
try:
|
|
29
|
+
sys.stdout.reconfigure(encoding='utf-8')
|
|
30
|
+
sys.stderr.reconfigure(encoding='utf-8')
|
|
31
|
+
except Exception:
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
BASE = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
35
|
+
HOOK_ERRORS_LOG = os.path.join(BASE, '.qoder', '.runtime', 'hook-errors.log')
|
|
36
|
+
|
|
37
|
+
# 工具名: 宿主原生工具(大写) + 兼容别名
|
|
38
|
+
WRITE_TOOLS = {'write', 'edit', 'multiedit', 'str_replace_editor', 'create_file'}
|
|
39
|
+
COMMIT_RE = re.compile(r'git\s+commit|team_sync.*push|wlkj\.py\s+sync\s+push', re.IGNORECASE)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _read_tool_info():
|
|
43
|
+
"""从 stdin 读 {tool_name, tool_input}。兼容多种字段名。取不到返回 ({}, None)。"""
|
|
44
|
+
try:
|
|
45
|
+
if sys.stdin.isatty():
|
|
46
|
+
return {}, None
|
|
47
|
+
raw = sys.stdin.read().strip()
|
|
48
|
+
except Exception:
|
|
49
|
+
return {}, None
|
|
50
|
+
if not raw:
|
|
51
|
+
return {}, None
|
|
52
|
+
try:
|
|
53
|
+
data = json.loads(raw)
|
|
54
|
+
except (ValueError, TypeError):
|
|
55
|
+
return {}, None
|
|
56
|
+
if not isinstance(data, dict):
|
|
57
|
+
return {}, None
|
|
58
|
+
# tool_name 兼容
|
|
59
|
+
tool_name = None
|
|
60
|
+
for k in ('tool_name', 'name', 'tool'):
|
|
61
|
+
v = data.get(k)
|
|
62
|
+
if isinstance(v, str) and v.strip():
|
|
63
|
+
tool_name = v.strip()
|
|
64
|
+
break
|
|
65
|
+
# tool_input 兼容
|
|
66
|
+
tool_input = data.get('tool_input') or data.get('input') or data.get('args') or data.get('arguments')
|
|
67
|
+
if not isinstance(tool_input, dict):
|
|
68
|
+
tool_input = {}
|
|
69
|
+
return tool_input, tool_name
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _file_paths_from_input(tool_input, tool_name):
|
|
73
|
+
"""从 tool_input 提取涉及的文件路径列表。兼容 Write/Edit/Bash 的不同参数名。"""
|
|
74
|
+
paths = []
|
|
75
|
+
for k in ('file_path', 'path', 'filename', 'filePath'):
|
|
76
|
+
v = tool_input.get(k)
|
|
77
|
+
if isinstance(v, str) and v:
|
|
78
|
+
paths.append(v)
|
|
79
|
+
# MultiEdit 可能多文件
|
|
80
|
+
for k in ('paths', 'files'):
|
|
81
|
+
v = tool_input.get(k)
|
|
82
|
+
if isinstance(v, list):
|
|
83
|
+
paths.extend(x for x in v if isinstance(x, str))
|
|
84
|
+
return paths
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _cmd_from_input(tool_input):
|
|
88
|
+
"""Bash 工具的命令文本。"""
|
|
89
|
+
for k in ('command', 'cmd'):
|
|
90
|
+
v = tool_input.get(k)
|
|
91
|
+
if isinstance(v, str):
|
|
92
|
+
return v
|
|
93
|
+
return ''
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _dev():
|
|
97
|
+
"""读当前开发者。失败返回 None。"""
|
|
98
|
+
df = os.path.join(BASE, '.qoder', '.developer')
|
|
99
|
+
try:
|
|
100
|
+
if not os.path.isfile(df):
|
|
101
|
+
return None
|
|
102
|
+
for line in open(df, encoding='utf-8'):
|
|
103
|
+
if 'name=' in line:
|
|
104
|
+
return line.split('name=', 1)[1].strip().split()[0]
|
|
105
|
+
except Exception:
|
|
106
|
+
pass
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _append_event(event, data):
|
|
111
|
+
"""best-effort append 一条事件到当前 dev 的 feedback.jsonl。避开 learn import 断链, 直接写文件。"""
|
|
112
|
+
try:
|
|
113
|
+
dev = _dev()
|
|
114
|
+
if not dev:
|
|
115
|
+
return
|
|
116
|
+
fb = os.path.join(BASE, 'workspace', 'members', dev, 'journal', 'feedback.jsonl')
|
|
117
|
+
os.makedirs(os.path.dirname(fb), exist_ok=True)
|
|
118
|
+
entry = {
|
|
119
|
+
'ts': datetime.now().isoformat(timespec='seconds'),
|
|
120
|
+
'dev': dev,
|
|
121
|
+
'event': event,
|
|
122
|
+
'data': data,
|
|
123
|
+
'_source': 'post-tool-use-hook', # 标记来源, 区别于 AI 主动埋点
|
|
124
|
+
}
|
|
125
|
+
with open(fb, 'a', encoding='utf-8') as f:
|
|
126
|
+
f.write(json.dumps(entry, ensure_ascii=False) + '\n')
|
|
127
|
+
except Exception:
|
|
128
|
+
pass # 铁律: 埋点失败绝不阻塞
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def main():
|
|
132
|
+
try:
|
|
133
|
+
tool_input, tool_name = _read_tool_info()
|
|
134
|
+
tn = (tool_name or '').lower()
|
|
135
|
+
|
|
136
|
+
# 1. Write/Edit 落到 PRD / task.json → 埋点
|
|
137
|
+
if tn in WRITE_TOOLS or tn == 'bash':
|
|
138
|
+
paths = _file_paths_from_input(tool_input, tool_name)
|
|
139
|
+
for p in paths:
|
|
140
|
+
plow = p.lower().replace('\\', '/')
|
|
141
|
+
if 'prd' in os.path.basename(plow).lower() and plow.endswith('.md'):
|
|
142
|
+
_append_event('prd_written', {'prd': os.path.basename(p), 'tool': tool_name})
|
|
143
|
+
if plow.endswith('task.json'):
|
|
144
|
+
_append_event('task_touched', {'task_json': os.path.basename(os.path.dirname(p)), 'tool': tool_name})
|
|
145
|
+
# v3.1 软提示: prototype HTML 写完但缺来源锚定 → stderr 提醒(不阻断)
|
|
146
|
+
if 'prototype' in os.path.basename(plow) and plow.endswith('.html'):
|
|
147
|
+
try:
|
|
148
|
+
if os.path.isfile(p):
|
|
149
|
+
with open(p, encoding='utf-8', errors='ignore') as _f:
|
|
150
|
+
_ht = _f.read()[:8192]
|
|
151
|
+
import re as _re
|
|
152
|
+
if not _re.search(r'@prototype-source:|x-source-pages', _ht):
|
|
153
|
+
sys.stderr.write(
|
|
154
|
+
'[prototype 提醒] %s 缺来源锚定注释。\n'
|
|
155
|
+
' → 在 <head> 加一行: <!-- @prototype-source: fywl-ui/.../index.vue -->\n'
|
|
156
|
+
' → 没做这一步 = 没读真实代码就手抄模板, eval A2 会扣 5 分。\n' % os.path.basename(p))
|
|
157
|
+
except Exception:
|
|
158
|
+
pass # 软提示, 失败静默
|
|
159
|
+
|
|
160
|
+
# 2. Bash 跑了 commit/push → 兜底埋点 commit_done
|
|
161
|
+
if tn == 'bash':
|
|
162
|
+
cmd = _cmd_from_input(tool_input)
|
|
163
|
+
if cmd and COMMIT_RE.search(cmd):
|
|
164
|
+
_append_event('commit_done', {'source': 'post-tool-use-hook', 'cmd': cmd[:120]})
|
|
165
|
+
|
|
166
|
+
sys.exit(0) # 永远放行
|
|
167
|
+
except SystemExit:
|
|
168
|
+
raise
|
|
169
|
+
except Exception as e:
|
|
170
|
+
try:
|
|
171
|
+
os.makedirs(os.path.dirname(HOOK_ERRORS_LOG), exist_ok=True)
|
|
172
|
+
with open(HOOK_ERRORS_LOG, 'a', encoding='utf-8') as f:
|
|
173
|
+
f.write('[{}] post-tool-use: {}\n'.format(
|
|
174
|
+
datetime.now().strftime('%Y-%m-%d %H:%M:%S'), str(e)[:200]))
|
|
175
|
+
except Exception:
|
|
176
|
+
pass
|
|
177
|
+
sys.exit(0)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
if __name__ == '__main__':
|
|
181
|
+
main()
|
|
@@ -198,7 +198,8 @@ def get_index_info():
|
|
|
198
198
|
# 检查是否有未标注的模块 (提示AI做语义标注)
|
|
199
199
|
try:
|
|
200
200
|
import subprocess as _sp
|
|
201
|
-
|
|
201
|
+
# v3.x: kg_semantic.py 已迁移到 domain/kg/graph/ (旧扁平路径不存在)
|
|
202
|
+
r = _sp.run([sys.executable, os.path.join(BASE, '.qoder', 'scripts', 'domain', 'kg', 'graph', 'kg_semantic.py'), '--check'],
|
|
202
203
|
capture_output=True, text=True, encoding='utf-8', timeout=5)
|
|
203
204
|
if r.stdout and 'semantic_labeling_needed' in r.stdout:
|
|
204
205
|
import json as _json
|
|
@@ -245,6 +246,81 @@ def get_style_info():
|
|
|
245
246
|
return NL.join(lines)
|
|
246
247
|
|
|
247
248
|
|
|
249
|
+
def get_usability_summary():
|
|
250
|
+
"""读工作流可用性指标, 把"不达标"项注入会话上下文。
|
|
251
|
+
|
|
252
|
+
吸收宿主 SessionStart hook 能力: 不用等用户跑 /wl-status, 每次开会话主动
|
|
253
|
+
暴露"工作流用没用起来"的红灯项。只展示红/黄 (绿的不打扰), 无数据则不显示。
|
|
254
|
+
(与 usability_score.py 同源逻辑, 但 hook 内联精简版, 避免 import 验证脚本拖慢启动)
|
|
255
|
+
"""
|
|
256
|
+
lines = []
|
|
257
|
+
try:
|
|
258
|
+
# U2 一次过审率 (从 eval-history 取首条)
|
|
259
|
+
eval_path = os.path.join(BASE, 'data', 'learning', 'eval-history.jsonl')
|
|
260
|
+
records = []
|
|
261
|
+
if os.path.isfile(eval_path):
|
|
262
|
+
with open(eval_path, encoding='utf-8', errors='replace') as f:
|
|
263
|
+
for ln in f:
|
|
264
|
+
ln = ln.strip()
|
|
265
|
+
if ln:
|
|
266
|
+
try:
|
|
267
|
+
records.append(json.loads(ln))
|
|
268
|
+
except Exception:
|
|
269
|
+
continue
|
|
270
|
+
# 取最近 7 天
|
|
271
|
+
from datetime import datetime, timedelta
|
|
272
|
+
cutoff = datetime.now() - timedelta(days=7)
|
|
273
|
+
recent = []
|
|
274
|
+
for r in records:
|
|
275
|
+
t = r.get('time', '')
|
|
276
|
+
try:
|
|
277
|
+
if datetime.fromisoformat(str(t).split('.')[0]) >= cutoff:
|
|
278
|
+
recent.append(r)
|
|
279
|
+
except Exception:
|
|
280
|
+
continue
|
|
281
|
+
|
|
282
|
+
issues = [] # 只收红/黄
|
|
283
|
+
if recent:
|
|
284
|
+
passed = sum(1 for r in recent if r.get('passed'))
|
|
285
|
+
rate = 100.0 * passed / len(recent)
|
|
286
|
+
if rate < 60: # 黄线 30, 红线 60
|
|
287
|
+
tag = '🔴' if rate < 30 else '🟡'
|
|
288
|
+
issues.append('%s 一次过审率 %.0f%% (<60%% 目标) — PRD 质量需提升' % (tag, rate))
|
|
289
|
+
|
|
290
|
+
# U4 规则违反率 (从 feedback)
|
|
291
|
+
fb_dir = os.path.join(BASE, 'workspace', 'members')
|
|
292
|
+
violations = 0
|
|
293
|
+
prds_gen = 0
|
|
294
|
+
if os.path.isdir(fb_dir):
|
|
295
|
+
for dev in os.listdir(fb_dir):
|
|
296
|
+
fb = os.path.join(fb_dir, dev, 'journal', 'feedback.jsonl')
|
|
297
|
+
if not os.path.isfile(fb):
|
|
298
|
+
continue
|
|
299
|
+
try:
|
|
300
|
+
with open(fb, encoding='utf-8', errors='replace') as f:
|
|
301
|
+
for ln in f:
|
|
302
|
+
if '"rule_violation"' in ln:
|
|
303
|
+
violations += 1
|
|
304
|
+
elif '"prd_accepted"' in ln:
|
|
305
|
+
prds_gen += 1
|
|
306
|
+
except Exception:
|
|
307
|
+
continue
|
|
308
|
+
if prds_gen and violations:
|
|
309
|
+
vrate = 100.0 * violations / prds_gen
|
|
310
|
+
if vrate > 10:
|
|
311
|
+
tag = '🔴' if vrate > 25 else '🟡'
|
|
312
|
+
issues.append('%s 规则违反率 %.0f%% (>10%% 目标) — 关键规则没拦住' % (tag, vrate))
|
|
313
|
+
|
|
314
|
+
if issues:
|
|
315
|
+
lines.append('## Workflow Usability (近7天)')
|
|
316
|
+
lines.append('工作流"用不用得起来"红灯 (详情: wlkj.py usability):')
|
|
317
|
+
for it in issues:
|
|
318
|
+
lines.append(' ' + it)
|
|
319
|
+
except Exception:
|
|
320
|
+
pass # hook 失败绝不阻塞会话
|
|
321
|
+
return NL.join(lines) if lines else ''
|
|
322
|
+
|
|
323
|
+
|
|
248
324
|
def main():
|
|
249
325
|
parts = []
|
|
250
326
|
parts.append('<qoder-context>')
|
|
@@ -273,6 +349,11 @@ def main():
|
|
|
273
349
|
for t in tasks:
|
|
274
350
|
parts.append(' [' + t['status'] + '] ' + t['name'] + ' - ' + t['title'])
|
|
275
351
|
|
|
352
|
+
usability = get_usability_summary()
|
|
353
|
+
if usability:
|
|
354
|
+
parts.append('')
|
|
355
|
+
parts.append(usability)
|
|
356
|
+
|
|
276
357
|
parts.append('')
|
|
277
358
|
|
|
278
359
|
idx_info = get_index_info()
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""stop-eval.py - Stop hook: Agent 说"完成"时强制 eval 门禁 (治"说做完了没真评测")
|
|
4
|
+
|
|
5
|
+
吸收宿主确定性 hook 能力: 把 EVA 门禁 (PRD 发布前 eval≥80%) 从"靠 AI 自觉跑"
|
|
6
|
+
升级为"Agent 准备结束回复时宿主拦住"。AI 说"PRD 做完了"但没跑 eval → 被拦回继续。
|
|
7
|
+
|
|
8
|
+
阻断契约: exit 0 放行; exit 2 阻断(stderr 反馈, 强制 Agent 续修)。
|
|
9
|
+
|
|
10
|
+
防递归 (关键!): Stop hook 被 exit 2 阻断后, Agent 续修完再次 Stop 会再次触发本 hook →
|
|
11
|
+
死循环。必须检查 stdin 的 stop_hook_active: 为 true 时直接 exit 0 放行 (Agent 已在本轮
|
|
12
|
+
被拦过一次, 不再二次拦)。
|
|
13
|
+
|
|
14
|
+
判断逻辑 (保守, 只拦明确场景):
|
|
15
|
+
- 本会话最近产出过 PRD 文件 (扫描 workspace/members/*/drafts + tasks/*/prd.md,
|
|
16
|
+
找近 10 分钟内 mtime 的 prd*.md)
|
|
17
|
+
- 且该 PRD 没有对应的 eval 记录 (eval-history.jsonl 里查不到该 prd 名)
|
|
18
|
+
→ exit 2 强制 eval
|
|
19
|
+
其它情况一律 exit 0 (绝不误拦普通对话/非 PRD 工作)。
|
|
20
|
+
|
|
21
|
+
stdin 字段名官方未公开, 兼容 stop_hook_active / hook_active 等多种写法。
|
|
22
|
+
"""
|
|
23
|
+
import json
|
|
24
|
+
import os
|
|
25
|
+
import sys
|
|
26
|
+
import time
|
|
27
|
+
from datetime import datetime
|
|
28
|
+
|
|
29
|
+
if sys.platform == 'win32':
|
|
30
|
+
try:
|
|
31
|
+
sys.stdout.reconfigure(encoding='utf-8')
|
|
32
|
+
sys.stderr.reconfigure(encoding='utf-8')
|
|
33
|
+
except Exception:
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
BASE = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
37
|
+
HOOK_ERRORS_LOG = os.path.join(BASE, '.qoder', '.runtime', 'hook-errors.log')
|
|
38
|
+
EVAL_HISTORY = os.path.join(BASE, 'data', 'learning', 'eval-history.jsonl')
|
|
39
|
+
RECENT_WINDOW = 600 # 10 分钟内产出的 PRD 视为本轮产物
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _is_reentry(stdin_data):
|
|
43
|
+
"""是否二次进入 (防递归)。兼容多字段名。"""
|
|
44
|
+
if not isinstance(stdin_data, dict):
|
|
45
|
+
return False
|
|
46
|
+
for k in ('stop_hook_active', 'hook_active', 'is_reentry'):
|
|
47
|
+
v = stdin_data.get(k)
|
|
48
|
+
if v in (True, 'true', 'True', 1, '1'):
|
|
49
|
+
return True
|
|
50
|
+
return False
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _read_stdin():
|
|
54
|
+
try:
|
|
55
|
+
if sys.stdin.isatty():
|
|
56
|
+
return {}
|
|
57
|
+
raw = sys.stdin.read().strip()
|
|
58
|
+
if not raw:
|
|
59
|
+
return {}
|
|
60
|
+
return json.loads(raw)
|
|
61
|
+
except Exception:
|
|
62
|
+
return {}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _is_prd_filename(fn):
|
|
66
|
+
"""判断文件名是否是 PRD 产物。
|
|
67
|
+
|
|
68
|
+
真实 PRD 有两类命名 (与 eval-history.jsonl 记录的 'prd' 字段对齐):
|
|
69
|
+
1. workspace/tasks/<task>/prd.md (任务级 PRD, 文件名含 prd)
|
|
70
|
+
2. workspace/members/<dev>/drafts/REQ-2026-XXX-<feature>.md
|
|
71
|
+
(AI 产出的 PRD 草稿, 命名 REQ-<id>-<feature>.md, 不含 'prd' 字样)
|
|
72
|
+
只匹配 'prd' 会漏掉第 2 类 (绝大多数真实产出) → 门禁形同虚设。
|
|
73
|
+
这里两类都认: 文件名含 'prd' 或 以 'REQ-' 开头 (eval-history 的真实记录格式)。
|
|
74
|
+
"""
|
|
75
|
+
low = fn.lower()
|
|
76
|
+
if not low.endswith('.md'):
|
|
77
|
+
return False
|
|
78
|
+
return 'prd' in low or low.startswith('req-')
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _recent_prd_files():
|
|
82
|
+
"""找近 RECENT_WINDOW 秒内产出的 PRD 文件 (绝对路径)。扫描 drafts + tasks。"""
|
|
83
|
+
now = time.time()
|
|
84
|
+
candidates = []
|
|
85
|
+
search_roots = [
|
|
86
|
+
os.path.join(BASE, 'workspace', 'members'),
|
|
87
|
+
os.path.join(BASE, 'workspace', 'tasks'),
|
|
88
|
+
os.path.join(BASE, 'data', 'docs', 'prd'),
|
|
89
|
+
]
|
|
90
|
+
for root in search_roots:
|
|
91
|
+
if not os.path.isdir(root):
|
|
92
|
+
continue
|
|
93
|
+
for dirpath, dirs, files in os.walk(root):
|
|
94
|
+
# 跳过无关大目录
|
|
95
|
+
dirs[:] = [d for d in dirs if d not in ('node_modules', '.git', '__pycache__', '.secrets')]
|
|
96
|
+
for fn in files:
|
|
97
|
+
if not _is_prd_filename(fn):
|
|
98
|
+
continue
|
|
99
|
+
fp = os.path.join(dirpath, fn)
|
|
100
|
+
try:
|
|
101
|
+
if now - os.path.getmtime(fp) <= RECENT_WINDOW:
|
|
102
|
+
candidates.append(fp)
|
|
103
|
+
except OSError:
|
|
104
|
+
continue
|
|
105
|
+
return candidates
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _evald_prds():
|
|
109
|
+
"""读 eval-history.jsonl, 返回【已通过门禁】的 prd 文件名集合。
|
|
110
|
+
|
|
111
|
+
门禁定义 (与消息承诺一致): 该 PRD 最近一次 eval 记录 passed=True 或 score_pct>=80。
|
|
112
|
+
只"跑过 eval 但没过线"的不算通过 → 仍需拦回去重测。
|
|
113
|
+
eval-history.jsonl 是 append-only, 同一 PRD 可能有多条 → 取最后一条为准。
|
|
114
|
+
"""
|
|
115
|
+
last = _last_record_by_prd()
|
|
116
|
+
done = set()
|
|
117
|
+
for key, r in last.items():
|
|
118
|
+
passed = r.get('passed')
|
|
119
|
+
score = r.get('score_pct')
|
|
120
|
+
is_ok = (passed is True) or (isinstance(score, (int, float)) and score >= 80)
|
|
121
|
+
if is_ok:
|
|
122
|
+
done.add(key)
|
|
123
|
+
return done
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _last_record_by_prd():
|
|
127
|
+
"""读 eval-history.jsonl, 返回 {prd_basename: 最后一条记录}。
|
|
128
|
+
|
|
129
|
+
append-only 文件, 后出现的记录覆盖前面的 → 取每个 PRD 的最新 eval。
|
|
130
|
+
被门禁诊断复用 (区分"没跑过 eval" vs "跑了但没过线")。
|
|
131
|
+
"""
|
|
132
|
+
last_by_prd = {}
|
|
133
|
+
if not os.path.isfile(EVAL_HISTORY):
|
|
134
|
+
return last_by_prd
|
|
135
|
+
try:
|
|
136
|
+
with open(EVAL_HISTORY, encoding='utf-8', errors='replace') as f:
|
|
137
|
+
for line in f:
|
|
138
|
+
line = line.strip()
|
|
139
|
+
if not line:
|
|
140
|
+
continue
|
|
141
|
+
try:
|
|
142
|
+
r = json.loads(line)
|
|
143
|
+
p = r.get('prd')
|
|
144
|
+
if p:
|
|
145
|
+
last_by_prd[os.path.basename(str(p))] = r
|
|
146
|
+
except Exception:
|
|
147
|
+
continue
|
|
148
|
+
except Exception:
|
|
149
|
+
pass
|
|
150
|
+
return last_by_prd
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def main():
|
|
154
|
+
try:
|
|
155
|
+
data = _read_stdin()
|
|
156
|
+
# 防递归: 二次进入直接放行
|
|
157
|
+
if _is_reentry(data):
|
|
158
|
+
sys.exit(0)
|
|
159
|
+
|
|
160
|
+
# 找近 10 分钟产出的 PRD, 检查是否已【通过】门禁
|
|
161
|
+
recent = _recent_prd_files()
|
|
162
|
+
if not recent:
|
|
163
|
+
sys.exit(0) # 本轮没产 PRD, 放行
|
|
164
|
+
evald = _evald_prds()
|
|
165
|
+
last_records = _last_record_by_prd()
|
|
166
|
+
# 区分两种未过门禁: 从没跑过 eval / 跑了但没过线
|
|
167
|
+
not_run = [p for p in recent if os.path.basename(p) not in last_records]
|
|
168
|
+
failed = [p for p in recent
|
|
169
|
+
if os.path.basename(p) in last_records
|
|
170
|
+
and os.path.basename(p) not in evald]
|
|
171
|
+
if not not_run and not failed:
|
|
172
|
+
sys.exit(0) # 全部通过门禁, 放行
|
|
173
|
+
|
|
174
|
+
# 有未过门禁的近期 PRD → 阻断, 给出可操作指引
|
|
175
|
+
parts = []
|
|
176
|
+
if not_run:
|
|
177
|
+
parts.append('未跑评测: ' + ', '.join(os.path.basename(p) for p in not_run[:3]))
|
|
178
|
+
if failed:
|
|
179
|
+
details = []
|
|
180
|
+
for p in failed[:3]:
|
|
181
|
+
rec = last_records.get(os.path.basename(p), {})
|
|
182
|
+
s = rec.get('score_pct')
|
|
183
|
+
details.append('%s (最近 %s)' % (os.path.basename(p),
|
|
184
|
+
('%s%%' % s) if s is not None else '未过'))
|
|
185
|
+
parts.append('评测未过线: ' + ', '.join(details))
|
|
186
|
+
sys.stderr.write(
|
|
187
|
+
'【EVA 门禁】检测到近期产出的 PRD 未通过质量门禁 (要求 eval ≥80%%):\n'
|
|
188
|
+
' %s\n'
|
|
189
|
+
'修复: python .qoder/scripts/orchestration/wlkj.py eval <PRD> [原型]\n'
|
|
190
|
+
' 针对评测报告的扣分项改 PRD, 重跑 eval 到 ≥80%% 再结束。\n'
|
|
191
|
+
% '; '.join(parts))
|
|
192
|
+
sys.exit(2)
|
|
193
|
+
except SystemExit:
|
|
194
|
+
raise
|
|
195
|
+
except Exception as e:
|
|
196
|
+
try:
|
|
197
|
+
os.makedirs(os.path.dirname(HOOK_ERRORS_LOG), exist_ok=True)
|
|
198
|
+
with open(HOOK_ERRORS_LOG, 'a', encoding='utf-8') as f:
|
|
199
|
+
f.write('[{}] stop-eval: {}\n'.format(
|
|
200
|
+
datetime.now().strftime('%Y-%m-%d %H:%M:%S'), str(e)[:200]))
|
|
201
|
+
except Exception:
|
|
202
|
+
pass
|
|
203
|
+
sys.exit(0) # 铁律: hook 崩溃绝不阻塞
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
if __name__ == '__main__':
|
|
207
|
+
main()
|