cowork-flow 0.0.48 → 0.0.49

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.
Files changed (43) hide show
  1. package/README.md +16 -1
  2. package/package.json +6 -3
  3. package/src/cli.js +6 -0
  4. package/src/commands/source-refresh.js +40 -0
  5. package/src/lib/copy-template.js +130 -1
  6. package/src/lib/host-assets.js +216 -71
  7. package/template/.codex/hooks.json +1 -1
  8. package/template/.cowork-flow/.version +1 -1
  9. package/template/.cowork-flow/scripts/adapters/cli/task.py +0 -2
  10. package/template/.cowork-flow/scripts/adapters/cli/task_archive_commands.py +12 -0
  11. package/template/.cowork-flow/scripts/adapters/cli/task_lifecycle_commands.py +18 -133
  12. package/template/.cowork-flow/scripts/adapters/cli/task_navigation.py +82 -1
  13. package/template/.cowork-flow/scripts/adapters/host/host_manifest.py +98 -114
  14. package/template/.cowork-flow/scripts/infra/skill_manifest.py +23 -3
  15. package/template/.cowork-flow/scripts/infra/storage/operation_log.py +40 -0
  16. package/template/.cowork-flow/scripts/infra/storage/state_store.py +356 -1
  17. package/template/.cowork-flow/scripts/services/context_discovery.py +156 -0
  18. package/template/.cowork-flow/scripts/services/context_jsonl.py +119 -0
  19. package/template/.cowork-flow/scripts/services/context_paths.py +186 -0
  20. package/template/.cowork-flow/scripts/services/lifecycle_checks.py +23 -4
  21. package/template/.cowork-flow/scripts/services/lifecycle_policy.py +128 -0
  22. package/template/.cowork-flow/scripts/services/plan_binding.py +146 -0
  23. package/template/.cowork-flow/scripts/services/task_archive.py +100 -14
  24. package/template/.cowork-flow/scripts/services/task_context.py +49 -479
  25. package/template/.cowork-flow/scripts/services/task_creation.py +31 -27
  26. package/template/.cowork-flow/scripts/services/task_lifecycle.py +155 -18
  27. package/template/.cowork-flow/spec/contracts/error-output-as-data.md +8 -0
  28. package/template/.cowork-flow/spec/runtime/index.md +1 -1
  29. package/template/.cowork-flow/spec/schemas/host-assets.schema.json +32 -6
  30. package/template/.zcode/.zcode-plugin/plugin.json +1 -1
  31. package/template/skills/batch-execution/scripts/batch_execution.py +111 -629
  32. package/template/skills/batch-execution/scripts/batch_recovery.py +94 -0
  33. package/template/skills/batch-execution/scripts/batch_state_machine.py +201 -0
  34. package/template/skills/batch-execution/scripts/batch_verification.py +326 -0
  35. package/template/skills/cowork-flow/SKILL.md +3 -0
  36. package/template/skills/party-mode/scripts/party_actions.py +95 -0
  37. package/template/skills/party-mode/scripts/party_mode_v2.py +129 -205
  38. package/template/skills/party-mode/scripts/party_reports.py +177 -0
  39. package/template/skills/party-mode/scripts/party_state_machine.py +189 -0
  40. package/template/skills/runtime-health/scripts/doctor.py +204 -15
  41. package/template/skills/task-review/scripts/review_check.py +135 -2
  42. package/template/skills/test-first/SKILL.md +18 -3
  43. package/template/skills/test-first/manifest.json +8 -4
@@ -12,7 +12,7 @@ from pathlib import Path
12
12
  from infra.paths import DIR_ARCHIVE, get_tasks_dir
13
13
  from runtime.session_state import clear_task_from_sessions
14
14
  from infra.archive_utils import archive_directory_resumable
15
- from services.task_context import read_context_jsonl_entries
15
+ from services.context_jsonl import read_context_jsonl_entries
16
16
  from services.task_repository import TaskRepository, TaskRepositoryError
17
17
  from services.task_utils import find_task_by_name
18
18
 
@@ -21,13 +21,37 @@ from kernel.task_state import DONE_STATUSES # noqa: F401
21
21
  ArchiveFinalizer = Callable[[], bool]
22
22
 
23
23
 
24
+ @dataclass(frozen=True)
25
+ class RollbackIssue:
26
+ """Best-effort compensation issue that did not replace the primary error."""
27
+
28
+ stage: str
29
+ path: Path
30
+ detail: str
31
+
32
+ def to_dict(self) -> dict[str, str]:
33
+ return {
34
+ "stage": self.stage,
35
+ "path": str(self.path),
36
+ "detail": self.detail,
37
+ }
38
+
39
+
24
40
  class TaskArchiveError(RuntimeError):
25
41
  """Raised when an archive_task action cannot complete safely."""
26
42
 
27
- def __init__(self, code: str, path: Path, detail: str) -> None:
43
+ def __init__(
44
+ self,
45
+ code: str,
46
+ path: Path,
47
+ detail: str,
48
+ *,
49
+ rollback_issues: tuple[RollbackIssue, ...] = (),
50
+ ) -> None:
28
51
  self.code = code
29
52
  self.path = path
30
53
  self.detail = detail
54
+ self.rollback_issues = tuple(rollback_issues)
31
55
  super().__init__(f"{code}: {detail}: {path}")
32
56
 
33
57
 
@@ -160,7 +184,7 @@ class TaskArchiveService:
160
184
  "archive finalizer failed",
161
185
  )
162
186
  except Exception as error:
163
- self._rollback(
187
+ rollback_issues = self._rollback(
164
188
  task_dir,
165
189
  destination,
166
190
  task_data,
@@ -168,17 +192,22 @@ class TaskArchiveService:
168
192
  context_snapshots,
169
193
  )
170
194
  if isinstance(error, TaskArchiveError):
195
+ error.rollback_issues = tuple(
196
+ (*error.rollback_issues, *rollback_issues)
197
+ )
171
198
  raise
172
199
  if isinstance(error, TaskRepositoryError):
173
200
  raise TaskArchiveError(
174
201
  "TASK-ARCHIVE-WRITE-001",
175
202
  error.path,
176
203
  error.detail,
204
+ rollback_issues=rollback_issues,
177
205
  ) from error
178
206
  raise TaskArchiveError(
179
207
  "TASK-ARCHIVE-FINALIZE-001",
180
208
  destination,
181
209
  f"archive finalizer failed: {error}",
210
+ rollback_issues=rollback_issues,
182
211
  ) from error
183
212
 
184
213
  def _relationship_updates(
@@ -313,15 +342,23 @@ class TaskArchiveService:
313
342
  self.repository.save(task_dir, changes)
314
343
  applied.append((task_dir, original))
315
344
  except TaskRepositoryError as error:
345
+ rollback_issues: list[RollbackIssue] = []
316
346
  for task_dir, original in reversed(applied):
317
347
  try:
318
348
  self.repository.replace(task_dir, original)
319
- except TaskRepositoryError:
320
- pass
349
+ except Exception as rollback_error:
350
+ rollback_issues.append(
351
+ RollbackIssue(
352
+ "relationship_restore",
353
+ Path(str(getattr(rollback_error, "path", task_dir / "task.json"))),
354
+ str(getattr(rollback_error, "detail", rollback_error)),
355
+ )
356
+ )
321
357
  raise TaskArchiveError(
322
358
  "TASK-ARCHIVE-RELATIONSHIP-001",
323
359
  error.path,
324
360
  error.detail,
361
+ rollback_issues=tuple(rollback_issues),
325
362
  ) from error
326
363
 
327
364
  def _rollback(
@@ -331,22 +368,71 @@ class TaskArchiveService:
331
368
  task_data: dict,
332
369
  relationship_updates: list[tuple[Path, dict, dict]],
333
370
  context_snapshots: dict[str, bytes],
334
- ) -> None:
371
+ ) -> tuple[RollbackIssue, ...]:
372
+ issues: list[RollbackIssue] = []
335
373
  for task_dir, original, _ in relationship_updates:
336
374
  try:
337
375
  self.repository.replace(task_dir, original)
338
- except TaskRepositoryError:
339
- pass
376
+ except Exception as error:
377
+ issues.append(
378
+ RollbackIssue(
379
+ "relationship_restore",
380
+ Path(str(getattr(error, "path", task_dir / "task.json"))),
381
+ str(getattr(error, "detail", error)),
382
+ )
383
+ )
340
384
 
385
+ directory_restore_attempted = False
341
386
  if destination.is_dir() and not source.exists():
342
- archive_directory_resumable(destination, source)
387
+ directory_restore_attempted = True
388
+ try:
389
+ move_result = archive_directory_resumable(destination, source)
390
+ except Exception as error: # best-effort compensation boundary
391
+ issues.append(
392
+ RollbackIssue(
393
+ "directory_restore",
394
+ source,
395
+ f"failed to restore archived directory: {error}",
396
+ )
397
+ )
398
+ else:
399
+ if not move_result.ok:
400
+ issues.append(
401
+ RollbackIssue(
402
+ "directory_restore",
403
+ move_result.destination,
404
+ move_result.message,
405
+ )
406
+ )
343
407
  if source.is_dir():
344
408
  for name, content in context_snapshots.items():
409
+ context_path = source / name
345
410
  try:
346
- (source / name).write_bytes(content)
347
- except OSError:
348
- pass
411
+ context_path.write_bytes(content)
412
+ except Exception as error:
413
+ issues.append(
414
+ RollbackIssue(
415
+ "context_restore",
416
+ context_path,
417
+ str(error),
418
+ )
419
+ )
349
420
  try:
350
421
  self.repository.replace(source, task_data)
351
- except TaskRepositoryError:
352
- pass
422
+ except Exception as error:
423
+ issues.append(
424
+ RollbackIssue(
425
+ "task_json_restore",
426
+ Path(str(getattr(error, "path", source / "task.json"))),
427
+ str(getattr(error, "detail", error)),
428
+ )
429
+ )
430
+ elif not source.exists() and not directory_restore_attempted:
431
+ issues.append(
432
+ RollbackIssue(
433
+ "directory_restore",
434
+ source,
435
+ "source task directory was not restored",
436
+ )
437
+ )
438
+ return tuple(issues)