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.
- package/README.md +16 -1
- package/package.json +6 -3
- package/src/cli.js +6 -0
- package/src/commands/source-refresh.js +40 -0
- package/src/lib/copy-template.js +130 -1
- package/src/lib/host-assets.js +216 -71
- package/template/.codex/hooks.json +1 -1
- package/template/.cowork-flow/.version +1 -1
- package/template/.cowork-flow/scripts/adapters/cli/task.py +0 -2
- package/template/.cowork-flow/scripts/adapters/cli/task_archive_commands.py +12 -0
- package/template/.cowork-flow/scripts/adapters/cli/task_lifecycle_commands.py +18 -133
- package/template/.cowork-flow/scripts/adapters/cli/task_navigation.py +82 -1
- package/template/.cowork-flow/scripts/adapters/host/host_manifest.py +98 -114
- package/template/.cowork-flow/scripts/infra/skill_manifest.py +23 -3
- package/template/.cowork-flow/scripts/infra/storage/operation_log.py +40 -0
- package/template/.cowork-flow/scripts/infra/storage/state_store.py +356 -1
- package/template/.cowork-flow/scripts/services/context_discovery.py +156 -0
- package/template/.cowork-flow/scripts/services/context_jsonl.py +119 -0
- package/template/.cowork-flow/scripts/services/context_paths.py +186 -0
- package/template/.cowork-flow/scripts/services/lifecycle_checks.py +23 -4
- package/template/.cowork-flow/scripts/services/lifecycle_policy.py +128 -0
- package/template/.cowork-flow/scripts/services/plan_binding.py +146 -0
- package/template/.cowork-flow/scripts/services/task_archive.py +100 -14
- package/template/.cowork-flow/scripts/services/task_context.py +49 -479
- package/template/.cowork-flow/scripts/services/task_creation.py +31 -27
- package/template/.cowork-flow/scripts/services/task_lifecycle.py +155 -18
- package/template/.cowork-flow/spec/contracts/error-output-as-data.md +8 -0
- package/template/.cowork-flow/spec/runtime/index.md +1 -1
- package/template/.cowork-flow/spec/schemas/host-assets.schema.json +32 -6
- package/template/.zcode/.zcode-plugin/plugin.json +1 -1
- package/template/skills/batch-execution/scripts/batch_execution.py +111 -629
- package/template/skills/batch-execution/scripts/batch_recovery.py +94 -0
- package/template/skills/batch-execution/scripts/batch_state_machine.py +201 -0
- package/template/skills/batch-execution/scripts/batch_verification.py +326 -0
- package/template/skills/cowork-flow/SKILL.md +3 -0
- package/template/skills/party-mode/scripts/party_actions.py +95 -0
- package/template/skills/party-mode/scripts/party_mode_v2.py +129 -205
- package/template/skills/party-mode/scripts/party_reports.py +177 -0
- package/template/skills/party-mode/scripts/party_state_machine.py +189 -0
- package/template/skills/runtime-health/scripts/doctor.py +204 -15
- package/template/skills/task-review/scripts/review_check.py +135 -2
- package/template/skills/test-first/SKILL.md +18 -3
- package/template/skills/test-first/manifest.json +8 -4
|
@@ -24,6 +24,20 @@ REQUIRED_HOST_NEUTRAL_CAPABILITIES = (
|
|
|
24
24
|
"party_board_action",
|
|
25
25
|
)
|
|
26
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"))
|
|
27
41
|
|
|
28
42
|
|
|
29
43
|
class HostManifestError(RuntimeError):
|
|
@@ -309,72 +323,47 @@ def _parse_scalar(value: str) -> object:
|
|
|
309
323
|
|
|
310
324
|
|
|
311
325
|
def _build_manifest(raw: dict[str, Any]) -> HostManifest:
|
|
326
|
+
_reject_unknown_fields(raw, MANIFEST_KEYS, "host asset manifest")
|
|
312
327
|
schema_version = raw.get("schemaVersion")
|
|
313
328
|
if schema_version != 1:
|
|
314
329
|
raise HostManifestError(
|
|
315
330
|
f"unsupported host asset schemaVersion: {schema_version}"
|
|
316
331
|
)
|
|
317
|
-
capability_values = _string_tuple(
|
|
318
|
-
|
|
319
|
-
"
|
|
320
|
-
|
|
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}")
|
|
321
336
|
platforms_raw = raw.get("platforms")
|
|
322
337
|
if not isinstance(platforms_raw, list) or not platforms_raw:
|
|
323
338
|
raise HostManifestError("platforms must be a non-empty array")
|
|
324
|
-
platforms = tuple(
|
|
325
|
-
_build_platform(item)
|
|
326
|
-
for item in platforms_raw
|
|
327
|
-
if isinstance(item, dict)
|
|
328
|
-
)
|
|
339
|
+
platforms = tuple(_build_platform(item, set(capability_values)) for item in platforms_raw if isinstance(item, dict))
|
|
329
340
|
if len(platforms) != len(platforms_raw):
|
|
330
341
|
raise HostManifestError("every platform must be an object")
|
|
342
|
+
_validate_platform_identity(platforms)
|
|
331
343
|
required_host_capabilities, capability_matrix = _build_capability_matrix(
|
|
332
|
-
raw.get("capabilityMatrix"),
|
|
333
|
-
capability_values,
|
|
334
|
-
tuple(platform.id for platform in platforms),
|
|
344
|
+
raw.get("capabilityMatrix"), capability_values, tuple(platform.id for platform in platforms)
|
|
335
345
|
)
|
|
336
346
|
sync_raw = raw.get("syncPolicy")
|
|
337
347
|
if not isinstance(sync_raw, dict):
|
|
338
348
|
raise HostManifestError("syncPolicy must be an object")
|
|
349
|
+
_reject_unknown_fields(sync_raw, SYNC_POLICY_KEYS, "syncPolicy")
|
|
339
350
|
return HostManifest(
|
|
340
351
|
schema_version=schema_version,
|
|
341
352
|
capability_values=capability_values,
|
|
342
353
|
required_host_capabilities=required_host_capabilities,
|
|
343
354
|
capability_matrix=capability_matrix,
|
|
344
355
|
platforms=platforms,
|
|
345
|
-
excluded_prefixes=_string_tuple(
|
|
346
|
-
raw.get("excludedPrefixes"),
|
|
347
|
-
"excludedPrefixes",
|
|
348
|
-
),
|
|
356
|
+
excluded_prefixes=_string_tuple(raw.get("excludedPrefixes"), "excludedPrefixes"),
|
|
349
357
|
sync_policy=SyncPolicy(
|
|
350
|
-
protected_files=_string_tuple(
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
),
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
"syncPolicy.protectedPrefixes",
|
|
357
|
-
),
|
|
358
|
-
safe_files=_string_tuple(
|
|
359
|
-
sync_raw.get("safeFiles"),
|
|
360
|
-
"syncPolicy.safeFiles",
|
|
361
|
-
),
|
|
362
|
-
safe_prefixes=_string_tuple(
|
|
363
|
-
sync_raw.get("safePrefixes"),
|
|
364
|
-
"syncPolicy.safePrefixes",
|
|
365
|
-
),
|
|
366
|
-
managed_block_files=_string_tuple(
|
|
367
|
-
sync_raw.get("managedBlockFiles"),
|
|
368
|
-
"syncPolicy.managedBlockFiles",
|
|
369
|
-
),
|
|
370
|
-
obsolete_files=_string_tuple(
|
|
371
|
-
sync_raw.get("obsoleteFiles"),
|
|
372
|
-
"syncPolicy.obsoleteFiles",
|
|
373
|
-
),
|
|
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"),
|
|
374
364
|
),
|
|
375
365
|
)
|
|
376
366
|
|
|
377
|
-
|
|
378
367
|
def _build_capability_matrix(
|
|
379
368
|
raw: object,
|
|
380
369
|
allowed: tuple[str, ...],
|
|
@@ -382,6 +371,7 @@ def _build_capability_matrix(
|
|
|
382
371
|
) -> tuple[tuple[str, ...], dict[str, dict[str, CapabilityDeclaration]]]:
|
|
383
372
|
if not isinstance(raw, dict):
|
|
384
373
|
raise HostManifestError("capabilityMatrix must be an object")
|
|
374
|
+
_reject_unknown_fields(raw, frozenset(("required", "hosts")), "capabilityMatrix")
|
|
385
375
|
required = _string_tuple(raw.get("required"), "capabilityMatrix.required")
|
|
386
376
|
if required != REQUIRED_HOST_NEUTRAL_CAPABILITIES:
|
|
387
377
|
expected = ", ".join(REQUIRED_HOST_NEUTRAL_CAPABILITIES)
|
|
@@ -389,119 +379,75 @@ def _build_capability_matrix(
|
|
|
389
379
|
hosts_raw = raw.get("hosts")
|
|
390
380
|
if not isinstance(hosts_raw, dict) or not hosts_raw:
|
|
391
381
|
raise HostManifestError("capabilityMatrix.hosts must be an object")
|
|
392
|
-
|
|
393
382
|
required_hosts = set(REQUIRED_CAPABILITY_MATRIX_HOSTS).union(platform_ids)
|
|
394
383
|
for host_id in sorted(required_hosts):
|
|
395
384
|
if host_id not in hosts_raw:
|
|
396
385
|
raise HostManifestError(f"capability matrix missing host: {host_id}")
|
|
397
|
-
|
|
398
386
|
allowed_values = set(allowed)
|
|
399
387
|
matrix: dict[str, dict[str, CapabilityDeclaration]] = {}
|
|
400
388
|
for host_id, capabilities_raw in hosts_raw.items():
|
|
401
389
|
if not isinstance(host_id, str) or not host_id.strip():
|
|
402
390
|
raise HostManifestError("capabilityMatrix host ids must be strings")
|
|
403
391
|
if not isinstance(capabilities_raw, dict):
|
|
404
|
-
raise HostManifestError(
|
|
405
|
-
f"capabilityMatrix host {host_id} must be an object"
|
|
406
|
-
)
|
|
392
|
+
raise HostManifestError(f"capabilityMatrix host {host_id} must be an object")
|
|
407
393
|
unknown = sorted(set(capabilities_raw) - set(required))
|
|
408
394
|
if unknown:
|
|
409
|
-
raise HostManifestError(
|
|
410
|
-
f"unknown host-neutral capability {host_id}:{unknown[0]}"
|
|
411
|
-
)
|
|
395
|
+
raise HostManifestError(f"unknown host-neutral capability {host_id}:{unknown[0]}")
|
|
412
396
|
matrix[host_id] = {}
|
|
413
397
|
for capability in required:
|
|
414
398
|
declaration = capabilities_raw.get(capability)
|
|
415
399
|
if not isinstance(declaration, dict):
|
|
416
|
-
raise HostManifestError(
|
|
417
|
-
|
|
418
|
-
)
|
|
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}")
|
|
419
402
|
status = declaration.get("status")
|
|
420
403
|
if not isinstance(status, str) or status not in allowed_values:
|
|
421
|
-
raise HostManifestError(
|
|
422
|
-
f"illegal host-neutral capability {host_id}:{capability}={status}"
|
|
423
|
-
)
|
|
404
|
+
raise HostManifestError(f"illegal host-neutral capability {host_id}:{capability}={status}")
|
|
424
405
|
fallback_raw = declaration.get("fallback")
|
|
425
406
|
fallback: str | None = None
|
|
426
407
|
if fallback_raw is not None:
|
|
427
408
|
if not isinstance(fallback_raw, str) or not fallback_raw.strip():
|
|
428
|
-
raise HostManifestError(
|
|
429
|
-
f"capability fallback must be a non-empty string: "
|
|
430
|
-
f"{host_id}:{capability}"
|
|
431
|
-
)
|
|
409
|
+
raise HostManifestError(f"capability fallback must be a non-empty string: {host_id}:{capability}")
|
|
432
410
|
fallback = fallback_raw.strip()
|
|
433
411
|
if status == "unsupported" and fallback is None:
|
|
434
|
-
raise HostManifestError(
|
|
435
|
-
|
|
436
|
-
f"{host_id}:{capability}"
|
|
437
|
-
)
|
|
438
|
-
matrix[host_id][capability] = CapabilityDeclaration(
|
|
439
|
-
status=status,
|
|
440
|
-
fallback=fallback,
|
|
441
|
-
)
|
|
412
|
+
raise HostManifestError(f"unsupported capability requires fallback: {host_id}:{capability}")
|
|
413
|
+
matrix[host_id][capability] = CapabilityDeclaration(status=status, fallback=fallback)
|
|
442
414
|
return required, matrix
|
|
443
415
|
|
|
444
|
-
|
|
445
|
-
|
|
416
|
+
def _build_platform(raw: dict[str, Any], allowed: set[str]) -> HostPlatform:
|
|
417
|
+
_reject_unknown_fields(raw, PLATFORM_KEYS, "platform")
|
|
446
418
|
platform_id = _required_string(raw, "id")
|
|
447
419
|
capabilities = raw.get("capabilities")
|
|
448
420
|
if not isinstance(capabilities, dict) or not capabilities:
|
|
449
|
-
raise HostManifestError(
|
|
450
|
-
f"platform {platform_id} capabilities must be an object"
|
|
451
|
-
)
|
|
421
|
+
raise HostManifestError(f"platform {platform_id} capabilities must be an object")
|
|
452
422
|
normalized_capabilities: dict[str, str] = {}
|
|
453
423
|
for name, value in capabilities.items():
|
|
454
|
-
if not isinstance(name, str) or not
|
|
455
|
-
raise HostManifestError(
|
|
456
|
-
|
|
457
|
-
)
|
|
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}")
|
|
458
428
|
normalized_capabilities[name] = value
|
|
459
429
|
command_targets_raw = raw.get("commandTargets")
|
|
460
430
|
if not isinstance(command_targets_raw, list):
|
|
461
|
-
raise HostManifestError(
|
|
462
|
-
|
|
463
|
-
)
|
|
464
|
-
command_targets = tuple(
|
|
465
|
-
CommandTarget(
|
|
466
|
-
config=_required_string(item, "config"),
|
|
467
|
-
format=_required_string(item, "format"),
|
|
468
|
-
target=_required_string(item, "target"),
|
|
469
|
-
)
|
|
470
|
-
for item in command_targets_raw
|
|
471
|
-
if isinstance(item, dict)
|
|
472
|
-
)
|
|
473
|
-
if len(command_targets) != len(command_targets_raw):
|
|
474
|
-
raise HostManifestError(
|
|
475
|
-
f"platform {platform_id} commandTargets must be objects"
|
|
476
|
-
)
|
|
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)
|
|
477
433
|
skill_target = raw.get("skillTarget")
|
|
478
|
-
if skill_target is not None
|
|
479
|
-
|
|
480
|
-
f"platform {platform_id} skillTarget must be a string or null"
|
|
481
|
-
)
|
|
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()
|
|
482
438
|
return HostPlatform(
|
|
483
439
|
id=platform_id,
|
|
484
440
|
display_name=_required_string(raw, "displayName"),
|
|
485
441
|
aliases=_string_tuple(raw.get("aliases"), f"{platform_id}.aliases"),
|
|
486
|
-
detect_any=_string_tuple(
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
),
|
|
490
|
-
asset_prefixes=_string_tuple(
|
|
491
|
-
raw.get("assetPrefixes"),
|
|
492
|
-
f"{platform_id}.assetPrefixes",
|
|
493
|
-
),
|
|
494
|
-
asset_files=_string_tuple(
|
|
495
|
-
raw.get("assetFiles"),
|
|
496
|
-
f"{platform_id}.assetFiles",
|
|
497
|
-
),
|
|
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"),
|
|
498
445
|
skill_target=skill_target,
|
|
499
446
|
adapter_path=_required_string(raw, "adapterPath"),
|
|
500
447
|
capabilities=normalized_capabilities,
|
|
501
448
|
command_targets=command_targets,
|
|
502
449
|
)
|
|
503
450
|
|
|
504
|
-
|
|
505
451
|
def _required_string(raw: dict[str, Any], key: str) -> str:
|
|
506
452
|
value = raw.get(key)
|
|
507
453
|
if not isinstance(value, str) or not value.strip():
|
|
@@ -509,14 +455,52 @@ def _required_string(raw: dict[str, Any], key: str) -> str:
|
|
|
509
455
|
return value.strip()
|
|
510
456
|
|
|
511
457
|
|
|
512
|
-
def _string_tuple(value: object, label: str) -> tuple[str, ...]:
|
|
458
|
+
def _string_tuple(value: object, label: str, *, unique: bool = False) -> tuple[str, ...]:
|
|
513
459
|
if not isinstance(value, list):
|
|
514
460
|
raise HostManifestError(f"{label} must be an array")
|
|
515
461
|
result: list[str] = []
|
|
462
|
+
seen: set[str] = set()
|
|
516
463
|
for item in value:
|
|
517
464
|
if not isinstance(item, str) or not item.strip():
|
|
518
|
-
raise HostManifestError(
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
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)
|
|
522
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]}")
|
|
@@ -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
|
|
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
|
|
387
|
+
(item.name, item.aliases, item.script)
|
|
368
388
|
for item in manifest.commands
|
|
369
389
|
),
|
|
370
390
|
)
|
|
@@ -70,3 +70,43 @@ class OperationLog:
|
|
|
70
70
|
if record.get("phase") not in FINAL_PHASES:
|
|
71
71
|
records.append(record)
|
|
72
72
|
return tuple(records)
|
|
73
|
+
def pending_facts(self) -> tuple[dict, ...]:
|
|
74
|
+
"""Return read-only facts about non-final operation records."""
|
|
75
|
+
if not self.operations_dir.is_dir():
|
|
76
|
+
return ()
|
|
77
|
+
facts: list[dict] = []
|
|
78
|
+
for path in sorted(self.operations_dir.glob("*.json")):
|
|
79
|
+
try:
|
|
80
|
+
record = self.state_store.load(path).data
|
|
81
|
+
except StateStoreError as error:
|
|
82
|
+
facts.append(
|
|
83
|
+
{
|
|
84
|
+
"path": str(path),
|
|
85
|
+
"phase": "unreadable",
|
|
86
|
+
"error": {
|
|
87
|
+
"code": error.code,
|
|
88
|
+
"path": str(error.path),
|
|
89
|
+
"detail": error.detail,
|
|
90
|
+
},
|
|
91
|
+
}
|
|
92
|
+
)
|
|
93
|
+
continue
|
|
94
|
+
phase = record.get("phase")
|
|
95
|
+
if phase == "committed":
|
|
96
|
+
continue
|
|
97
|
+
participants = record.get("participants")
|
|
98
|
+
facts.append(
|
|
99
|
+
{
|
|
100
|
+
"path": str(path),
|
|
101
|
+
"operation_id": str(record.get("operation_id") or path.stem),
|
|
102
|
+
"kind": str(record.get("kind") or "unknown"),
|
|
103
|
+
"phase": str(phase or "unknown"),
|
|
104
|
+
"participant_count": (
|
|
105
|
+
len(participants)
|
|
106
|
+
if isinstance(participants, list)
|
|
107
|
+
else 0
|
|
108
|
+
),
|
|
109
|
+
"error": record.get("error"),
|
|
110
|
+
}
|
|
111
|
+
)
|
|
112
|
+
return tuple(facts)
|