cowork-flow 0.0.1
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/README.md +193 -0
- package/bin/cowork-flow.js +9 -0
- package/package.json +28 -0
- package/src/cli.js +61 -0
- package/src/commands/init.js +88 -0
- package/src/commands/sync.js +33 -0
- package/src/commands/update.js +60 -0
- package/src/lib/copy-template.js +189 -0
- package/src/lib/package-info.js +49 -0
- package/src/lib/paths.js +6 -0
- package/template/.agent/skills/break-loop/SKILL.md +131 -0
- package/template/.agent/skills/check-cross-layer/SKILL.md +158 -0
- package/template/.agent/skills/finish-work/SKILL.md +163 -0
- package/template/.agent/skills/record-session/SKILL.md +78 -0
- package/template/.agent/skills/start/SKILL.md +390 -0
- package/template/.agent/skills/update-spec/SKILL.md +335 -0
- package/template/.cowork-flow/.version +1 -0
- package/template/.cowork-flow/changes/archive/.gitkeep +1 -0
- package/template/.cowork-flow/config.yaml +49 -0
- package/template/.cowork-flow/plans/.gitkeep +1 -0
- package/template/.cowork-flow/scripts/add_session.py +441 -0
- package/template/.cowork-flow/scripts/change.py +314 -0
- package/template/.cowork-flow/scripts/common/__init__.py +84 -0
- package/template/.cowork-flow/scripts/common/config.py +128 -0
- package/template/.cowork-flow/scripts/common/developer.py +205 -0
- package/template/.cowork-flow/scripts/common/files.py +24 -0
- package/template/.cowork-flow/scripts/common/git_context.py +628 -0
- package/template/.cowork-flow/scripts/common/paths.py +350 -0
- package/template/.cowork-flow/scripts/common/task_utils.py +200 -0
- package/template/.cowork-flow/scripts/get_context.py +16 -0
- package/template/.cowork-flow/scripts/get_developer.py +26 -0
- package/template/.cowork-flow/scripts/init_developer.py +51 -0
- package/template/.cowork-flow/scripts/task.py +1378 -0
- package/template/.cowork-flow/spec/backend/database-guidelines.md +26 -0
- package/template/.cowork-flow/spec/backend/directory-structure.md +27 -0
- package/template/.cowork-flow/spec/backend/error-handling.md +20 -0
- package/template/.cowork-flow/spec/backend/index.md +36 -0
- package/template/.cowork-flow/spec/backend/logging-guidelines.md +20 -0
- package/template/.cowork-flow/spec/backend/quality-guidelines.md +24 -0
- package/template/.cowork-flow/spec/frontend/component-guidelines.md +19 -0
- package/template/.cowork-flow/spec/frontend/directory-structure.md +18 -0
- package/template/.cowork-flow/spec/frontend/hook-guidelines.md +17 -0
- package/template/.cowork-flow/spec/frontend/index.md +28 -0
- package/template/.cowork-flow/spec/frontend/quality-guidelines.md +23 -0
- package/template/.cowork-flow/spec/frontend/state-management.md +18 -0
- package/template/.cowork-flow/spec/frontend/type-safety.md +18 -0
- package/template/.cowork-flow/spec/guides/code-reuse-thinking-guide.md +26 -0
- package/template/.cowork-flow/spec/guides/cross-layer-thinking-guide.md +21 -0
- package/template/.cowork-flow/spec/guides/index.md +31 -0
- package/template/.cowork-flow/spec/guides/pre-implementation-checklist.md +15 -0
- package/template/.cowork-flow/tasks/.gitkeep +1 -0
- package/template/.cowork-flow/tasks/archive/.gitkeep +1 -0
- package/template/.cowork-flow/workflow.md +411 -0
- package/template/.cowork-flow/workspace/index.md +95 -0
- package/template/.superpowers/brainstorming/SKILL.md +164 -0
- package/template/.superpowers/brainstorming/scripts/frame-template.html +214 -0
- package/template/.superpowers/brainstorming/scripts/helper.js +88 -0
- package/template/.superpowers/brainstorming/scripts/server.cjs +354 -0
- package/template/.superpowers/brainstorming/scripts/start-server.sh +148 -0
- package/template/.superpowers/brainstorming/scripts/stop-server.sh +56 -0
- package/template/.superpowers/brainstorming/spec-document-reviewer-prompt.md +49 -0
- package/template/.superpowers/brainstorming/visual-companion.md +287 -0
- package/template/.superpowers/dispatching-parallel-agents/SKILL.md +182 -0
- package/template/.superpowers/executing-plans/SKILL.md +70 -0
- package/template/.superpowers/finishing-a-development-branch/SKILL.md +251 -0
- package/template/.superpowers/receiving-code-review/SKILL.md +213 -0
- package/template/.superpowers/requesting-code-review/SKILL.md +103 -0
- package/template/.superpowers/requesting-code-review/code-reviewer.md +168 -0
- package/template/.superpowers/subagent-driven-development/SKILL.md +279 -0
- package/template/.superpowers/subagent-driven-development/code-quality-reviewer-prompt.md +25 -0
- package/template/.superpowers/subagent-driven-development/implementer-prompt.md +113 -0
- package/template/.superpowers/subagent-driven-development/spec-reviewer-prompt.md +61 -0
- package/template/.superpowers/systematic-debugging/CREATION-LOG.md +119 -0
- package/template/.superpowers/systematic-debugging/SKILL.md +296 -0
- package/template/.superpowers/systematic-debugging/condition-based-waiting-example.ts +158 -0
- package/template/.superpowers/systematic-debugging/condition-based-waiting.md +115 -0
- package/template/.superpowers/systematic-debugging/defense-in-depth.md +122 -0
- package/template/.superpowers/systematic-debugging/find-polluter.sh +63 -0
- package/template/.superpowers/systematic-debugging/root-cause-tracing.md +169 -0
- package/template/.superpowers/systematic-debugging/test-academic.md +14 -0
- package/template/.superpowers/systematic-debugging/test-pressure-1.md +58 -0
- package/template/.superpowers/systematic-debugging/test-pressure-2.md +68 -0
- package/template/.superpowers/systematic-debugging/test-pressure-3.md +69 -0
- package/template/.superpowers/test-driven-development/SKILL.md +371 -0
- package/template/.superpowers/test-driven-development/testing-anti-patterns.md +299 -0
- package/template/.superpowers/using-git-worktrees/SKILL.md +215 -0
- package/template/.superpowers/using-superpowers/SKILL.md +117 -0
- package/template/.superpowers/using-superpowers/references/codex-tools.md +59 -0
- package/template/.superpowers/using-superpowers/references/copilot-tools.md +42 -0
- package/template/.superpowers/using-superpowers/references/gemini-tools.md +51 -0
- package/template/.superpowers/verification-before-completion/SKILL.md +139 -0
- package/template/.superpowers/writing-plans/SKILL.md +152 -0
- package/template/.superpowers/writing-plans/plan-document-reviewer-prompt.md +49 -0
- package/template/.superpowers/writing-skills/SKILL.md +655 -0
- package/template/.superpowers/writing-skills/anthropic-best-practices.md +1150 -0
- package/template/.superpowers/writing-skills/examples/CLAUDE_MD_TESTING.md +189 -0
- package/template/.superpowers/writing-skills/graphviz-conventions.dot +172 -0
- package/template/.superpowers/writing-skills/persuasion-principles.md +187 -0
- package/template/.superpowers/writing-skills/render-graphs.js +168 -0
- package/template/.superpowers/writing-skills/testing-skills-with-subagents.md +384 -0
- package/template/AGENTS.md +116 -0
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Common path utilities for Cowork Flow workflow.
|
|
4
|
+
|
|
5
|
+
Provides:
|
|
6
|
+
get_repo_root - Get repository root directory
|
|
7
|
+
get_developer - Get developer name
|
|
8
|
+
get_workspace_dir - Get developer workspace directory
|
|
9
|
+
get_tasks_dir - Get tasks directory
|
|
10
|
+
get_active_journal_file - Get current journal file
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import re
|
|
16
|
+
from datetime import datetime
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# =============================================================================
|
|
21
|
+
# Path Constants (change here to rename directories)
|
|
22
|
+
# =============================================================================
|
|
23
|
+
|
|
24
|
+
# Directory names
|
|
25
|
+
DIR_WORKFLOW = ".cowork-flow"
|
|
26
|
+
DIR_AGENT = ".agent"
|
|
27
|
+
DIR_CHANGES = "changes"
|
|
28
|
+
DIR_WORKSPACE = "workspace"
|
|
29
|
+
DIR_TASKS = "tasks"
|
|
30
|
+
DIR_ARCHIVE = "archive"
|
|
31
|
+
DIR_SPEC = "spec"
|
|
32
|
+
DIR_SCRIPTS = "scripts"
|
|
33
|
+
|
|
34
|
+
# File names
|
|
35
|
+
FILE_DEVELOPER = ".developer"
|
|
36
|
+
FILE_CURRENT_TASK = ".current-task"
|
|
37
|
+
FILE_TASK_JSON = "task.json"
|
|
38
|
+
FILE_JOURNAL_PREFIX = "journal-"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# =============================================================================
|
|
42
|
+
# Repository Root
|
|
43
|
+
# =============================================================================
|
|
44
|
+
|
|
45
|
+
def get_repo_root(start_path: Path | None = None) -> Path:
|
|
46
|
+
"""Find the nearest directory containing .cowork-flow/ folder.
|
|
47
|
+
|
|
48
|
+
This handles nested git repos correctly (e.g., test project inside another repo).
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
start_path: Starting directory to search from. Defaults to current directory.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
Path to repository root, or current directory if no .cowork-flow/ found.
|
|
55
|
+
"""
|
|
56
|
+
current = (start_path or Path.cwd()).absolute()
|
|
57
|
+
|
|
58
|
+
while current != current.parent:
|
|
59
|
+
if (current / DIR_WORKFLOW).is_dir():
|
|
60
|
+
return current
|
|
61
|
+
current = current.parent
|
|
62
|
+
|
|
63
|
+
# Fallback to current directory if no .cowork-flow/ found
|
|
64
|
+
return Path.cwd().absolute()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# =============================================================================
|
|
68
|
+
# Developer
|
|
69
|
+
# =============================================================================
|
|
70
|
+
|
|
71
|
+
def get_developer(repo_root: Path | None = None) -> str | None:
|
|
72
|
+
"""Get developer name from .developer file.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
repo_root: Repository root path. Defaults to auto-detected.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Developer name or None if not initialized.
|
|
79
|
+
"""
|
|
80
|
+
if repo_root is None:
|
|
81
|
+
repo_root = get_repo_root()
|
|
82
|
+
|
|
83
|
+
dev_file = repo_root / DIR_WORKFLOW / FILE_DEVELOPER
|
|
84
|
+
|
|
85
|
+
if not dev_file.is_file():
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
content = dev_file.read_text(encoding="utf-8")
|
|
90
|
+
for line in content.splitlines():
|
|
91
|
+
if line.startswith("name="):
|
|
92
|
+
return line.split("=", 1)[1].strip()
|
|
93
|
+
except (OSError, IOError):
|
|
94
|
+
pass
|
|
95
|
+
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def check_developer(repo_root: Path | None = None) -> bool:
|
|
100
|
+
"""Check if developer is initialized.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
repo_root: Repository root path. Defaults to auto-detected.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
True if developer is initialized.
|
|
107
|
+
"""
|
|
108
|
+
return get_developer(repo_root) is not None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# =============================================================================
|
|
112
|
+
# Tasks Directory
|
|
113
|
+
# =============================================================================
|
|
114
|
+
|
|
115
|
+
def get_tasks_dir(repo_root: Path | None = None) -> Path:
|
|
116
|
+
"""Get tasks directory path.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
repo_root: Repository root path. Defaults to auto-detected.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
Path to tasks directory.
|
|
123
|
+
"""
|
|
124
|
+
if repo_root is None:
|
|
125
|
+
repo_root = get_repo_root()
|
|
126
|
+
return repo_root / DIR_WORKFLOW / DIR_TASKS
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# =============================================================================
|
|
130
|
+
# Workspace Directory
|
|
131
|
+
# =============================================================================
|
|
132
|
+
|
|
133
|
+
def get_workspace_dir(repo_root: Path | None = None) -> Path | None:
|
|
134
|
+
"""Get developer workspace directory.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
repo_root: Repository root path. Defaults to auto-detected.
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
Path to workspace directory or None if developer not set.
|
|
141
|
+
"""
|
|
142
|
+
if repo_root is None:
|
|
143
|
+
repo_root = get_repo_root()
|
|
144
|
+
|
|
145
|
+
developer = get_developer(repo_root)
|
|
146
|
+
if developer:
|
|
147
|
+
return repo_root / DIR_WORKFLOW / DIR_WORKSPACE / developer
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# =============================================================================
|
|
152
|
+
# Journal File
|
|
153
|
+
# =============================================================================
|
|
154
|
+
|
|
155
|
+
def get_active_journal_file(repo_root: Path | None = None) -> Path | None:
|
|
156
|
+
"""Get the current active journal file.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
repo_root: Repository root path. Defaults to auto-detected.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
Path to active journal file or None if not found.
|
|
163
|
+
"""
|
|
164
|
+
if repo_root is None:
|
|
165
|
+
repo_root = get_repo_root()
|
|
166
|
+
|
|
167
|
+
workspace_dir = get_workspace_dir(repo_root)
|
|
168
|
+
if workspace_dir is None or not workspace_dir.is_dir():
|
|
169
|
+
return None
|
|
170
|
+
|
|
171
|
+
latest: Path | None = None
|
|
172
|
+
highest = 0
|
|
173
|
+
|
|
174
|
+
for f in workspace_dir.glob(f"{FILE_JOURNAL_PREFIX}*.md"):
|
|
175
|
+
if not f.is_file():
|
|
176
|
+
continue
|
|
177
|
+
|
|
178
|
+
# Extract number from filename
|
|
179
|
+
name = f.stem # e.g., "journal-1"
|
|
180
|
+
match = re.search(r"(\d+)$", name)
|
|
181
|
+
if match:
|
|
182
|
+
num = int(match.group(1))
|
|
183
|
+
if num > highest:
|
|
184
|
+
highest = num
|
|
185
|
+
latest = f
|
|
186
|
+
|
|
187
|
+
return latest
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def count_lines(file_path: Path) -> int:
|
|
191
|
+
"""Count lines in a file.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
file_path: Path to file.
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
Number of lines, or 0 if file doesn't exist.
|
|
198
|
+
"""
|
|
199
|
+
if not file_path.is_file():
|
|
200
|
+
return 0
|
|
201
|
+
|
|
202
|
+
try:
|
|
203
|
+
with file_path.open("r", encoding="utf-8") as f:
|
|
204
|
+
return sum(1 for _ in f)
|
|
205
|
+
except OSError:
|
|
206
|
+
return 0
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
# =============================================================================
|
|
210
|
+
# Current Task Management
|
|
211
|
+
# =============================================================================
|
|
212
|
+
|
|
213
|
+
def _get_current_task_file(repo_root: Path | None = None) -> Path:
|
|
214
|
+
"""Get .current-task file path.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
repo_root: Repository root path. Defaults to auto-detected.
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
Path to .current-task file.
|
|
221
|
+
"""
|
|
222
|
+
if repo_root is None:
|
|
223
|
+
repo_root = get_repo_root()
|
|
224
|
+
return repo_root / DIR_WORKFLOW / FILE_CURRENT_TASK
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def get_current_task(repo_root: Path | None = None) -> str | None:
|
|
228
|
+
"""Get current task directory path (relative to repo_root).
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
repo_root: Repository root path. Defaults to auto-detected.
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
Relative path to current task directory or None.
|
|
235
|
+
"""
|
|
236
|
+
current_file = _get_current_task_file(repo_root)
|
|
237
|
+
|
|
238
|
+
if not current_file.is_file():
|
|
239
|
+
return None
|
|
240
|
+
|
|
241
|
+
try:
|
|
242
|
+
return current_file.read_text(encoding="utf-8").strip()
|
|
243
|
+
except (OSError, IOError):
|
|
244
|
+
return None
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def get_current_task_abs(repo_root: Path | None = None) -> Path | None:
|
|
248
|
+
"""Get current task directory absolute path.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
repo_root: Repository root path. Defaults to auto-detected.
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
Absolute path to current task directory or None.
|
|
255
|
+
"""
|
|
256
|
+
if repo_root is None:
|
|
257
|
+
repo_root = get_repo_root()
|
|
258
|
+
|
|
259
|
+
relative = get_current_task(repo_root)
|
|
260
|
+
if relative:
|
|
261
|
+
return repo_root / relative
|
|
262
|
+
return None
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def set_current_task(task_path: str, repo_root: Path | None = None) -> bool:
|
|
266
|
+
"""Set current task.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
task_path: Task directory path (relative to repo_root).
|
|
270
|
+
repo_root: Repository root path. Defaults to auto-detected.
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
True on success, False on error.
|
|
274
|
+
"""
|
|
275
|
+
if repo_root is None:
|
|
276
|
+
repo_root = get_repo_root()
|
|
277
|
+
|
|
278
|
+
if not task_path:
|
|
279
|
+
return False
|
|
280
|
+
|
|
281
|
+
# Verify task directory exists
|
|
282
|
+
full_path = repo_root / task_path
|
|
283
|
+
if not full_path.is_dir():
|
|
284
|
+
return False
|
|
285
|
+
|
|
286
|
+
current_file = _get_current_task_file(repo_root)
|
|
287
|
+
|
|
288
|
+
try:
|
|
289
|
+
current_file.write_text(task_path, encoding="utf-8")
|
|
290
|
+
return True
|
|
291
|
+
except (OSError, IOError):
|
|
292
|
+
return False
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def clear_current_task(repo_root: Path | None = None) -> bool:
|
|
296
|
+
"""Clear current task.
|
|
297
|
+
|
|
298
|
+
Args:
|
|
299
|
+
repo_root: Repository root path. Defaults to auto-detected.
|
|
300
|
+
|
|
301
|
+
Returns:
|
|
302
|
+
True on success.
|
|
303
|
+
"""
|
|
304
|
+
current_file = _get_current_task_file(repo_root)
|
|
305
|
+
|
|
306
|
+
try:
|
|
307
|
+
if current_file.is_file():
|
|
308
|
+
current_file.unlink()
|
|
309
|
+
return True
|
|
310
|
+
except (OSError, IOError):
|
|
311
|
+
return False
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def has_current_task(repo_root: Path | None = None) -> bool:
|
|
315
|
+
"""Check if has current task.
|
|
316
|
+
|
|
317
|
+
Args:
|
|
318
|
+
repo_root: Repository root path. Defaults to auto-detected.
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
True if current task is set.
|
|
322
|
+
"""
|
|
323
|
+
return get_current_task(repo_root) is not None
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
# =============================================================================
|
|
327
|
+
# Task ID Generation
|
|
328
|
+
# =============================================================================
|
|
329
|
+
|
|
330
|
+
def generate_task_date_prefix() -> str:
|
|
331
|
+
"""Generate task ID based on date (MM-DD format).
|
|
332
|
+
|
|
333
|
+
Returns:
|
|
334
|
+
Date prefix string (e.g., "01-21").
|
|
335
|
+
"""
|
|
336
|
+
return datetime.now().strftime("%m-%d")
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
# =============================================================================
|
|
340
|
+
# Main Entry (for testing)
|
|
341
|
+
# =============================================================================
|
|
342
|
+
|
|
343
|
+
if __name__ == "__main__":
|
|
344
|
+
repo = get_repo_root()
|
|
345
|
+
print(f"Repository root: {repo}")
|
|
346
|
+
print(f"Developer: {get_developer(repo)}")
|
|
347
|
+
print(f"Tasks dir: {get_tasks_dir(repo)}")
|
|
348
|
+
print(f"Workspace dir: {get_workspace_dir(repo)}")
|
|
349
|
+
print(f"Journal file: {get_active_journal_file(repo)}")
|
|
350
|
+
print(f"Current task: {get_current_task(repo)}")
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Task utility functions.
|
|
4
|
+
|
|
5
|
+
Provides:
|
|
6
|
+
is_safe_task_path - Validate task path is safe to operate on
|
|
7
|
+
find_task_by_name - Find task directory by name
|
|
8
|
+
archive_task_dir - Archive task to monthly directory
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import shutil
|
|
14
|
+
import sys
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from .paths import get_repo_root
|
|
19
|
+
|
|
20
|
+
PATH_TRAVERSAL_PREFIXES = ("./", "../")
|
|
21
|
+
PATH_TRAVERSAL_PART = ".."
|
|
22
|
+
UNSAFE_TASK_PATHS = (".", "..")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# =============================================================================
|
|
26
|
+
# Path Safety
|
|
27
|
+
# =============================================================================
|
|
28
|
+
|
|
29
|
+
def is_safe_task_path(task_path: str, repo_root: Path | None = None) -> bool:
|
|
30
|
+
"""Check if a relative task path is safe to operate on.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
task_path: Task path (relative to repo_root).
|
|
34
|
+
repo_root: Repository root path. Defaults to auto-detected.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
True if safe, False if dangerous.
|
|
38
|
+
"""
|
|
39
|
+
if repo_root is None:
|
|
40
|
+
repo_root = get_repo_root()
|
|
41
|
+
|
|
42
|
+
# Check empty or null
|
|
43
|
+
if not task_path or task_path == "null":
|
|
44
|
+
print("Error: empty or null task path", file=sys.stderr)
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
# Reject absolute paths
|
|
48
|
+
if task_path.startswith("/"):
|
|
49
|
+
print(f"Error: absolute path not allowed: {task_path}", file=sys.stderr)
|
|
50
|
+
return False
|
|
51
|
+
|
|
52
|
+
# Reject ".", "..", paths starting with "./" or "../", or containing ".."
|
|
53
|
+
has_path_traversal = (
|
|
54
|
+
task_path in UNSAFE_TASK_PATHS
|
|
55
|
+
or task_path.startswith(PATH_TRAVERSAL_PREFIXES)
|
|
56
|
+
or PATH_TRAVERSAL_PART in task_path
|
|
57
|
+
)
|
|
58
|
+
if has_path_traversal:
|
|
59
|
+
print(f"Error: path traversal not allowed: {task_path}", file=sys.stderr)
|
|
60
|
+
return False
|
|
61
|
+
|
|
62
|
+
# Final check: ensure resolved path is not the repo root
|
|
63
|
+
abs_path = repo_root / task_path
|
|
64
|
+
if abs_path.exists():
|
|
65
|
+
try:
|
|
66
|
+
resolved = abs_path.resolve()
|
|
67
|
+
root_resolved = repo_root.resolve()
|
|
68
|
+
if resolved == root_resolved:
|
|
69
|
+
print(f"Error: path resolves to repo root: {task_path}", file=sys.stderr)
|
|
70
|
+
return False
|
|
71
|
+
except (OSError, IOError):
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
return True
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# =============================================================================
|
|
78
|
+
# Task Lookup
|
|
79
|
+
# =============================================================================
|
|
80
|
+
|
|
81
|
+
def find_task_by_name(task_name: str, tasks_dir: Path) -> Path | None:
|
|
82
|
+
"""Find task directory by name (exact or suffix match).
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
task_name: Task name to find.
|
|
86
|
+
tasks_dir: Tasks directory path.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
Absolute path to task directory, or None if not found.
|
|
90
|
+
"""
|
|
91
|
+
if not task_name or not tasks_dir or not tasks_dir.is_dir():
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
# Try exact match first
|
|
95
|
+
exact_match = tasks_dir / task_name
|
|
96
|
+
if exact_match.is_dir():
|
|
97
|
+
return exact_match
|
|
98
|
+
|
|
99
|
+
# Try suffix match (e.g., "my-task" matches "01-21-my-task")
|
|
100
|
+
for candidate in tasks_dir.iterdir():
|
|
101
|
+
if candidate.is_dir() and candidate.name.endswith(f"-{task_name}"):
|
|
102
|
+
return candidate
|
|
103
|
+
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# =============================================================================
|
|
108
|
+
# Archive Operations
|
|
109
|
+
# =============================================================================
|
|
110
|
+
|
|
111
|
+
def archive_task_dir(task_dir_abs: Path, repo_root: Path | None = None) -> Path | None:
|
|
112
|
+
"""Archive a task directory to archive/{YYYY-MM}/.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
task_dir_abs: Absolute path to task directory.
|
|
116
|
+
repo_root: Repository root path. Defaults to auto-detected.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
Path to archived directory, or None on error.
|
|
120
|
+
"""
|
|
121
|
+
if not task_dir_abs.is_dir():
|
|
122
|
+
print(f"Error: task directory not found: {task_dir_abs}", file=sys.stderr)
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
# Get tasks directory (parent of the task)
|
|
126
|
+
tasks_dir = task_dir_abs.parent
|
|
127
|
+
archive_dir = tasks_dir / "archive"
|
|
128
|
+
year_month = datetime.now().strftime("%Y-%m")
|
|
129
|
+
month_dir = archive_dir / year_month
|
|
130
|
+
|
|
131
|
+
# Create archive directory
|
|
132
|
+
try:
|
|
133
|
+
month_dir.mkdir(parents=True, exist_ok=True)
|
|
134
|
+
except (OSError, IOError) as e:
|
|
135
|
+
print(f"Error: Failed to create archive directory: {e}", file=sys.stderr)
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
# Move task to archive
|
|
139
|
+
task_name = task_dir_abs.name
|
|
140
|
+
archive_dest = month_dir / task_name
|
|
141
|
+
|
|
142
|
+
if archive_dest.exists():
|
|
143
|
+
print(f"Error: archive target already exists: {archive_dest}", file=sys.stderr)
|
|
144
|
+
print(f"Source task directory is still present: {task_dir_abs}", file=sys.stderr)
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
shutil.move(str(task_dir_abs), str(archive_dest))
|
|
149
|
+
except (OSError, IOError, shutil.Error) as e:
|
|
150
|
+
print(f"Error: Failed to move task to archive: {e}", file=sys.stderr)
|
|
151
|
+
if archive_dest.exists() and task_dir_abs.exists():
|
|
152
|
+
print("Error: archive move left both source and target paths present.", file=sys.stderr)
|
|
153
|
+
print(f" Source: {task_dir_abs}", file=sys.stderr)
|
|
154
|
+
print(f" Target: {archive_dest}", file=sys.stderr)
|
|
155
|
+
elif archive_dest.exists():
|
|
156
|
+
print(f"Error: archive target path exists after failure: {archive_dest}", file=sys.stderr)
|
|
157
|
+
elif task_dir_abs.exists():
|
|
158
|
+
print(f"Error: source task directory is still present: {task_dir_abs}", file=sys.stderr)
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
return archive_dest
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def archive_task_complete(
|
|
165
|
+
task_dir_abs: Path,
|
|
166
|
+
repo_root: Path | None = None
|
|
167
|
+
) -> dict[str, str]:
|
|
168
|
+
"""Complete archive workflow: archive directory.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
task_dir_abs: Absolute path to task directory.
|
|
172
|
+
repo_root: Repository root path. Defaults to auto-detected.
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
Dict with archive result info.
|
|
176
|
+
"""
|
|
177
|
+
if not task_dir_abs.is_dir():
|
|
178
|
+
print(f"Error: task directory not found: {task_dir_abs}", file=sys.stderr)
|
|
179
|
+
return {}
|
|
180
|
+
|
|
181
|
+
archive_dest = archive_task_dir(task_dir_abs, repo_root)
|
|
182
|
+
if archive_dest:
|
|
183
|
+
return {"archived_to": str(archive_dest)}
|
|
184
|
+
|
|
185
|
+
return {}
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# =============================================================================
|
|
189
|
+
# Main Entry (for testing)
|
|
190
|
+
# =============================================================================
|
|
191
|
+
|
|
192
|
+
if __name__ == "__main__":
|
|
193
|
+
from .paths import get_tasks_dir
|
|
194
|
+
|
|
195
|
+
repo = get_repo_root()
|
|
196
|
+
tasks = get_tasks_dir(repo)
|
|
197
|
+
|
|
198
|
+
print(f"Tasks dir: {tasks}")
|
|
199
|
+
print(f"is_safe_task_path('.cowork-flow/tasks/test'): {is_safe_task_path('.cowork-flow/tasks/test', repo)}")
|
|
200
|
+
print(f"is_safe_task_path('../test'): {is_safe_task_path('../test', repo)}")
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Get Session Context for AI Agent.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
python3 get_context.py Output context in text format
|
|
7
|
+
python3 get_context.py --json Output context in JSON format
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from common.git_context import main
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
if __name__ == "__main__":
|
|
16
|
+
main()
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Get current developer name.
|
|
4
|
+
|
|
5
|
+
This is a wrapper that uses common/paths.py
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
from common.paths import get_developer
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def main() -> None:
|
|
16
|
+
"""CLI entry point."""
|
|
17
|
+
developer = get_developer()
|
|
18
|
+
if developer:
|
|
19
|
+
print(developer)
|
|
20
|
+
else:
|
|
21
|
+
print("Developer not initialized", file=sys.stderr)
|
|
22
|
+
sys.exit(1)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
if __name__ == "__main__":
|
|
26
|
+
main()
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Initialize developer for workflow.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
python3 init_developer.py <developer-name>
|
|
7
|
+
|
|
8
|
+
This creates:
|
|
9
|
+
- .cowork-flow/.developer file with developer info
|
|
10
|
+
- .cowork-flow/workspace/<name>/ directory structure
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import sys
|
|
16
|
+
|
|
17
|
+
from common.paths import (
|
|
18
|
+
DIR_WORKFLOW,
|
|
19
|
+
FILE_DEVELOPER,
|
|
20
|
+
get_developer,
|
|
21
|
+
)
|
|
22
|
+
from common.developer import init_developer
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def main() -> None:
|
|
26
|
+
"""CLI entry point."""
|
|
27
|
+
if len(sys.argv) < 2:
|
|
28
|
+
print(f"Usage: {sys.argv[0]} <developer-name>")
|
|
29
|
+
print()
|
|
30
|
+
print("Example:")
|
|
31
|
+
print(f" {sys.argv[0]} john")
|
|
32
|
+
sys.exit(1)
|
|
33
|
+
|
|
34
|
+
name = sys.argv[1]
|
|
35
|
+
|
|
36
|
+
# Check if already initialized
|
|
37
|
+
existing = get_developer()
|
|
38
|
+
if existing:
|
|
39
|
+
print(f"Developer already initialized: {existing}")
|
|
40
|
+
print()
|
|
41
|
+
print(f"To reinitialize, remove {DIR_WORKFLOW}/{FILE_DEVELOPER} first")
|
|
42
|
+
sys.exit(0)
|
|
43
|
+
|
|
44
|
+
if init_developer(name):
|
|
45
|
+
sys.exit(0)
|
|
46
|
+
else:
|
|
47
|
+
sys.exit(1)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
if __name__ == "__main__":
|
|
51
|
+
main()
|