cowork-flow 0.0.22 → 0.0.23

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cowork-flow",
3
- "version": "0.0.22",
3
+ "version": "0.0.23",
4
4
  "description": "CLI for installing and maintaining cowork-flow collaboration templates.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1 +1 @@
1
- 0.0.22
1
+ 0.0.23
@@ -9,6 +9,7 @@ import re
9
9
  import sys
10
10
  from datetime import datetime
11
11
  from pathlib import Path
12
+ from typing import Iterable
12
13
 
13
14
  from common.archive_utils import archive_directory_resumable
14
15
  from common.paths import (
@@ -161,6 +162,63 @@ def _normalize_archive_metadata(repo_root: Path, change_dir: Path) -> None:
161
162
  _write_metadata(change_dir, metadata)
162
163
 
163
164
 
165
+ def _same_path(left: Path, right: Path) -> bool:
166
+ try:
167
+ return left.resolve() == right.resolve()
168
+ except OSError:
169
+ return left.absolute() == right.absolute()
170
+
171
+
172
+ def _task_link_matches(repo_root: Path, value: object, task_paths: Iterable[Path]) -> bool:
173
+ resolved = _resolve_link(repo_root, "tasks", value)
174
+ if resolved is None:
175
+ return False
176
+ return any(_same_path(resolved, task_path) for task_path in task_paths)
177
+
178
+
179
+ def linked_active_changes_for_task(repo_root: Path, task_paths: Iterable[Path]) -> list[str]:
180
+ """Return active change slugs whose metadata points at one of the task paths."""
181
+ changes_dir = _changes_dir(repo_root)
182
+ if not changes_dir.is_dir():
183
+ return []
184
+
185
+ paths = tuple(task_paths)
186
+ slugs: list[str] = []
187
+ for change_dir in sorted(changes_dir.iterdir()):
188
+ if not change_dir.is_dir() or change_dir.name == DIR_ARCHIVE:
189
+ continue
190
+ metadata = _read_metadata(change_dir)
191
+ if metadata.get("status") == "archived":
192
+ continue
193
+ if _task_link_matches(repo_root, metadata.get("task"), paths):
194
+ slugs.append(change_dir.name)
195
+ return slugs
196
+
197
+
198
+ def archive_change_by_slug(repo_root: Path, slug: str) -> Path | None:
199
+ source = _change_dir(repo_root, slug)
200
+ if source.is_dir():
201
+ _normalize_archive_metadata(repo_root, source)
202
+
203
+ if not validate_change(repo_root, slug, quiet=True):
204
+ return None
205
+
206
+ month = datetime.now().astimezone().strftime("%Y-%m")
207
+ destination = _archive_dir(repo_root) / month / slug
208
+ result = archive_directory_resumable(source, destination)
209
+ if not result.ok:
210
+ level = "Warning" if result.partial else "Error"
211
+ print(f"{level}: {result.message}", file=sys.stderr)
212
+ return None
213
+
214
+ metadata = _read_metadata(destination)
215
+ metadata["status"] = "archived"
216
+ metadata["archived_at"] = _now_iso()
217
+ metadata["task"] = _normalize_task_link(repo_root, metadata.get("task"))
218
+ _write_metadata(destination, metadata)
219
+ return destination
220
+
221
+
164
222
 
165
223
  def _display_path(repo_root: Path, path: Path) -> str:
166
224
  try:
@@ -272,27 +330,9 @@ def validate_command(args: argparse.Namespace) -> int:
272
330
  def archive_change(args: argparse.Namespace) -> int:
273
331
  repo_root = get_repo_root()
274
332
  slug = args.slug
275
- source = _change_dir(repo_root, slug)
276
- if source.is_dir():
277
- _normalize_archive_metadata(repo_root, source)
278
-
279
- if not validate_change(repo_root, slug, quiet=True):
333
+ if archive_change_by_slug(repo_root, slug) is None:
280
334
  return 1
281
335
 
282
- month = datetime.now().astimezone().strftime("%Y-%m")
283
- destination = _archive_dir(repo_root) / month / slug
284
- result = archive_directory_resumable(source, destination)
285
- if not result.ok:
286
- level = "Warning" if result.partial else "Error"
287
- print(f"{level}: {result.message}", file=sys.stderr)
288
- return 1
289
-
290
- metadata = _read_metadata(destination)
291
- metadata["status"] = "archived"
292
- metadata["archived_at"] = _now_iso()
293
- metadata["task"] = _normalize_task_link(repo_root, metadata.get("task"))
294
- _write_metadata(destination, metadata)
295
-
296
336
  print(f"archived {slug}")
297
337
  return 0
298
338
 
@@ -14,7 +14,7 @@ Usage:
14
14
  ./.cowork-flow/run task complete [dir] # Mark task completed
15
15
  ./.cowork-flow/run task current # Show active session task
16
16
  ./.cowork-flow/run task finish # Clear active session task
17
- ./.cowork-flow/run task archive <task-name> # Archive completed task
17
+ ./.cowork-flow/run task archive <task-name> # Archive completed task and linked changes
18
18
  ./.cowork-flow/run task list # List active tasks
19
19
  ./.cowork-flow/run task list-archive [month] # List archived tasks
20
20
  ./.cowork-flow/run task add-subtask <parent-dir> <child-dir> # Link child to parent
@@ -47,6 +47,7 @@ from common.active_task import (
47
47
  from common.paths import (
48
48
  DIR_WORKFLOW,
49
49
  DIR_AGENTS,
50
+ DIR_CHANGES,
50
51
  DIR_TASKS,
51
52
  DIR_SPEC,
52
53
  DIR_ARCHIVE,
@@ -1120,6 +1121,40 @@ def _print_blockers(blockers: list[str]) -> None:
1120
1121
  print(f" - {blocker}")
1121
1122
 
1122
1123
 
1124
+ def _linked_active_changes_for_task(repo_root: Path, task_dir: Path) -> list[str]:
1125
+ from change import linked_active_changes_for_task
1126
+
1127
+ return linked_active_changes_for_task(repo_root, (task_dir,))
1128
+
1129
+
1130
+ def _linked_changes_ready_for_archive(repo_root: Path, slugs: list[str]) -> bool:
1131
+ from change import validate_change
1132
+
1133
+ ready = True
1134
+ for slug in slugs:
1135
+ if not validate_change(repo_root, slug, quiet=True):
1136
+ print(
1137
+ colored(f"Error: Linked change is not ready to archive: {slug}", Colors.RED),
1138
+ file=sys.stderr,
1139
+ )
1140
+ ready = False
1141
+ return ready
1142
+
1143
+
1144
+ def _archive_linked_changes(repo_root: Path, slugs: list[str]) -> bool:
1145
+ from change import archive_change_by_slug
1146
+
1147
+ for slug in slugs:
1148
+ if archive_change_by_slug(repo_root, slug) is None:
1149
+ print(
1150
+ colored(f"Error: Failed to archive linked change: {slug}", Colors.RED),
1151
+ file=sys.stderr,
1152
+ )
1153
+ return False
1154
+ print(colored(f"Archived linked change: {slug}", Colors.GREEN), file=sys.stderr)
1155
+ return True
1156
+
1157
+
1123
1158
  def cmd_next(args: argparse.Namespace) -> int:
1124
1159
  """Print the next safe workflow action without mutating state."""
1125
1160
  repo_root = get_repo_root()
@@ -1216,7 +1251,10 @@ def cmd_next(args: argparse.Namespace) -> int:
1216
1251
  if status in DONE_STATUSES:
1217
1252
  print("Next action: finalize, commit, archive, and record session")
1218
1253
  print("Command: git status --short")
1254
+ linked_changes = _linked_active_changes_for_task(repo_root, task_dir)
1219
1255
  print(f"Then: ./.cowork-flow/run task archive {Path(task_path).name}")
1256
+ for slug in linked_changes:
1257
+ print(f"Then: ./.cowork-flow/run change archive {slug} (handled by task archive)")
1220
1258
  _print_blockers(blockers)
1221
1259
  return 0
1222
1260
 
@@ -1231,7 +1269,7 @@ def cmd_next(args: argparse.Namespace) -> int:
1231
1269
  # =============================================================================
1232
1270
 
1233
1271
  def cmd_archive(args: argparse.Namespace) -> int:
1234
- """Archive completed task."""
1272
+ """Archive completed task and linked changes."""
1235
1273
  repo_root = get_repo_root()
1236
1274
  task_name = args.name
1237
1275
 
@@ -1258,6 +1296,10 @@ def cmd_archive(args: argparse.Namespace) -> int:
1258
1296
  if task_json_path.is_file():
1259
1297
  task_data = _read_json_file(task_json_path)
1260
1298
 
1299
+ linked_changes = _linked_active_changes_for_task(repo_root, task_dir)
1300
+ if linked_changes and not _linked_changes_ready_for_archive(repo_root, linked_changes):
1301
+ return 1
1302
+
1261
1303
  # Archive
1262
1304
  result = archive_task_complete(task_dir, repo_root)
1263
1305
  if "archived_to" not in result:
@@ -1284,6 +1326,9 @@ def cmd_archive(args: argparse.Namespace) -> int:
1284
1326
  year_month = archive_dest.parent.name
1285
1327
  print(colored(f"Archived: {dir_name} -> archive/{year_month}/", Colors.GREEN), file=sys.stderr)
1286
1328
 
1329
+ if linked_changes and not _archive_linked_changes(repo_root, linked_changes):
1330
+ return 1
1331
+
1287
1332
  # Auto-commit only when explicitly requested.
1288
1333
  if getattr(args, "commit", False) and not getattr(args, "no_commit", False):
1289
1334
  _auto_commit_archive(dir_name, repo_root)
@@ -1297,13 +1342,19 @@ def cmd_archive(args: argparse.Namespace) -> int:
1297
1342
 
1298
1343
 
1299
1344
  def _auto_commit_archive(task_name: str, repo_root: Path) -> None:
1300
- """Stage .cowork-flow/tasks/ changes and commit after archive."""
1345
+ """Stage task/change archive changes and commit after archive."""
1301
1346
  tasks_rel = f"{DIR_WORKFLOW}/{DIR_TASKS}"
1302
- _run_git_command(["add", "-A", tasks_rel], cwd=repo_root)
1347
+ changes_rel = f"{DIR_WORKFLOW}/{DIR_CHANGES}"
1348
+ archive_rels = [
1349
+ rel
1350
+ for rel in (tasks_rel, changes_rel)
1351
+ if (repo_root / rel).exists()
1352
+ ]
1353
+ _run_git_command(["add", "-A", *archive_rels], cwd=repo_root)
1303
1354
 
1304
1355
  # Check if there are staged changes
1305
1356
  rc, _, _ = _run_git_command(
1306
- ["diff", "--cached", "--quiet", "--", tasks_rel], cwd=repo_root
1357
+ ["diff", "--cached", "--quiet", "--", *archive_rels], cwd=repo_root
1307
1358
  )
1308
1359
  if rc == 0:
1309
1360
  print("[OK] No task changes to commit.", file=sys.stderr)
@@ -1564,7 +1615,7 @@ Usage:
1564
1615
  ./.cowork-flow/run task complete [dir] Mark task completed
1565
1616
  ./.cowork-flow/run task finish Clear active session task
1566
1617
  ./.cowork-flow/run task next [dir] Show next safe workflow action
1567
- ./.cowork-flow/run task archive <task-name> Archive completed task
1618
+ ./.cowork-flow/run task archive <task-name> Archive completed task and linked changes
1568
1619
  ./.cowork-flow/run task add-subtask <parent> <child> Link child task to parent
1569
1620
  ./.cowork-flow/run task remove-subtask <parent> <child> Unlink child from parent
1570
1621
  ./.cowork-flow/run task list [--mine] [--status <status>] List tasks
@@ -214,6 +214,8 @@ L2 任务在 `task start` 前必须通过 readiness gate;同一 blocker 列表
214
214
  git add <expected files>
215
215
  git commit -m "<message>"
216
216
  ```
217
+ `task archive <task-name>` 会归档 task,并自动归档 `change.yaml.task`
218
+ 指向该 task 的 active change;无法通过 `change validate` 的 change 不会被自动归档。
217
219
 
218
220
  ## 9. 恢复规则
219
221