@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
@@ -0,0 +1,159 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ guard.py - 角色能力边界守卫 (解耦的执行点)
4
+
5
+ 把角色从"标签"变成"边界"。各角色在能力边界内自治, 互不越界 = 解耦。
6
+
7
+ 现状: role.py 只查询角色名, config.yaml 的 permissions 声明了但从不强制。
8
+ 本次: guard.py 在"产出动作前"校验角色能否产出该契约类型, 越权则降级 (不报错)。
9
+
10
+ 设计原则 (与方案一致):
11
+ - 降级而非阻塞: 越权不报错/不崩溃, 而是降级 (转给有权限的人 / 标记待确认)
12
+ 这与契约层的"降级不停摆"哲学一致
13
+ - 软边界: 默认不强制 (ensure=False), 调用方显式 ensure=True 才硬拦
14
+ - 可观测: 每次越权都留痕 (返回 Result + hint), 让 AI/用户知道发生了什么
15
+
16
+ 核心 API:
17
+ check_produce(contract_type, role=None, ensure=False) -> Result
18
+ 检查当前角色能否产出某契约
19
+ ensure=False (默认): 越权返回 WARN (降级提示, 不阻塞)
20
+ ensure=True: 越权返回 FAIL (硬拦, 给破坏性操作用)
21
+ current_role() -> str | None
22
+ 当前角色名 (identity.get_role 的便捷封装)
23
+
24
+ Usage:
25
+ from foundation.identity.guard import check_produce
26
+ r = check_produce('unit-test')
27
+ if not r.ok:
28
+ print(r.hint) # "你的角色 pm 不能产出 unit-test, 需 dev/test 角色"
29
+ return # 降级: 不执行产出
30
+ # ... 继续产出 unit-test
31
+ """
32
+
33
+ import os
34
+ import sys
35
+
36
+ try:
37
+ from foundation.data.result import Result, CODE
38
+ from .identity import get_role, can_produce, can_consume, get_role_config
39
+ except ImportError:
40
+ _COMMON_DIR = os.path.dirname(os.path.abspath(__file__))
41
+ if _COMMON_DIR not in sys.path:
42
+ sys.path.insert(0, _COMMON_DIR)
43
+ from result import Result, CODE
44
+ from identity import get_role, can_produce, can_consume, get_role_config
45
+
46
+ __all__ = [
47
+ "check_produce",
48
+ "check_consume",
49
+ "current_role",
50
+ "suggest_role_for",
51
+ ]
52
+
53
+ # 角色中文名映射 (给提示文案用)
54
+ _ROLE_CN = {
55
+ "pm": "产品经理",
56
+ "design": "设计师",
57
+ "dev": "开发",
58
+ "test": "测试",
59
+ "admin": "管理员",
60
+ }
61
+
62
+
63
+ def current_role() -> str:
64
+ """当前角色名。未初始化/未注册 → 回退 'pm' (软引导, 与 role.py 一致)。"""
65
+ return get_role() or "pm"
66
+
67
+
68
+ def _role_cn(role: str) -> str:
69
+ return _ROLE_CN.get(role, role)
70
+
71
+
72
+ def suggest_role_for(contract_type: str) -> list:
73
+ """建议"哪些角色能产出该契约", 给越权提示用。"""
74
+ # 反查 config.yaml 哪些角色的 can_produce 含此类型或 '*'
75
+ try:
76
+ from .identity import _load_roles_config
77
+ except ImportError:
78
+ from identity import _load_roles_config
79
+ roles_cfg = _load_roles_config()
80
+ out = []
81
+ for r, cfg in roles_cfg.items():
82
+ caps = list(cfg.get("can_produce", []) or [])
83
+ if "*" in caps or contract_type in caps:
84
+ out.append(r)
85
+ return out
86
+
87
+
88
+ def check_produce(
89
+ contract_type: str,
90
+ role: str = None,
91
+ ensure: bool = False,
92
+ ) -> Result:
93
+ """检查角色能否产出某契约类型。
94
+
95
+ Args:
96
+ contract_type: 契约类型 (prd/prototype/spec/design-spec/code/unit-test/test-report)
97
+ role: 角色名, 不传则用当前角色
98
+ ensure: False=越权返回WARN(降级); True=越权返回FAIL(硬拦)
99
+
100
+ Returns:
101
+ Result.ok + CODE.OK: 允许产出
102
+ Result (WARN): 越权但降级 (ensure=False), 带 hint 提示该找谁
103
+ Result (FAIL): 越权且硬拦 (ensure=True)
104
+ """
105
+ role = role or current_role()
106
+ if can_produce(role, contract_type):
107
+ return Result.success(msg="✓ {} 可产出 {}".format(_role_cn(role), contract_type))
108
+ # 越权
109
+ suggest = suggest_role_for(contract_type)
110
+ suggest_cn = "、".join(_role_cn(r) for r in suggest) if suggest else "对应角色"
111
+ hint = "你的角色({})不能产出 {}, 需 {} 角色。用 /wl-init 切换或转交。".format(
112
+ _role_cn(role), contract_type, suggest_cn
113
+ )
114
+ if ensure:
115
+ return Result.failure(
116
+ "[越权] {} 不能产出 {}".format(_role_cn(role), contract_type),
117
+ code=CODE.AUTHZ,
118
+ hint=hint,
119
+ )
120
+ # 降级: 不阻塞, 但强烈提示
121
+ return Result.warning(
122
+ "⚠️ {} 通常不产出 {} (建议 {})".format(_role_cn(role), contract_type, suggest_cn),
123
+ code=CODE.AUTHZ,
124
+ hint=hint,
125
+ )
126
+
127
+
128
+ def check_consume(
129
+ capability: str,
130
+ role: str = None,
131
+ ) -> Result:
132
+ """检查角色能否读取/消费某能力。越权则只读降级 (永远不阻塞)。"""
133
+ role = role or current_role()
134
+ if can_consume(role, capability):
135
+ return Result.success(msg="✓ {} 可消费 {}".format(_role_cn(role), capability))
136
+ return Result.warning(
137
+ "⚠️ {} 读取 {} 超出常规职责 (只读降级)".format(_role_cn(role), capability),
138
+ code=CODE.AUTHZ,
139
+ hint="这是软提示, 仍可继续 (越权只读降级, 不阻塞)",
140
+ )
141
+
142
+
143
+ # ============================================================
144
+ # CLI (供 skill 脚本诊断用)
145
+ # ============================================================
146
+
147
+ if __name__ == "__main__":
148
+ import json
149
+ role = current_role()
150
+ print("当前角色: {} ({})".format(role, _role_cn(role)))
151
+ cfg = get_role_config(role)
152
+ print(" can_produce: {}".format(cfg.get("can_produce", [])))
153
+ print(" can_consume: {}".format(cfg.get("can_consume", [])))
154
+ if len(sys.argv) > 1:
155
+ ct = sys.argv[1]
156
+ r = check_produce(ct)
157
+ print("\n能否产出 {}: {}".format(ct, "能" if r.ok and not r.is_warn else "否"))
158
+ if r.hint:
159
+ print(" -> {}".format(r.hint))
@@ -41,8 +41,8 @@ from typing import Optional, Dict, Any, Union
41
41
 
42
42
  # 相对导入兄弟模块
43
43
  try:
44
- from .atomicio import atomic_write_json, safe_read_json
45
- from .paths import get_repo_root, MEMBERS_DIR
44
+ from foundation.io.atomicio import atomic_write_json, safe_read_json
45
+ from foundation.core.paths import get_repo_root, MEMBERS_DIR
46
46
  except ImportError:
47
47
  _COMMON_DIR = os.path.dirname(os.path.abspath(__file__))
48
48
  if _COMMON_DIR not in sys.path:
@@ -52,7 +52,7 @@ except ImportError:
52
52
  _SCRIPTS_DIR = os.path.dirname(_COMMON_DIR)
53
53
  if _SCRIPTS_DIR not in sys.path:
54
54
  sys.path.insert(0, _SCRIPTS_DIR)
55
- from common.paths import get_repo_root, MEMBERS_DIR
55
+ from foundation.core.paths import get_repo_root, MEMBERS_DIR
56
56
 
57
57
  __all__ = [
58
58
  "IdentityError",
@@ -65,9 +65,16 @@ __all__ = [
65
65
  "verify_signature",
66
66
  "require_member",
67
67
  "ensure_current_member",
68
+ # ★ 角色能力 (v3.0 合并自 role.py) ★
69
+ "get_role",
70
+ "get_role_config",
71
+ "get_capabilities",
72
+ "get_permissions",
73
+ "can_produce",
74
+ "can_consume",
68
75
  ]
69
76
 
70
- # 退出码 (全脚本统一): 4 = authz_denied
77
+ # 退出码 (与 common/result.py 的 EXIT[AUTHZ] 对齐): 4 = authz_denied
71
78
  EXIT_AUTHZ = 4
72
79
 
73
80
  # 名字合法字符: 字母/数字/中文/下划线/短横, 2-32 字符
@@ -96,7 +103,12 @@ def _member_json(name: str, repo_root: Optional[Union[str, Path]] = None) -> Pat
96
103
 
97
104
 
98
105
  def _key_file(name: str, repo_root: Optional[Union[str, Path]] = None) -> Path:
99
- return _member_dir(name, repo_root) / ".signing_key"
106
+ """签名密钥路径 (v3.0: 收进 .private/signing_key, 向后兼容老 .signing_key)。"""
107
+ try:
108
+ from foundation.core.paths import get_private_file
109
+ return get_private_file("signing_key", name)
110
+ except Exception:
111
+ return _member_dir(name, repo_root) / ".signing_key"
100
112
 
101
113
 
102
114
  # ============================================================
@@ -192,6 +204,10 @@ def register_member(
192
204
  or "%s <unknown@local>" % name
193
205
  ),
194
206
  "joined_at": joined_at,
207
+ # 禅道账号映射 (v3.1.3): 本地 developer 名(如 hupan56) ↔ 禅道 account(如 hupan)
208
+ # 不一致时, zentao_sync 靠此过滤归属 (见 contracts/isolation.md「禅道同步」)。
209
+ # 首次注册留空 (None), 成员后续填; 已注册的保留原值不覆盖。
210
+ "zentao_account": (existing.get("zentao_account") if existing else None),
195
211
  }
196
212
  atomic_write_json(mj, record)
197
213
  return record
@@ -260,11 +276,11 @@ def require_member(name: Optional[str] = None, repo_root: Optional[Union[str, Pa
260
276
  """
261
277
  if name is None:
262
278
  try:
263
- from .developer_paths import get_developer
279
+ from foundation.core.paths import get_developer
264
280
  except ImportError:
265
281
  _this = os.path.dirname(os.path.abspath(__file__))
266
282
  sys.path.insert(0, _this)
267
- from paths import get_developer # type: ignore
283
+ from foundation.core.paths import get_developer # type: ignore
268
284
  name = get_developer(repo_root)
269
285
  if not name:
270
286
  sys.stderr.write("[authz] 拒绝: 未设置开发者身份。先跑 /wl-init 注册。\n")
@@ -280,11 +296,118 @@ def require_member(name: Optional[str] = None, repo_root: Optional[Union[str, Pa
280
296
  def ensure_current_member(repo_root: Optional[Union[str, Path]] = None) -> Optional[str]:
281
297
  """软检查: 返回当前已验证的成员名, 或 None (不 exit)。给非破坏性命令用。"""
282
298
  try:
283
- from paths import get_developer
299
+ from foundation.core.paths import get_developer
284
300
  except ImportError:
285
301
  sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
286
- from paths import get_developer # type: ignore
302
+ from foundation.core.paths import get_developer # type: ignore
287
303
  name = get_developer(repo_root)
288
304
  if name and verify_member(name, repo_root):
289
305
  return name
290
306
  return None
307
+
308
+
309
+ # ============================================================
310
+ # 角色能力 (v3.0 合并自 role.py —— 角色从"标签"升级为"能力边界")
311
+ #
312
+ # config.yaml 的 roles 段升级为能力声明:
313
+ # roles.<role>.can_produce: [prd, prototype, ...] 允许产出的契约类型
314
+ # roles.<role>.can_consume: ["*", prd, ...] 允许读取的能力
315
+ # guard.py 据此强制边界: 越权降级 (不报错), 各角色在边界内自治 = 解耦
316
+ #
317
+ # 向后兼容: get_role / get_permissions 保留 (老脚本可继续用)
318
+ # ============================================================
319
+
320
+ def _load_roles_config(repo_root: Optional[Union[str, Path]] = None) -> Dict[str, Any]:
321
+ """读 config.yaml 的 roles 段。失败返回 {} (软降级, 不崩)。"""
322
+ if repo_root is None:
323
+ repo_root = get_repo_root()
324
+ cfg = Path(repo_root) / ".qoder" / "config.yaml"
325
+ if not cfg.is_file():
326
+ return {}
327
+ try:
328
+ import yaml
329
+ with open(cfg, encoding="utf-8") as f:
330
+ data = yaml.safe_load(f) or {}
331
+ return data.get("roles", {}) or {}
332
+ except Exception:
333
+ return {}
334
+
335
+
336
+ def get_role(name: Optional[str] = None, repo_root: Optional[Union[str, Path]] = None) -> Optional[str]:
337
+ """返回成员的角色名 (pm/design/dev/test/admin)。
338
+
339
+ 优先级:
340
+ 1. .developer 里记录的 role (init 时写)
341
+ 2. member.json 里的 role
342
+ 3. None (未注册)
343
+ 无角色默认回退 'pm' (软引导, 不报错) —— 与原 role.py 行为一致。
344
+ """
345
+ if not name:
346
+ # 从 .developer 读
347
+ try:
348
+ from foundation.core.paths import get_developer, get_developer_info
349
+ except ImportError:
350
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
351
+ from foundation.core.paths import get_developer, get_developer_info # type: ignore
352
+ info = get_developer_info(repo_root) if "get_developer_info" in dir() else None
353
+ if isinstance(info, dict) and info.get("role"):
354
+ return info["role"]
355
+ name = get_developer(repo_root)
356
+ if not name:
357
+ return None
358
+ m = get_member(name, repo_root)
359
+ return m.get("role") if m else None
360
+
361
+
362
+ def get_role_config(role: Optional[str], repo_root: Optional[Union[str, Path]] = None) -> Dict[str, Any]:
363
+ """返回某角色的完整配置 (can_produce/can_consume/commands/permissions/name)。
364
+
365
+ 角色不存在或 config 缺失 → 返回 {} (软降级)。
366
+ """
367
+ if not role:
368
+ return {}
369
+ return _load_roles_config(repo_root).get(role, {}) or {}
370
+
371
+
372
+ def get_permissions(role: Optional[str], repo_root: Optional[Union[str, Path]] = None) -> list:
373
+ """遗留 API: 返回角色的 permissions 列表 (向后兼容)。空则 []。"""
374
+ return list(get_role_config(role, repo_root).get("permissions", []) or [])
375
+
376
+
377
+ def get_capabilities(
378
+ role: Optional[str], kind: str = "produce", repo_root: Optional[Union[str, Path]] = None
379
+ ) -> list:
380
+ """返回角色的能力列表。
381
+
382
+ Args:
383
+ role: 角色名
384
+ kind: 'produce' (能产出哪些契约) 或 'consume' (能读哪些)
385
+ Returns:
386
+ 能力列表, 含 '*' 表示无限制。未知角色返回 [] (从严, 无能力)。
387
+ """
388
+ key = "can_produce" if kind == "produce" else "can_consume"
389
+ caps = get_role_config(role, repo_root).get(key, [])
390
+ return list(caps) if caps else []
391
+
392
+
393
+ def _has(cap_list: list, contract_type: str) -> bool:
394
+ """判断 contract_type 是否在能力列表内 ('*' 通配)。"""
395
+ if not cap_list:
396
+ return False
397
+ if "*" in cap_list:
398
+ return True
399
+ return contract_type in cap_list
400
+
401
+
402
+ def can_produce(
403
+ role: Optional[str], contract_type: str, repo_root: Optional[Union[str, Path]] = None
404
+ ) -> bool:
405
+ """该角色能否产出指定契约类型? guard.py 的核心查询。"""
406
+ return _has(get_capabilities(role, "produce", repo_root), contract_type)
407
+
408
+
409
+ def can_consume(
410
+ role: Optional[str], capability: str, repo_root: Optional[Union[str, Path]] = None
411
+ ) -> bool:
412
+ """该角色能否读取/消费指定能力? 越权则只读降级。"""
413
+ return _has(get_capabilities(role, "consume", repo_root), capability)
@@ -0,0 +1,60 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ roles.py - 角色章节过滤 (单一真源, 消除 kg.py↔kg_mcp_server.py 逐字重复)
4
+
5
+ 现状: ROLE_SECTIONS 字典 + _filter_by_role 函数在 kg.py(:645-672) 和
6
+ kg_mcp_server.py(:249-276) 逐字重复。改一处忘改另一处 → CLI 和 MCP 行为分叉。
7
+
8
+ 本模块作为单一真源, kg.py 和 kg_mcp_server.py 都从这里 import。
9
+
10
+ 过滤规则: 按角色裁剪 context_pack 输出的章节 (## 1. / ## 2. / ## 2b. 等),
11
+ 让不同角色只看到与其职责相关的段落, 减少噪音。
12
+
13
+ Usage:
14
+ from foundation.identity.roles import filter_by_role, ROLE_SECTIONS
15
+ text = filter_by_role(context_pack_output, role='pm')
16
+ """
17
+
18
+ import re
19
+
20
+ __all__ = ["ROLE_SECTIONS", "filter_by_role"]
21
+
22
+ # 各角色可见的 context_pack 章节号
23
+ # (与 kg.py ROLE_SECTIONS / kg_mcp_server.py ROLE_SECTIONS 对齐, 此处为唯一真源)
24
+ ROLE_SECTIONS = {
25
+ 'pm': {'1', '2', '2b', '3', '4', '5', '7'}, # PM: 字段+PRD+API,无代码噪音
26
+ 'design': {'2', '2b', '3', '6'}, # 设计: 页面+布局+字段+风格
27
+ 'dev': {'1', '3', '5', '7'}, # 开发: 代码+字段+API+wiki
28
+ 'test': {'1', '4', '5'}, # 测试: 代码+PRD+API
29
+ 'admin': {'4', '5'}, # 管理: PRD+API 概览
30
+ }
31
+
32
+
33
+ def filter_by_role(text, role):
34
+ """按角色过滤 context_pack 输出的章节。
35
+
36
+ Args:
37
+ text: context_pack 的多段输出 (## 1. xxx / ## 2. xxx ...)
38
+ role: 角色名 (pm/design/dev/test/admin), 未知角色不过滤
39
+
40
+ Returns:
41
+ 过滤后的文本 (只保留该角色可见章节)
42
+ """
43
+ visible = ROLE_SECTIONS.get((role or '').lower())
44
+ if not visible:
45
+ return text # 未知角色不过滤
46
+ # 按 "## 数字." 切段 (含 2b 这种带字母的)
47
+ sections = re.split(r'(\n## \d+[ab]?\.)', text)
48
+ result = sections[0]
49
+ i = 1
50
+ while i < len(sections):
51
+ header = sections[i]
52
+ num_m = re.match(r'\n## (\d+[ab]?)\.', header)
53
+ if num_m and num_m.group(1) in visible:
54
+ result += header
55
+ if i + 1 < len(sections):
56
+ result += sections[i + 1]
57
+ i += 2
58
+ else:
59
+ i += 2
60
+ return result.strip()
@@ -28,15 +28,30 @@ from pathlib import Path
28
28
  from typing import Any
29
29
 
30
30
  # 支持的 AI 平台及其会话环境变量
31
- _ENV_SESSION_KEYS: tuple[tuple[str, tuple[str, ...]], ...] = (
32
- ("qoder", ("QODER_SESSION_ID",)),
33
- ("codex", ("CODEX_SESSION_ID", "CODEX_THREAD_ID")),
34
- ("claude", ("CLAUDE_SESSION_ID", "CLAUDE_CODE_SESSION_ID")),
35
- ("cursor", ("CURSOR_SESSION_ID",)),
36
- ("opencode", ("OPENCODE_SESSION_ID", "OPENCODE_RUN_ID")),
37
- ("gemini", ("GEMINI_SESSION_ID",)),
38
- ("copilot", ("COPILOT_SESSION_ID",)),
39
- )
31
+ # -----------------------------------------------------------------------------
32
+ # 单一事实源: nexus.registry.HOST_TABLE(宿主能力抽象层)。
33
+ # 本表从 HOST_TABLE 派生,保持 tuple[tuple[str, tuple[str, ...]]] 格式兼容。
34
+ # 新增宿主只需改 nexus.HOST_TABLE 一处,active_task 自动跟随。
35
+ # nexus 不可用时回退到内联定义(向后兼容,零破坏)。
36
+ # -----------------------------------------------------------------------------
37
+ try:
38
+ from capability.registry import HOST_TABLE as _HOST_TABLE
39
+
40
+ _ENV_SESSION_KEYS: tuple[tuple[str, tuple[str, ...]], ...] = tuple(
41
+ (name, tuple(info.get("detect", ())))
42
+ for name, info in _HOST_TABLE.items()
43
+ if name != "cli" and info.get("detect")
44
+ )
45
+ except ImportError:
46
+ _ENV_SESSION_KEYS = (
47
+ ("qoderwork", ("QODER_SESSION_ID",)),
48
+ ("codex", ("CODEX_SESSION_ID", "CODEX_THREAD_ID")),
49
+ ("claude", ("CLAUDE_SESSION_ID", "CLAUDE_CODE_SESSION_ID")),
50
+ ("cursor", ("CURSOR_SESSION_ID",)),
51
+ ("opencode", ("OPENCODE_SESSION_ID", "OPENCODE_RUN_ID")),
52
+ ("gemini", ("GEMINI_SESSION_ID",)),
53
+ ("copilot", ("COPILOT_SESSION_ID",)),
54
+ )
40
55
 
41
56
 
42
57
  @dataclass(frozen=True)
@@ -127,7 +142,7 @@ def resolve_active_task(repo_root: Path | None = None) -> ActiveTask:
127
142
  ActiveTask 对象。
128
143
  """
129
144
  if repo_root is None:
130
- from common.paths import get_repo_root
145
+ from foundation.core.paths import get_repo_root
131
146
  repo_root = get_repo_root()
132
147
 
133
148
  # 1. 尝试会话级
@@ -147,7 +162,7 @@ def resolve_active_task(repo_root: Path | None = None) -> ActiveTask:
147
162
  pass
148
163
 
149
164
  # 2. 后备: 全局文件
150
- from common.paths import get_current_task
165
+ from foundation.core.paths import get_current_task
151
166
  global_task = get_current_task(repo_root)
152
167
  if global_task:
153
168
  return ActiveTask(
@@ -170,13 +185,13 @@ def set_active_task(task_path: str, repo_root: Path | None = None) -> ActiveTask
170
185
  ActiveTask 对象, 无会话身份则返回 None。
171
186
  """
172
187
  if repo_root is None:
173
- from common.paths import get_repo_root
188
+ from foundation.core.paths import get_repo_root
174
189
  repo_root = get_repo_root()
175
190
 
176
191
  session_key = resolve_context_key()
177
192
  if not session_key:
178
193
  # 无会话身份, 使用全局后备
179
- from common.paths import set_current_task
194
+ from foundation.core.paths import set_current_task
180
195
  set_current_task(task_path, repo_root)
181
196
  return ActiveTask(
182
197
  task_path=task_path,
@@ -208,7 +223,7 @@ def clear_active_task(repo_root: Path | None = None) -> ActiveTask:
208
223
  清除前的 ActiveTask 状态。
209
224
  """
210
225
  if repo_root is None:
211
- from common.paths import get_repo_root
226
+ from foundation.core.paths import get_repo_root
212
227
  repo_root = get_repo_root()
213
228
 
214
229
  # 获取清除前的状态
@@ -224,7 +239,7 @@ def clear_active_task(repo_root: Path | None = None) -> ActiveTask:
224
239
  pass
225
240
 
226
241
  # 也清除全局后备
227
- from common.paths import set_current_task
242
+ from foundation.core.paths import set_current_task
228
243
  set_current_task(None, repo_root)
229
244
 
230
245
  return active
@@ -57,20 +57,21 @@ THEME_COLORS = {
57
57
 
58
58
 
59
59
  def _load_config():
60
- """加载飞书配置 (config.yaml + 环境变量)。"""
61
- # 读 config.yaml 的 feishu 段
60
+ """加载飞书配置 (config.yaml + 环境变量)。
61
+
62
+ v3.0: 优先用 Config 单例 (带缓存), 失败回退文本解析。
63
+ """
62
64
  config = {}
63
65
  try:
64
- import yaml
65
- from .paths import get_repo_root
66
- cfg_path = os.path.join(str(get_repo_root()), '.qoder', 'config.yaml')
67
- with open(cfg_path, encoding='utf-8') as f:
68
- full = yaml.safe_load(f) or {}
69
- config = full.get('feishu') or {}
66
+ # 优先: Config 单例 (mtime 缓存, 避免每次重读)
67
+ from foundation.core.config import Config, SECTIONS
68
+ config = Config.load().section(SECTIONS.FEISHU, {})
70
69
  except Exception:
71
- # PyYAML 不可用, 用简单解析
70
+ pass
71
+ if not config:
72
+ # 回退: 简单文本解析 (PyYAML 不可用时)
72
73
  try:
73
- from .paths import get_repo_root
74
+ from foundation.core.paths import get_repo_root
74
75
  cfg_path = os.path.join(str(get_repo_root()), '.qoder', 'config.yaml')
75
76
  in_feishu = False
76
77
  with open(cfg_path, encoding='utf-8') as f:
@@ -12,7 +12,7 @@ of these maps in other scripts - import from here.
12
12
  # 性能优化 A6: 从 40 词扩展到 ~120 词, 覆盖库存/采购/供应商/客户/财务/工单等通用业务域
13
13
  CN_MAP = {
14
14
  # 考勤/人事
15
- '考勤': 'attendance clock', '打卡': 'clock', '签到': 'sign',
15
+ '考勤': 'attendance', '打卡': 'clock', '签到': 'sign',
16
16
  '薪资': 'salary', '工资': 'salary', '请假': 'leave', '休假': 'leave',
17
17
  '加班': 'overtime', '员工': 'employee staff', '用户': 'user',
18
18
  '组织': 'org department', '部门': 'department', '岗位': 'position role',
@@ -108,8 +108,8 @@ CN_MAP = {
108
108
  # so building and querying use the same semantics.
109
109
  BUSINESS_PATH_MAP = {
110
110
  '保险': ['insurance', 'Insurance', 'ins-', '/ins/', 'vehlife/insurance'],
111
- '考勤': ['attendance', 'Attendance', 'clock', 'Clock', 'time/clock', 'TimeClock'],
112
- '打卡': ['clock', 'Clock', 'photoClock', 'PhotoClock'],
111
+ '考勤': ['attendance', 'Attendance', 'Attend', 'timeManage', 'TimeWork'],
112
+ '打卡': ['clock', 'Clock', 'photoClock', 'PhotoClock', 'time/clock', 'TimeClock'],
113
113
  '车辆': ['vehicle', 'Vehicle', 'veh', 'vehlife', '/veh/'],
114
114
  '品质': ['quality', 'Quality', 'QualityInspection', 'patrol', 'Patrol'],
115
115
  '巡查': ['inspection', 'Inspection', 'patrol', 'Patrol'],