cowork-flow 0.0.5 → 0.0.8

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.
@@ -3,30 +3,8 @@ set -u
3
3
 
4
4
  MIN_PYTHON_LABEL="Python 3.8+"
5
5
  VERSION_CHECK='import sys; raise SystemExit(0 if sys.version_info >= (3, 8) else 1)'
6
-
7
6
  WORKFLOW_DIR=$(CDPATH= cd "$(dirname "$0")" && pwd) || exit 1
8
- SCRIPTS_DIR="$WORKFLOW_DIR/scripts"
9
-
10
- print_usage() {
11
- cat <<'EOF'
12
- 用法:
13
- ./.cowork-flow/run <command> [args...]
14
- ./.cowork-flow/run python [python-args...]
15
-
16
- 常用 command:
17
- resume
18
- task
19
- change
20
- get-context
21
- get-developer
22
- init-developer
23
- add-session
24
- agent-team
25
-
26
- 解释器选择顺序:
27
- COWORK_FLOW_PYTHON -> PYTHON -> python3 -> python -> py -3
28
- EOF
29
- }
7
+ RUNNER_SCRIPT="$WORKFLOW_DIR/scripts/run.py"
30
8
 
31
9
  candidate_is_valid() {
32
10
  candidate_cmd=$1
@@ -51,8 +29,8 @@ select_explicit_python() {
51
29
  return 0
52
30
  fi
53
31
 
54
- echo "错误:$env_name 指向的解释器不可用或版本低于 ${MIN_PYTHON_LABEL}:$env_value" >&2
55
- echo "请设置 $env_name 为可执行的 Python 3.8+ 解释器路径。" >&2
32
+ echo "Error: $env_name does not point to a usable ${MIN_PYTHON_LABEL} interpreter: $env_value" >&2
33
+ echo "Set $env_name to an executable Python 3.8+ interpreter path." >&2
56
34
  exit 127
57
35
  }
58
36
 
@@ -86,81 +64,16 @@ select_python() {
86
64
  return 0
87
65
  fi
88
66
 
89
- echo "错误:需要 ${MIN_PYTHON_LABEL},但未找到可用解释器。" >&2
90
- echo "已尝试:COWORK_FLOW_PYTHON、PYTHON、python3、python、py -3。" >&2
91
- echo "可设置 COWORK_FLOW_PYTHON=/path/to/python3 后重试。" >&2
67
+ echo "Error: ${MIN_PYTHON_LABEL} is required, but no usable interpreter was found." >&2
68
+ echo "Tried: COWORK_FLOW_PYTHON, PYTHON, python3, python, py -3." >&2
69
+ echo "Set COWORK_FLOW_PYTHON=/path/to/python3 and retry." >&2
92
70
  exit 127
93
71
  }
94
72
 
95
- exec_python() {
96
- select_python
97
-
98
- if [ -n "$SELECTED_PYTHON_ARG" ]; then
99
- exec "$SELECTED_PYTHON" "$SELECTED_PYTHON_ARG" "$@"
100
- fi
101
-
102
- exec "$SELECTED_PYTHON" "$@"
103
- }
104
-
105
- run_script() {
106
- script_path=$1
107
- shift
108
-
109
- if [ ! -f "$script_path" ]; then
110
- echo "错误:找不到脚本:$script_path" >&2
111
- exit 2
112
- fi
113
-
114
- exec_python "$script_path" "$@"
115
- }
73
+ select_python
116
74
 
117
- if [ "$#" -eq 0 ]; then
118
- print_usage >&2
119
- exit 2
75
+ if [ -n "$SELECTED_PYTHON_ARG" ]; then
76
+ exec "$SELECTED_PYTHON" "$SELECTED_PYTHON_ARG" "$RUNNER_SCRIPT" "$@"
120
77
  fi
121
78
 
122
- command_name=$1
123
- shift
124
-
125
- case "$command_name" in
126
- -h|--help|help)
127
- print_usage
128
- exit 0
129
- ;;
130
- python)
131
- exec_python "$@"
132
- ;;
133
- resume)
134
- run_script "$SCRIPTS_DIR/resume.py" "$@"
135
- ;;
136
- task)
137
- run_script "$SCRIPTS_DIR/task.py" "$@"
138
- ;;
139
- change)
140
- run_script "$SCRIPTS_DIR/change.py" "$@"
141
- ;;
142
- get-context|get_context)
143
- run_script "$SCRIPTS_DIR/get_context.py" "$@"
144
- ;;
145
- get-developer|get_developer)
146
- run_script "$SCRIPTS_DIR/get_developer.py" "$@"
147
- ;;
148
- init-developer|init_developer)
149
- run_script "$SCRIPTS_DIR/init_developer.py" "$@"
150
- ;;
151
- add-session|add_session)
152
- run_script "$SCRIPTS_DIR/add_session.py" "$@"
153
- ;;
154
- agent-team|agent_team)
155
- run_script "$SCRIPTS_DIR/agent_team.py" "$@"
156
- ;;
157
- *)
158
- if [ -f "$SCRIPTS_DIR/$command_name.py" ]; then
159
- run_script "$SCRIPTS_DIR/$command_name.py" "$@"
160
- fi
161
-
162
- echo "错误:未知 cowork-flow 命令:$command_name" >&2
163
- print_usage >&2
164
- exit 2
165
- ;;
166
- esac
79
+ exec "$SELECTED_PYTHON" "$RUNNER_SCRIPT" "$@"
@@ -4,108 +4,20 @@ setlocal EnableExtensions
4
4
  set "MIN_PYTHON_LABEL=Python 3.8+"
5
5
  set "VERSION_CHECK=import sys; raise SystemExit(0 if sys.version_info >= (3, 8) else 1)"
6
6
  set "WORKFLOW_DIR=%~dp0"
7
- set "SCRIPTS_DIR=%WORKFLOW_DIR%scripts"
7
+ set "RUNNER_SCRIPT=%WORKFLOW_DIR%scripts\run.py"
8
8
  set "SELECTED_PYTHON="
9
9
  set "SELECTED_PYTHON_ARG="
10
- set "REST_ARGS="
11
10
 
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
- )
11
+ call :select_python
12
+ if errorlevel 1 exit /b %ERRORLEVEL%
18
13
 
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
- if /I "%COMMAND_NAME%"=="agent-team" goto run_agent_team
35
- if /I "%COMMAND_NAME%"=="agent_team" goto run_agent_team
36
-
37
- if exist "%SCRIPTS_DIR%\%COMMAND_NAME%.py" (
38
- call :run_script "%SCRIPTS_DIR%\%COMMAND_NAME%.py"
39
- exit /b %ERRORLEVEL%
14
+ if not "%SELECTED_PYTHON_ARG%"=="" (
15
+ "%SELECTED_PYTHON%" "%SELECTED_PYTHON_ARG%" "%RUNNER_SCRIPT%" %*
16
+ ) else (
17
+ "%SELECTED_PYTHON%" "%RUNNER_SCRIPT%" %*
40
18
  )
41
-
42
- echo 错误:未知 cowork-flow 命令:%COMMAND_NAME% 1>&2
43
- call :print_usage 1>&2
44
- exit /b 2
45
-
46
- :usage
47
- call :print_usage
48
- exit /b 0
49
-
50
- :usage_error
51
- call :print_usage 1>&2
52
- exit /b 2
53
-
54
- :run_python_passthrough
55
- call :exec_python %REST_ARGS%
56
- exit /b %ERRORLEVEL%
57
-
58
- :run_resume
59
- call :run_script "%SCRIPTS_DIR%\resume.py"
60
- exit /b %ERRORLEVEL%
61
-
62
- :run_task
63
- call :run_script "%SCRIPTS_DIR%\task.py"
64
- exit /b %ERRORLEVEL%
65
-
66
- :run_change
67
- call :run_script "%SCRIPTS_DIR%\change.py"
68
- exit /b %ERRORLEVEL%
69
-
70
- :run_get_context
71
- call :run_script "%SCRIPTS_DIR%\get_context.py"
72
- exit /b %ERRORLEVEL%
73
-
74
- :run_get_developer
75
- call :run_script "%SCRIPTS_DIR%\get_developer.py"
76
19
  exit /b %ERRORLEVEL%
77
20
 
78
- :run_init_developer
79
- call :run_script "%SCRIPTS_DIR%\init_developer.py"
80
- exit /b %ERRORLEVEL%
81
-
82
- :run_add_session
83
- call :run_script "%SCRIPTS_DIR%\add_session.py"
84
- exit /b %ERRORLEVEL%
85
-
86
- :run_agent_team
87
- call :run_script "%SCRIPTS_DIR%\agent_team.py"
88
- exit /b %ERRORLEVEL%
89
-
90
- :print_usage
91
- echo 用法:
92
- echo .\.cowork-flow\run.cmd ^<command^> [args...]
93
- echo .\.cowork-flow\run.cmd python [python-args...]
94
- echo.
95
- echo 常用 command:
96
- echo resume
97
- echo task
98
- echo change
99
- echo get-context
100
- echo get-developer
101
- echo init-developer
102
- echo add-session
103
- echo agent-team
104
- echo.
105
- echo 解释器选择顺序:
106
- echo COWORK_FLOW_PYTHON -^> PYTHON -^> python3 -^> python -^> py -3
107
- exit /b 0
108
-
109
21
  :candidate_is_valid
110
22
  set "CANDIDATE_CMD=%~1"
111
23
  set "CANDIDATE_ARG=%~2"
@@ -133,8 +45,8 @@ if not errorlevel 1 (
133
45
  exit /b 0
134
46
  )
135
47
 
136
- echo 错误:%ENV_NAME% 指向的解释器不可用或版本低于 %MIN_PYTHON_LABEL%:%ENV_VALUE% 1>&2
137
- echo 请设置 %ENV_NAME% 为可执行的 Python 3.8+ 解释器路径。 1>&2
48
+ echo Error: %ENV_NAME% does not point to a usable %MIN_PYTHON_LABEL% interpreter: %ENV_VALUE% 1>&2
49
+ echo Set %ENV_NAME% to an executable Python 3.8+ interpreter path. 1>&2
138
50
  exit /b 127
139
51
 
140
52
  :select_python
@@ -170,29 +82,7 @@ if not errorlevel 1 (
170
82
  exit /b 0
171
83
  )
172
84
 
173
- echo 错误:需要 %MIN_PYTHON_LABEL%,但未找到可用解释器。 1>&2
174
- echo 已尝试:COWORK_FLOW_PYTHON、PYTHON、python3、python、py -3。 1>&2
175
- echo 可设置 COWORK_FLOW_PYTHON=C:\Path\To\python.exe 后重试。 1>&2
85
+ echo Error: %MIN_PYTHON_LABEL% is required, but no usable interpreter was found. 1>&2
86
+ echo Tried: COWORK_FLOW_PYTHON, PYTHON, python3, python, py -3. 1>&2
87
+ echo Set COWORK_FLOW_PYTHON=C:\Path\To\python.exe and retry. 1>&2
176
88
  exit /b 127
177
-
178
- :exec_python
179
- call :select_python
180
- if errorlevel 1 exit /b %ERRORLEVEL%
181
-
182
- if not "%SELECTED_PYTHON_ARG%"=="" (
183
- "%SELECTED_PYTHON%" "%SELECTED_PYTHON_ARG%" %*
184
- ) else (
185
- "%SELECTED_PYTHON%" %*
186
- )
187
- exit /b %ERRORLEVEL%
188
-
189
- :run_script
190
- set "SCRIPT_PATH=%~1"
191
-
192
- if not exist "%SCRIPT_PATH%" (
193
- echo 错误:找不到脚本:%SCRIPT_PATH% 1>&2
194
- exit /b 2
195
- )
196
-
197
- call :exec_python "%SCRIPT_PATH%" %REST_ARGS%
198
- exit /b %ERRORLEVEL%
@@ -15,6 +15,7 @@ from common.agent_team import (
15
15
  build_initial_metrics,
16
16
  build_initial_status,
17
17
  parse_plan,
18
+ load_agent_registry,
18
19
  render_assignment_prompt,
19
20
  render_dispatch_plan,
20
21
  write_json,
@@ -31,7 +32,7 @@ default_adapter: codex
31
32
 
32
33
  agents:
33
34
  implementer:
34
- codex_type: worker
35
+ agent_type: worker
35
36
  capabilities:
36
37
  - implementation
37
38
  - test-writing
@@ -45,9 +46,51 @@ agents:
45
46
  - "test/**"
46
47
  risk_limits:
47
48
  max_parallel_write_conflicts: 0
49
+ prompt: |
50
+ Implement the smallest change that satisfies the approved plan and tests.
51
+ Write or update focused regression tests first, keep edits scoped, and report exact verification commands.
52
+
53
+ tester:
54
+ agent_type: worker
55
+ capabilities:
56
+ - test-writing
57
+ - verification
58
+ preferred_task_types:
59
+ - test
60
+ - verification
61
+ file_patterns:
62
+ - "tests/**"
63
+ - "test/**"
64
+ - "src/**"
65
+ - "template/**"
66
+ risk_limits:
67
+ max_parallel_write_conflicts: 0
68
+ prompt: |
69
+ Design tests that fail for the broken behavior and pass for the intended behavior.
70
+ Prefer clear behavior assertions over implementation details, and include the command that proves the result.
71
+
72
+ debugger:
73
+ agent_type: worker
74
+ capabilities:
75
+ - debugging
76
+ - implementation
77
+ - verification
78
+ preferred_task_types:
79
+ - bugfix
80
+ - code
81
+ file_patterns:
82
+ - "src/**"
83
+ - "template/**"
84
+ - "tests/**"
85
+ - "test/**"
86
+ risk_limits:
87
+ max_parallel_write_conflicts: 0
88
+ prompt: |
89
+ Find the root cause before changing code. Reproduce the failure, compare with nearby working patterns,
90
+ then make the smallest fix and verify the original symptom.
48
91
 
49
92
  spec-reviewer:
50
- codex_type: default
93
+ agent_type: default
51
94
  capabilities:
52
95
  - spec-review
53
96
  - acceptance-check
@@ -61,12 +104,16 @@ agents:
61
104
  - "template/**"
62
105
  risk_limits:
63
106
  max_parallel_write_conflicts: 0
107
+ prompt: |
108
+ Review the proposal, spec, plan, and task PRD for contradictions, missing acceptance criteria,
109
+ unclear scope, and behavior that is not covered by verification.
64
110
 
65
111
  quality-reviewer:
66
- codex_type: default
112
+ agent_type: default
67
113
  capabilities:
68
114
  - code-quality-review
69
115
  - test-review
116
+ - verification
70
117
  preferred_task_types:
71
118
  - review
72
119
  - verification
@@ -77,14 +124,18 @@ agents:
77
124
  - "test/**"
78
125
  risk_limits:
79
126
  max_parallel_write_conflicts: 0
127
+ prompt: |
128
+ Review the diff for correctness, maintainability, focused scope, and meaningful tests.
129
+ Check that verification evidence matches the behavior being claimed.
80
130
 
81
131
  docs-agent:
82
- codex_type: worker
132
+ agent_type: worker
83
133
  capabilities:
84
134
  - documentation
85
135
  - workflow-writing
86
136
  preferred_task_types:
87
137
  - docs
138
+ - documentation
88
139
  file_patterns:
89
140
  - "README.md"
90
141
  - "AGENTS.md"
@@ -92,6 +143,30 @@ agents:
92
143
  - ".agent/skills/**"
93
144
  risk_limits:
94
145
  max_parallel_write_conflicts: 0
146
+ prompt: |
147
+ Write concise project documentation that reflects actual commands, files, and workflow behavior.
148
+ Avoid aspirational text that is not backed by the implementation.
149
+
150
+ release-reviewer:
151
+ agent_type: default
152
+ capabilities:
153
+ - release-review
154
+ - acceptance-check
155
+ - verification
156
+ preferred_task_types:
157
+ - release
158
+ - review
159
+ file_patterns:
160
+ - "package.json"
161
+ - "package-lock.json"
162
+ - "scripts/**"
163
+ - "README.md"
164
+ - ".cowork-flow/changes/**"
165
+ risk_limits:
166
+ max_parallel_write_conflicts: 0
167
+ prompt: |
168
+ Review release-facing changes for versioning, packaging, documentation, and command safety.
169
+ Confirm that generated artifacts and publish steps are intentional.
95
170
  """,
96
171
  "adapters.yaml": """# Agent Team Adapters
97
172
 
@@ -309,7 +384,8 @@ def cmd_prepare(args: argparse.Namespace) -> int:
309
384
  ):
310
385
  directory.mkdir(parents=True, exist_ok=True)
311
386
 
312
- dispatch_plan = build_dispatch_plan(tasks)
387
+ registry = load_agent_registry(_agent_team_config_dir(repo_root) / "agents.yaml")
388
+ dispatch_plan = build_dispatch_plan(tasks, registry)
313
389
  (runtime_dir / "dispatch-plan.yaml").write_text(
314
390
  render_dispatch_plan(dispatch_plan),
315
391
  encoding="utf-8",
@@ -317,9 +393,9 @@ def cmd_prepare(args: argparse.Namespace) -> int:
317
393
  write_json(runtime_dir / "status.json", build_initial_status(dispatch_plan))
318
394
  write_json(runtime_dir / "metrics.json", build_initial_metrics(dispatch_plan))
319
395
  write_json(
320
- adapters_dir / "codex.json",
396
+ adapters_dir / f"{dispatch_plan['adapter']}.json",
321
397
  {
322
- "adapter": "codex",
398
+ "adapter": dispatch_plan["adapter"],
323
399
  "mode": "coordinator-dispatched",
324
400
  "assignmentCount": len(dispatch_plan["assignments"]),
325
401
  },
@@ -371,7 +447,7 @@ def cmd_next(args: argparse.Namespace) -> int:
371
447
  for assignment_id, assignment in ready:
372
448
  print(
373
449
  f"{assignment_id}\trole={assignment['role']}\t"
374
- f"agent={assignment['recommended_agent']}\tcodex_type={assignment['codex_type']}"
450
+ f"agent={assignment['recommended_agent']}\tagent_type={assignment['agent_type']}"
375
451
  )
376
452
  return 0
377
453
 
@@ -6,12 +6,18 @@ from __future__ import annotations
6
6
 
7
7
  import argparse
8
8
  import re
9
- import shutil
10
9
  import sys
11
10
  from datetime import datetime
12
11
  from pathlib import Path
13
12
 
14
- from common.paths import DIR_ARCHIVE, DIR_CHANGES, DIR_WORKFLOW, get_repo_root
13
+ from common.archive_utils import archive_directory_resumable
14
+ from common.paths import (
15
+ DIR_ARCHIVE,
16
+ DIR_CHANGES,
17
+ DIR_WORKFLOW,
18
+ generate_task_date_prefix,
19
+ get_repo_root,
20
+ )
15
21
 
16
22
 
17
23
  SLUG_PATTERN = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
@@ -107,9 +113,52 @@ def _resolve_link(repo_root: Path, base_dir: str, value: object) -> Path | None:
107
113
  return raw_path
108
114
 
109
115
  direct = repo_root / raw_path
110
- if direct.exists():
116
+ workflow_base = Path(DIR_WORKFLOW) / base_dir
117
+ if direct.exists() or raw_path.parts[:2] == workflow_base.parts:
111
118
  return direct
112
- return repo_root / DIR_WORKFLOW / base_dir / raw_path
119
+ return repo_root / workflow_base / raw_path
120
+
121
+
122
+ def _archived_task_link(repo_root: Path, task_name: str) -> str | None:
123
+ archive_root = repo_root / DIR_WORKFLOW / "tasks" / DIR_ARCHIVE
124
+ if not archive_root.is_dir():
125
+ return None
126
+
127
+ matches = [path for path in archive_root.glob(f"*/{task_name}") if path.is_dir()]
128
+ if len(matches) != 1:
129
+ return None
130
+
131
+ relative = matches[0].relative_to(archive_root)
132
+ return str(Path(DIR_ARCHIVE) / relative).replace("\\", "/")
133
+
134
+
135
+ def _normalize_task_link(repo_root: Path, value: object) -> object:
136
+ if not isinstance(value, str) or not value.strip():
137
+ return value
138
+
139
+ target = _resolve_link(repo_root, "tasks", value)
140
+ tasks_root = repo_root / DIR_WORKFLOW / "tasks"
141
+ archive_root = tasks_root / DIR_ARCHIVE
142
+ if target is None:
143
+ return value
144
+
145
+ if not target.exists():
146
+ return _archived_task_link(repo_root, target.name) or value
147
+
148
+ try:
149
+ relative = target.resolve().relative_to(archive_root.resolve())
150
+ except ValueError:
151
+ return value
152
+
153
+ return str(Path(DIR_ARCHIVE) / relative).replace("\\", "/")
154
+
155
+
156
+ def _normalize_archive_metadata(repo_root: Path, change_dir: Path) -> None:
157
+ metadata = _read_metadata(change_dir)
158
+ normalized_task = _normalize_task_link(repo_root, metadata.get("task"))
159
+ if normalized_task != metadata.get("task"):
160
+ metadata["task"] = normalized_task
161
+ _write_metadata(change_dir, metadata)
113
162
 
114
163
 
115
164
  def _validate_link(repo_root: Path, metadata: dict[str, object], field: str, base_dir: str) -> str | None:
@@ -178,14 +227,15 @@ def create_change(args: argparse.Namespace) -> int:
178
227
  if not _validate_slug(slug):
179
228
  return 2
180
229
 
181
- change_dir = _change_dir(repo_root, slug)
230
+ dir_name = f"{generate_task_date_prefix()}-{slug}"
231
+ change_dir = _change_dir(repo_root, dir_name)
182
232
  if change_dir.exists():
183
- print(f"Error: change already exists: {slug}", file=sys.stderr)
233
+ print(f"Error: change already exists: {dir_name}", file=sys.stderr)
184
234
  return 1
185
235
 
186
236
  change_dir.mkdir(parents=True)
187
237
  (change_dir / "proposal.md").write_text(
188
- f"# {slug}\n\nDescribe the proposed behavior change.\n",
238
+ f"# {dir_name}\n\nDescribe the proposed behavior change.\n",
189
239
  encoding="utf-8",
190
240
  )
191
241
  (change_dir / "design.md").write_text("", encoding="utf-8")
@@ -193,7 +243,7 @@ def create_change(args: argparse.Namespace) -> int:
193
243
  _write_metadata(
194
244
  change_dir,
195
245
  {
196
- "slug": slug,
246
+ "slug": dir_name,
197
247
  "status": "draft",
198
248
  "level": args.level,
199
249
  "created_at": _now_iso(),
@@ -203,7 +253,7 @@ def create_change(args: argparse.Namespace) -> int:
203
253
  },
204
254
  )
205
255
 
206
- print(f"created {slug}")
256
+ print(f"created {dir_name}")
207
257
  return 0
208
258
 
209
259
 
@@ -215,22 +265,25 @@ def validate_command(args: argparse.Namespace) -> int:
215
265
  def archive_change(args: argparse.Namespace) -> int:
216
266
  repo_root = get_repo_root()
217
267
  slug = args.slug
268
+ source = _change_dir(repo_root, slug)
269
+ if source.is_dir():
270
+ _normalize_archive_metadata(repo_root, source)
271
+
218
272
  if not validate_change(repo_root, slug, quiet=True):
219
273
  return 1
220
274
 
221
- source = _change_dir(repo_root, slug)
222
275
  month = datetime.now().astimezone().strftime("%Y-%m")
223
276
  destination = _archive_dir(repo_root) / month / slug
224
- if destination.exists():
225
- print(f"Error: archive destination already exists: {destination}", file=sys.stderr)
277
+ result = archive_directory_resumable(source, destination)
278
+ if not result.ok:
279
+ level = "Warning" if result.partial else "Error"
280
+ print(f"{level}: {result.message}", file=sys.stderr)
226
281
  return 1
227
282
 
228
- destination.parent.mkdir(parents=True, exist_ok=True)
229
- shutil.move(str(source), str(destination))
230
-
231
283
  metadata = _read_metadata(destination)
232
284
  metadata["status"] = "archived"
233
285
  metadata["archived_at"] = _now_iso()
286
+ metadata["task"] = _normalize_task_link(repo_root, metadata.get("task"))
234
287
  _write_metadata(destination, metadata)
235
288
 
236
289
  print(f"archived {slug}")