cowork-flow 0.0.47 → 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 (90) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +102 -23
  3. package/package.json +8 -3
  4. package/src/cli.js +6 -0
  5. package/src/commands/init.js +0 -83
  6. package/src/commands/source-refresh.js +40 -0
  7. package/src/commands/sync.js +6 -0
  8. package/src/commands/update.js +59 -6
  9. package/src/lib/copy-template.js +200 -50
  10. package/src/lib/host-assets.js +367 -64
  11. package/src/lib/plan-applier.js +47 -0
  12. package/template/.claude/hooks/inject-workflow-state.py +7 -0
  13. package/template/.codex/hooks/inject-workflow-state.py +7 -0
  14. package/template/.codex/hooks.json +1 -1
  15. package/template/.cowork-flow/.version +1 -1
  16. package/template/.cowork-flow/config.yaml +0 -11
  17. package/template/.cowork-flow/scripts/adapters/cli/init_developer.py +0 -1
  18. package/template/.cowork-flow/scripts/adapters/cli/subagent.py +7 -4
  19. package/template/.cowork-flow/scripts/adapters/cli/task.py +8 -484
  20. package/template/.cowork-flow/scripts/adapters/cli/task_archive_commands.py +15 -134
  21. package/template/.cowork-flow/scripts/adapters/cli/task_context_commands.py +7 -1
  22. package/template/.cowork-flow/scripts/adapters/cli/task_lifecycle_commands.py +388 -0
  23. package/template/.cowork-flow/scripts/adapters/cli/task_navigation.py +163 -2
  24. package/template/.cowork-flow/scripts/adapters/git/git_context.py +16 -193
  25. package/template/.cowork-flow/scripts/adapters/host/host_manifest.py +170 -84
  26. package/template/.cowork-flow/scripts/infra/config.py +0 -17
  27. package/template/.cowork-flow/scripts/infra/developer_profile.py +0 -95
  28. package/template/.cowork-flow/scripts/infra/paths.py +0 -79
  29. package/template/.cowork-flow/scripts/infra/skill_manifest.py +23 -3
  30. package/template/.cowork-flow/scripts/infra/storage/operation_log.py +40 -0
  31. package/template/.cowork-flow/scripts/infra/storage/state_store.py +356 -1
  32. package/template/.cowork-flow/scripts/kernel/workflow_route.py +15 -15
  33. package/template/.cowork-flow/scripts/run.py +0 -5
  34. package/template/.cowork-flow/scripts/services/context_discovery.py +156 -0
  35. package/template/.cowork-flow/scripts/services/context_jsonl.py +119 -0
  36. package/template/.cowork-flow/scripts/services/context_paths.py +186 -0
  37. package/template/.cowork-flow/scripts/services/lifecycle_checks.py +186 -69
  38. package/template/.cowork-flow/scripts/services/lifecycle_policy.py +128 -0
  39. package/template/.cowork-flow/scripts/services/plan_binding.py +146 -0
  40. package/template/.cowork-flow/scripts/services/readiness.py +3 -170
  41. package/template/.cowork-flow/scripts/services/task_archive.py +115 -24
  42. package/template/.cowork-flow/scripts/services/task_context.py +103 -365
  43. package/template/.cowork-flow/scripts/services/task_creation.py +40 -5
  44. package/template/.cowork-flow/scripts/services/task_lifecycle.py +155 -18
  45. package/template/.cowork-flow/scripts/services/task_routing.py +80 -67
  46. package/template/.cowork-flow/scripts/services/task_utils.py +0 -146
  47. package/template/.cowork-flow/scripts/services/workflow_runtime.py +1 -1
  48. package/template/.cowork-flow/spec/contracts/capabilities.md +23 -0
  49. package/template/.cowork-flow/spec/contracts/decision-anchor.md +2 -2
  50. package/template/.cowork-flow/spec/contracts/error-output-as-data.md +8 -0
  51. package/template/.cowork-flow/spec/contracts/index.md +1 -0
  52. package/template/.cowork-flow/spec/contracts/party-mode-v2-board.md +20 -0
  53. package/template/.cowork-flow/spec/contracts/plan-binding.md +50 -0
  54. package/template/.cowork-flow/spec/contracts/skill-owned-actions.md +31 -7
  55. package/template/.cowork-flow/spec/guides/index.md +3 -1
  56. package/template/.cowork-flow/spec/index.md +5 -4
  57. package/template/.cowork-flow/spec/references/definition-of-done.md +2 -3
  58. package/template/.cowork-flow/spec/references/orchestration-patterns.md +1 -1
  59. package/template/.cowork-flow/spec/runtime/contract-registry.json +14 -0
  60. package/template/.cowork-flow/spec/runtime/host-assets.json +73 -3
  61. package/template/.cowork-flow/spec/runtime/index.md +1 -1
  62. package/template/.cowork-flow/spec/schemas/host-assets.schema.json +150 -8
  63. package/template/.zcode/.zcode-plugin/plugin.json +1 -1
  64. package/template/skills/batch-execution/SKILL.md +37 -3
  65. package/template/skills/batch-execution/scripts/batch_action.py +4 -0
  66. package/template/skills/batch-execution/scripts/batch_execution.py +120 -499
  67. package/template/skills/batch-execution/scripts/batch_recovery.py +94 -0
  68. package/template/skills/batch-execution/scripts/batch_state_machine.py +201 -0
  69. package/template/skills/batch-execution/scripts/batch_verification.py +326 -0
  70. package/template/skills/brainstorming/SKILL.md +8 -7
  71. package/template/skills/cowork-flow/SKILL.md +3 -0
  72. package/template/skills/party-mode/scripts/party_action_contract.py +113 -0
  73. package/template/skills/party-mode/scripts/party_actions.py +95 -0
  74. package/template/skills/party-mode/scripts/party_board_store.py +148 -0
  75. package/template/skills/party-mode/scripts/party_mode_v2.py +345 -317
  76. package/template/skills/party-mode/scripts/party_reports.py +177 -0
  77. package/template/skills/party-mode/scripts/party_state_machine.py +189 -0
  78. package/template/skills/runtime-health/SKILL.md +14 -2
  79. package/template/skills/runtime-health/scripts/doctor.py +278 -41
  80. package/template/skills/task-planning/SKILL.md +105 -61
  81. package/template/skills/task-review/SKILL.md +4 -4
  82. package/template/skills/task-review/scripts/review_check.py +277 -20
  83. package/template/skills/test-first/SKILL.md +18 -3
  84. package/template/skills/test-first/manifest.json +8 -4
  85. package/template/.cowork-flow/changes/archive/.gitkeep +0 -1
  86. package/template/.cowork-flow/scripts/adapters/cli/add_session.py +0 -465
  87. package/template/.cowork-flow/scripts/adapters/cli/change.py +0 -412
  88. package/template/.cowork-flow/workspace/index.md +0 -102
  89. package/template/.zcode/scaffold/AGENTS.md +0 -145
  90. package/template/.zcode/scaffold/CLAUDE.md +0 -5
@@ -17,6 +17,28 @@ except ModuleNotFoundError: # pragma: no cover - Python < 3.11
17
17
  MANIFEST_PATH = Path(".cowork-flow/spec/runtime/host-assets.json")
18
18
  SCHEMA_PATH = Path(".cowork-flow/spec/schemas/host-assets.schema.json")
19
19
 
20
+ REQUIRED_HOST_NEUTRAL_CAPABILITIES = (
21
+ "task_action",
22
+ "subagent_dispatch",
23
+ "file_write",
24
+ "party_board_action",
25
+ )
26
+ REQUIRED_CAPABILITY_MATRIX_HOSTS = ("codex", "claude-code", "opencode", "zcode")
27
+ CAPABILITY_STATUS_VALUES = (
28
+ "native",
29
+ "shim",
30
+ "plugin",
31
+ "external",
32
+ "experimental",
33
+ "unsupported",
34
+ )
35
+ MANIFEST_KEYS = frozenset(("schemaVersion", "capabilityValues", "capabilityMatrix", "platforms", "excludedPrefixes", "syncPolicy"))
36
+ PLATFORM_KEYS = frozenset(("id", "displayName", "aliases", "detectAny", "assetPrefixes", "assetFiles", "skillTarget", "adapterPath", "capabilities", "commandTargets"))
37
+ SYNC_POLICY_KEYS = frozenset(("protectedFiles", "protectedPrefixes", "safeFiles", "safePrefixes", "managedBlockFiles", "obsoleteFiles"))
38
+ COMMAND_TARGET_KEYS = frozenset(("config", "format", "target"))
39
+ COMMAND_TARGET_FORMATS = frozenset(("json", "toml", "yaml"))
40
+ CAPABILITY_DECLARATION_KEYS = frozenset(("status", "fallback"))
41
+
20
42
 
21
43
  class HostManifestError(RuntimeError):
22
44
  """Raised when the Host Asset Manifest is unavailable or invalid."""
@@ -29,6 +51,12 @@ class CommandTarget:
29
51
  target: str
30
52
 
31
53
 
54
+ @dataclass(frozen=True)
55
+ class CapabilityDeclaration:
56
+ status: str
57
+ fallback: str | None
58
+
59
+
32
60
  @dataclass(frozen=True)
33
61
  class HostPlatform:
34
62
  id: str
@@ -57,6 +85,8 @@ class SyncPolicy:
57
85
  class HostManifest:
58
86
  schema_version: int
59
87
  capability_values: tuple[str, ...]
88
+ required_host_capabilities: tuple[str, ...]
89
+ capability_matrix: dict[str, dict[str, CapabilityDeclaration]]
60
90
  platforms: tuple[HostPlatform, ...]
61
91
  excluded_prefixes: tuple[str, ...]
62
92
  sync_policy: SyncPolicy
@@ -80,6 +110,18 @@ class HostManifest:
80
110
  return platform.id
81
111
  raise HostManifestError(f"unknown host platform alias: {alias}")
82
112
 
113
+ def host_capability(
114
+ self,
115
+ host_id: str,
116
+ capability: str,
117
+ ) -> CapabilityDeclaration:
118
+ try:
119
+ return self.capability_matrix[host_id][capability]
120
+ except KeyError as error:
121
+ raise HostManifestError(
122
+ f"unknown host-neutral capability: {host_id}:{capability}"
123
+ ) from error
124
+
83
125
 
84
126
  def load_host_manifest(template_root: Path) -> HostManifest:
85
127
  path = Path(template_root) / MANIFEST_PATH
@@ -281,125 +323,131 @@ def _parse_scalar(value: str) -> object:
281
323
 
282
324
 
283
325
  def _build_manifest(raw: dict[str, Any]) -> HostManifest:
326
+ _reject_unknown_fields(raw, MANIFEST_KEYS, "host asset manifest")
284
327
  schema_version = raw.get("schemaVersion")
285
328
  if schema_version != 1:
286
329
  raise HostManifestError(
287
330
  f"unsupported host asset schemaVersion: {schema_version}"
288
331
  )
289
- capability_values = _string_tuple(
290
- raw.get("capabilityValues"),
291
- "capabilityValues",
292
- )
332
+ capability_values = _string_tuple(raw.get("capabilityValues"), "capabilityValues", unique=True)
333
+ if capability_values != CAPABILITY_STATUS_VALUES:
334
+ expected = ", ".join(CAPABILITY_STATUS_VALUES)
335
+ raise HostManifestError(f"capabilityValues must be: {expected}")
293
336
  platforms_raw = raw.get("platforms")
294
337
  if not isinstance(platforms_raw, list) or not platforms_raw:
295
338
  raise HostManifestError("platforms must be a non-empty array")
296
- platforms = tuple(
297
- _build_platform(item)
298
- for item in platforms_raw
299
- if isinstance(item, dict)
300
- )
339
+ platforms = tuple(_build_platform(item, set(capability_values)) for item in platforms_raw if isinstance(item, dict))
301
340
  if len(platforms) != len(platforms_raw):
302
341
  raise HostManifestError("every platform must be an object")
342
+ _validate_platform_identity(platforms)
343
+ required_host_capabilities, capability_matrix = _build_capability_matrix(
344
+ raw.get("capabilityMatrix"), capability_values, tuple(platform.id for platform in platforms)
345
+ )
303
346
  sync_raw = raw.get("syncPolicy")
304
347
  if not isinstance(sync_raw, dict):
305
348
  raise HostManifestError("syncPolicy must be an object")
349
+ _reject_unknown_fields(sync_raw, SYNC_POLICY_KEYS, "syncPolicy")
306
350
  return HostManifest(
307
351
  schema_version=schema_version,
308
352
  capability_values=capability_values,
353
+ required_host_capabilities=required_host_capabilities,
354
+ capability_matrix=capability_matrix,
309
355
  platforms=platforms,
310
- excluded_prefixes=_string_tuple(
311
- raw.get("excludedPrefixes"),
312
- "excludedPrefixes",
313
- ),
356
+ excluded_prefixes=_string_tuple(raw.get("excludedPrefixes"), "excludedPrefixes"),
314
357
  sync_policy=SyncPolicy(
315
- protected_files=_string_tuple(
316
- sync_raw.get("protectedFiles"),
317
- "syncPolicy.protectedFiles",
318
- ),
319
- protected_prefixes=_string_tuple(
320
- sync_raw.get("protectedPrefixes"),
321
- "syncPolicy.protectedPrefixes",
322
- ),
323
- safe_files=_string_tuple(
324
- sync_raw.get("safeFiles"),
325
- "syncPolicy.safeFiles",
326
- ),
327
- safe_prefixes=_string_tuple(
328
- sync_raw.get("safePrefixes"),
329
- "syncPolicy.safePrefixes",
330
- ),
331
- managed_block_files=_string_tuple(
332
- sync_raw.get("managedBlockFiles"),
333
- "syncPolicy.managedBlockFiles",
334
- ),
335
- obsolete_files=_string_tuple(
336
- sync_raw.get("obsoleteFiles"),
337
- "syncPolicy.obsoleteFiles",
338
- ),
358
+ protected_files=_string_tuple(sync_raw.get("protectedFiles"), "syncPolicy.protectedFiles"),
359
+ protected_prefixes=_string_tuple(sync_raw.get("protectedPrefixes"), "syncPolicy.protectedPrefixes"),
360
+ safe_files=_string_tuple(sync_raw.get("safeFiles"), "syncPolicy.safeFiles"),
361
+ safe_prefixes=_string_tuple(sync_raw.get("safePrefixes"), "syncPolicy.safePrefixes"),
362
+ managed_block_files=_string_tuple(sync_raw.get("managedBlockFiles"), "syncPolicy.managedBlockFiles"),
363
+ obsolete_files=_string_tuple(sync_raw.get("obsoleteFiles"), "syncPolicy.obsoleteFiles"),
339
364
  ),
340
365
  )
341
366
 
342
-
343
- def _build_platform(raw: dict[str, Any]) -> HostPlatform:
367
+ def _build_capability_matrix(
368
+ raw: object,
369
+ allowed: tuple[str, ...],
370
+ platform_ids: tuple[str, ...],
371
+ ) -> tuple[tuple[str, ...], dict[str, dict[str, CapabilityDeclaration]]]:
372
+ if not isinstance(raw, dict):
373
+ raise HostManifestError("capabilityMatrix must be an object")
374
+ _reject_unknown_fields(raw, frozenset(("required", "hosts")), "capabilityMatrix")
375
+ required = _string_tuple(raw.get("required"), "capabilityMatrix.required")
376
+ if required != REQUIRED_HOST_NEUTRAL_CAPABILITIES:
377
+ expected = ", ".join(REQUIRED_HOST_NEUTRAL_CAPABILITIES)
378
+ raise HostManifestError(f"capabilityMatrix.required must be: {expected}")
379
+ hosts_raw = raw.get("hosts")
380
+ if not isinstance(hosts_raw, dict) or not hosts_raw:
381
+ raise HostManifestError("capabilityMatrix.hosts must be an object")
382
+ required_hosts = set(REQUIRED_CAPABILITY_MATRIX_HOSTS).union(platform_ids)
383
+ for host_id in sorted(required_hosts):
384
+ if host_id not in hosts_raw:
385
+ raise HostManifestError(f"capability matrix missing host: {host_id}")
386
+ allowed_values = set(allowed)
387
+ matrix: dict[str, dict[str, CapabilityDeclaration]] = {}
388
+ for host_id, capabilities_raw in hosts_raw.items():
389
+ if not isinstance(host_id, str) or not host_id.strip():
390
+ raise HostManifestError("capabilityMatrix host ids must be strings")
391
+ if not isinstance(capabilities_raw, dict):
392
+ raise HostManifestError(f"capabilityMatrix host {host_id} must be an object")
393
+ unknown = sorted(set(capabilities_raw) - set(required))
394
+ if unknown:
395
+ raise HostManifestError(f"unknown host-neutral capability {host_id}:{unknown[0]}")
396
+ matrix[host_id] = {}
397
+ for capability in required:
398
+ declaration = capabilities_raw.get(capability)
399
+ if not isinstance(declaration, dict):
400
+ raise HostManifestError(f"missing host-neutral capability {host_id}:{capability}")
401
+ _reject_unknown_fields(declaration, CAPABILITY_DECLARATION_KEYS, f"capabilityMatrix.hosts.{host_id}.{capability}")
402
+ status = declaration.get("status")
403
+ if not isinstance(status, str) or status not in allowed_values:
404
+ raise HostManifestError(f"illegal host-neutral capability {host_id}:{capability}={status}")
405
+ fallback_raw = declaration.get("fallback")
406
+ fallback: str | None = None
407
+ if fallback_raw is not None:
408
+ if not isinstance(fallback_raw, str) or not fallback_raw.strip():
409
+ raise HostManifestError(f"capability fallback must be a non-empty string: {host_id}:{capability}")
410
+ fallback = fallback_raw.strip()
411
+ if status == "unsupported" and fallback is None:
412
+ raise HostManifestError(f"unsupported capability requires fallback: {host_id}:{capability}")
413
+ matrix[host_id][capability] = CapabilityDeclaration(status=status, fallback=fallback)
414
+ return required, matrix
415
+
416
+ def _build_platform(raw: dict[str, Any], allowed: set[str]) -> HostPlatform:
417
+ _reject_unknown_fields(raw, PLATFORM_KEYS, "platform")
344
418
  platform_id = _required_string(raw, "id")
345
419
  capabilities = raw.get("capabilities")
346
420
  if not isinstance(capabilities, dict) or not capabilities:
347
- raise HostManifestError(
348
- f"platform {platform_id} capabilities must be an object"
349
- )
421
+ raise HostManifestError(f"platform {platform_id} capabilities must be an object")
350
422
  normalized_capabilities: dict[str, str] = {}
351
423
  for name, value in capabilities.items():
352
- if not isinstance(name, str) or not isinstance(value, str):
353
- raise HostManifestError(
354
- f"platform {platform_id} capabilities must be strings"
355
- )
424
+ if not isinstance(name, str) or not name.strip():
425
+ raise HostManifestError(f"platform {platform_id} capabilities must use string keys")
426
+ if not isinstance(value, str) or value not in allowed:
427
+ raise HostManifestError(f"platform {platform_id} illegal capability status {name}={value}")
356
428
  normalized_capabilities[name] = value
357
429
  command_targets_raw = raw.get("commandTargets")
358
430
  if not isinstance(command_targets_raw, list):
359
- raise HostManifestError(
360
- f"platform {platform_id} commandTargets must be an array"
361
- )
362
- command_targets = tuple(
363
- CommandTarget(
364
- config=_required_string(item, "config"),
365
- format=_required_string(item, "format"),
366
- target=_required_string(item, "target"),
367
- )
368
- for item in command_targets_raw
369
- if isinstance(item, dict)
370
- )
371
- if len(command_targets) != len(command_targets_raw):
372
- raise HostManifestError(
373
- f"platform {platform_id} commandTargets must be objects"
374
- )
431
+ raise HostManifestError(f"platform {platform_id} commandTargets must be an array")
432
+ command_targets = tuple(_build_command_target(item, platform_id) for item in command_targets_raw)
375
433
  skill_target = raw.get("skillTarget")
376
- if skill_target is not None and not isinstance(skill_target, str):
377
- raise HostManifestError(
378
- f"platform {platform_id} skillTarget must be a string or null"
379
- )
434
+ if skill_target is not None:
435
+ if not isinstance(skill_target, str) or not skill_target.strip():
436
+ raise HostManifestError(f"platform {platform_id} skillTarget must be a string or null")
437
+ skill_target = skill_target.strip()
380
438
  return HostPlatform(
381
439
  id=platform_id,
382
440
  display_name=_required_string(raw, "displayName"),
383
441
  aliases=_string_tuple(raw.get("aliases"), f"{platform_id}.aliases"),
384
- detect_any=_string_tuple(
385
- raw.get("detectAny"),
386
- f"{platform_id}.detectAny",
387
- ),
388
- asset_prefixes=_string_tuple(
389
- raw.get("assetPrefixes"),
390
- f"{platform_id}.assetPrefixes",
391
- ),
392
- asset_files=_string_tuple(
393
- raw.get("assetFiles"),
394
- f"{platform_id}.assetFiles",
395
- ),
442
+ detect_any=_string_tuple(raw.get("detectAny"), f"{platform_id}.detectAny"),
443
+ asset_prefixes=_string_tuple(raw.get("assetPrefixes"), f"{platform_id}.assetPrefixes"),
444
+ asset_files=_string_tuple(raw.get("assetFiles"), f"{platform_id}.assetFiles"),
396
445
  skill_target=skill_target,
397
446
  adapter_path=_required_string(raw, "adapterPath"),
398
447
  capabilities=normalized_capabilities,
399
448
  command_targets=command_targets,
400
449
  )
401
450
 
402
-
403
451
  def _required_string(raw: dict[str, Any], key: str) -> str:
404
452
  value = raw.get(key)
405
453
  if not isinstance(value, str) or not value.strip():
@@ -407,14 +455,52 @@ def _required_string(raw: dict[str, Any], key: str) -> str:
407
455
  return value.strip()
408
456
 
409
457
 
410
- def _string_tuple(value: object, label: str) -> tuple[str, ...]:
458
+ def _string_tuple(value: object, label: str, *, unique: bool = False) -> tuple[str, ...]:
411
459
  if not isinstance(value, list):
412
460
  raise HostManifestError(f"{label} must be an array")
413
461
  result: list[str] = []
462
+ seen: set[str] = set()
414
463
  for item in value:
415
464
  if not isinstance(item, str) or not item.strip():
416
- raise HostManifestError(
417
- f"{label} entries must be non-empty strings"
418
- )
419
- result.append(item.strip())
465
+ raise HostManifestError(f"{label} entries must be non-empty strings")
466
+ normalized = item.strip()
467
+ if unique and normalized in seen:
468
+ raise HostManifestError(f"{label} entries must be unique: {normalized}")
469
+ seen.add(normalized)
470
+ result.append(normalized)
420
471
  return tuple(result)
472
+
473
+
474
+ def _build_command_target(raw: object, platform_id: str) -> CommandTarget:
475
+ if not isinstance(raw, dict):
476
+ raise HostManifestError(f"platform {platform_id} commandTargets must be objects")
477
+ _reject_unknown_fields(raw, COMMAND_TARGET_KEYS, f"platform {platform_id} commandTarget")
478
+ format_value = _required_string(raw, "format")
479
+ if format_value not in COMMAND_TARGET_FORMATS:
480
+ raise HostManifestError(f"platform {platform_id} commandTarget.format must be json, toml, or yaml")
481
+ return CommandTarget(
482
+ config=_required_string(raw, "config"),
483
+ format=format_value,
484
+ target=_required_string(raw, "target"),
485
+ )
486
+
487
+
488
+ def _validate_platform_identity(platforms: tuple[HostPlatform, ...]) -> None:
489
+ ids: set[str] = set()
490
+ aliases: dict[str, str] = {}
491
+ for platform in platforms:
492
+ if platform.id in ids:
493
+ raise HostManifestError(f"duplicate platform id: {platform.id}")
494
+ ids.add(platform.id)
495
+ for alias in platform.aliases:
496
+ normalized = alias.lower()
497
+ owner = aliases.get(normalized)
498
+ if owner is not None:
499
+ raise HostManifestError(f"duplicate platform alias {alias}: {owner}, {platform.id}")
500
+ aliases[normalized] = platform.id
501
+
502
+
503
+ def _reject_unknown_fields(raw: dict[str, Any], allowed: frozenset[str], label: str) -> None:
504
+ unknown = sorted(set(raw) - allowed)
505
+ if unknown:
506
+ raise HostManifestError(f"{label} unknown field: {unknown[0]}")
@@ -13,8 +13,6 @@ from infra.paths import DIR_WORKFLOW, get_repo_root
13
13
 
14
14
 
15
15
  # Defaults
16
- DEFAULT_SESSION_COMMIT_MESSAGE = "chore: record journal"
17
- DEFAULT_MAX_JOURNAL_LINES = 2000
18
16
  DEFAULT_CODEX_DISPATCH_MODE = "sub-agent"
19
17
  DEFAULT_PARTY_MODE_V2_MIN_AGENTS = 3
20
18
  DEFAULT_PARTY_MODE_V2_MAX_AGENTS = 5
@@ -108,21 +106,6 @@ def _load_config(repo_root: Path | None = None) -> dict:
108
106
  return {}
109
107
 
110
108
 
111
- def get_session_commit_message(repo_root: Path | None = None) -> str:
112
- """Get the commit message for auto-committing session records."""
113
- config = _load_config(repo_root)
114
- return config.get("session_commit_message", DEFAULT_SESSION_COMMIT_MESSAGE)
115
-
116
-
117
- def get_max_journal_lines(repo_root: Path | None = None) -> int:
118
- """Get the maximum lines per journal file."""
119
- config = _load_config(repo_root)
120
- value = config.get("max_journal_lines", DEFAULT_MAX_JOURNAL_LINES)
121
- try:
122
- return int(value)
123
- except (ValueError, TypeError):
124
- return DEFAULT_MAX_JOURNAL_LINES
125
-
126
109
 
127
110
  def get_hooks(event: str, repo_root: Path | None = None) -> list[str]:
128
111
  """Get hook commands for a lifecycle event.
@@ -16,10 +16,8 @@ from pathlib import Path
16
16
 
17
17
  from infra.paths import (
18
18
  DIR_WORKFLOW,
19
- DIR_WORKSPACE,
20
19
  DIR_TASKS,
21
20
  FILE_DEVELOPER,
22
- FILE_JOURNAL_PREFIX,
23
21
  get_repo_root,
24
22
  get_developer,
25
23
  check_developer,
@@ -30,75 +28,12 @@ from infra.paths import (
30
28
  # Developer Initialization
31
29
  # =============================================================================
32
30
 
33
- INITIAL_JOURNAL_NUMBER = 1
34
- INDEX_FILE_NAME = "index.md"
35
-
36
-
37
- def _initial_journal_content(name: str, today: str) -> str:
38
- """Build the initial journal file content."""
39
- return f"""# Development Journal - {name} (Part 1)
40
-
41
- > AI development session journal
42
- > Start date: {today}
43
-
44
- ---
45
-
46
- """
47
-
48
-
49
- def _initial_index_content(name: str) -> str:
50
- """Build the initial workspace index content."""
51
- return f"""# Workspace Index - {name}
52
-
53
- > Tracks AI development session records.
54
-
55
- ---
56
-
57
- ## Current Status
58
-
59
- <!-- @@@auto:current-status -->
60
- - **Current file**: `journal-1.md`
61
- - **Total Sessions**: 0
62
- - **Last Active**: -
63
- <!-- @@@/auto:current-status -->
64
-
65
- ---
66
-
67
- ## Active Documents
68
-
69
- <!-- @@@auto:active-documents -->
70
- | File | Lines | Status |
71
- |------|------|------|
72
- | `journal-1.md` | ~0 | Current |
73
- <!-- @@@/auto:active-documents -->
74
-
75
- ---
76
-
77
- ## Session History
78
-
79
- <!-- @@@auto:session-history -->
80
- | # | Date | Title | Commit |
81
- |---|------|------|------|
82
- <!-- @@@/auto:session-history -->
83
-
84
- ---
85
-
86
- ## Notes
87
-
88
- - Sessions are appended to the journal file
89
- - A new journal file is created automatically after the current file exceeds 2000 lines
90
- - Use `add_session.py` to record sessions
91
- - New records use English text; existing records can remain as they are
92
- """
93
-
94
31
 
95
32
  def init_developer(name: str, repo_root: Path | None = None) -> bool:
96
33
  """Initialize developer.
97
34
 
98
35
  Creates:
99
36
  - .cowork-flow/.developer file with developer info
100
- - .cowork-flow/workspace/<name>/ directory structure
101
- - Initial journal file and index.md
102
37
 
103
38
  Args:
104
39
  name: Developer name.
@@ -115,7 +50,6 @@ def init_developer(name: str, repo_root: Path | None = None) -> bool:
115
50
  repo_root = get_repo_root()
116
51
 
117
52
  dev_file = repo_root / DIR_WORKFLOW / FILE_DEVELOPER
118
- workspace_dir = repo_root / DIR_WORKFLOW / DIR_WORKSPACE / name
119
53
 
120
54
  # Create .developer file
121
55
  initialized_at = datetime.now().isoformat()
@@ -128,37 +62,9 @@ def init_developer(name: str, repo_root: Path | None = None) -> bool:
128
62
  print(f"Error: failed to create .developer file: {e}", file=sys.stderr)
129
63
  return False
130
64
 
131
- # Create workspace directory structure
132
- try:
133
- workspace_dir.mkdir(parents=True, exist_ok=True)
134
- except (OSError, IOError) as e:
135
- print(f"Error: failed to create workspace directory: {e}", file=sys.stderr)
136
- return False
137
-
138
- # Create initial journal file
139
- journal_file = workspace_dir / f"{FILE_JOURNAL_PREFIX}{INITIAL_JOURNAL_NUMBER}.md"
140
- if not journal_file.exists():
141
- today = datetime.now().strftime("%Y-%m-%d")
142
- journal_content = _initial_journal_content(name, today)
143
- try:
144
- journal_file.write_text(journal_content, encoding="utf-8")
145
- except (OSError, IOError) as e:
146
- print(f"Error: failed to create journal file: {e}", file=sys.stderr)
147
- return False
148
-
149
- # Create index.md with markers for auto-update
150
- index_file = workspace_dir / INDEX_FILE_NAME
151
- if not index_file.exists():
152
- index_content = _initial_index_content(name)
153
- try:
154
- index_file.write_text(index_content, encoding="utf-8")
155
- except (OSError, IOError) as e:
156
- print(f"Error: failed to create index.md: {e}", file=sys.stderr)
157
- return False
158
65
 
159
66
  print(f"Developer initialized: {name}")
160
67
  print(f" .developer file: {dev_file}")
161
- print(f" Workspace directory: {workspace_dir}")
162
68
 
163
69
  return True
164
70
 
@@ -193,7 +99,6 @@ def show_developer_info(repo_root: Path | None = None) -> None:
193
99
  print("Developer: not initialized")
194
100
  else:
195
101
  print(f"Developer: {developer}")
196
- print(f"Workspace: {DIR_WORKFLOW}/{DIR_WORKSPACE}/{developer}/")
197
102
  print(f"Tasks directory: {DIR_WORKFLOW}/{DIR_TASKS}/")
198
103
 
199
104
 
@@ -5,9 +5,7 @@ Common path utilities for Cowork Flow workflow.
5
5
  Provides:
6
6
  get_repo_root - Get repository root directory
7
7
  get_developer - Get developer name
8
- get_workspace_dir - Get developer workspace directory
9
8
  get_tasks_dir - Get tasks directory
10
- get_active_journal_file - Get current journal file
11
9
  """
12
10
 
13
11
  from __future__ import annotations
@@ -24,8 +22,6 @@ from pathlib import Path
24
22
  # Directory names
25
23
  DIR_WORKFLOW = ".cowork-flow"
26
24
  DIR_AGENTS = ".agents"
27
- DIR_CHANGES = "changes"
28
- DIR_WORKSPACE = "workspace"
29
25
  DIR_TASKS = "tasks"
30
26
  DIR_ARCHIVE = "archive"
31
27
  DIR_SPEC = "spec"
@@ -34,7 +30,6 @@ DIR_SCRIPTS = "scripts"
34
30
  # File names
35
31
  FILE_DEVELOPER = ".developer"
36
32
  FILE_TASK_JSON = "task.json"
37
- FILE_JOURNAL_PREFIX = "journal-"
38
33
  TASK_DATE_PREFIX_PATTERN = re.compile(r"^\d{2}-\d{2}-")
39
34
 
40
35
 
@@ -129,78 +124,6 @@ def get_tasks_dir(repo_root: Path | None = None) -> Path:
129
124
  return repo_root / DIR_WORKFLOW / DIR_TASKS
130
125
 
131
126
 
132
- # =============================================================================
133
- # Workspace Directory
134
- # =============================================================================
135
-
136
- def get_workspace_dir(repo_root: Path | None = None) -> Path | None:
137
- """Get developer workspace directory.
138
-
139
- Args:
140
- repo_root: Repository root path. Defaults to auto-detected.
141
-
142
- Returns:
143
- Path to workspace directory or None if developer not set.
144
- """
145
- if repo_root is None:
146
- repo_root = get_repo_root()
147
-
148
- developer = get_developer(repo_root)
149
- if developer:
150
- return repo_root / DIR_WORKFLOW / DIR_WORKSPACE / developer
151
- return None
152
-
153
-
154
- # =============================================================================
155
- # Journal File
156
- # =============================================================================
157
-
158
- def get_active_journal_file(repo_root: Path | None = None) -> Path | None:
159
- """Get the current active journal file.
160
-
161
- Args:
162
- repo_root: Repository root path. Defaults to auto-detected.
163
-
164
- Returns:
165
- Path to active journal file or None if not found.
166
- """
167
- if repo_root is None:
168
- repo_root = get_repo_root()
169
-
170
- workspace_dir = get_workspace_dir(repo_root)
171
- if workspace_dir is None or not workspace_dir.is_dir():
172
- return None
173
-
174
- latest: Path | None = None
175
- highest = 0
176
-
177
- for f in workspace_dir.glob(f"{FILE_JOURNAL_PREFIX}*.md"):
178
- if not f.is_file():
179
- continue
180
-
181
- # Extract number from filename
182
- name = f.stem # e.g., "journal-1"
183
- match = re.search(r"(\d+)$", name)
184
- if match:
185
- num = int(match.group(1))
186
- if num > highest:
187
- highest = num
188
- latest = f
189
-
190
- return latest
191
-
192
-
193
- def count_lines(file_path: Path) -> int:
194
- """Count lines in a file.
195
-
196
- Args:
197
- file_path: Path to file.
198
-
199
- Returns:
200
- Number of lines, or 0 if file doesn't exist.
201
- """
202
- if not file_path.is_file():
203
- return 0
204
127
 
205
128
  try:
206
129
  with file_path.open("r", encoding="utf-8") as f:
@@ -238,5 +161,3 @@ if __name__ == "__main__":
238
161
  print(f"Repository root: {repo}")
239
162
  print(f"Developer: {get_developer(repo)}")
240
163
  print(f"Tasks dir: {get_tasks_dir(repo)}")
241
- print(f"Workspace dir: {get_workspace_dir(repo)}")
242
- print(f"Journal file: {get_active_journal_file(repo)}")
@@ -312,8 +312,28 @@ def _parse_manifest(path: Path, raw: dict[str, Any]) -> SkillManifest:
312
312
  )
313
313
 
314
314
 
315
+ def _replica_precedence(repo_root: Path, manifest: SkillManifest) -> tuple[int, str]:
316
+ root = Path(repo_root).resolve()
317
+ source_template = Path(__file__).resolve().parents[3] / "skills"
318
+ ordered_roots = (
319
+ root / "template" / "skills",
320
+ source_template,
321
+ root / "skills",
322
+ root / ".agents" / "skills",
323
+ root / ".claude" / "skills",
324
+ )
325
+ manifest_path = manifest.path.resolve()
326
+ for index, candidate in enumerate(ordered_roots):
327
+ try:
328
+ manifest_path.relative_to(candidate.resolve())
329
+ except ValueError:
330
+ continue
331
+ return (index, str(manifest_path))
332
+ return (len(ordered_roots), str(manifest_path))
333
+
334
+
315
335
  def load_skill_manifests(repo_root: Path) -> tuple[SkillManifest, ...]:
316
- """Load every valid replica and reject invalid or conflicting metadata."""
336
+ """Load valid Skill metadata and prefer tracked source replicas."""
317
337
  manifests: dict[str, list[SkillManifest]] = {}
318
338
  for root in skill_roots(repo_root):
319
339
  if not root.is_dir():
@@ -331,7 +351,7 @@ def load_skill_manifests(repo_root: Path) -> tuple[SkillManifest, ...]:
331
351
  if any(_manifest_signature(replica) != canonical for replica in replicas[1:]):
332
352
  paths = ", ".join(str(replica.path) for replica in replicas)
333
353
  raise SkillManifestError(f"conflicting Skill manifest replicas: {skill}: {paths}")
334
- result.append(replicas[0])
354
+ result.append(sorted(replicas, key=lambda replica: _replica_precedence(repo_root, replica))[0])
335
355
  command_owners: dict[str, str] = {}
336
356
  for manifest in result:
337
357
  for command in manifest.commands:
@@ -364,7 +384,7 @@ def _manifest_signature(manifest: SkillManifest) -> tuple[object, ...]:
364
384
  for item in manifest.context_rules
365
385
  ),
366
386
  tuple(
367
- (item.name, item.aliases, item.script, item.script_digest)
387
+ (item.name, item.aliases, item.script)
368
388
  for item in manifest.commands
369
389
  ),
370
390
  )