cowork-flow 0.0.2 → 0.0.4

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.
@@ -0,0 +1,191 @@
1
+ @echo off
2
+ setlocal EnableExtensions
3
+
4
+ set "MIN_PYTHON_LABEL=Python 3.8+"
5
+ set "VERSION_CHECK=import sys; raise SystemExit(0 if sys.version_info >= (3, 8) else 1)"
6
+ set "WORKFLOW_DIR=%~dp0"
7
+ set "SCRIPTS_DIR=%WORKFLOW_DIR%scripts"
8
+ set "SELECTED_PYTHON="
9
+ set "SELECTED_PYTHON_ARG="
10
+ set "REST_ARGS="
11
+
12
+ if "%~1"=="" goto usage_error
13
+
14
+ set "COMMAND_NAME=%~1"
15
+ if not "%~2"=="" (
16
+ for /F "tokens=1,* delims= " %%A in ("%*") do set "REST_ARGS=%%B"
17
+ )
18
+
19
+ if /I "%COMMAND_NAME%"=="-h" goto usage
20
+ if /I "%COMMAND_NAME%"=="--help" goto usage
21
+ if /I "%COMMAND_NAME%"=="help" goto usage
22
+ if /I "%COMMAND_NAME%"=="python" goto run_python_passthrough
23
+ if /I "%COMMAND_NAME%"=="resume" goto run_resume
24
+ if /I "%COMMAND_NAME%"=="task" goto run_task
25
+ if /I "%COMMAND_NAME%"=="change" goto run_change
26
+ if /I "%COMMAND_NAME%"=="get-context" goto run_get_context
27
+ if /I "%COMMAND_NAME%"=="get_context" goto run_get_context
28
+ if /I "%COMMAND_NAME%"=="get-developer" goto run_get_developer
29
+ if /I "%COMMAND_NAME%"=="get_developer" goto run_get_developer
30
+ if /I "%COMMAND_NAME%"=="init-developer" goto run_init_developer
31
+ if /I "%COMMAND_NAME%"=="init_developer" goto run_init_developer
32
+ if /I "%COMMAND_NAME%"=="add-session" goto run_add_session
33
+ if /I "%COMMAND_NAME%"=="add_session" goto run_add_session
34
+
35
+ if exist "%SCRIPTS_DIR%\%COMMAND_NAME%.py" (
36
+ call :run_script "%SCRIPTS_DIR%\%COMMAND_NAME%.py"
37
+ exit /b %ERRORLEVEL%
38
+ )
39
+
40
+ echo 错误:未知 cowork-flow 命令:%COMMAND_NAME% 1>&2
41
+ call :print_usage 1>&2
42
+ exit /b 2
43
+
44
+ :usage
45
+ call :print_usage
46
+ exit /b 0
47
+
48
+ :usage_error
49
+ call :print_usage 1>&2
50
+ exit /b 2
51
+
52
+ :run_python_passthrough
53
+ call :exec_python %REST_ARGS%
54
+ exit /b %ERRORLEVEL%
55
+
56
+ :run_resume
57
+ call :run_script "%SCRIPTS_DIR%\resume.py"
58
+ exit /b %ERRORLEVEL%
59
+
60
+ :run_task
61
+ call :run_script "%SCRIPTS_DIR%\task.py"
62
+ exit /b %ERRORLEVEL%
63
+
64
+ :run_change
65
+ call :run_script "%SCRIPTS_DIR%\change.py"
66
+ exit /b %ERRORLEVEL%
67
+
68
+ :run_get_context
69
+ call :run_script "%SCRIPTS_DIR%\get_context.py"
70
+ exit /b %ERRORLEVEL%
71
+
72
+ :run_get_developer
73
+ call :run_script "%SCRIPTS_DIR%\get_developer.py"
74
+ exit /b %ERRORLEVEL%
75
+
76
+ :run_init_developer
77
+ call :run_script "%SCRIPTS_DIR%\init_developer.py"
78
+ exit /b %ERRORLEVEL%
79
+
80
+ :run_add_session
81
+ call :run_script "%SCRIPTS_DIR%\add_session.py"
82
+ exit /b %ERRORLEVEL%
83
+
84
+ :print_usage
85
+ echo 用法:
86
+ echo .\.cowork-flow\run.cmd ^<command^> [args...]
87
+ echo .\.cowork-flow\run.cmd python [python-args...]
88
+ echo.
89
+ echo 常用 command:
90
+ echo resume
91
+ echo task
92
+ echo change
93
+ echo get-context
94
+ echo get-developer
95
+ echo init-developer
96
+ echo add-session
97
+ echo.
98
+ echo 解释器选择顺序:
99
+ echo COWORK_FLOW_PYTHON -^> PYTHON -^> python3 -^> python -^> py -3
100
+ exit /b 0
101
+
102
+ :candidate_is_valid
103
+ set "CANDIDATE_CMD=%~1"
104
+ set "CANDIDATE_ARG=%~2"
105
+
106
+ where "%CANDIDATE_CMD%" >nul 2>nul
107
+ if errorlevel 1 (
108
+ if not exist "%CANDIDATE_CMD%" exit /b 1
109
+ )
110
+
111
+ if not "%CANDIDATE_ARG%"=="" (
112
+ "%CANDIDATE_CMD%" "%CANDIDATE_ARG%" -c "%VERSION_CHECK%" >nul 2>nul
113
+ ) else (
114
+ "%CANDIDATE_CMD%" -c "%VERSION_CHECK%" >nul 2>nul
115
+ )
116
+ exit /b %ERRORLEVEL%
117
+
118
+ :select_explicit_python
119
+ set "ENV_NAME=%~1"
120
+ set "ENV_VALUE=%~2"
121
+
122
+ call :candidate_is_valid "%ENV_VALUE%" ""
123
+ if not errorlevel 1 (
124
+ set "SELECTED_PYTHON=%ENV_VALUE%"
125
+ set "SELECTED_PYTHON_ARG="
126
+ exit /b 0
127
+ )
128
+
129
+ echo 错误:%ENV_NAME% 指向的解释器不可用或版本低于 %MIN_PYTHON_LABEL%:%ENV_VALUE% 1>&2
130
+ echo 请设置 %ENV_NAME% 为可执行的 Python 3.8+ 解释器路径。 1>&2
131
+ exit /b 127
132
+
133
+ :select_python
134
+ set "SELECTED_PYTHON="
135
+ set "SELECTED_PYTHON_ARG="
136
+
137
+ if not "%COWORK_FLOW_PYTHON%"=="" (
138
+ call :select_explicit_python "COWORK_FLOW_PYTHON" "%COWORK_FLOW_PYTHON%"
139
+ exit /b %ERRORLEVEL%
140
+ )
141
+
142
+ if not "%PYTHON%"=="" (
143
+ call :select_explicit_python "PYTHON" "%PYTHON%"
144
+ exit /b %ERRORLEVEL%
145
+ )
146
+
147
+ call :candidate_is_valid "python3" ""
148
+ if not errorlevel 1 (
149
+ set "SELECTED_PYTHON=python3"
150
+ exit /b 0
151
+ )
152
+
153
+ call :candidate_is_valid "python" ""
154
+ if not errorlevel 1 (
155
+ set "SELECTED_PYTHON=python"
156
+ exit /b 0
157
+ )
158
+
159
+ call :candidate_is_valid "py" "-3"
160
+ if not errorlevel 1 (
161
+ set "SELECTED_PYTHON=py"
162
+ set "SELECTED_PYTHON_ARG=-3"
163
+ exit /b 0
164
+ )
165
+
166
+ echo 错误:需要 %MIN_PYTHON_LABEL%,但未找到可用解释器。 1>&2
167
+ echo 已尝试:COWORK_FLOW_PYTHON、PYTHON、python3、python、py -3。 1>&2
168
+ echo 可设置 COWORK_FLOW_PYTHON=C:\Path\To\python.exe 后重试。 1>&2
169
+ exit /b 127
170
+
171
+ :exec_python
172
+ call :select_python
173
+ if errorlevel 1 exit /b %ERRORLEVEL%
174
+
175
+ if not "%SELECTED_PYTHON_ARG%"=="" (
176
+ "%SELECTED_PYTHON%" "%SELECTED_PYTHON_ARG%" %*
177
+ ) else (
178
+ "%SELECTED_PYTHON%" %*
179
+ )
180
+ exit /b %ERRORLEVEL%
181
+
182
+ :run_script
183
+ set "SCRIPT_PATH=%~1"
184
+
185
+ if not exist "%SCRIPT_PATH%" (
186
+ echo 错误:找不到脚本:%SCRIPT_PATH% 1>&2
187
+ exit /b 2
188
+ )
189
+
190
+ call :exec_python "%SCRIPT_PATH%" %REST_ARGS%
191
+ exit /b %ERRORLEVEL%
@@ -4,8 +4,8 @@
4
4
  Add a new session to journal file and update index.md.
5
5
 
6
6
  Usage:
7
- python3 add_session.py --title "Title" --commit "hash" --summary "Summary"
8
- echo "content" | python3 add_session.py --title "Title" --commit "hash"
7
+ ./.cowork-flow/run add-session --title "Title" --commit "hash" --summary "Summary"
8
+ echo "content" | ./.cowork-flow/run add-session --title "Title" --commit "hash"
9
9
  """
10
10
 
11
11
  from __future__ import annotations
@@ -92,11 +92,8 @@ def _has_text(path: Path) -> bool:
92
92
  return path.is_file() and bool(path.read_text(encoding="utf-8").strip())
93
93
 
94
94
 
95
- def _spec_files(change_dir: Path) -> list[Path]:
96
- specs_dir = change_dir / "specs"
97
- if not specs_dir.is_dir():
98
- return []
99
- return sorted(path for path in specs_dir.rglob("spec.md") if path.is_file())
95
+ def _spec_file(change_dir: Path) -> Path:
96
+ return change_dir / "spec.md"
100
97
 
101
98
 
102
99
  def _resolve_link(repo_root: Path, base_dir: str, value: object) -> Path | None:
@@ -151,10 +148,8 @@ def validate_change(repo_root: Path, slug: str, quiet: bool = False) -> bool:
151
148
  errors.append("proposal.md is missing or empty")
152
149
 
153
150
  documentation_only = metadata.get("documentation_only") is True
154
- if not documentation_only:
155
- specs = _spec_files(change_dir)
156
- if not specs or not any(_has_text(spec) for spec in specs):
157
- errors.append("specs must include at least one non-empty behavior spec")
151
+ if not documentation_only and not _has_text(_spec_file(change_dir)):
152
+ errors.append("spec.md is missing or empty")
158
153
 
159
154
  if level == "L2" and not _has_text(change_dir / "design.md"):
160
155
  errors.append("design.md is required for L2 changes")
@@ -189,13 +184,12 @@ def create_change(args: argparse.Namespace) -> int:
189
184
  return 1
190
185
 
191
186
  change_dir.mkdir(parents=True)
192
- (change_dir / "specs").mkdir()
193
187
  (change_dir / "proposal.md").write_text(
194
188
  f"# {slug}\n\nDescribe the proposed behavior change.\n",
195
189
  encoding="utf-8",
196
190
  )
197
191
  (change_dir / "design.md").write_text("", encoding="utf-8")
198
- (change_dir / "specs" / ".gitkeep").write_text("", encoding="utf-8")
192
+ _spec_file(change_dir).write_text("", encoding="utf-8")
199
193
  _write_metadata(
200
194
  change_dir,
201
195
  {
@@ -174,7 +174,7 @@ def ensure_developer(repo_root: Path | None = None) -> None:
174
174
 
175
175
  if not check_developer(repo_root):
176
176
  print("Error: developer identity has not been initialized.", file=sys.stderr)
177
- print(f"Run: python3 ./{DIR_WORKFLOW}/scripts/init_developer.py <your-name>", file=sys.stderr)
177
+ print(f"Run: ./{DIR_WORKFLOW}/run init-developer <your-name>", file=sys.stderr)
178
178
  sys.exit(1)
179
179
 
180
180
 
@@ -31,6 +31,8 @@ from .paths import (
31
31
  get_tasks_dir,
32
32
  )
33
33
 
34
+ CONTEXT_JSONL_FILES = ("implement.jsonl", "check.jsonl", "debug.jsonl")
35
+
34
36
  # =============================================================================
35
37
  # Helper Functions
36
38
  # =============================================================================
@@ -191,6 +193,93 @@ def _get_current_task_snapshot(repo_root: Path) -> CurrentTaskSnapshot:
191
193
  )
192
194
 
193
195
 
196
+ def _jsonl_file_references(jsonl_path: Path) -> list[str]:
197
+ """读取 JSONL 中声明的文件引用,忽略损坏行。"""
198
+ references: list[str] = []
199
+ if not jsonl_path.is_file():
200
+ return references
201
+
202
+ try:
203
+ lines = jsonl_path.read_text(encoding="utf-8").splitlines()
204
+ except (OSError, IOError):
205
+ return references
206
+
207
+ for line in lines:
208
+ if not line.strip():
209
+ continue
210
+ try:
211
+ data = json.loads(line)
212
+ except json.JSONDecodeError:
213
+ continue
214
+
215
+ file_path = data.get("file")
216
+ if isinstance(file_path, str) and file_path.strip():
217
+ references.append(file_path.strip())
218
+
219
+ return references
220
+
221
+
222
+ def _task_plan_references(repo_root: Path, snapshot: CurrentTaskSnapshot) -> list[str]:
223
+ """从当前任务上下文中提取 plan 文件引用。"""
224
+ if not snapshot.path:
225
+ return []
226
+
227
+ task_dir = repo_root / snapshot.path
228
+ plan_refs: list[str] = []
229
+ seen: set[str] = set()
230
+
231
+ data = snapshot.data or {}
232
+ metadata_candidates = [
233
+ data.get("plan"),
234
+ data.get("planFile"),
235
+ data.get("meta", {}).get("plan") if isinstance(data.get("meta"), dict) else None,
236
+ data.get("meta", {}).get("planFile") if isinstance(data.get("meta"), dict) else None,
237
+ ]
238
+
239
+ for candidate in metadata_candidates:
240
+ if isinstance(candidate, str) and candidate.startswith(f"{DIR_WORKFLOW}/plans/"):
241
+ seen.add(candidate)
242
+ plan_refs.append(candidate)
243
+
244
+ for jsonl_name in CONTEXT_JSONL_FILES:
245
+ for reference in _jsonl_file_references(task_dir / jsonl_name):
246
+ if not reference.startswith(f"{DIR_WORKFLOW}/plans/"):
247
+ continue
248
+ if reference in seen:
249
+ continue
250
+ seen.add(reference)
251
+ plan_refs.append(reference)
252
+
253
+ return plan_refs
254
+
255
+
256
+ def _build_resume_checklist(
257
+ repo_root: Path,
258
+ snapshot: CurrentTaskSnapshot,
259
+ ) -> dict[str, list[str]]:
260
+ """构建最小恢复清单,只返回路径和命令,不展开文件内容。"""
261
+ commands = [f"./{DIR_WORKFLOW}/run resume"]
262
+ read_files: list[str] = []
263
+ notes: list[str] = []
264
+
265
+ if not snapshot.path:
266
+ notes.append("No current task. Use `.agent/skills/start` to classify the next file-changing request.")
267
+ notes.append("Do not bulk-read `.cowork-flow/spec/` or workspace journals; read details only after a task is selected.")
268
+ return {"commands": commands, "readFiles": read_files, "notes": notes}
269
+
270
+ current_task = snapshot.path
271
+ commands.append(f"./{DIR_WORKFLOW}/run task list-context {current_task}")
272
+
273
+ if snapshot.has_prd:
274
+ read_files.append(f"{current_task}/prd.md")
275
+
276
+ read_files.extend(_task_plan_references(repo_root, snapshot))
277
+ notes.append("Read current plan status only when continuing implementation.")
278
+ notes.append("Do not bulk-read `.cowork-flow/spec/` or workspace journals; follow JSONL references on demand.")
279
+
280
+ return {"commands": commands, "readFiles": read_files, "notes": notes}
281
+
282
+
194
283
  def _current_task_json(snapshot: CurrentTaskSnapshot) -> dict | None:
195
284
  """Build the record-mode currentTask JSON object."""
196
285
  current_task = snapshot.path
@@ -205,6 +294,45 @@ def _current_task_json(snapshot: CurrentTaskSnapshot) -> dict | None:
205
294
  }
206
295
 
207
296
 
297
+ def _append_resume_checklist(
298
+ lines: list[str],
299
+ repo_root: Path,
300
+ snapshot: CurrentTaskSnapshot,
301
+ ) -> None:
302
+ """向文本上下文追加最小恢复清单。"""
303
+ lines.append("## RESUME CHECKLIST")
304
+ checklist = _build_resume_checklist(repo_root, snapshot)
305
+ commands = checklist["commands"]
306
+ read_files = checklist["readFiles"]
307
+
308
+ lines.append(f"- Recovery entrypoint (rerun only if context is stale): {commands[0]}")
309
+
310
+ if not snapshot.path:
311
+ lines.append("- No current task. Run `.agent/skills/start` before editing files.")
312
+ lines.append("- Do not bulk-read .cowork-flow/spec/ or workspace journals; choose a task first.")
313
+ lines.append("")
314
+ return
315
+
316
+ current_task = snapshot.path
317
+ prd_path = f"{current_task}/prd.md"
318
+ if prd_path in read_files:
319
+ lines.append(f"- Read current task PRD: {prd_path}")
320
+ else:
321
+ lines.append(f"- Current task PRD missing: {prd_path}")
322
+
323
+ lines.append(f"- List task context before reading details: {commands[1]}")
324
+
325
+ plan_files = [path for path in read_files if path.startswith(f"{DIR_WORKFLOW}/plans/")]
326
+ if plan_files:
327
+ for plan_file in plan_files:
328
+ lines.append(f"- Read current plan status: {plan_file}")
329
+ else:
330
+ lines.append("- No plan reference found in task context; do not search all plans unless needed.")
331
+
332
+ lines.append("- Do not bulk-read .cowork-flow/spec/ or workspace journals; follow JSONL references on demand.")
333
+ lines.append("")
334
+
335
+
208
336
  def _append_current_task(
209
337
  lines: list[str],
210
338
  snapshot: CurrentTaskSnapshot,
@@ -297,6 +425,7 @@ def get_context_json(repo_root: Path | None = None) -> dict:
297
425
  tasks_dir = get_tasks_dir(repo_root)
298
426
  journal_file = get_active_journal_file(repo_root)
299
427
  git_snapshot = _get_git_snapshot(repo_root)
428
+ current_task_snapshot = _get_current_task_snapshot(repo_root)
300
429
 
301
430
  journal_lines = 0
302
431
  journal_relative = ""
@@ -331,6 +460,7 @@ def get_context_json(repo_root: Path | None = None) -> dict:
331
460
  "lines": journal_lines,
332
461
  "nearLimit": journal_lines > 1800,
333
462
  },
463
+ "resumeChecklist": _build_resume_checklist(repo_root, current_task_snapshot),
334
464
  }
335
465
 
336
466
 
@@ -373,7 +503,7 @@ def get_context_text(repo_root: Path | None = None) -> str:
373
503
  lines.append("## DEVELOPER")
374
504
  if not developer:
375
505
  lines.append(
376
- f"ERROR: Not initialized. Run: python3 ./{DIR_WORKFLOW}/{DIR_SCRIPTS}/init_developer.py <name>"
506
+ f"ERROR: Not initialized. Run: ./{DIR_WORKFLOW}/run init-developer <name>"
377
507
  )
378
508
  return "\n".join(lines)
379
509
 
@@ -383,13 +513,15 @@ def get_context_text(repo_root: Path | None = None) -> str:
383
513
  git_snapshot = _get_git_snapshot(repo_root)
384
514
  _append_git_status(lines, git_snapshot, repo_root)
385
515
  _append_recent_commits(lines, git_snapshot)
516
+ current_task_snapshot = _get_current_task_snapshot(repo_root)
386
517
  _append_current_task(
387
518
  lines,
388
- _get_current_task_snapshot(repo_root),
519
+ current_task_snapshot,
389
520
  include_created=True,
390
521
  include_description=True,
391
522
  include_prd_hint=True,
392
523
  )
524
+ _append_resume_checklist(lines, repo_root, current_task_snapshot)
393
525
 
394
526
  # Active tasks
395
527
  lines.append("## ACTIVE TASKS")
@@ -503,11 +635,14 @@ def get_context_record_json(repo_root: Path | None = None) -> dict:
503
635
  "meta": data.get("meta", {}),
504
636
  })
505
637
 
638
+ current_task_snapshot = _get_current_task_snapshot(repo_root)
639
+
506
640
  return {
507
641
  "developer": developer or "",
508
642
  "git": _git_json(git_snapshot),
509
643
  "myTasks": my_tasks,
510
- "currentTask": _current_task_json(_get_current_task_snapshot(repo_root)),
644
+ "currentTask": _current_task_json(current_task_snapshot),
645
+ "resumeChecklist": _build_resume_checklist(repo_root, current_task_snapshot),
511
646
  }
512
647
 
513
648
 
@@ -535,7 +670,7 @@ def get_context_text_record(repo_root: Path | None = None) -> str:
535
670
  developer = get_developer(repo_root)
536
671
  if not developer:
537
672
  lines.append(
538
- f"ERROR: Not initialized. Run: python3 ./{DIR_WORKFLOW}/{DIR_SCRIPTS}/init_developer.py <name>"
673
+ f"ERROR: Not initialized. Run: ./{DIR_WORKFLOW}/run init-developer <name>"
539
674
  )
540
675
  return "\n".join(lines)
541
676
 
@@ -570,7 +705,9 @@ def get_context_text_record(repo_root: Path | None = None) -> str:
570
705
  git_snapshot = _get_git_snapshot(repo_root)
571
706
  _append_git_status(lines, git_snapshot, repo_root)
572
707
  _append_recent_commits(lines, git_snapshot)
573
- _append_current_task(lines, _get_current_task_snapshot(repo_root))
708
+ current_task_snapshot = _get_current_task_snapshot(repo_root)
709
+ _append_current_task(lines, current_task_snapshot)
710
+ _append_resume_checklist(lines, repo_root, current_task_snapshot)
574
711
 
575
712
  lines.append("========================================")
576
713
 
@@ -3,8 +3,8 @@
3
3
  Get Session Context for AI Agent.
4
4
 
5
5
  Usage:
6
- python3 get_context.py Output context in text format
7
- python3 get_context.py --json Output context in JSON format
6
+ ./.cowork-flow/run get-context Output context in text format
7
+ ./.cowork-flow/run get-context --json Output context in JSON format
8
8
  """
9
9
 
10
10
  from __future__ import annotations
@@ -3,7 +3,7 @@
3
3
  Initialize developer for workflow.
4
4
 
5
5
  Usage:
6
- python3 init_developer.py <developer-name>
6
+ ./.cowork-flow/run init-developer <developer-name>
7
7
 
8
8
  This creates:
9
9
  - .cowork-flow/.developer file with developer info
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env python3
2
+ """恢复 cowork-flow 会话的最小入口。"""
3
+
4
+ from __future__ import annotations
5
+
6
+ from common.git_context import get_context_text
7
+
8
+
9
+ def main() -> int:
10
+ print("========================================")
11
+ print("COWORK-FLOW RESUME")
12
+ print("========================================")
13
+ print("Use this after new sessions, long-task resumes, or context compression.")
14
+ print("Read RESUME CHECKLIST first; load details on demand.")
15
+ print("")
16
+ print(get_context_text())
17
+ return 0
18
+
19
+
20
+ if __name__ == "__main__":
21
+ raise SystemExit(main())