@hupan56/wlkj 2.2.4 → 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.
@@ -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