@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.
- package/bin/cli.js +213 -0
- package/package.json +11 -0
- package/templates/cli.js +198 -0
- package/templates/qoder/commands/wl-code.md +43 -0
- package/templates/qoder/commands/wl-commit.md +30 -0
- package/templates/qoder/commands/wl-init.md +80 -0
- package/templates/qoder/commands/wl-insight.md +51 -0
- package/templates/qoder/commands/wl-prd.md +199 -0
- package/templates/qoder/commands/wl-report.md +166 -0
- package/templates/qoder/commands/wl-search.md +52 -0
- package/templates/qoder/commands/wl-spec.md +18 -0
- package/templates/qoder/commands/wl-status.md +51 -0
- package/templates/qoder/commands/wl-task.md +71 -0
- package/templates/qoder/commands/wl-test.md +42 -0
- package/templates/qoder/config.toml +5 -0
- package/templates/qoder/config.yaml +141 -0
- package/templates/qoder/hooks/inject-workflow-state.py +117 -0
- package/templates/qoder/hooks/session-start.py +204 -0
- package/templates/qoder/rules/wl-pipeline.md +105 -0
- package/templates/qoder/scripts/add_session.py +245 -0
- package/templates/qoder/scripts/benchmark.py +209 -0
- package/templates/qoder/scripts/build_style_index.py +268 -0
- package/templates/qoder/scripts/code_index.py +41 -0
- package/templates/qoder/scripts/collect_prds.py +31 -0
- package/templates/qoder/scripts/common/__init__.py +0 -0
- package/templates/qoder/scripts/common/active_task.py +230 -0
- package/templates/qoder/scripts/common/atomicio.py +172 -0
- package/templates/qoder/scripts/common/developer.py +161 -0
- package/templates/qoder/scripts/common/eval_api.py +144 -0
- package/templates/qoder/scripts/common/feishu.py +278 -0
- package/templates/qoder/scripts/common/filelock.py +211 -0
- package/templates/qoder/scripts/common/identity.py +285 -0
- package/templates/qoder/scripts/common/mentions.py +134 -0
- package/templates/qoder/scripts/common/paths.py +311 -0
- package/templates/qoder/scripts/common/reqid.py +218 -0
- package/templates/qoder/scripts/common/search_engine.py +205 -0
- package/templates/qoder/scripts/common/task_utils.py +342 -0
- package/templates/qoder/scripts/common/terms.py +234 -0
- package/templates/qoder/scripts/common/utf8.py +38 -0
- package/templates/qoder/scripts/context_pack.py +196 -0
- package/templates/qoder/scripts/eval_prd.py +225 -0
- package/templates/qoder/scripts/export.py +487 -0
- package/templates/qoder/scripts/git_sync.py +1087 -0
- package/templates/qoder/scripts/handoff.py +22 -0
- package/templates/qoder/scripts/init_developer.py +76 -0
- package/templates/qoder/scripts/init_doctor.py +527 -0
- package/templates/qoder/scripts/install_qoderwork.py +339 -0
- package/templates/qoder/scripts/learn.py +67 -0
- package/templates/qoder/scripts/notify.py +5 -0
- package/templates/qoder/scripts/parse_prds.py +33 -0
- package/templates/qoder/scripts/report.py +281 -0
- package/templates/qoder/scripts/role.py +39 -0
- package/templates/qoder/scripts/run_weekly_update.bat +17 -0
- package/templates/qoder/scripts/run_weekly_update.sh +20 -0
- package/templates/qoder/scripts/search_index.py +352 -0
- package/templates/qoder/scripts/setup.py +453 -0
- package/templates/qoder/scripts/setup_weekly_cron.bat +22 -0
- package/templates/qoder/scripts/setup_weekly_cron.sh +19 -0
- package/templates/qoder/scripts/status.py +389 -0
- package/templates/qoder/scripts/syncgate.py +330 -0
- package/templates/qoder/scripts/task.py +954 -0
- package/templates/qoder/scripts/team.py +29 -0
- package/templates/qoder/scripts/team_sync.py +419 -0
- package/templates/qoder/scripts/workspace_init.py +102 -0
- package/templates/qoder/settings.json +53 -0
- package/templates/qoder/skills/design-review/SKILL.md +25 -0
- package/templates/qoder/skills/prd-generator/SKILL.md +180 -0
- package/templates/qoder/skills/prd-review/SKILL.md +36 -0
- package/templates/qoder/skills/prototype-generator/SKILL.md +141 -0
- package/templates/qoder/skills/spec-coder/SKILL.md +69 -0
- package/templates/qoder/skills/spec-generator/SKILL.md +67 -0
- package/templates/qoder/skills/test-generator/SKILL.md +72 -0
- package/templates/qoder/skills/wl-commit/SKILL.md +76 -0
- package/templates/qoder/skills/wl-init/SKILL.md +67 -0
- package/templates/qoder/skills/wl-insight/SKILL.md +81 -0
- package/templates/qoder/skills/wl-report/SKILL.md +87 -0
- package/templates/qoder/skills/wl-search/SKILL.md +75 -0
- package/templates/qoder/skills/wl-status/SKILL.md +61 -0
- package/templates/qoder/skills/wl-task/SKILL.md +58 -0
- package/templates/qoder/templates/prd-full-template.md +103 -0
- package/templates/qoder/templates/prd-quick-template.md +69 -0
- package/templates/qoder/templates/prototype-app.html +344 -0
- package/templates/qoder/templates/prototype-web.html +310 -0
- package/templates/root/AGENTS.md +182 -0
- package/templates/root/README-pipeline.md +56 -0
- package/templates/root/ROLES.md +85 -0
- package/templates/root//346/226/260/346/211/213/346/214/207/345/215/227.md +186 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# team.py - Team member registration
|
|
2
|
+
import os, json, sys
|
|
3
|
+
NL = chr(10)
|
|
4
|
+
BASE = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
5
|
+
|
|
6
|
+
def add_member(name, role):
|
|
7
|
+
members_dir = os.path.join(BASE, 'workspace', 'members')
|
|
8
|
+
member_dir = os.path.join(members_dir, name)
|
|
9
|
+
os.makedirs(os.path.join(member_dir, 'journal'), exist_ok=True)
|
|
10
|
+
os.makedirs(os.path.join(member_dir, 'drafts'), exist_ok=True)
|
|
11
|
+
os.makedirs(os.path.join(member_dir, 'inbox'), exist_ok=True)
|
|
12
|
+
info = {'name': name, 'role': role}
|
|
13
|
+
with open(os.path.join(member_dir, 'member.json'), 'w') as f:
|
|
14
|
+
json.dump(info, f, indent=2)
|
|
15
|
+
print('OK: member ' + name + ' (' + role + ') added')
|
|
16
|
+
|
|
17
|
+
def list_members():
|
|
18
|
+
md = os.path.join(BASE, 'workspace', 'members')
|
|
19
|
+
if not os.path.isdir(md): print('No members'); return
|
|
20
|
+
for d in sorted(os.listdir(md)):
|
|
21
|
+
info_f = os.path.join(md, d, 'member.json')
|
|
22
|
+
if os.path.isfile(info_f):
|
|
23
|
+
info = json.load(open(info_f))
|
|
24
|
+
print(d + ' [' + info.get('role', '?') + ']')
|
|
25
|
+
|
|
26
|
+
if __name__ == '__main__':
|
|
27
|
+
if len(sys.argv) < 2: list_members()
|
|
28
|
+
elif sys.argv[1] == 'add' and len(sys.argv) >= 4: add_member(sys.argv[2], sys.argv[3])
|
|
29
|
+
else: list_members()
|
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
QODER Team Sync - 无感 git 同步引擎
|
|
5
|
+
|
|
6
|
+
产品经理完全不需要懂 git。所有 /wl- 命令在产出文件后自动调用本脚本,
|
|
7
|
+
把"我的产出"同步到团队仓库,并拉取别人的最新产出。
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
python team_sync.py pull # 拉取团队最新 (会话开始/init 时)
|
|
11
|
+
python team_sync.py push [-m "msg"] # 提交并推送我的产出 (发布动作后)
|
|
12
|
+
python team_sync.py status # 查看同步状态 (落后/领先多少)
|
|
13
|
+
|
|
14
|
+
设计原则 (避免冲突 > 解决冲突):
|
|
15
|
+
- 只 stage 协作产出区: workspace/ + data/docs/ (+ data/index/ 周五更新时)
|
|
16
|
+
- 个人产出天然隔离: 每人只写 workspace/members/{自己}/
|
|
17
|
+
- REQ 编号唯一 -> specs/prd/ 文件名不冲突
|
|
18
|
+
- push 前 pull --rebase --autostash, 失败自动重试 3 次
|
|
19
|
+
- rebase 冲突时: 中止 rebase 保留本地提交, 输出 SYNC_CONFLICT 标记
|
|
20
|
+
(AI 看到标记后负责解决冲突, 人类用户永远不需要碰 git)
|
|
21
|
+
|
|
22
|
+
Exit codes: 0 = ok, 1 = error, 3 = conflict needs AI resolution
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import os
|
|
26
|
+
import sys
|
|
27
|
+
import subprocess
|
|
28
|
+
from datetime import datetime
|
|
29
|
+
|
|
30
|
+
if sys.platform == 'win32':
|
|
31
|
+
try:
|
|
32
|
+
sys.stdout.reconfigure(encoding='utf-8')
|
|
33
|
+
except (AttributeError, IOError):
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
37
|
+
from common.paths import get_repo_root, get_developer
|
|
38
|
+
from common.filelock import FileLock, LockTimeoutError
|
|
39
|
+
from syncgate import run_gates, SAFE_EXTENSIONS
|
|
40
|
+
|
|
41
|
+
BASE = str(get_repo_root())
|
|
42
|
+
|
|
43
|
+
# 协作产出区: 自动同步只碰这些路径, 永远不动 .qoder/ 引擎和源码区
|
|
44
|
+
SYNC_SCOPES = ['workspace', 'data/docs', 'data/index']
|
|
45
|
+
|
|
46
|
+
PULL_MARKER = os.path.join(BASE, '.qoder', '.runtime', 'last-pull')
|
|
47
|
+
PUSH_LOCK = os.path.join(BASE, '.qoder', '.runtime', 'team-sync.lock')
|
|
48
|
+
|
|
49
|
+
MAX_PUSH_RETRY = 3
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def git(*args, check=False):
|
|
53
|
+
"""git 命令包装。git 未安装时返回 rc=127 的伪结果, 不崩溃。"""
|
|
54
|
+
try:
|
|
55
|
+
r = subprocess.run(['git'] + list(args), cwd=BASE, capture_output=True,
|
|
56
|
+
text=True, encoding='utf-8', errors='replace')
|
|
57
|
+
except FileNotFoundError:
|
|
58
|
+
# git 未安装 — 返回伪 CompletedProcess, 让上层把"无 git"当"无远端/无仓库"处理
|
|
59
|
+
import types
|
|
60
|
+
r = types.SimpleNamespace(returncode=127, stdout='', stderr='git not installed',
|
|
61
|
+
encoding='utf-8')
|
|
62
|
+
if check and r.returncode != 0:
|
|
63
|
+
print('git {} failed: {}'.format(' '.join(args), r.stderr.strip()[:300]))
|
|
64
|
+
return r
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def current_branch():
|
|
68
|
+
r = git('rev-parse', '--abbrev-ref', 'HEAD')
|
|
69
|
+
return r.stdout.strip() if r.returncode == 0 else None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def has_remote():
|
|
73
|
+
r = git('remote')
|
|
74
|
+
return 'origin' in r.stdout.split()
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def rebase_in_progress():
|
|
78
|
+
return os.path.isdir(os.path.join(BASE, '.git', 'rebase-merge')) or \
|
|
79
|
+
os.path.isdir(os.path.join(BASE, '.git', 'rebase-apply'))
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def touch_pull_marker():
|
|
83
|
+
os.makedirs(os.path.dirname(PULL_MARKER), exist_ok=True)
|
|
84
|
+
with open(PULL_MARKER, 'w', encoding='utf-8') as f:
|
|
85
|
+
f.write(datetime.now().isoformat())
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _notify_prd_publications(staged_files):
|
|
89
|
+
"""D2: 检测 staged 里的 PRD 发布, 推飞书通知。
|
|
90
|
+
|
|
91
|
+
PRD 路径模式: data/docs/prd/REQ-*.md 或 workspace/specs/prd/REQ-*.md
|
|
92
|
+
只对"新增"的 PRD 推送 (修改已有 PRD 不通知, 避免噪音)。
|
|
93
|
+
"""
|
|
94
|
+
import re as _re
|
|
95
|
+
try:
|
|
96
|
+
from common.feishu import notify_prd_published, is_enabled
|
|
97
|
+
except ImportError:
|
|
98
|
+
return
|
|
99
|
+
if not is_enabled('prd_published'):
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
req_pattern = _re.compile(r'REQ-(\d{4})-(\d{3,4})', _re.IGNORECASE)
|
|
103
|
+
published = []
|
|
104
|
+
for f in staged_files:
|
|
105
|
+
norm = f.replace('\\', '/').lower()
|
|
106
|
+
if '/prd/' not in norm:
|
|
107
|
+
continue
|
|
108
|
+
if not f.lower().endswith('.md'):
|
|
109
|
+
continue
|
|
110
|
+
base = os.path.basename(f)
|
|
111
|
+
m = req_pattern.search(base)
|
|
112
|
+
if not m:
|
|
113
|
+
continue
|
|
114
|
+
req_id = 'REQ-{}-{:03d}'.format(m.group(1), int(m.group(2)))
|
|
115
|
+
# 提取标题 (从文件第一行)
|
|
116
|
+
title = base
|
|
117
|
+
try:
|
|
118
|
+
full = os.path.join(BASE, f) if not os.path.isabs(f) else f
|
|
119
|
+
if os.path.isfile(full):
|
|
120
|
+
with open(full, encoding='utf-8', errors='replace') as fh:
|
|
121
|
+
for line in fh:
|
|
122
|
+
line = line.strip().lstrip('#').strip()
|
|
123
|
+
if line:
|
|
124
|
+
title = line[:60]
|
|
125
|
+
break
|
|
126
|
+
except Exception:
|
|
127
|
+
pass
|
|
128
|
+
published.append((req_id, title))
|
|
129
|
+
|
|
130
|
+
for req_id, title in published:
|
|
131
|
+
try:
|
|
132
|
+
notify_prd_published(req_id, title, eval_pct=None)
|
|
133
|
+
except Exception:
|
|
134
|
+
pass
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def report_conflict(stderr):
|
|
138
|
+
"""rebase 冲突: 中止并输出结构化标记, 由会话中的 AI 接手解决"""
|
|
139
|
+
git('rebase', '--abort')
|
|
140
|
+
print('SYNC_CONFLICT: 自动同步遇到冲突, 已安全回退 (本地产出未丢失)。')
|
|
141
|
+
print('AI 请执行: git pull --rebase origin {} 并解决冲突后 git push。'.format(current_branch()))
|
|
142
|
+
print('冲突详情: ' + stderr.strip()[:300])
|
|
143
|
+
return 3
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def do_pull(quiet=False):
|
|
147
|
+
"""拉取团队最新。安全: autostash 保护未提交改动。"""
|
|
148
|
+
if rebase_in_progress():
|
|
149
|
+
print('SYNC_CONFLICT: 仓库处于未完成的 rebase 状态, AI 请先处理 (git rebase --abort 或 --continue)。')
|
|
150
|
+
return 3
|
|
151
|
+
if not has_remote():
|
|
152
|
+
if not quiet:
|
|
153
|
+
print('No remote configured - local-only mode, skip pull.')
|
|
154
|
+
touch_pull_marker()
|
|
155
|
+
return 0
|
|
156
|
+
|
|
157
|
+
branch = current_branch()
|
|
158
|
+
if not branch or branch == 'HEAD':
|
|
159
|
+
print('ERROR: detached HEAD or no branch - AI please check.')
|
|
160
|
+
return 1
|
|
161
|
+
|
|
162
|
+
r = git('pull', '--rebase', '--autostash', 'origin', branch)
|
|
163
|
+
if r.returncode != 0:
|
|
164
|
+
if 'CONFLICT' in (r.stdout + r.stderr) or rebase_in_progress():
|
|
165
|
+
return report_conflict(r.stderr or r.stdout)
|
|
166
|
+
print('Pull failed (network/remote?): ' + (r.stderr or r.stdout).strip()[:200])
|
|
167
|
+
print('继续离线工作, 产出不会丢失, 下次同步会自动补推。')
|
|
168
|
+
return 1
|
|
169
|
+
|
|
170
|
+
touch_pull_marker()
|
|
171
|
+
out = (r.stdout or '').strip()
|
|
172
|
+
if not quiet:
|
|
173
|
+
if 'up to date' in out.lower() or 'up-to-date' in out.lower():
|
|
174
|
+
print('Already up to date.')
|
|
175
|
+
else:
|
|
176
|
+
print('Pulled latest from team.')
|
|
177
|
+
return 0
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def summarize_changes():
|
|
181
|
+
"""生成人类可读的提交摘要 (基于 staged 文件)"""
|
|
182
|
+
r = git('diff', '--cached', '--name-only')
|
|
183
|
+
files = [f for f in r.stdout.strip().splitlines() if f.strip()]
|
|
184
|
+
if not files:
|
|
185
|
+
return None, 0
|
|
186
|
+
cats = {'prd': 0, 'prototype': 0, 'task': 0, 'journal': 0, 'index': 0, 'other': 0}
|
|
187
|
+
for f in files:
|
|
188
|
+
fl = f.lower()
|
|
189
|
+
if 'prototype' in fl and fl.endswith('.html'):
|
|
190
|
+
cats['prototype'] += 1
|
|
191
|
+
elif '/prd/' in fl or fl.split('/')[-1].startswith(('req-', 'prd-')):
|
|
192
|
+
cats['prd'] += 1
|
|
193
|
+
elif '/tasks/' in fl:
|
|
194
|
+
cats['task'] += 1
|
|
195
|
+
elif '/journal/' in fl:
|
|
196
|
+
cats['journal'] += 1
|
|
197
|
+
elif 'data/index/' in fl:
|
|
198
|
+
cats['index'] += 1
|
|
199
|
+
else:
|
|
200
|
+
cats['other'] += 1
|
|
201
|
+
parts = []
|
|
202
|
+
label = {'prd': 'PRD', 'prototype': '原型', 'task': '任务', 'journal': '日志',
|
|
203
|
+
'index': '索引', 'other': '其他'}
|
|
204
|
+
for k, v in cats.items():
|
|
205
|
+
if v:
|
|
206
|
+
parts.append('{}x{}'.format(label[k], v))
|
|
207
|
+
return ' '.join(parts), len(files)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _stage_scopes_safely(skip_secret=False):
|
|
211
|
+
"""用 allowlist 替代危险的 git add -A。
|
|
212
|
+
|
|
213
|
+
只 stage SYNC_SCOPES 下、扩展名在 SAFE_EXTENSIONS 里的文件。
|
|
214
|
+
其他文件 (如误放的 .env/.zip/.docx) 被跳过并打印警告。
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
(staged_count, skipped_files)
|
|
218
|
+
"""
|
|
219
|
+
staged = 0
|
|
220
|
+
skipped = []
|
|
221
|
+
for scope in SYNC_SCOPES:
|
|
222
|
+
scope_path = os.path.join(BASE, scope)
|
|
223
|
+
if not os.path.isdir(scope_path):
|
|
224
|
+
continue
|
|
225
|
+
# 用 git ls-files 拿已跟踪的 + git status 拿未跟踪的, 逐个判断扩展名
|
|
226
|
+
# 已跟踪文件: 直接 add (它们已经在版本控制, 不可能是新秘密)
|
|
227
|
+
r = git('ls-files', '--', scope)
|
|
228
|
+
tracked = [f for f in r.stdout.strip().splitlines() if f.strip()]
|
|
229
|
+
for f in tracked:
|
|
230
|
+
git('add', '--', f)
|
|
231
|
+
staged += 1
|
|
232
|
+
# 未跟踪的新文件: 按扩展名过滤
|
|
233
|
+
r = git('status', '--porcelain', '--', scope)
|
|
234
|
+
for line in r.stdout.strip().splitlines():
|
|
235
|
+
if not line.strip():
|
|
236
|
+
continue
|
|
237
|
+
# porcelain 格式: "XY filepath" (XY 是 2 字符状态)
|
|
238
|
+
status = line[:2]
|
|
239
|
+
fpath = line[3:]
|
|
240
|
+
if status[0] == '?' or status[1] == '?': # 未跟踪
|
|
241
|
+
ext = os.path.splitext(fpath)[1].lower()
|
|
242
|
+
if ext in SAFE_EXTENSIONS:
|
|
243
|
+
git('add', '--', fpath)
|
|
244
|
+
staged += 1
|
|
245
|
+
else:
|
|
246
|
+
skipped.append(fpath)
|
|
247
|
+
elif 'D' not in status:
|
|
248
|
+
# 修改/重命名等 (非删除) —— 已跟踪, 直接 add
|
|
249
|
+
# 重命名格式 "R old -> new", 取 new
|
|
250
|
+
if '->' in fpath:
|
|
251
|
+
fpath = fpath.split('->')[-1].strip().strip('"')
|
|
252
|
+
git('add', '--', fpath)
|
|
253
|
+
staged += 1
|
|
254
|
+
if skipped:
|
|
255
|
+
print('[gate] 跳过 {} 个非白名单文件 (可能含二进制/秘密, 手动处理):'.format(len(skipped)))
|
|
256
|
+
for s in skipped[:10]:
|
|
257
|
+
print(' ' + s)
|
|
258
|
+
return staged, skipped
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def do_push(message=None, skip_eval=False, skip_secret=False):
|
|
262
|
+
"""提交我的产出并推送。零信任门禁 + 文件锁 + rebase 重试。
|
|
263
|
+
|
|
264
|
+
门禁 (任一失败则拒绝 commit/push):
|
|
265
|
+
1. 身份强制 (已注册 + 有本地密钥)
|
|
266
|
+
2. git 作者与注册身份一致
|
|
267
|
+
3. 秘密扫描 (AWS/GitHub/私钥/密码)
|
|
268
|
+
4. EVA PRD 质量门禁 (>=80%)
|
|
269
|
+
"""
|
|
270
|
+
if rebase_in_progress():
|
|
271
|
+
print('SYNC_CONFLICT: 仓库处于未完成的 rebase 状态, AI 请先处理。')
|
|
272
|
+
return 3
|
|
273
|
+
|
|
274
|
+
dev = get_developer()
|
|
275
|
+
|
|
276
|
+
# === 文件锁: 串行化 push, 避免并发 stage/commit 交叉 ===
|
|
277
|
+
os.makedirs(os.path.dirname(PUSH_LOCK), exist_ok=True)
|
|
278
|
+
try:
|
|
279
|
+
lock_ctx = FileLock(PUSH_LOCK, timeout=60, stale_seconds=300)
|
|
280
|
+
lock_ctx.acquire()
|
|
281
|
+
except LockTimeoutError as e:
|
|
282
|
+
print('SYNC_BUSY: 另一个同步正在进行, 等待超时。稍后重试。')
|
|
283
|
+
print(' ' + str(e))
|
|
284
|
+
return 2 # lock contention
|
|
285
|
+
|
|
286
|
+
try:
|
|
287
|
+
return _do_push_locked(message, dev, skip_eval, skip_secret)
|
|
288
|
+
finally:
|
|
289
|
+
lock_ctx.release()
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _do_push_locked(message, dev, skip_eval, skip_secret):
|
|
293
|
+
"""锁内的实际 push 逻辑。"""
|
|
294
|
+
# 1. allowlist staging
|
|
295
|
+
staged_count, skipped = _stage_scopes_safely(skip_secret)
|
|
296
|
+
|
|
297
|
+
# 2. 取 staged 文件清单 (给门禁用)
|
|
298
|
+
r = git('diff', '--cached', '--name-only')
|
|
299
|
+
staged_files = [f for f in r.stdout.strip().splitlines() if f.strip()]
|
|
300
|
+
|
|
301
|
+
summary, count = summarize_changes()
|
|
302
|
+
|
|
303
|
+
# 3. 零信任门禁 (只在有东西要提交时才跑, 避免空提交报错)
|
|
304
|
+
if staged_files:
|
|
305
|
+
passed, reason = run_gates(
|
|
306
|
+
staged_files, dev, BASE,
|
|
307
|
+
skip_eval=skip_eval, skip_secret=skip_secret,
|
|
308
|
+
)
|
|
309
|
+
if not passed:
|
|
310
|
+
print(reason)
|
|
311
|
+
# 撤销 staging (用户修完再来)
|
|
312
|
+
git('reset', 'HEAD', '--')
|
|
313
|
+
return 1
|
|
314
|
+
|
|
315
|
+
# 4. commit
|
|
316
|
+
if summary:
|
|
317
|
+
msg = message or '[wl-sync] {}: {}'.format(dev or 'unknown', summary)
|
|
318
|
+
r = git('commit', '-m', msg)
|
|
319
|
+
if r.returncode != 0:
|
|
320
|
+
print('Commit failed: ' + (r.stderr or r.stdout).strip()[:200])
|
|
321
|
+
return 1
|
|
322
|
+
print('Committed {} files: {}'.format(count, msg))
|
|
323
|
+
else:
|
|
324
|
+
print('Nothing new to commit.')
|
|
325
|
+
|
|
326
|
+
if not has_remote():
|
|
327
|
+
print('No remote configured - committed locally only.')
|
|
328
|
+
return 0
|
|
329
|
+
|
|
330
|
+
branch = current_branch()
|
|
331
|
+
if not branch or branch == 'HEAD':
|
|
332
|
+
print('ERROR: detached HEAD - AI please check.')
|
|
333
|
+
return 1
|
|
334
|
+
|
|
335
|
+
# 5. 检查是否有待推送的提交
|
|
336
|
+
git('fetch', 'origin', branch)
|
|
337
|
+
r = git('rev-list', '--count', 'origin/{}..HEAD'.format(branch))
|
|
338
|
+
ahead = int(r.stdout.strip() or 0) if r.returncode == 0 else 1
|
|
339
|
+
if ahead == 0:
|
|
340
|
+
print('Already in sync with team.')
|
|
341
|
+
touch_pull_marker()
|
|
342
|
+
return 0
|
|
343
|
+
|
|
344
|
+
# 6. pull --rebase + push, 失败重试
|
|
345
|
+
for attempt in range(1, MAX_PUSH_RETRY + 1):
|
|
346
|
+
r = git('pull', '--rebase', '--autostash', 'origin', branch)
|
|
347
|
+
if r.returncode != 0:
|
|
348
|
+
if 'CONFLICT' in (r.stdout + r.stderr) or rebase_in_progress():
|
|
349
|
+
return report_conflict(r.stderr or r.stdout)
|
|
350
|
+
print('Pull failed (attempt {}): {}'.format(attempt, (r.stderr or r.stdout).strip()[:150]))
|
|
351
|
+
|
|
352
|
+
r = git('push', 'origin', branch)
|
|
353
|
+
if r.returncode == 0:
|
|
354
|
+
touch_pull_marker()
|
|
355
|
+
print('Synced to team. (attempt {})'.format(attempt))
|
|
356
|
+
# D2: 检测 PRD 发布并推飞书通知
|
|
357
|
+
_notify_prd_publications(staged_files)
|
|
358
|
+
return 0
|
|
359
|
+
if attempt < MAX_PUSH_RETRY:
|
|
360
|
+
print('Push rejected (someone pushed first?), retrying...')
|
|
361
|
+
|
|
362
|
+
print('Push failed after {} attempts: 产出已本地提交不会丢失, 下次同步自动补推。'.format(MAX_PUSH_RETRY))
|
|
363
|
+
return 1
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def do_status():
|
|
367
|
+
branch = current_branch()
|
|
368
|
+
print('Branch: {}'.format(branch))
|
|
369
|
+
print('Developer: {}'.format(get_developer() or 'NOT SET'))
|
|
370
|
+
|
|
371
|
+
if not has_remote():
|
|
372
|
+
print('Remote: none (local-only mode)')
|
|
373
|
+
return 0
|
|
374
|
+
|
|
375
|
+
git('fetch', 'origin', branch)
|
|
376
|
+
r = git('rev-list', '--left-right', '--count', 'origin/{}...HEAD'.format(branch))
|
|
377
|
+
if r.returncode == 0 and r.stdout.strip():
|
|
378
|
+
behind, ahead = r.stdout.split()
|
|
379
|
+
print('Ahead (待推送): {}, Behind (待拉取): {}'.format(ahead, behind))
|
|
380
|
+
|
|
381
|
+
r = git('status', '--short', '--', *SYNC_SCOPES)
|
|
382
|
+
dirty = [l for l in r.stdout.strip().splitlines() if l.strip()]
|
|
383
|
+
print('未同步的本地产出: {} 个文件'.format(len(dirty)))
|
|
384
|
+
for l in dirty[:10]:
|
|
385
|
+
print(' ' + l)
|
|
386
|
+
|
|
387
|
+
if os.path.isfile(PULL_MARKER):
|
|
388
|
+
with open(PULL_MARKER, encoding='utf-8') as f:
|
|
389
|
+
print('Last pull: ' + f.read().strip())
|
|
390
|
+
return 0
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def main():
|
|
394
|
+
args = sys.argv[1:]
|
|
395
|
+
cmd = args[0] if args else 'status'
|
|
396
|
+
message = None
|
|
397
|
+
if '-m' in args:
|
|
398
|
+
i = args.index('-m')
|
|
399
|
+
if i + 1 < len(args):
|
|
400
|
+
message = args[i + 1]
|
|
401
|
+
skip_eval = '--skip-eval' in args
|
|
402
|
+
skip_secret = '--skip-secret' in args
|
|
403
|
+
|
|
404
|
+
if not os.path.isdir(os.path.join(BASE, '.git')):
|
|
405
|
+
print('Not a git repository - skip sync.')
|
|
406
|
+
return 0
|
|
407
|
+
|
|
408
|
+
if cmd == 'pull':
|
|
409
|
+
return do_pull()
|
|
410
|
+
if cmd == 'push':
|
|
411
|
+
return do_push(message, skip_eval=skip_eval, skip_secret=skip_secret)
|
|
412
|
+
if cmd == 'status':
|
|
413
|
+
return do_status()
|
|
414
|
+
print('Usage: team_sync.py pull|push|status [-m "message"] [--skip-eval] [--skip-secret]')
|
|
415
|
+
return 1
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
if __name__ == '__main__':
|
|
419
|
+
sys.exit(main())
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
QODER Pipeline - Workspace Init
|
|
5
|
+
|
|
6
|
+
Creates personal workspace under team workspace:
|
|
7
|
+
workspace/members/{developer}/
|
|
8
|
+
journal/
|
|
9
|
+
drafts/
|
|
10
|
+
inbox/
|
|
11
|
+
|
|
12
|
+
Also syncs latest from git (git pull).
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
python .qoder/scripts/workspace_init.py <developer-name>
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import os
|
|
21
|
+
import subprocess
|
|
22
|
+
import sys
|
|
23
|
+
from datetime import datetime
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
# Add scripts to path
|
|
27
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
28
|
+
|
|
29
|
+
from common.paths import get_repo_root, get_developer, FILE_DEVELOPER
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def init_workspace(name: str) -> bool:
|
|
33
|
+
"""Initialize personal workspace under team workspace."""
|
|
34
|
+
if not name:
|
|
35
|
+
print("Error: developer name required", file=sys.stderr)
|
|
36
|
+
return False
|
|
37
|
+
|
|
38
|
+
repo_root = get_repo_root()
|
|
39
|
+
|
|
40
|
+
# 1. Create .developer file in .qoder/ (engine)
|
|
41
|
+
dev_file = repo_root / ".qoder" / FILE_DEVELOPER
|
|
42
|
+
if dev_file.is_file():
|
|
43
|
+
existing = get_developer(repo_root)
|
|
44
|
+
if existing:
|
|
45
|
+
print(f"Developer already: {existing}")
|
|
46
|
+
print(f"Workspace: workspace/members/{existing}/")
|
|
47
|
+
return True
|
|
48
|
+
|
|
49
|
+
dev_file.write_text(
|
|
50
|
+
f"name={name}\ninitialized_at={datetime.now().isoformat()}\n",
|
|
51
|
+
encoding="utf-8"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# 2. Create personal workspace under team workspace
|
|
55
|
+
personal = repo_root / "workspace" / "members" / name
|
|
56
|
+
for sub in ["journal", "drafts", "inbox"]:
|
|
57
|
+
(personal / sub).mkdir(parents=True, exist_ok=True)
|
|
58
|
+
|
|
59
|
+
# 3. Create initial journal
|
|
60
|
+
today = datetime.now().strftime("%Y-%m-%d")
|
|
61
|
+
journal = personal / "journal" / f"journal-1.md"
|
|
62
|
+
if not journal.exists():
|
|
63
|
+
journal.write_text(
|
|
64
|
+
f"# Journal - {name}\n\n> Started: {today}\n\n---\n\n",
|
|
65
|
+
encoding="utf-8"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# 4. Git pull (sync latest)
|
|
69
|
+
try:
|
|
70
|
+
result = subprocess.run(
|
|
71
|
+
["git", "pull"],
|
|
72
|
+
capture_output=True, text=True, cwd=str(repo_root), timeout=30
|
|
73
|
+
)
|
|
74
|
+
pull_msg = result.stdout.strip() if result.returncode == 0 else "already up to date"
|
|
75
|
+
except Exception:
|
|
76
|
+
pull_msg = "skipped (no remote)"
|
|
77
|
+
|
|
78
|
+
print(f"Developer: {name}")
|
|
79
|
+
print(f"Personal workspace: workspace/members/{name}/")
|
|
80
|
+
print(f" journal/ session logs")
|
|
81
|
+
print(f" drafts/ personal drafts")
|
|
82
|
+
print(f" inbox/ pending items")
|
|
83
|
+
print(f"Git sync: {pull_msg}")
|
|
84
|
+
return True
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def main():
|
|
88
|
+
if len(sys.argv) < 2:
|
|
89
|
+
print("Usage: python .qoder/scripts/workspace_init.py <your-name>")
|
|
90
|
+
print("Example: python .qoder/scripts/workspace_init.py zhangsan")
|
|
91
|
+
sys.exit(1)
|
|
92
|
+
|
|
93
|
+
name = sys.argv[1]
|
|
94
|
+
if init_workspace(name):
|
|
95
|
+
print("\nNext: say 'help me generate PRD' in Codex to start pipeline")
|
|
96
|
+
sys.exit(0)
|
|
97
|
+
else:
|
|
98
|
+
sys.exit(1)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
if __name__ == "__main__":
|
|
102
|
+
main()
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(*)",
|
|
5
|
+
"Read(*)",
|
|
6
|
+
"Write(*)",
|
|
7
|
+
"Edit(*)",
|
|
8
|
+
"Glob(*)",
|
|
9
|
+
"Grep(*)",
|
|
10
|
+
"WebFetch(*)",
|
|
11
|
+
"WebSearch(*)",
|
|
12
|
+
"mcp__*"
|
|
13
|
+
],
|
|
14
|
+
"deny": []
|
|
15
|
+
},
|
|
16
|
+
"hooks": {
|
|
17
|
+
"SessionStart": [
|
|
18
|
+
{
|
|
19
|
+
"matcher": "startup",
|
|
20
|
+
"hooks": [
|
|
21
|
+
{
|
|
22
|
+
"command": "python .qoder/hooks/session-start.py || python3 .qoder/hooks/session-start.py"
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"matcher": "clear",
|
|
28
|
+
"hooks": [
|
|
29
|
+
{
|
|
30
|
+
"command": "python .qoder/hooks/session-start.py || python3 .qoder/hooks/session-start.py"
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"matcher": "compact",
|
|
36
|
+
"hooks": [
|
|
37
|
+
{
|
|
38
|
+
"command": "python .qoder/hooks/session-start.py || python3 .qoder/hooks/session-start.py"
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
}
|
|
42
|
+
],
|
|
43
|
+
"UserPromptSubmit": [
|
|
44
|
+
{
|
|
45
|
+
"hooks": [
|
|
46
|
+
{
|
|
47
|
+
"command": "python .qoder/hooks/inject-workflow-state.py || python3 .qoder/hooks/inject-workflow-state.py"
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: design-review
|
|
3
|
+
description: "评审设计交付物的完整性。Review design artifacts for completeness. 用户说'评审设计''设计稿看一下''检查交互稿'时触发。"
|
|
4
|
+
trigger: "user invokes /review design, asks to check design, or designer submits design artifacts"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Design Review Skill
|
|
8
|
+
|
|
9
|
+
## ⚙️ 自取上下文(Quest / QoderWork 无 hook 注入,必须自读)
|
|
10
|
+
|
|
11
|
+
- `.qoder/.developer` — 当前评审人
|
|
12
|
+
- 设计文件路径由用户指定,或扫 `workspace/members/{dev}/drafts/prototype-*.html`
|
|
13
|
+
|
|
14
|
+
## 检查项 (Checklist)
|
|
15
|
+
- [ ] 组件规格完整(components.json)
|
|
16
|
+
- [ ] 交互流程覆盖所有状态(interaction-flow.md)
|
|
17
|
+
- [ ] 技术约束已标注
|
|
18
|
+
- [ ] 响应式/设计 Token 已定义
|
|
19
|
+
- [ ] 符合 PRD 需求
|
|
20
|
+
- [ ] **图标来自真源(data/index/icon-reference.json),无 emoji**(硬性)
|
|
21
|
+
- [ ] 颜色来自真源(Web: vben-style-reference.json;APP: Vant 变量)
|
|
22
|
+
|
|
23
|
+
## 输出
|
|
24
|
+
|
|
25
|
+
报告:PASS 或 列出需修复的问题清单
|