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 +1 @@
|
|
|
1
|
-
0.0.
|
|
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
|
-
|
|
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
|
|
1345
|
+
"""Stage task/change archive changes and commit after archive."""
|
|
1301
1346
|
tasks_rel = f"{DIR_WORKFLOW}/{DIR_TASKS}"
|
|
1302
|
-
|
|
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", "--",
|
|
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
|
|