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
@@ -38,8 +38,6 @@ from adapters.cli.task_context_commands import (
38
38
  from adapters.cli.task_create_command import cmd_create, ensure_tasks_dir
39
39
  from adapters.cli import task_navigation
40
40
  from adapters.cli.task_lifecycle_commands import (
41
- _optional_readiness_blockers,
42
- _task_start_blockers,
43
41
  cmd_complete,
44
42
  cmd_current,
45
43
  cmd_finish,
@@ -94,6 +94,17 @@ def _archive_error_message(task_name: str, error: TaskArchiveError) -> str:
94
94
  return f"Failed to archive task: {error.detail}"
95
95
 
96
96
 
97
+ def _print_rollback_issues(error: TaskArchiveError) -> None:
98
+ if not error.rollback_issues:
99
+ return
100
+ print("Rollback issues:", file=sys.stderr)
101
+ for issue in error.rollback_issues:
102
+ print(
103
+ f"- {issue.stage}: {issue.path} ? {issue.detail}",
104
+ file=sys.stderr,
105
+ )
106
+
107
+
97
108
  def _archive_task(repo_root, task_name: str, task_dir):
98
109
  try:
99
110
  return TaskArchiveService(repo_root).archive(
@@ -103,6 +114,7 @@ def _archive_task(repo_root, task_name: str, task_dir):
103
114
  except TaskArchiveError as error:
104
115
  message = _archive_error_message(task_name, error)
105
116
  print(colored(f"Error: {message}", Colors.RED), file=sys.stderr)
117
+ _print_rollback_issues(error)
106
118
  return None
107
119
 
108
120
 
@@ -6,7 +6,6 @@ from __future__ import annotations
6
6
 
7
7
  import argparse
8
8
  import sys
9
- from datetime import datetime
10
9
  from pathlib import Path
11
10
 
12
11
  if __package__:
@@ -14,13 +13,7 @@ if __package__:
14
13
  else:
15
14
  import _bootstrap # noqa: F401
16
15
 
17
- from adapters.cli.task_context_commands import PLANNED_FILE_HINT
18
- from services.task_context import TaskContextService
19
- from services.task_lifecycle import (
20
- LifecyclePreflightFailure,
21
- LifecycleResult,
22
- TaskLifecycleService,
23
- )
16
+ from services.task_lifecycle import LifecycleResult, TaskLifecycleService
24
17
  from adapters.cli.task_support import (
25
18
  Colors,
26
19
  colored,
@@ -33,13 +26,7 @@ from infra.paths import (
33
26
  FILE_TASK_JSON,
34
27
  get_repo_root,
35
28
  )
36
- from runtime.session_state import clear_active_task, get_active_task, is_main_session
37
-
38
-
39
- def _allow_spec_file_modifications(repo_root: Path, execution_context) -> bool:
40
- if execution_context.is_worker or execution_context.is_subagent:
41
- return False
42
- return is_main_session(repo_root)
29
+ from runtime.session_state import clear_active_task, get_active_task
43
30
 
44
31
 
45
32
  def _report_check_block(title: str, blockers: tuple[str, ...]) -> int:
@@ -116,102 +103,8 @@ def _resolve_status_task_dir(
116
103
  return task_dir
117
104
 
118
105
 
119
- def _task_start_blockers(task_dir: Path) -> list[str]:
120
- return list(
121
- TaskContextService(get_repo_root(task_dir)).start_blockers(task_dir)
122
- )
123
-
124
-
125
- def _task_context_validation_issues(
126
- task_dir: Path,
127
- repo_root: Path,
128
- quiet: bool = False,
129
- ) -> list[str]:
130
- del quiet
131
- return list(
132
- TaskContextService(repo_root).validation_issue_summaries(task_dir)
133
- )
134
-
135
-
136
- def _refresh_task_artifact_placeholders(
137
- task_dir: Path,
138
- repo_root: Path,
139
- ) -> list[str]:
140
- service = TaskContextService(repo_root)
141
- service.ensure_task_artifact_placeholders(task_dir)
142
- return _task_context_validation_issues(task_dir, repo_root)
143
-
144
-
145
- def _optional_readiness_blockers(
146
- repo_root: Path,
147
- task_dir: Path,
148
- ) -> list[str]:
149
- try:
150
- from services.readiness import task_readiness_blockers
151
- except Exception:
152
- return []
153
- try:
154
- blockers = task_readiness_blockers(repo_root, task_dir)
155
- except Exception:
156
- return [
157
- "readiness check failed; run task next <dir> --validate and inspect "
158
- "task readiness artifacts"
159
- ]
160
- return [str(blocker) for blocker in blockers if str(blocker).strip()]
161
-
162
-
163
- def _start_preflight(
164
- task_dir: Path,
165
- repo_root: Path,
166
- ) -> LifecyclePreflightFailure | None:
167
- blockers = _task_start_blockers(task_dir)
168
- if blockers:
169
- return LifecyclePreflightFailure(
170
- code="TASK-START-001",
171
- title="Task is not ready to start yet",
172
- blockers=tuple(blockers),
173
- hint=(
174
- "write decision-anchor.md and required context JSONL files, "
175
- "then retry `task next <dir> --run`"
176
- ),
177
- )
178
-
179
- validation_issues = _task_context_validation_issues(
180
- task_dir,
181
- repo_root,
182
- )
183
- if validation_issues:
184
- validation_issues = _refresh_task_artifact_placeholders(task_dir, repo_root)
185
- if validation_issues:
186
- return LifecyclePreflightFailure(
187
- code="TASK-CONTEXT-001",
188
- title="Task context validation failed",
189
- blockers=tuple(validation_issues),
190
- hint=(
191
- "run ./.cowork-flow/run task next <dir> --validate and fix the "
192
- f"reported issues. {PLANNED_FILE_HINT}"
193
- ),
194
- )
195
-
196
- readiness_blockers = _optional_readiness_blockers(
197
- repo_root,
198
- task_dir,
199
- )
200
- if readiness_blockers:
201
- return LifecyclePreflightFailure(
202
- code="TASK-READINESS-001",
203
- title="Task readiness failed",
204
- blockers=tuple(readiness_blockers),
205
- hint=(
206
- "run ./.cowork-flow/run task next <dir> and complete the "
207
- "reported readiness artifacts"
208
- ),
209
- )
210
- return None
211
-
212
-
213
106
  def _report_lifecycle_preflight(
214
- result: LifecyclePreflightFailure | LifecycleResult,
107
+ result: LifecycleResult,
215
108
  ) -> int:
216
109
  print(colored(f"Error: {result.title}", Colors.RED), file=sys.stderr)
217
110
  for blocker in result.blockers:
@@ -264,11 +157,11 @@ def _resolve_start_task(
264
157
  def _run_auto_start(
265
158
  args: argparse.Namespace,
266
159
  full_path: Path,
267
- preflight,
160
+ service: TaskLifecycleService,
268
161
  ) -> int:
269
- failure = preflight(full_path)
270
- if failure is not None:
271
- return _report_lifecycle_preflight(failure)
162
+ readiness = service.start_readiness(full_path)
163
+ if not readiness.ok:
164
+ return _report_lifecycle_preflight(readiness)
272
165
  from adapters.cli.batch_mode import run_batch_entry
273
166
 
274
167
  return run_batch_entry(get_repo_root(), full_path, args)
@@ -276,9 +169,7 @@ def _run_auto_start(
276
169
 
277
170
  def _report_start_failure(
278
171
  result: LifecycleResult,
279
- service: TaskLifecycleService,
280
172
  ) -> int:
281
- del service
282
173
  if result.title:
283
174
  return _report_lifecycle_preflight(result)
284
175
  if result.code == "LIFECYCLE-TRANSITION-001":
@@ -354,21 +245,17 @@ def cmd_start(args: argparse.Namespace) -> int:
354
245
  if full_path is None:
355
246
  return 1
356
247
 
357
- def preflight(
358
- task_dir: Path,
359
- ) -> LifecyclePreflightFailure | None:
360
- return _start_preflight(task_dir, repo_root)
361
-
248
+ service = TaskLifecycleService(repo_root)
362
249
  if getattr(args, "auto", False):
363
- return _run_auto_start(args, full_path, preflight)
250
+ return _run_auto_start(args, full_path, service)
364
251
 
365
- service = TaskLifecycleService(repo_root)
366
- result = service.start(full_path, preflight=preflight)
252
+ result = service.start(full_path)
367
253
  if not result.ok:
368
- return _report_start_failure(result, service)
254
+ return _report_start_failure(result)
369
255
 
370
256
  _report_start_success(result, repo_root, full_path)
371
- _run_hooks("after_start", full_path / FILE_TASK_JSON, repo_root)
257
+ if "after_start" in getattr(result, "emitted_events", ()):
258
+ _run_hooks("after_start", full_path / FILE_TASK_JSON, repo_root)
372
259
  return 0
373
260
 
374
261
 
@@ -383,10 +270,7 @@ def cmd_review(args: argparse.Namespace) -> int:
383
270
  service = TaskLifecycleService(repo_root)
384
271
  result = service.review(
385
272
  task_dir,
386
- allow_spec_file_modifications=_allow_spec_file_modifications(
387
- repo_root,
388
- execution_context,
389
- ),
273
+ execution_context=execution_context,
390
274
  )
391
275
  if not result.ok:
392
276
  if result.code == "LIFECYCLE-TRANSITION-001":
@@ -415,6 +299,7 @@ def cmd_review(args: argparse.Namespace) -> int:
415
299
  def cmd_complete(args: argparse.Namespace) -> int:
416
300
  """Mark a task completed after final check."""
417
301
  repo_root = get_repo_root()
302
+ execution_context = execution_context_from_namespace(args)
418
303
  task_dir = _resolve_status_task_dir(args, repo_root)
419
304
  if task_dir is None:
420
305
  return 1
@@ -422,8 +307,7 @@ def cmd_complete(args: argparse.Namespace) -> int:
422
307
  service = TaskLifecycleService(repo_root)
423
308
  result = service.complete(
424
309
  task_dir,
425
- completed_at=datetime.now().strftime("%Y-%m-%d"),
426
- allow_spec_file_modifications=is_main_session(repo_root),
310
+ execution_context=execution_context,
427
311
  )
428
312
  if not result.ok:
429
313
  if result.code == "LIFECYCLE-TRANSITION-001":
@@ -475,7 +359,8 @@ def cmd_finish(args: argparse.Namespace) -> int:
475
359
  Colors.GREEN,
476
360
  )
477
361
  )
478
- if task_json_path.is_file():
362
+ emitted_events = ("after_finish",) if task_json_path.is_file() else ()
363
+ if "after_finish" in emitted_events:
479
364
  _run_hooks("after_finish", task_json_path, repo_root)
480
365
  return 0
481
366
 
@@ -165,6 +165,76 @@ def _attach_recovery(payload: dict[str, object], repo_root: Path) -> dict[str, o
165
165
  return payload
166
166
 
167
167
 
168
+ def _add_read_first_entry(
169
+ entries: list[dict[str, str]],
170
+ seen: set[str],
171
+ file_path: str,
172
+ reason: str,
173
+ ) -> None:
174
+ normalized = file_path.replace("\\", "/").strip()
175
+ if not normalized or normalized in seen:
176
+ return
177
+ seen.add(normalized)
178
+ entry = {"file": normalized}
179
+ if reason.strip():
180
+ entry["reason"] = reason.strip()
181
+ entries.append(entry)
182
+
183
+
184
+ def _attach_implement_read_first(
185
+ payload: dict[str, object],
186
+ repo_root: Path,
187
+ task_path: str | None,
188
+ ) -> dict[str, object]:
189
+ action = payload.get("action")
190
+ if not isinstance(action, dict) or action.get("id") != "implement_change":
191
+ return payload
192
+ if not task_path:
193
+ return payload
194
+
195
+ task_dir = repo_root / task_path
196
+ if not task_dir.is_dir():
197
+ return payload
198
+
199
+ entries: list[dict[str, str]] = []
200
+ seen: set[str] = set()
201
+ task_prefix = task_path.rstrip("/")
202
+ _add_read_first_entry(
203
+ entries,
204
+ seen,
205
+ f"{task_prefix}/decision-anchor.md",
206
+ "Task decision anchor",
207
+ )
208
+ try:
209
+ task_data = TaskRepository(repo_root).load(task_dir)
210
+ except TaskRepositoryError:
211
+ task_data = {}
212
+ meta = task_data.get("meta") if isinstance(task_data, dict) else None
213
+ plan_file = meta.get("planFile") if isinstance(meta, dict) else None
214
+ if isinstance(plan_file, str):
215
+ _add_read_first_entry(entries, seen, plan_file, "Linked implementation plan")
216
+ _add_read_first_entry(
217
+ entries,
218
+ seen,
219
+ f"{task_prefix}/implement.jsonl",
220
+ "Implementation context index",
221
+ )
222
+ for context_entry in TaskContextService(repo_root).entries(task_dir, "implement"):
223
+ file_value = context_entry.get("file")
224
+ if not isinstance(file_value, str):
225
+ continue
226
+ reason = context_entry.get("reason")
227
+ _add_read_first_entry(
228
+ entries,
229
+ seen,
230
+ file_value,
231
+ reason if isinstance(reason, str) else "Implementation context",
232
+ )
233
+ if entries:
234
+ payload["readFirst"] = entries
235
+ return payload
236
+
237
+
168
238
  def _print_recovery(payload: dict[str, object]) -> None:
169
239
  recovery = payload.get("recovery")
170
240
  if not isinstance(recovery, dict):
@@ -206,20 +276,24 @@ def build_navigation_payload(
206
276
  blockers: list[str],
207
277
  active_target: bool,
208
278
  task_path: str | None = None,
279
+ repo_root: Path | None = None,
209
280
  ) -> dict[str, object]:
210
281
  intent = getattr(args, "intent", None) or _default_intent(
211
282
  status,
212
283
  blockers,
213
284
  active_target,
214
285
  )
215
- return route_request(
286
+ root = repo_root if repo_root is not None else get_repo_root()
287
+ payload = route_request(
216
288
  status=status,
217
289
  intent=intent,
218
290
  context=_routing_context(args),
219
291
  blockers=blockers,
220
292
  active_target=active_target,
221
293
  task_path=task_path,
294
+ repo_root=root,
222
295
  )
296
+ return _attach_implement_read_first(payload, root, task_path)
223
297
 
224
298
 
225
299
  def _print_json_route(
@@ -229,6 +303,7 @@ def _print_json_route(
229
303
  blockers: list[str],
230
304
  active_target: bool,
231
305
  task_path: str | None = None,
306
+ repo_root: Path | None = None,
232
307
  ) -> None:
233
308
  payload = build_navigation_payload(
234
309
  args=args,
@@ -236,6 +311,7 @@ def _print_json_route(
236
311
  blockers=blockers,
237
312
  active_target=active_target,
238
313
  task_path=task_path,
314
+ repo_root=repo_root,
239
315
  )
240
316
  print(json.dumps(payload, ensure_ascii=False, sort_keys=False))
241
317
 
@@ -274,6 +350,7 @@ def _navigation_target(args, repo_root: Path, structured: bool):
274
350
  blockers=[],
275
351
  active_target=False,
276
352
  task_path=None,
353
+ repo_root=repo_root,
277
354
  )
278
355
  _attach_recovery(payload, repo_root)
279
356
  print(json.dumps(payload, ensure_ascii=False, sort_keys=False))
@@ -286,6 +363,7 @@ def _navigation_target(args, repo_root: Path, structured: bool):
286
363
  task_path=None,
287
364
  blockers=[],
288
365
  active_target=False,
366
+ repo_root=repo_root,
289
367
  )
290
368
  _attach_recovery(payload, repo_root)
291
369
  _print_text_payload(payload)
@@ -302,6 +380,7 @@ def _print_stale_route(args, task_path: str, source: str, active_target: bool) -
302
380
  blockers=blockers,
303
381
  active_target=active_target,
304
382
  task_path=task_path,
383
+ repo_root=get_repo_root(),
305
384
  )
306
385
  return
307
386
  print("Status: stale")
@@ -332,6 +411,7 @@ def _print_text_route(
332
411
  task_path=task_path,
333
412
  blockers=blockers,
334
413
  active_target=active_target,
414
+ repo_root=get_repo_root(),
335
415
  )
336
416
  _print_text_payload(payload)
337
417
 
@@ -361,6 +441,7 @@ def cmd_next(args) -> int:
361
441
  blockers=blockers,
362
442
  active_target=active_target,
363
443
  task_path=task_path,
444
+ repo_root=repo_root,
364
445
  )
365
446
  return 0
366
447