@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,162 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """task_query.py - task 查询层 (v3.0 f6 从 task.py 拆出)"""
4
+ # v3.0 路径自举
5
+ import os as _o, sys as _s, argparse
6
+ from pathlib import Path
7
+ from datetime import datetime
8
+ _cp = _o.path.join(_o.path.dirname(_o.path.dirname(_o.path.abspath(__file__))), 'foundation')
9
+ if _cp not in _s.path: _s.path.insert(0, _cp)
10
+ from bootstrap import setup; setup()
11
+ from foundation.core.paths import get_repo_root
12
+
13
+ import os, sys, json, shutil
14
+ from datetime import datetime
15
+ from pathlib import Path
16
+ from foundation.core.paths import (
17
+ DIR_TASKS, DIR_ARCHIVE, FILE_TASK_JSON,
18
+ get_repo_root, get_developer, get_tasks_dir,
19
+ )
20
+ from foundation.integrations.active_task import (
21
+ clear_active_task, resolve_active_task, resolve_context_key, set_active_task,
22
+ )
23
+ from foundation.data.task_utils import (
24
+ resolve_task_dir, run_task_hooks, load_task_json, write_task_json,
25
+ modify_task_json, assert_can_modify_task,
26
+ )
27
+ from foundation.io.atomicio import atomic_write_json, safe_read_json, atomic_write_text
28
+
29
+
30
+ def cmd_list(args: argparse.Namespace) -> int:
31
+ """列出活跃任务。支持 --mine / --status / --ready / --blocked 过滤。"""
32
+ repo_root = get_repo_root()
33
+ tasks_dir = get_tasks_dir(repo_root)
34
+ developer = get_developer(repo_root)
35
+ active = resolve_active_task(repo_root)
36
+
37
+ filter_mine = args.mine
38
+ filter_status = args.status
39
+ filter_ready = getattr(args, "ready", False)
40
+ filter_blocked = getattr(args, "blocked", False)
41
+
42
+ if not tasks_dir.is_dir():
43
+ print("No tasks directory found")
44
+ return 0
45
+
46
+ # 预加载所有任务 (用于 --ready/--blocked 判断依赖是否完成)
47
+ all_tasks = {}
48
+ for d in sorted(tasks_dir.iterdir()):
49
+ if d.is_dir():
50
+ data = load_task_json(d)
51
+ if data:
52
+ all_tasks[d.name] = data
53
+
54
+ def is_dep_completed(dep_name: str) -> bool:
55
+ dep = all_tasks.get(dep_name)
56
+ return bool(dep and dep.get("status") == "completed")
57
+
58
+ tasks = []
59
+ for name, task_data in all_tasks.items():
60
+ d = tasks_dir / name
61
+
62
+ # 过滤: --mine
63
+ if filter_mine and developer not in (
64
+ task_data.get("assignee"), task_data.get("creator")
65
+ ):
66
+ continue
67
+ if filter_status and task_data.get("status") != filter_status:
68
+ continue
69
+
70
+ # --ready: 未完成 且 所有 blocked_by 已完成
71
+ if filter_ready:
72
+ if task_data.get("status") == "completed":
73
+ continue
74
+ blocked_by = task_data.get("blocked_by") or []
75
+ if any(not is_dep_completed(dep) for dep in blocked_by):
76
+ continue
77
+
78
+ # --blocked: 有未完成的 blocked_by
79
+ if filter_blocked:
80
+ blocked_by = task_data.get("blocked_by") or []
81
+ open_blocks = [dep for dep in blocked_by if not is_dep_completed(dep)]
82
+ if not open_blocks:
83
+ continue
84
+
85
+ tasks.append((name, task_data))
86
+
87
+ if not tasks:
88
+ print("No tasks found" + (
89
+ " (no tasks ready to start)" if filter_ready else
90
+ " (no blocked tasks)" if filter_blocked else ""
91
+ ))
92
+ return 0
93
+
94
+ print(f"Tasks ({len(tasks)}):")
95
+ for name, data in tasks:
96
+ is_current = active.task_path and name in active.task_path
97
+ marker = " *" if is_current else " "
98
+ assignee = data.get("assignee", "?")
99
+ status = data.get("status", "?")
100
+ priority = data.get("priority", "?")
101
+ title = data.get("title", name)
102
+ extras = []
103
+ due = data.get("due_date")
104
+ if due:
105
+ extras.append(f"due:{due}")
106
+ blocked_by = data.get("blocked_by") or []
107
+ open_blocks = [dep for dep in blocked_by if not is_dep_completed(dep)]
108
+ if open_blocks:
109
+ extras.append(f"blocked:{','.join(open_blocks)}")
110
+ extra_str = f" [{', '.join(extras)}]" if extras else ""
111
+ print(f"{marker} [{priority}] [{status}] {name} ({assignee}) - {title}{extra_str}")
112
+
113
+ return 0
114
+
115
+
116
+ # =============================================================================
117
+ # 命令: show
118
+ # =============================================================================
119
+
120
+ def cmd_show(args: argparse.Namespace) -> int:
121
+ """显示任务详情。"""
122
+ repo_root = get_repo_root()
123
+ full_path = resolve_task_dir(args.dir, repo_root)
124
+
125
+ if not full_path.is_dir():
126
+ print(f"Error: Task not found: {args.dir}", file=sys.stderr)
127
+ return 1
128
+
129
+ task_data = load_task_json(full_path)
130
+ if not task_data:
131
+ print(f"Error: Missing task.json in {full_path.name}", file=sys.stderr)
132
+ return 1
133
+
134
+ print(f"Task: {full_path.name}")
135
+ for key in ("title", "status", "priority", "rice", "assignee", "creator",
136
+ "created_at", "updated_at", "parent", "tags",
137
+ "due_date", "start_date", "estimate_hours"):
138
+ val = task_data.get(key)
139
+ if val not in (None, [], ""):
140
+ print(f" {key}: {val}")
141
+ children = task_data.get("children") or []
142
+ if children:
143
+ print(f" children: {', '.join(children)}")
144
+ blocked_by = task_data.get("blocked_by") or []
145
+ if blocked_by:
146
+ print(f" blocked_by: {', '.join(blocked_by)}")
147
+ blocks = task_data.get("blocks") or []
148
+ if blocks:
149
+ print(f" blocks: {', '.join(blocks)}")
150
+ stage_ts = task_data.get("stage_ts") or {}
151
+ if any(stage_ts.values()):
152
+ print(f" stage_ts: {stage_ts}")
153
+ prd_file = full_path / "prd.md"
154
+ if prd_file.is_file():
155
+ print(f" prd: workspace/tasks/{full_path.name}/prd.md")
156
+ return 0
157
+
158
+
159
+ # =============================================================================
160
+ # 命令: add-subtask / remove-subtask
161
+ # =============================================================================
162
+
@@ -0,0 +1,425 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """task_relations.py - task 关系/排期层 (v3.0 f6 从 task.py 拆出)"""
4
+ # v3.0 路径自举
5
+ import os as _o, sys as _s, argparse
6
+ from pathlib import Path
7
+ from datetime import datetime
8
+ _cp = _o.path.join(_o.path.dirname(_o.path.dirname(_o.path.abspath(__file__))), 'foundation')
9
+ if _cp not in _s.path: _s.path.insert(0, _cp)
10
+ from bootstrap import setup; setup()
11
+ from foundation.core.paths import get_repo_root
12
+
13
+ import os, sys, json, shutil
14
+ from datetime import datetime
15
+ from pathlib import Path
16
+ from foundation.core.paths import (
17
+ DIR_TASKS, DIR_ARCHIVE, FILE_TASK_JSON,
18
+ get_repo_root, get_developer, get_tasks_dir,
19
+ )
20
+ from foundation.integrations.active_task import (
21
+ clear_active_task, resolve_active_task, resolve_context_key, set_active_task,
22
+ )
23
+ from foundation.data.task_utils import (
24
+ resolve_task_dir, run_task_hooks, load_task_json, write_task_json,
25
+ modify_task_json, assert_can_modify_task,
26
+ )
27
+ from foundation.io.atomicio import atomic_write_json, safe_read_json, atomic_write_text
28
+
29
+
30
+ def cmd_add_subtask(args: argparse.Namespace) -> int:
31
+ """添加子任务关联。"""
32
+ repo_root = get_repo_root()
33
+ parent_dir = resolve_task_dir(args.parent, repo_root)
34
+ child_dir = resolve_task_dir(args.child, repo_root)
35
+
36
+ if not parent_dir.is_dir():
37
+ print(f"Error: Parent task not found: {args.parent}", file=sys.stderr)
38
+ return 1
39
+ if not child_dir.is_dir():
40
+ print(f"Error: Child task not found: {args.child}", file=sys.stderr)
41
+ return 1
42
+
43
+ # 零信任 ACL: 只有 parent 的 creator/assignee/admin 能加子任务
44
+ try:
45
+ assert_can_modify_task(parent_dir, "add-subtask", repo_root)
46
+ except PermissionError as e:
47
+ print(str(e), file=sys.stderr)
48
+ return 4
49
+
50
+ parent_data = load_task_json(parent_dir)
51
+ child_data = load_task_json(child_dir)
52
+
53
+ if not parent_data or not child_data:
54
+ print("Error: Missing task.json", file=sys.stderr)
55
+ return 1
56
+
57
+ child_name = child_dir.name
58
+ children = parent_data.get("children", [])
59
+ if child_name not in children:
60
+ children.append(child_name)
61
+ parent_data["children"] = children
62
+ parent_data["updated_at"] = datetime.now().isoformat()
63
+ write_task_json(parent_dir, parent_data)
64
+
65
+ # 子任务记录父任务
66
+ child_data["parent"] = parent_dir.name
67
+ child_data["updated_at"] = datetime.now().isoformat()
68
+ write_task_json(child_dir, child_data)
69
+
70
+ print(f"Subtask added: {parent_dir.name} -> {child_dir.name}")
71
+ return 0
72
+
73
+
74
+ def cmd_remove_subtask(args: argparse.Namespace) -> int:
75
+ """移除子任务关联。"""
76
+ repo_root = get_repo_root()
77
+ parent_dir = resolve_task_dir(args.parent, repo_root)
78
+ child_dir = resolve_task_dir(args.child, repo_root)
79
+
80
+ # 零信任 ACL
81
+ if parent_dir.is_dir():
82
+ try:
83
+ assert_can_modify_task(parent_dir, "remove-subtask", repo_root)
84
+ except PermissionError as e:
85
+ print(str(e), file=sys.stderr)
86
+ return 4
87
+
88
+ parent_data = load_task_json(parent_dir)
89
+ child_data = load_task_json(child_dir)
90
+
91
+ if parent_data:
92
+ children = parent_data.get("children", [])
93
+ if child_dir.name in children:
94
+ children.remove(child_dir.name)
95
+ parent_data["children"] = children
96
+ parent_data["updated_at"] = datetime.now().isoformat()
97
+ write_task_json(parent_dir, parent_data)
98
+
99
+ if child_data:
100
+ child_data["parent"] = None
101
+ child_data["updated_at"] = datetime.now().isoformat()
102
+ write_task_json(child_dir, child_data)
103
+
104
+ print(f"Subtask removed: {parent_dir.name} -x- {child_dir.name}")
105
+ return 0
106
+
107
+
108
+ # =============================================================================
109
+ # 工具函数
110
+ # =============================================================================
111
+
112
+ def _generate_slug(title: str) -> str:
113
+ """从标题生成 URL-safe slug。"""
114
+ # 简单处理: 小写, 空格转连字符, 只保留字母数字和连字符
115
+ slug = title.lower().strip()
116
+ slug = "".join(c if c.isalnum() or c in " -_" else "" for c in slug)
117
+ slug = slug.replace(" ", "-").replace("_", "-")
118
+ slug = "-".join(part for part in slug.split("-") if part)
119
+ return slug[:50] if slug else "untitled"
120
+
121
+
122
+ # =============================================================================
123
+ # 命令: set-due / block / unblock / gantt (B2 能力增强)
124
+ # =============================================================================
125
+
126
+ def cmd_set_due(args: argparse.Namespace) -> int:
127
+ """设置任务截止日期。用法: task.py set-due <task> <YYYY-MM-DD>"""
128
+ repo_root = get_repo_root()
129
+ full_path = resolve_task_dir(args.task, repo_root)
130
+ if not full_path.is_dir():
131
+ print(f"Error: Task not found: {args.task}", file=sys.stderr)
132
+ return 1
133
+ # ACL
134
+ try:
135
+ assert_can_modify_task(full_path, "set-due", repo_root)
136
+ except PermissionError as e:
137
+ print(str(e), file=sys.stderr)
138
+ return 4
139
+ def _set_due_mutator(data):
140
+ data["due_date"] = args.due_date
141
+ data["updated_at"] = datetime.now().isoformat()
142
+ if not modify_task_json(full_path, _set_due_mutator):
143
+ print("Error: Missing task.json", file=sys.stderr)
144
+ return 1
145
+ print(f"Set due_date: {args.due_date} ({full_path.name})")
146
+ return 0
147
+
148
+
149
+ def cmd_reassign(args: argparse.Namespace) -> int:
150
+ """改派任务负责人。用法: task.py reassign <task> <new_assignee>
151
+
152
+ 场景: PM 建任务后改派给开发(多角色协作必需)。
153
+ ACL: 只有 creator/admin 能改派(防恶意抢占/甩锅)。
154
+ 保留改派历史(assignee_history), 便于追溯。
155
+ """
156
+ repo_root = get_repo_root()
157
+ full_path = resolve_task_dir(args.task, repo_root)
158
+ if not full_path.is_dir():
159
+ print(f"Error: Task not found: {args.task}", file=sys.stderr)
160
+ return 1
161
+ # ACL: 只有 creator/admin 能改派 (assignee 自己不能转手甩锅)
162
+ try:
163
+ assert_can_modify_task(full_path, "reassign", repo_root)
164
+ except PermissionError as e:
165
+ print(str(e), file=sys.stderr)
166
+ return 4
167
+
168
+ new_assignee = (args.new_assignee or "").strip()
169
+ if not new_assignee:
170
+ print("Error: new_assignee 不能为空", file=sys.stderr)
171
+ return 1
172
+
173
+ changed = {"from": None, "to": new_assignee}
174
+
175
+ def _reassign_mutator(data):
176
+ old = data.get("assignee")
177
+ changed["from"] = old
178
+ data["assignee"] = new_assignee
179
+ data["updated_at"] = datetime.now().isoformat()
180
+ # 记录改派历史 (追溯用)
181
+ hist = data.get("assignee_history") or []
182
+ hist.append({
183
+ "from": old,
184
+ "to": new_assignee,
185
+ "by": get_developer(repo_root),
186
+ "at": datetime.now().isoformat(),
187
+ })
188
+ data["assignee_history"] = hist
189
+
190
+ if not modify_task_json(full_path, _reassign_mutator):
191
+ print("Error: Missing task.json", file=sys.stderr)
192
+ return 1
193
+
194
+ if changed["from"] == new_assignee:
195
+ print(f"Note: assignee 已是 {new_assignee}, 无变化 ({full_path.name})")
196
+ else:
197
+ print(f"Reassigned: {changed['from']} -> {new_assignee} ({full_path.name})")
198
+ print(f" 改派人: {get_developer(repo_root)} (已记录到 assignee_history)")
199
+ return 0
200
+
201
+
202
+ def cmd_block(args: argparse.Namespace) -> int:
203
+ """标记任务被另一任务阻塞。自动维护反向 blocks 关系。
204
+ 用法: task.py block <task> <blocked-by-task>
205
+ 语义: <task> 被 <blocked-by-task> 阻塞 (要等它完成)。
206
+ """
207
+ repo_root = get_repo_root()
208
+ task_dir = resolve_task_dir(args.task, repo_root)
209
+ blocker_dir = resolve_task_dir(args.blocker, repo_root)
210
+ if not task_dir.is_dir():
211
+ print(f"Error: Task not found: {args.task}", file=sys.stderr)
212
+ return 1
213
+ if not blocker_dir.is_dir():
214
+ print(f"Error: Blocker task not found: {args.blocker}", file=sys.stderr)
215
+ return 1
216
+ # ACL: 只有 task 的 owner 能加依赖
217
+ try:
218
+ assert_can_modify_task(task_dir, "block", repo_root)
219
+ except PermissionError as e:
220
+ print(str(e), file=sys.stderr)
221
+ return 4
222
+
223
+ # 检测循环依赖
224
+ if _would_create_cycle(task_dir.name, blocker_dir.name, repo_root):
225
+ print(f"Error: 循环依赖检测到 ({blocker_dir.name} 已直接/间接依赖 {task_dir.name})",
226
+ file=sys.stderr)
227
+ return 1
228
+
229
+ # 检查是否已存在依赖关系 (先读, 幂等)
230
+ task_data = load_task_json(task_dir)
231
+ if not task_data:
232
+ print("Error: Missing task.json", file=sys.stderr)
233
+ return 1
234
+ if blocker_dir.name in (task_data.get("blocked_by") or []):
235
+ print(f"Already blocked by {blocker_dir.name}")
236
+ return 0
237
+
238
+ # 锁内更新 task.blocked_by
239
+ def _block_mutator(data):
240
+ blocked_by = data.get("blocked_by") or []
241
+ if blocker_dir.name not in blocked_by:
242
+ blocked_by.append(blocker_dir.name)
243
+ data["blocked_by"] = blocked_by
244
+ data["updated_at"] = datetime.now().isoformat()
245
+ modify_task_json(task_dir, _block_mutator)
246
+
247
+ # 锁内反向更新 blocker.blocks
248
+ def _block_reverse_mutator(data):
249
+ blocks = data.get("blocks") or []
250
+ if task_dir.name not in blocks:
251
+ blocks.append(task_dir.name)
252
+ data["blocks"] = blocks
253
+ data["updated_at"] = datetime.now().isoformat()
254
+ modify_task_json(blocker_dir, _block_reverse_mutator)
255
+
256
+ print(f"{task_dir.name} now blocked by {blocker_dir.name}")
257
+ return 0
258
+
259
+
260
+ def cmd_unblock(args: argparse.Namespace) -> int:
261
+ """移除阻塞关系。用法: task.py unblock <task> <blocked-by-task>"""
262
+ repo_root = get_repo_root()
263
+ task_dir = resolve_task_dir(args.task, repo_root)
264
+ blocker_dir = resolve_task_dir(args.blocker, repo_root)
265
+ if task_dir.is_dir():
266
+ try:
267
+ assert_can_modify_task(task_dir, "unblock", repo_root)
268
+ except PermissionError as e:
269
+ print(str(e), file=sys.stderr)
270
+ return 4
271
+
272
+ removed = False
273
+ # 锁内从 task.blocked_by 删
274
+ if task_dir.is_dir():
275
+ def _unblock_mutator(data):
276
+ nonlocal removed
277
+ if blocker_dir.name in (data.get("blocked_by") or []):
278
+ data["blocked_by"] = [x for x in (data.get("blocked_by") or []) if x != blocker_dir.name]
279
+ data["updated_at"] = datetime.now().isoformat()
280
+ removed = True
281
+ modify_task_json(task_dir, _unblock_mutator)
282
+
283
+ # 锁内从 blocker.blocks 删
284
+ if blocker_dir.is_dir():
285
+ def _unblock_reverse_mutator(data):
286
+ nonlocal removed
287
+ if task_dir.name in (data.get("blocks") or []):
288
+ data["blocks"] = [x for x in (data.get("blocks") or []) if x != task_dir.name]
289
+ data["updated_at"] = datetime.now().isoformat()
290
+ removed = True
291
+ modify_task_json(blocker_dir, _unblock_reverse_mutator)
292
+
293
+ if removed:
294
+ print(f"Removed block: {task_dir.name} no longer blocked by {blocker_dir.name}")
295
+ else:
296
+ print(f"No such block relationship found")
297
+ return 0
298
+
299
+
300
+ def _would_create_cycle(task_name: str, blocker_name: str, repo_root) -> bool:
301
+ """检测添加 task←blocker 后是否会形成循环依赖。
302
+ 即: blocker 是否(直接/间接)已经依赖 task?
303
+ """
304
+ if task_name == blocker_name:
305
+ return True
306
+ # BFS: 从 blocker 出发, 沿 blocked_by 走, 看能否回到 task
307
+ visited = set()
308
+ queue = [blocker_name]
309
+ tasks_dir = get_tasks_dir(repo_root)
310
+ while queue:
311
+ current = queue.pop(0)
312
+ if current in visited:
313
+ continue
314
+ visited.add(current)
315
+ if current == task_name:
316
+ return True
317
+ cur_dir = tasks_dir / current
318
+ data = load_task_json(cur_dir) if cur_dir.is_dir() else None
319
+ if data:
320
+ for dep in (data.get("blocked_by") or []):
321
+ if dep not in visited:
322
+ queue.append(dep)
323
+ return False
324
+
325
+
326
+ def _is_task_ready(task_data: dict) -> bool:
327
+ """任务是否"可立即开始": 未完成 且 无未完成的 blocked_by。"""
328
+ if task_data.get("status") == "completed":
329
+ return False
330
+ blocked_by = task_data.get("blocked_by") or []
331
+ # blocked_by 为空 = 可开始 (这里简化: 不深入查依赖是否完成, 由调用方保证)
332
+ return len(blocked_by) == 0
333
+
334
+
335
+ def cmd_gantt(args: argparse.Namespace) -> int:
336
+ """渲染 markdown 甘特图 (按 start_date/due_date 排期)。
337
+ 用法: task.py gantt [--weeks 4]
338
+ """
339
+ repo_root = get_repo_root()
340
+ tasks_dir = get_tasks_dir(repo_root)
341
+ if not tasks_dir.is_dir():
342
+ print("No tasks directory")
343
+ return 0
344
+
345
+ from datetime import date, timedelta
346
+ today = date.today()
347
+ weeks = args.weeks if hasattr(args, "weeks") else 4
348
+ end = today + timedelta(weeks=weeks)
349
+
350
+ # 收集有日期的任务
351
+ items = []
352
+ for d in sorted(tasks_dir.iterdir()):
353
+ if not d.is_dir():
354
+ continue
355
+ data = load_task_json(d)
356
+ if not data:
357
+ continue
358
+ due = data.get("due_date")
359
+ start = data.get("start_date")
360
+ if not due and not start:
361
+ continue
362
+ items.append({
363
+ "name": d.name,
364
+ "title": data.get("title", d.name),
365
+ "start": start,
366
+ "due": due,
367
+ "status": data.get("status", "?"),
368
+ "assignee": data.get("assignee", "?"),
369
+ })
370
+
371
+ if not items:
372
+ print("No tasks with dates. 用 'task.py set-due <task> <YYYY-MM-DD>' 添加截止日期。")
373
+ return 0
374
+
375
+ # 按开始/截止日期排序
376
+ items.sort(key=lambda x: (x.get("start") or x.get("due") or "9999"))
377
+
378
+ print(f"# 任务排期 ({today} ~ {end})\n")
379
+ print("| 任务 | 负责人 | 开始 | 截止 | 状态 |")
380
+ print("|------|--------|------|------|------|")
381
+ for it in items:
382
+ start = it.get("start") or "-"
383
+ due = it.get("due") or "-"
384
+ # 标记逾期
385
+ if it.get("due") and it["status"] != "completed":
386
+ try:
387
+ due_date = date.fromisoformat(it["due"])
388
+ if due_date < today:
389
+ due = f"⚠️ {due} (逾期)"
390
+ except ValueError:
391
+ pass
392
+ print(f"| {it['name']} | {it['assignee']} | {start} | {due} | {it['status']} |")
393
+
394
+ # 简单时间轴 (ASCII)
395
+ print(f"\n## 时间轴 ({weeks} 周)\n")
396
+ print("```")
397
+ for it in items:
398
+ start_s = it.get("start") or it.get("due")
399
+ if not start_s:
400
+ continue
401
+ try:
402
+ start_d = date.fromisoformat(start_s) if it.get("start") else today
403
+ except ValueError:
404
+ start_d = today
405
+ due_s = it.get("due")
406
+ try:
407
+ due_d = date.fromisoformat(due_s) if due_s else start_d + timedelta(days=7)
408
+ except ValueError:
409
+ due_d = start_d + timedelta(days=7)
410
+ # 计算相对今天的偏移
411
+ start_off = max(0, (start_d - today).days)
412
+ due_off = min(weeks * 7, (due_d - today).days)
413
+ if due_off < 0:
414
+ bar = "[逾期]" + "=" * 5
415
+ else:
416
+ bar = " " * (start_off // 2) + "[" + "=" * max(1, (due_off - start_off) // 2) + "]"
417
+ print(f"{it['name'][:20]:20} {bar}")
418
+ print("```")
419
+ return 0
420
+
421
+
422
+ # =============================================================================
423
+ # Main
424
+ # =============================================================================
425
+