@hupan56/wlkj 2.2.3 → 2.2.5
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 +532 -532
- package/package.json +28 -28
- package/templates/qoder/hooks/inject-workflow-state.py +117 -117
- package/templates/qoder/hooks/session-start.py +204 -204
- package/templates/qoder/scripts/common/developer.py +231 -161
- package/templates/qoder/scripts/common/paths.py +310 -310
- package/templates/qoder/scripts/common/task_utils.py +392 -387
- package/templates/qoder/scripts/init_developer.py +75 -75
- package/templates/qoder/scripts/install_qoderwork.py +367 -367
- package/templates/qoder/scripts/role.py +39 -39
- package/templates/qoder/scripts/syncgate.py +333 -333
- package/templates/qoder/scripts/team_sync.py +439 -439
- package/templates/qoder/skills/design-review/SKILL.md +25 -25
- package/templates/qoder/skills/prd-generator/SKILL.md +180 -180
- package/templates/qoder/skills/prd-review/SKILL.md +36 -36
- package/templates/qoder/skills/prototype-generator/SKILL.md +141 -141
- package/templates/qoder/skills/spec-coder/SKILL.md +68 -68
- package/templates/qoder/skills/spec-generator/SKILL.md +66 -66
- package/templates/qoder/skills/test-generator/SKILL.md +71 -71
- package/templates/root/AGENTS.md +182 -182
|
@@ -1,311 +1,311 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
"""
|
|
3
|
-
Path management - central path config
|
|
4
|
-
|
|
5
|
-
All paths managed from one place:
|
|
6
|
-
- .qoder/ = engine (AI reads)
|
|
7
|
-
- workspace/ = work area (humans see)
|
|
8
|
-
- Strictly separated
|
|
9
|
-
|
|
10
|
-
Reference: Trellis path management
|
|
11
|
-
"""
|
|
12
|
-
|
|
13
|
-
import os
|
|
14
|
-
import sys
|
|
15
|
-
from pathlib import Path
|
|
16
|
-
from typing import Optional, Dict
|
|
17
|
-
|
|
18
|
-
# Project root (auto-detect: find .qoder/ upward)
|
|
19
|
-
def _find_repo_root() -> Path:
|
|
20
|
-
"""Find project root by searching for .qoder/ directory."""
|
|
21
|
-
current = Path(__file__).resolve().parent
|
|
22
|
-
for _ in range(10):
|
|
23
|
-
if (current / ".qoder").is_dir():
|
|
24
|
-
return current
|
|
25
|
-
parent = current.parent
|
|
26
|
-
if parent == current:
|
|
27
|
-
break
|
|
28
|
-
current = parent
|
|
29
|
-
return Path(__file__).resolve().parent.parent.parent.parent.parent
|
|
30
|
-
|
|
31
|
-
PROJECT_ROOT = _find_repo_root()
|
|
32
|
-
|
|
33
|
-
# =============================================================================
|
|
34
|
-
# Engine paths (.qoder/)
|
|
35
|
-
# =============================================================================
|
|
36
|
-
|
|
37
|
-
QODER_DIR = PROJECT_ROOT / ".qoder"
|
|
38
|
-
RUNTIME_DIR = QODER_DIR / ".runtime"
|
|
39
|
-
SESSIONS_DIR = RUNTIME_DIR / "sessions"
|
|
40
|
-
ARCHIVE_DIR = QODER_DIR / "archive"
|
|
41
|
-
RULES_DIR = QODER_DIR / "rules"
|
|
42
|
-
CONTEXT_DIR = QODER_DIR / "context"
|
|
43
|
-
SKILLS_DIR = QODER_DIR / "skills"
|
|
44
|
-
AGENTS_DIR = QODER_DIR / "agents"
|
|
45
|
-
HOOKS_DIR = QODER_DIR / "hooks"
|
|
46
|
-
SCRIPTS_DIR = QODER_DIR / "scripts"
|
|
47
|
-
|
|
48
|
-
# =============================================================================
|
|
49
|
-
# Workspace paths (workspace/)
|
|
50
|
-
# =============================================================================
|
|
51
|
-
|
|
52
|
-
WORKSPACE_DIR = PROJECT_ROOT / "workspace"
|
|
53
|
-
SPECS_DIR = WORKSPACE_DIR / "specs"
|
|
54
|
-
PRD_DIR = SPECS_DIR / "prd"
|
|
55
|
-
TASKS_DIR = WORKSPACE_DIR / "tasks"
|
|
56
|
-
CONSTITUTION_DIR = WORKSPACE_DIR / "constitution"
|
|
57
|
-
MEMBERS_DIR = WORKSPACE_DIR / "members"
|
|
58
|
-
|
|
59
|
-
# =============================================================================
|
|
60
|
-
# Developer files
|
|
61
|
-
# =============================================================================
|
|
62
|
-
|
|
63
|
-
DEVELOPER_FILE = QODER_DIR / ".developer"
|
|
64
|
-
CURRENT_TASK_FILE = QODER_DIR / ".current-task"
|
|
65
|
-
|
|
66
|
-
# =============================================================================
|
|
67
|
-
# Legacy compatibility constants (used by task.py, developer.py, etc.)
|
|
68
|
-
# =============================================================================
|
|
69
|
-
|
|
70
|
-
DIR_TASKS = "workspace/tasks"
|
|
71
|
-
DIR_ARCHIVE = ".qoder/archive"
|
|
72
|
-
DIR_WORKFLOW = ".qoder"
|
|
73
|
-
DIR_WORKSPACE = "workspace/members"
|
|
74
|
-
DIR_SPECS = "workspace/specs"
|
|
75
|
-
|
|
76
|
-
FILE_TASK_JSON = "task.json"
|
|
77
|
-
FILE_DEVELOPER = ".developer"
|
|
78
|
-
FILE_JOURNAL_PREFIX = "journal-"
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
# =============================================================================
|
|
82
|
-
# Core functions
|
|
83
|
-
# =============================================================================
|
|
84
|
-
|
|
85
|
-
def get_repo_root() -> Path:
|
|
86
|
-
"""Get project root directory."""
|
|
87
|
-
return PROJECT_ROOT
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
def ensure_dirs():
|
|
91
|
-
"""Ensure all necessary directories exist."""
|
|
92
|
-
dirs = [
|
|
93
|
-
WORKSPACE_DIR, SPECS_DIR, PRD_DIR, TASKS_DIR,
|
|
94
|
-
CONSTITUTION_DIR, MEMBERS_DIR,
|
|
95
|
-
RUNTIME_DIR, SESSIONS_DIR, ARCHIVE_DIR,
|
|
96
|
-
]
|
|
97
|
-
for d in dirs:
|
|
98
|
-
d.mkdir(parents=True, exist_ok=True)
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
def _parse_developer_file(content: str) -> Dict[str, str]:
|
|
102
|
-
"""Parse .developer content. Accepts both 'name=x' and 'name: x' formats."""
|
|
103
|
-
info = {}
|
|
104
|
-
for line in content.splitlines():
|
|
105
|
-
line = line.strip()
|
|
106
|
-
if not line or line.startswith("#"):
|
|
107
|
-
continue
|
|
108
|
-
for sep in ("=", ":"):
|
|
109
|
-
if sep in line:
|
|
110
|
-
k, v = line.split(sep, 1)
|
|
111
|
-
info[k.strip()] = v.strip()
|
|
112
|
-
break
|
|
113
|
-
return info
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
def get_developer(repo_root: Optional[Path] = None) -> Optional[str]:
|
|
117
|
-
"""Get current developer name from .developer file."""
|
|
118
|
-
if repo_root is None:
|
|
119
|
-
repo_root = PROJECT_ROOT
|
|
120
|
-
dev_file = repo_root / ".qoder" / ".developer"
|
|
121
|
-
if not dev_file.is_file():
|
|
122
|
-
return None
|
|
123
|
-
try:
|
|
124
|
-
content = dev_file.read_text(encoding="utf-8")
|
|
125
|
-
except UnicodeDecodeError:
|
|
126
|
-
try:
|
|
127
|
-
content = dev_file.read_text(encoding="gbk")
|
|
128
|
-
except (OSError, IOError, UnicodeDecodeError):
|
|
129
|
-
return None
|
|
130
|
-
except (OSError, IOError):
|
|
131
|
-
return None
|
|
132
|
-
return _parse_developer_file(content).get("name") or None
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
def check_developer(repo_root: Optional[Path] = None) -> bool:
|
|
136
|
-
"""Check if developer is initialized."""
|
|
137
|
-
return get_developer(repo_root) is not None
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
def get_developer_info(repo_root: Optional[Path] = None) -> Optional[Dict]:
|
|
141
|
-
"""Get developer info as dict."""
|
|
142
|
-
if repo_root is None:
|
|
143
|
-
repo_root = PROJECT_ROOT
|
|
144
|
-
dev_file = repo_root / ".qoder" / ".developer"
|
|
145
|
-
if not dev_file.is_file():
|
|
146
|
-
return None
|
|
147
|
-
try:
|
|
148
|
-
content = dev_file.read_text(encoding="utf-8")
|
|
149
|
-
except UnicodeDecodeError:
|
|
150
|
-
try:
|
|
151
|
-
content = dev_file.read_text(encoding="gbk")
|
|
152
|
-
except (OSError, IOError, UnicodeDecodeError):
|
|
153
|
-
return None
|
|
154
|
-
except (OSError, IOError):
|
|
155
|
-
return None
|
|
156
|
-
info = _parse_developer_file(content)
|
|
157
|
-
return info if info else None
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
def get_developer_dir(developer_name: str) -> Path:
|
|
161
|
-
"""Get developer personal directory under workspace/members/."""
|
|
162
|
-
dev_dir = MEMBERS_DIR / developer_name
|
|
163
|
-
dev_dir.mkdir(parents=True, exist_ok=True)
|
|
164
|
-
return dev_dir
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
def get_developer_journal_dir(developer_name: str) -> Path:
|
|
168
|
-
"""Get developer journal directory."""
|
|
169
|
-
journal_dir = get_developer_dir(developer_name) / "journal"
|
|
170
|
-
journal_dir.mkdir(parents=True, exist_ok=True)
|
|
171
|
-
return journal_dir
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
def get_developer_drafts_dir(developer_name: str) -> Path:
|
|
175
|
-
"""Get developer drafts directory."""
|
|
176
|
-
drafts_dir = get_developer_dir(developer_name) / "drafts"
|
|
177
|
-
drafts_dir.mkdir(parents=True, exist_ok=True)
|
|
178
|
-
return drafts_dir
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
def get_workspace_dir(repo_root: Optional[Path] = None) -> Optional[Path]:
|
|
182
|
-
"""Get developer workspace directory."""
|
|
183
|
-
dev = get_developer(repo_root)
|
|
184
|
-
if not dev:
|
|
185
|
-
return None
|
|
186
|
-
return MEMBERS_DIR / dev
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
def get_active_journal_file(repo_root: Optional[Path] = None) -> Optional[Path]:
|
|
190
|
-
"""Get developer active journal file."""
|
|
191
|
-
ws = get_workspace_dir(repo_root)
|
|
192
|
-
if not ws:
|
|
193
|
-
return None
|
|
194
|
-
journal_dir = ws / "journal"
|
|
195
|
-
if not journal_dir.is_dir():
|
|
196
|
-
return None
|
|
197
|
-
journals = sorted(journal_dir.glob("journal-*.md"))
|
|
198
|
-
return journals[-1] if journals else None
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
def get_tasks_dir(repo_root: Optional[Path] = None) -> Path:
|
|
202
|
-
"""Get tasks directory (workspace/tasks/)."""
|
|
203
|
-
if repo_root is None:
|
|
204
|
-
repo_root = PROJECT_ROOT
|
|
205
|
-
tasks = repo_root / "workspace" / "tasks"
|
|
206
|
-
tasks.mkdir(parents=True, exist_ok=True)
|
|
207
|
-
return tasks
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
def _current_task_file(repo_root: Path, developer: Optional[str] = None) -> Path:
|
|
211
|
-
"""当前任务文件路径。
|
|
212
|
-
|
|
213
|
-
零信任隔离: 按开发者命名, 避免共享机器上 A 的 current-task 被 B 覆盖。
|
|
214
|
-
developer=None 时回退到旧的全局 .current-task (向后兼容读取)。
|
|
215
|
-
"""
|
|
216
|
-
repo_root = Path(repo_root) # 容忍 str 输入
|
|
217
|
-
runtime = repo_root / ".qoder" / ".runtime"
|
|
218
|
-
if developer:
|
|
219
|
-
# 文件名只允许安全字符 (member name 已校验, 但防御性 sanitize)
|
|
220
|
-
safe = "".join(c for c in developer if c.isalnum() or c in "_-") or "anon"
|
|
221
|
-
return runtime / f"current-task.{safe}"
|
|
222
|
-
return repo_root / ".qoder" / ".current-task"
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
def _read_task_file(path: Path) -> Optional[str]:
|
|
226
|
-
"""读任务文件, 支持 UTF-8 和 GBK fallback (审计 H6: Notepad 默认 GBK)。"""
|
|
227
|
-
if not path.is_file():
|
|
228
|
-
return None
|
|
229
|
-
for enc in ("utf-8", "gbk", "utf-8-sig"):
|
|
230
|
-
try:
|
|
231
|
-
content = path.read_text(encoding=enc).strip()
|
|
232
|
-
return content if content else None
|
|
233
|
-
except (OSError, IOError, UnicodeDecodeError):
|
|
234
|
-
continue
|
|
235
|
-
return None
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
def get_current_task(
|
|
239
|
-
repo_root: Optional[Path] = None,
|
|
240
|
-
developer: Optional[str] = None,
|
|
241
|
-
) -> Optional[str]:
|
|
242
|
-
"""Get current task.
|
|
243
|
-
|
|
244
|
-
优先读按开发者隔离的 .runtime/current-task.{dev};
|
|
245
|
-
若无则回退到旧的全局 .current-task (向后兼容, 会自动迁移)。
|
|
246
|
-
developer=None 时自动用 get_developer() 推断。
|
|
247
|
-
"""
|
|
248
|
-
if repo_root is None:
|
|
249
|
-
repo_root = PROJECT_ROOT
|
|
250
|
-
if developer is None:
|
|
251
|
-
developer = get_developer(repo_root)
|
|
252
|
-
# 1. 优先读 developer 隔离的文件
|
|
253
|
-
if developer:
|
|
254
|
-
ct = _read_task_file(_current_task_file(repo_root, developer))
|
|
255
|
-
if ct:
|
|
256
|
-
return ct
|
|
257
|
-
# 2. 回退到旧全局文件 (兼容历史)
|
|
258
|
-
legacy = _read_task_file(_current_task_file(repo_root, None))
|
|
259
|
-
return legacy
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
def set_current_task(
|
|
263
|
-
task_path: Optional[str],
|
|
264
|
-
repo_root: Optional[Path] = None,
|
|
265
|
-
developer: Optional[str] = None,
|
|
266
|
-
) -> None:
|
|
267
|
-
"""Set current task (按开发者隔离写入)。
|
|
268
|
-
|
|
269
|
-
零信任: 写入 .runtime/current-task.{dev}, 不再写全局 .current-task。
|
|
270
|
-
若 task_path=None 则清除该开发者的指针 (不影响他人)。
|
|
271
|
-
"""
|
|
272
|
-
if repo_root is None:
|
|
273
|
-
repo_root = PROJECT_ROOT
|
|
274
|
-
if developer is None:
|
|
275
|
-
developer = get_developer(repo_root)
|
|
276
|
-
ct_file = _current_task_file(repo_root, developer)
|
|
277
|
-
try:
|
|
278
|
-
ct_file.parent.mkdir(parents=True, exist_ok=True)
|
|
279
|
-
if task_path:
|
|
280
|
-
ct_file.write_text(task_path, encoding="utf-8")
|
|
281
|
-
else:
|
|
282
|
-
ct_file.unlink(missing_ok=True)
|
|
283
|
-
except (OSError, IOError) as e:
|
|
284
|
-
print(f"Warning: Failed to write current-task: {e}", file=sys.stderr)
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
def get_module_spec_dir(module: Optional[str] = None) -> Path:
|
|
288
|
-
"""Get module spec directory."""
|
|
289
|
-
if module:
|
|
290
|
-
spec_dir = SPECS_DIR / module
|
|
291
|
-
else:
|
|
292
|
-
spec_dir = SPECS_DIR
|
|
293
|
-
spec_dir.mkdir(parents=True, exist_ok=True)
|
|
294
|
-
return spec_dir
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
def get_task_dir(slug: str) -> Path:
|
|
298
|
-
"""Get task directory by slug."""
|
|
299
|
-
from datetime import datetime
|
|
300
|
-
date_prefix = datetime.now().strftime("%m-%d")
|
|
301
|
-
task_dir = TASKS_DIR / "{0}-{1}".format(date_prefix, slug)
|
|
302
|
-
task_dir.mkdir(parents=True, exist_ok=True)
|
|
303
|
-
return task_dir
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
def count_lines(file_path: Path) -> int:
|
|
307
|
-
"""Count lines in a file."""
|
|
308
|
-
try:
|
|
309
|
-
return sum(1 for _ in file_path.open("r", encoding="utf-8"))
|
|
310
|
-
except (OSError, IOError):
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Path management - central path config
|
|
4
|
+
|
|
5
|
+
All paths managed from one place:
|
|
6
|
+
- .qoder/ = engine (AI reads)
|
|
7
|
+
- workspace/ = work area (humans see)
|
|
8
|
+
- Strictly separated
|
|
9
|
+
|
|
10
|
+
Reference: Trellis path management
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Optional, Dict
|
|
17
|
+
|
|
18
|
+
# Project root (auto-detect: find .qoder/ upward)
|
|
19
|
+
def _find_repo_root() -> Path:
|
|
20
|
+
"""Find project root by searching for .qoder/ directory."""
|
|
21
|
+
current = Path(__file__).resolve().parent
|
|
22
|
+
for _ in range(10):
|
|
23
|
+
if (current / ".qoder").is_dir():
|
|
24
|
+
return current
|
|
25
|
+
parent = current.parent
|
|
26
|
+
if parent == current:
|
|
27
|
+
break
|
|
28
|
+
current = parent
|
|
29
|
+
return Path(__file__).resolve().parent.parent.parent.parent.parent
|
|
30
|
+
|
|
31
|
+
PROJECT_ROOT = _find_repo_root()
|
|
32
|
+
|
|
33
|
+
# =============================================================================
|
|
34
|
+
# Engine paths (.qoder/)
|
|
35
|
+
# =============================================================================
|
|
36
|
+
|
|
37
|
+
QODER_DIR = PROJECT_ROOT / ".qoder"
|
|
38
|
+
RUNTIME_DIR = QODER_DIR / ".runtime"
|
|
39
|
+
SESSIONS_DIR = RUNTIME_DIR / "sessions"
|
|
40
|
+
ARCHIVE_DIR = QODER_DIR / "archive"
|
|
41
|
+
RULES_DIR = QODER_DIR / "rules"
|
|
42
|
+
CONTEXT_DIR = QODER_DIR / "context"
|
|
43
|
+
SKILLS_DIR = QODER_DIR / "skills"
|
|
44
|
+
AGENTS_DIR = QODER_DIR / "agents"
|
|
45
|
+
HOOKS_DIR = QODER_DIR / "hooks"
|
|
46
|
+
SCRIPTS_DIR = QODER_DIR / "scripts"
|
|
47
|
+
|
|
48
|
+
# =============================================================================
|
|
49
|
+
# Workspace paths (workspace/)
|
|
50
|
+
# =============================================================================
|
|
51
|
+
|
|
52
|
+
WORKSPACE_DIR = PROJECT_ROOT / "workspace"
|
|
53
|
+
SPECS_DIR = WORKSPACE_DIR / "specs"
|
|
54
|
+
PRD_DIR = SPECS_DIR / "prd"
|
|
55
|
+
TASKS_DIR = WORKSPACE_DIR / "tasks"
|
|
56
|
+
CONSTITUTION_DIR = WORKSPACE_DIR / "constitution"
|
|
57
|
+
MEMBERS_DIR = WORKSPACE_DIR / "members"
|
|
58
|
+
|
|
59
|
+
# =============================================================================
|
|
60
|
+
# Developer files
|
|
61
|
+
# =============================================================================
|
|
62
|
+
|
|
63
|
+
DEVELOPER_FILE = QODER_DIR / ".developer"
|
|
64
|
+
CURRENT_TASK_FILE = QODER_DIR / ".current-task"
|
|
65
|
+
|
|
66
|
+
# =============================================================================
|
|
67
|
+
# Legacy compatibility constants (used by task.py, developer.py, etc.)
|
|
68
|
+
# =============================================================================
|
|
69
|
+
|
|
70
|
+
DIR_TASKS = "workspace/tasks"
|
|
71
|
+
DIR_ARCHIVE = ".qoder/archive"
|
|
72
|
+
DIR_WORKFLOW = ".qoder"
|
|
73
|
+
DIR_WORKSPACE = "workspace/members"
|
|
74
|
+
DIR_SPECS = "workspace/specs"
|
|
75
|
+
|
|
76
|
+
FILE_TASK_JSON = "task.json"
|
|
77
|
+
FILE_DEVELOPER = ".developer"
|
|
78
|
+
FILE_JOURNAL_PREFIX = "journal-"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# =============================================================================
|
|
82
|
+
# Core functions
|
|
83
|
+
# =============================================================================
|
|
84
|
+
|
|
85
|
+
def get_repo_root() -> Path:
|
|
86
|
+
"""Get project root directory."""
|
|
87
|
+
return PROJECT_ROOT
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def ensure_dirs():
|
|
91
|
+
"""Ensure all necessary directories exist."""
|
|
92
|
+
dirs = [
|
|
93
|
+
WORKSPACE_DIR, SPECS_DIR, PRD_DIR, TASKS_DIR,
|
|
94
|
+
CONSTITUTION_DIR, MEMBERS_DIR,
|
|
95
|
+
RUNTIME_DIR, SESSIONS_DIR, ARCHIVE_DIR,
|
|
96
|
+
]
|
|
97
|
+
for d in dirs:
|
|
98
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _parse_developer_file(content: str) -> Dict[str, str]:
|
|
102
|
+
"""Parse .developer content. Accepts both 'name=x' and 'name: x' formats."""
|
|
103
|
+
info = {}
|
|
104
|
+
for line in content.splitlines():
|
|
105
|
+
line = line.strip()
|
|
106
|
+
if not line or line.startswith("#"):
|
|
107
|
+
continue
|
|
108
|
+
for sep in ("=", ":"):
|
|
109
|
+
if sep in line:
|
|
110
|
+
k, v = line.split(sep, 1)
|
|
111
|
+
info[k.strip()] = v.strip()
|
|
112
|
+
break
|
|
113
|
+
return info
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def get_developer(repo_root: Optional[Path] = None) -> Optional[str]:
|
|
117
|
+
"""Get current developer name from .developer file."""
|
|
118
|
+
if repo_root is None:
|
|
119
|
+
repo_root = PROJECT_ROOT
|
|
120
|
+
dev_file = repo_root / ".qoder" / ".developer"
|
|
121
|
+
if not dev_file.is_file():
|
|
122
|
+
return None
|
|
123
|
+
try:
|
|
124
|
+
content = dev_file.read_text(encoding="utf-8")
|
|
125
|
+
except UnicodeDecodeError:
|
|
126
|
+
try:
|
|
127
|
+
content = dev_file.read_text(encoding="gbk")
|
|
128
|
+
except (OSError, IOError, UnicodeDecodeError):
|
|
129
|
+
return None
|
|
130
|
+
except (OSError, IOError):
|
|
131
|
+
return None
|
|
132
|
+
return _parse_developer_file(content).get("name") or None
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def check_developer(repo_root: Optional[Path] = None) -> bool:
|
|
136
|
+
"""Check if developer is initialized."""
|
|
137
|
+
return get_developer(repo_root) is not None
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def get_developer_info(repo_root: Optional[Path] = None) -> Optional[Dict]:
|
|
141
|
+
"""Get developer info as dict."""
|
|
142
|
+
if repo_root is None:
|
|
143
|
+
repo_root = PROJECT_ROOT
|
|
144
|
+
dev_file = repo_root / ".qoder" / ".developer"
|
|
145
|
+
if not dev_file.is_file():
|
|
146
|
+
return None
|
|
147
|
+
try:
|
|
148
|
+
content = dev_file.read_text(encoding="utf-8")
|
|
149
|
+
except UnicodeDecodeError:
|
|
150
|
+
try:
|
|
151
|
+
content = dev_file.read_text(encoding="gbk")
|
|
152
|
+
except (OSError, IOError, UnicodeDecodeError):
|
|
153
|
+
return None
|
|
154
|
+
except (OSError, IOError):
|
|
155
|
+
return None
|
|
156
|
+
info = _parse_developer_file(content)
|
|
157
|
+
return info if info else None
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def get_developer_dir(developer_name: str) -> Path:
|
|
161
|
+
"""Get developer personal directory under workspace/members/."""
|
|
162
|
+
dev_dir = MEMBERS_DIR / developer_name
|
|
163
|
+
dev_dir.mkdir(parents=True, exist_ok=True)
|
|
164
|
+
return dev_dir
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def get_developer_journal_dir(developer_name: str) -> Path:
|
|
168
|
+
"""Get developer journal directory."""
|
|
169
|
+
journal_dir = get_developer_dir(developer_name) / "journal"
|
|
170
|
+
journal_dir.mkdir(parents=True, exist_ok=True)
|
|
171
|
+
return journal_dir
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def get_developer_drafts_dir(developer_name: str) -> Path:
|
|
175
|
+
"""Get developer drafts directory."""
|
|
176
|
+
drafts_dir = get_developer_dir(developer_name) / "drafts"
|
|
177
|
+
drafts_dir.mkdir(parents=True, exist_ok=True)
|
|
178
|
+
return drafts_dir
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def get_workspace_dir(repo_root: Optional[Path] = None) -> Optional[Path]:
|
|
182
|
+
"""Get developer workspace directory."""
|
|
183
|
+
dev = get_developer(repo_root)
|
|
184
|
+
if not dev:
|
|
185
|
+
return None
|
|
186
|
+
return MEMBERS_DIR / dev
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def get_active_journal_file(repo_root: Optional[Path] = None) -> Optional[Path]:
|
|
190
|
+
"""Get developer active journal file."""
|
|
191
|
+
ws = get_workspace_dir(repo_root)
|
|
192
|
+
if not ws:
|
|
193
|
+
return None
|
|
194
|
+
journal_dir = ws / "journal"
|
|
195
|
+
if not journal_dir.is_dir():
|
|
196
|
+
return None
|
|
197
|
+
journals = sorted(journal_dir.glob("journal-*.md"))
|
|
198
|
+
return journals[-1] if journals else None
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def get_tasks_dir(repo_root: Optional[Path] = None) -> Path:
|
|
202
|
+
"""Get tasks directory (workspace/tasks/)."""
|
|
203
|
+
if repo_root is None:
|
|
204
|
+
repo_root = PROJECT_ROOT
|
|
205
|
+
tasks = repo_root / "workspace" / "tasks"
|
|
206
|
+
tasks.mkdir(parents=True, exist_ok=True)
|
|
207
|
+
return tasks
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _current_task_file(repo_root: Path, developer: Optional[str] = None) -> Path:
|
|
211
|
+
"""当前任务文件路径。
|
|
212
|
+
|
|
213
|
+
零信任隔离: 按开发者命名, 避免共享机器上 A 的 current-task 被 B 覆盖。
|
|
214
|
+
developer=None 时回退到旧的全局 .current-task (向后兼容读取)。
|
|
215
|
+
"""
|
|
216
|
+
repo_root = Path(repo_root) # 容忍 str 输入
|
|
217
|
+
runtime = repo_root / ".qoder" / ".runtime"
|
|
218
|
+
if developer:
|
|
219
|
+
# 文件名只允许安全字符 (member name 已校验, 但防御性 sanitize)
|
|
220
|
+
safe = "".join(c for c in developer if c.isalnum() or c in "_-") or "anon"
|
|
221
|
+
return runtime / f"current-task.{safe}"
|
|
222
|
+
return repo_root / ".qoder" / ".current-task"
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _read_task_file(path: Path) -> Optional[str]:
|
|
226
|
+
"""读任务文件, 支持 UTF-8 和 GBK fallback (审计 H6: Notepad 默认 GBK)。"""
|
|
227
|
+
if not path.is_file():
|
|
228
|
+
return None
|
|
229
|
+
for enc in ("utf-8", "gbk", "utf-8-sig"):
|
|
230
|
+
try:
|
|
231
|
+
content = path.read_text(encoding=enc).strip()
|
|
232
|
+
return content if content else None
|
|
233
|
+
except (OSError, IOError, UnicodeDecodeError):
|
|
234
|
+
continue
|
|
235
|
+
return None
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def get_current_task(
|
|
239
|
+
repo_root: Optional[Path] = None,
|
|
240
|
+
developer: Optional[str] = None,
|
|
241
|
+
) -> Optional[str]:
|
|
242
|
+
"""Get current task.
|
|
243
|
+
|
|
244
|
+
优先读按开发者隔离的 .runtime/current-task.{dev};
|
|
245
|
+
若无则回退到旧的全局 .current-task (向后兼容, 会自动迁移)。
|
|
246
|
+
developer=None 时自动用 get_developer() 推断。
|
|
247
|
+
"""
|
|
248
|
+
if repo_root is None:
|
|
249
|
+
repo_root = PROJECT_ROOT
|
|
250
|
+
if developer is None:
|
|
251
|
+
developer = get_developer(repo_root)
|
|
252
|
+
# 1. 优先读 developer 隔离的文件
|
|
253
|
+
if developer:
|
|
254
|
+
ct = _read_task_file(_current_task_file(repo_root, developer))
|
|
255
|
+
if ct:
|
|
256
|
+
return ct
|
|
257
|
+
# 2. 回退到旧全局文件 (兼容历史)
|
|
258
|
+
legacy = _read_task_file(_current_task_file(repo_root, None))
|
|
259
|
+
return legacy
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def set_current_task(
|
|
263
|
+
task_path: Optional[str],
|
|
264
|
+
repo_root: Optional[Path] = None,
|
|
265
|
+
developer: Optional[str] = None,
|
|
266
|
+
) -> None:
|
|
267
|
+
"""Set current task (按开发者隔离写入)。
|
|
268
|
+
|
|
269
|
+
零信任: 写入 .runtime/current-task.{dev}, 不再写全局 .current-task。
|
|
270
|
+
若 task_path=None 则清除该开发者的指针 (不影响他人)。
|
|
271
|
+
"""
|
|
272
|
+
if repo_root is None:
|
|
273
|
+
repo_root = PROJECT_ROOT
|
|
274
|
+
if developer is None:
|
|
275
|
+
developer = get_developer(repo_root)
|
|
276
|
+
ct_file = _current_task_file(repo_root, developer)
|
|
277
|
+
try:
|
|
278
|
+
ct_file.parent.mkdir(parents=True, exist_ok=True)
|
|
279
|
+
if task_path:
|
|
280
|
+
ct_file.write_text(task_path, encoding="utf-8")
|
|
281
|
+
else:
|
|
282
|
+
ct_file.unlink(missing_ok=True)
|
|
283
|
+
except (OSError, IOError) as e:
|
|
284
|
+
print(f"Warning: Failed to write current-task: {e}", file=sys.stderr)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def get_module_spec_dir(module: Optional[str] = None) -> Path:
|
|
288
|
+
"""Get module spec directory."""
|
|
289
|
+
if module:
|
|
290
|
+
spec_dir = SPECS_DIR / module
|
|
291
|
+
else:
|
|
292
|
+
spec_dir = SPECS_DIR
|
|
293
|
+
spec_dir.mkdir(parents=True, exist_ok=True)
|
|
294
|
+
return spec_dir
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def get_task_dir(slug: str) -> Path:
|
|
298
|
+
"""Get task directory by slug."""
|
|
299
|
+
from datetime import datetime
|
|
300
|
+
date_prefix = datetime.now().strftime("%m-%d")
|
|
301
|
+
task_dir = TASKS_DIR / "{0}-{1}".format(date_prefix, slug)
|
|
302
|
+
task_dir.mkdir(parents=True, exist_ok=True)
|
|
303
|
+
return task_dir
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def count_lines(file_path: Path) -> int:
|
|
307
|
+
"""Count lines in a file."""
|
|
308
|
+
try:
|
|
309
|
+
return sum(1 for _ in file_path.open("r", encoding="utf-8"))
|
|
310
|
+
except (OSError, IOError):
|
|
311
311
|
return 0
|