@hupan56/wlkj 2.0.0

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 (87) hide show
  1. package/bin/cli.js +213 -0
  2. package/package.json +11 -0
  3. package/templates/cli.js +198 -0
  4. package/templates/qoder/commands/wl-code.md +43 -0
  5. package/templates/qoder/commands/wl-commit.md +30 -0
  6. package/templates/qoder/commands/wl-init.md +80 -0
  7. package/templates/qoder/commands/wl-insight.md +51 -0
  8. package/templates/qoder/commands/wl-prd.md +199 -0
  9. package/templates/qoder/commands/wl-report.md +166 -0
  10. package/templates/qoder/commands/wl-search.md +52 -0
  11. package/templates/qoder/commands/wl-spec.md +18 -0
  12. package/templates/qoder/commands/wl-status.md +51 -0
  13. package/templates/qoder/commands/wl-task.md +71 -0
  14. package/templates/qoder/commands/wl-test.md +42 -0
  15. package/templates/qoder/config.toml +5 -0
  16. package/templates/qoder/config.yaml +141 -0
  17. package/templates/qoder/hooks/inject-workflow-state.py +117 -0
  18. package/templates/qoder/hooks/session-start.py +204 -0
  19. package/templates/qoder/rules/wl-pipeline.md +105 -0
  20. package/templates/qoder/scripts/add_session.py +245 -0
  21. package/templates/qoder/scripts/benchmark.py +209 -0
  22. package/templates/qoder/scripts/build_style_index.py +268 -0
  23. package/templates/qoder/scripts/code_index.py +41 -0
  24. package/templates/qoder/scripts/collect_prds.py +31 -0
  25. package/templates/qoder/scripts/common/__init__.py +0 -0
  26. package/templates/qoder/scripts/common/active_task.py +230 -0
  27. package/templates/qoder/scripts/common/atomicio.py +172 -0
  28. package/templates/qoder/scripts/common/developer.py +161 -0
  29. package/templates/qoder/scripts/common/eval_api.py +144 -0
  30. package/templates/qoder/scripts/common/feishu.py +278 -0
  31. package/templates/qoder/scripts/common/filelock.py +211 -0
  32. package/templates/qoder/scripts/common/identity.py +285 -0
  33. package/templates/qoder/scripts/common/mentions.py +134 -0
  34. package/templates/qoder/scripts/common/paths.py +311 -0
  35. package/templates/qoder/scripts/common/reqid.py +218 -0
  36. package/templates/qoder/scripts/common/search_engine.py +205 -0
  37. package/templates/qoder/scripts/common/task_utils.py +342 -0
  38. package/templates/qoder/scripts/common/terms.py +234 -0
  39. package/templates/qoder/scripts/common/utf8.py +38 -0
  40. package/templates/qoder/scripts/context_pack.py +196 -0
  41. package/templates/qoder/scripts/eval_prd.py +225 -0
  42. package/templates/qoder/scripts/export.py +487 -0
  43. package/templates/qoder/scripts/git_sync.py +1087 -0
  44. package/templates/qoder/scripts/handoff.py +22 -0
  45. package/templates/qoder/scripts/init_developer.py +76 -0
  46. package/templates/qoder/scripts/init_doctor.py +527 -0
  47. package/templates/qoder/scripts/install_qoderwork.py +339 -0
  48. package/templates/qoder/scripts/learn.py +67 -0
  49. package/templates/qoder/scripts/notify.py +5 -0
  50. package/templates/qoder/scripts/parse_prds.py +33 -0
  51. package/templates/qoder/scripts/report.py +281 -0
  52. package/templates/qoder/scripts/role.py +39 -0
  53. package/templates/qoder/scripts/run_weekly_update.bat +17 -0
  54. package/templates/qoder/scripts/run_weekly_update.sh +20 -0
  55. package/templates/qoder/scripts/search_index.py +352 -0
  56. package/templates/qoder/scripts/setup.py +453 -0
  57. package/templates/qoder/scripts/setup_weekly_cron.bat +22 -0
  58. package/templates/qoder/scripts/setup_weekly_cron.sh +19 -0
  59. package/templates/qoder/scripts/status.py +389 -0
  60. package/templates/qoder/scripts/syncgate.py +330 -0
  61. package/templates/qoder/scripts/task.py +954 -0
  62. package/templates/qoder/scripts/team.py +29 -0
  63. package/templates/qoder/scripts/team_sync.py +419 -0
  64. package/templates/qoder/scripts/workspace_init.py +102 -0
  65. package/templates/qoder/settings.json +53 -0
  66. package/templates/qoder/skills/design-review/SKILL.md +25 -0
  67. package/templates/qoder/skills/prd-generator/SKILL.md +180 -0
  68. package/templates/qoder/skills/prd-review/SKILL.md +36 -0
  69. package/templates/qoder/skills/prototype-generator/SKILL.md +141 -0
  70. package/templates/qoder/skills/spec-coder/SKILL.md +69 -0
  71. package/templates/qoder/skills/spec-generator/SKILL.md +67 -0
  72. package/templates/qoder/skills/test-generator/SKILL.md +72 -0
  73. package/templates/qoder/skills/wl-commit/SKILL.md +76 -0
  74. package/templates/qoder/skills/wl-init/SKILL.md +67 -0
  75. package/templates/qoder/skills/wl-insight/SKILL.md +81 -0
  76. package/templates/qoder/skills/wl-report/SKILL.md +87 -0
  77. package/templates/qoder/skills/wl-search/SKILL.md +75 -0
  78. package/templates/qoder/skills/wl-status/SKILL.md +61 -0
  79. package/templates/qoder/skills/wl-task/SKILL.md +58 -0
  80. package/templates/qoder/templates/prd-full-template.md +103 -0
  81. package/templates/qoder/templates/prd-quick-template.md +69 -0
  82. package/templates/qoder/templates/prototype-app.html +344 -0
  83. package/templates/qoder/templates/prototype-web.html +310 -0
  84. package/templates/root/AGENTS.md +182 -0
  85. package/templates/root/README-pipeline.md +56 -0
  86. package/templates/root/ROLES.md +85 -0
  87. package/templates/root//346/226/260/346/211/213/346/214/207/345/215/227.md +186 -0
@@ -0,0 +1,389 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ status.py - 项目状态计算引擎 (支撑 /wl-status 命令)
5
+
6
+ /wl-status 的 markdown 指令由 AI 执行, 但数据计算 (健康分/周期时间/阻塞图/
7
+ 截止日期) 由本脚本提供, 保证数据准确。
8
+
9
+ Usage:
10
+ python status.py # 全景: current + roadmap + health
11
+ python status.py health # 只算健康分
12
+ python status.py cycle # 只算周期时间 (从 stage_ts)
13
+ python status.py deadlines [--days 7] # 未来 N 天的截止任务
14
+ python status.py blocked # 被阻塞的任务图
15
+ """
16
+
17
+ import argparse
18
+ import json
19
+ import os
20
+ import sys
21
+ from datetime import datetime, date, timedelta
22
+ from pathlib import Path
23
+
24
+ try:
25
+ sys.stdout.reconfigure(encoding='utf-8', errors='replace')
26
+ except (AttributeError, TypeError, OSError):
27
+ pass
28
+
29
+ THIS_DIR = os.path.dirname(os.path.abspath(__file__))
30
+ sys.path.insert(0, THIS_DIR)
31
+ from common.paths import get_repo_root, get_developer, get_tasks_dir
32
+ from common.task_utils import load_task_json
33
+ from common.atomicio import safe_read_json
34
+
35
+ BASE = get_repo_root()
36
+ INDEX_DIR = BASE / 'data' / 'index'
37
+
38
+
39
+ def load_all_tasks():
40
+ """加载所有活跃任务 (workspace/tasks/)。"""
41
+ tasks_dir = get_tasks_dir(BASE)
42
+ if not tasks_dir.is_dir():
43
+ return {}
44
+ out = {}
45
+ for d in tasks_dir.iterdir():
46
+ if d.is_dir():
47
+ data = load_task_json(d)
48
+ if data:
49
+ out[d.name] = data
50
+ return out
51
+
52
+
53
+ # ============================================================
54
+ # 周期时间分析 (B4: cycle time)
55
+ # ============================================================
56
+
57
+ def compute_cycle_times(tasks):
58
+ """从 stage_ts 算各阶段耗时。
59
+
60
+ Returns:
61
+ {
62
+ "avg_total_hours": float, # created -> completed 平均
63
+ "avg_dev_hours": float, # started -> completed 平均 (纯开发)
64
+ "samples": int, # 有完整时间戳的任务数
65
+ "by_task": [{name, total_h, dev_h, ...}]
66
+ }
67
+ """
68
+ samples = []
69
+ for name, data in tasks.items():
70
+ if data.get("status") != "completed":
71
+ continue
72
+ ts = data.get("stage_ts") or {}
73
+ created = ts.get("created")
74
+ started = ts.get("started")
75
+ completed = ts.get("completed")
76
+ if not created or not completed:
77
+ continue
78
+ try:
79
+ t_created = datetime.fromisoformat(created)
80
+ t_completed = datetime.fromisoformat(completed)
81
+ total_h = (t_completed - t_created).total_seconds() / 3600
82
+ dev_h = None
83
+ if started:
84
+ t_started = datetime.fromisoformat(started)
85
+ dev_h = (t_completed - t_started).total_seconds() / 3600
86
+ samples.append({
87
+ "name": name,
88
+ "title": data.get("title", name),
89
+ "total_hours": round(total_h, 1),
90
+ "dev_hours": round(dev_h, 1) if dev_h is not None else None,
91
+ })
92
+ except (ValueError, TypeError):
93
+ continue
94
+
95
+ if not samples:
96
+ return {"avg_total_hours": 0, "avg_dev_hours": 0, "samples": 0, "by_task": []}
97
+
98
+ avg_total = sum(s["total_hours"] for s in samples) / len(samples)
99
+ dev_samples = [s for s in samples if s["dev_hours"] is not None]
100
+ avg_dev = sum(s["dev_hours"] for s in dev_samples) / len(dev_samples) if dev_samples else 0
101
+
102
+ return {
103
+ "avg_total_hours": round(avg_total, 1),
104
+ "avg_dev_hours": round(avg_dev, 1),
105
+ "samples": len(samples),
106
+ "by_task": sorted(samples, key=lambda x: -x["total_hours"])[:10],
107
+ }
108
+
109
+
110
+ # ============================================================
111
+ # 截止日期提醒 (B4: deadlines)
112
+ # ============================================================
113
+
114
+ def compute_deadlines(tasks, days=7):
115
+ """未来 N 天内到期的任务 + 已逾期任务。
116
+
117
+ Returns:
118
+ {
119
+ "overdue": [...], # 已逾期未完成
120
+ "due_soon": [...], # 未来 days 天内到期
121
+ "no_date_count": int, # 无截止日期的活跃任务数
122
+ }
123
+ """
124
+ today = date.today()
125
+ horizon = today + timedelta(days=days)
126
+ overdue = []
127
+ due_soon = []
128
+ no_date = 0
129
+
130
+ for name, data in tasks.items():
131
+ if data.get("status") == "completed":
132
+ continue
133
+ due = data.get("due_date")
134
+ if not due:
135
+ no_date += 1
136
+ continue
137
+ try:
138
+ due_d = date.fromisoformat(due)
139
+ except ValueError:
140
+ continue
141
+ item = {
142
+ "name": name,
143
+ "title": data.get("title", name),
144
+ "due_date": due,
145
+ "assignee": data.get("assignee", "?"),
146
+ "priority": data.get("priority", "?"),
147
+ "days_left": (due_d - today).days,
148
+ }
149
+ if due_d < today:
150
+ overdue.append(item)
151
+ elif due_d <= horizon:
152
+ due_soon.append(item)
153
+
154
+ overdue.sort(key=lambda x: x["days_left"])
155
+ due_soon.sort(key=lambda x: x["days_left"])
156
+ return {
157
+ "overdue": overdue,
158
+ "due_soon": due_soon,
159
+ "no_date_count": no_date,
160
+ }
161
+
162
+
163
+ # ============================================================
164
+ # 阻塞图 (B4: blocked)
165
+ # ============================================================
166
+
167
+ def compute_blocked_graph(tasks):
168
+ """分析任务间的阻塞关系。
169
+
170
+ Returns:
171
+ {
172
+ "blocked_tasks": [{name, blocked_by: [...open...], title}],
173
+ "blocking_count": {blocker_name: count}, # 谁阻塞了最多任务
174
+ }
175
+ """
176
+ blocked = []
177
+ blocking_count = {}
178
+ for name, data in tasks.items():
179
+ if data.get("status") == "completed":
180
+ continue
181
+ blocked_by = data.get("blocked_by") or []
182
+ open_blocks = []
183
+ for dep in blocked_by:
184
+ dep_data = tasks.get(dep)
185
+ if not dep_data or dep_data.get("status") != "completed":
186
+ open_blocks.append(dep)
187
+ blocking_count[dep] = blocking_count.get(dep, 0) + 1
188
+ if open_blocks:
189
+ blocked.append({
190
+ "name": name,
191
+ "title": data.get("title", name),
192
+ "blocked_by": open_blocks,
193
+ "assignee": data.get("assignee", "?"),
194
+ })
195
+ return {
196
+ "blocked_tasks": blocked,
197
+ "blocking_count": dict(sorted(blocking_count.items(), key=lambda x: -x[1])[:5]),
198
+ }
199
+
200
+
201
+ # ============================================================
202
+ # 健康分 (综合)
203
+ # ============================================================
204
+
205
+ def compute_health(tasks):
206
+ """加权健康分 (满分 5)。
207
+
208
+ 维度: EVA 合格率 / 索引新鲜度 / 按时交付 / 团队同步 / 流水线 / 学习
209
+ """
210
+ scores = {}
211
+
212
+ # 1. EVA 合格率 (25%)
213
+ # eval-history 是 jsonl (每行一个 JSON), 不能用 safe_read_json
214
+ eval_path = BASE / '.qoder' / 'learning' / 'eval-history.jsonl'
215
+ if eval_path.is_file():
216
+ records = []
217
+ try:
218
+ with open(eval_path, encoding='utf-8') as f:
219
+ for line in f:
220
+ line = line.strip()
221
+ if line:
222
+ records.append(json.loads(line))
223
+ recent = records[-10:]
224
+ if recent:
225
+ passed = sum(1 for r in recent if r.get('passed'))
226
+ scores['eva'] = (passed / len(recent)) * 5
227
+ else:
228
+ scores['eva'] = 3.0 # 无数据, 中性
229
+ except Exception:
230
+ scores['eva'] = 3.0
231
+ else:
232
+ scores['eva'] = 3.0
233
+
234
+ # 2. 索引新鲜度 (20%)
235
+ meta = safe_read_json(INDEX_DIR / '.index-meta.json', default={}) or {}
236
+ last_sync = meta.get('last_sync', '')
237
+ try:
238
+ ts = str(last_sync).replace('T', ' ').split('.')[0].strip()
239
+ sync_dt = datetime.strptime(ts, '%Y-%m-%d %H:%M')
240
+ age_days = (datetime.now() - sync_dt).days
241
+ if age_days <= 7:
242
+ scores['index'] = 5.0
243
+ elif age_days <= 14:
244
+ scores['index'] = 3.0
245
+ else:
246
+ scores['index'] = 1.0
247
+ except Exception:
248
+ scores['index'] = 2.0
249
+
250
+ # 3. 按时交付 (20%) - 从 deadlines 算逾期率
251
+ deadlines = compute_deadlines(tasks, days=0)
252
+ total_with_due = len(deadlines['overdue']) + len(deadlines['due_soon']) + 1
253
+ overdue_rate = len(deadlines['overdue']) / total_with_due if total_with_due else 0
254
+ scores['on_time'] = max(0, 5 - overdue_rate * 10)
255
+
256
+ # 4. 团队同步 (15%) - 简化: 有 ahead 提交 = 有未同步
257
+ scores['sync'] = 4.0 # 默认良好 (详细检查由 team_sync status 做)
258
+
259
+ # 5. 流水线流转 (10%) - 任务在各阶段分布
260
+ statuses = {}
261
+ for data in tasks.values():
262
+ s = data.get('status', '?')
263
+ statuses[s] = statuses.get(s, 0) + 1
264
+ # 有 in_progress 且不全卡 planning = 健康
265
+ if statuses.get('in_progress', 0) > 0:
266
+ scores['pipeline'] = 4.0
267
+ elif statuses.get('planning', 0) > 0:
268
+ scores['pipeline'] = 3.0
269
+ else:
270
+ scores['pipeline'] = 2.0
271
+
272
+ # 6. 学习 (10%)
273
+ feedback_path = BASE / '.qoder' / 'learning' / 'feedback.jsonl'
274
+ if feedback_path.is_file():
275
+ try:
276
+ with open(feedback_path, encoding='utf-8') as f:
277
+ fb_count = sum(1 for _ in f)
278
+ scores['learning'] = min(5.0, 2.0 + fb_count * 0.1)
279
+ except Exception:
280
+ scores['learning'] = 2.0
281
+ else:
282
+ scores['learning'] = 2.0
283
+
284
+ # 加权
285
+ weights = {
286
+ 'eva': 0.25, 'index': 0.20, 'on_time': 0.20,
287
+ 'sync': 0.15, 'pipeline': 0.10, 'learning': 0.10,
288
+ }
289
+ total = sum(scores[k] * weights[k] for k in weights)
290
+ return {
291
+ 'total': round(total, 2),
292
+ 'scores': scores,
293
+ 'weights': weights,
294
+ 'verdict': '健康' if total >= 4 else ('有风险' if total >= 3 else '需关注'),
295
+ }
296
+
297
+
298
+ # ============================================================
299
+ # 渲染
300
+ # ============================================================
301
+
302
+ def render_full():
303
+ tasks = load_all_tasks()
304
+ print('=' * 50)
305
+ print('项目状态总览')
306
+ print('=' * 50)
307
+ print(f'\n活跃任务: {len(tasks)} 个')
308
+ dev = get_developer(BASE)
309
+ if dev:
310
+ my_tasks = [n for n, d in tasks.items() if dev in (d.get('assignee'), d.get('creator'))]
311
+ print(f'我的任务: {len(my_tasks)} 个 (开发者: {dev})')
312
+
313
+ # 健康分
314
+ print('\n--- 健康度 ---')
315
+ health = compute_health(tasks)
316
+ print(f"总分: {health['total']}/5 ({health['verdict']})")
317
+ for k, v in health['scores'].items():
318
+ w = health['weights'][k]
319
+ print(f" {k:12} {v:.1f} × {w:.0%}")
320
+
321
+ # 阻塞
322
+ print('\n--- 阻塞图 ---')
323
+ bg = compute_blocked_graph(tasks)
324
+ if bg['blocked_tasks']:
325
+ print(f"被阻塞的任务: {len(bg['blocked_tasks'])} 个")
326
+ for bt in bg['blocked_tasks'][:10]:
327
+ print(f" {bt['name']} ({bt['assignee']}) <- {', '.join(bt['blocked_by'])}")
328
+ if bg['blocking_count']:
329
+ print(f"阻塞最多的: {bg['blocking_count']}")
330
+ else:
331
+ print('无被阻塞的任务')
332
+
333
+ # 截止日期
334
+ print('\n--- 截止日期 (未来 7 天) ---')
335
+ dl = compute_deadlines(tasks, days=7)
336
+ if dl['overdue']:
337
+ print(f"⚠️ 已逾期: {len(dl['overdue'])} 个")
338
+ for item in dl['overdue'][:5]:
339
+ print(f" {item['name']} ({item['days_left']}天前) [{item['priority']}] {item['title']}")
340
+ if dl['due_soon']:
341
+ print(f"即将到期: {len(dl['due_soon'])} 个")
342
+ for item in dl['due_soon'][:5]:
343
+ print(f" {item['name']} (还剩{item['days_left']}天) [{item['priority']}] {item['title']}")
344
+ if not dl['overdue'] and not dl['due_soon']:
345
+ print('未来 7 天无截止任务')
346
+ print(f"无截止日期的活跃任务: {dl['no_date_count']} 个")
347
+
348
+ # 周期时间
349
+ print('\n--- 周期时间 ---')
350
+ ct = compute_cycle_times(tasks)
351
+ if ct['samples'] > 0:
352
+ print(f"已完成样本: {ct['samples']} 个")
353
+ print(f"平均总周期: {ct['avg_total_hours']} 小时 ({ct['avg_total_hours']/24:.1f} 天)")
354
+ print(f"平均开发时长: {ct['avg_dev_hours']} 小时")
355
+ if ct['by_task']:
356
+ print("最慢的 5 个:")
357
+ for s in ct['by_task'][:5]:
358
+ print(f" {s['name']}: {s['total_hours']}h (开发 {s['dev_hours']}h)")
359
+ else:
360
+ print('暂无已完成任务的时间戳数据 (stage_ts 在 B3 后才有)')
361
+
362
+
363
+ def main():
364
+ parser = argparse.ArgumentParser(description='项目状态计算')
365
+ parser.add_argument('scope', nargs='?', default='all',
366
+ choices=['all', 'health', 'cycle', 'deadlines', 'blocked'])
367
+ parser.add_argument('--days', type=int, default=7, help='截止日期 horizon 天数')
368
+ args = parser.parse_args()
369
+
370
+ tasks = load_all_tasks()
371
+
372
+ if args.scope == 'health':
373
+ h = compute_health(tasks)
374
+ print(json.dumps(h, indent=2, ensure_ascii=False))
375
+ elif args.scope == 'cycle':
376
+ c = compute_cycle_times(tasks)
377
+ print(json.dumps(c, indent=2, ensure_ascii=False))
378
+ elif args.scope == 'deadlines':
379
+ d = compute_deadlines(tasks, args.days)
380
+ print(json.dumps(d, indent=2, ensure_ascii=False))
381
+ elif args.scope == 'blocked':
382
+ b = compute_blocked_graph(tasks)
383
+ print(json.dumps(b, indent=2, ensure_ascii=False))
384
+ else:
385
+ render_full()
386
+
387
+
388
+ if __name__ == '__main__':
389
+ main()