xrefkit 0.4.5__tar.gz → 0.4.7__tar.gz
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.
- {xrefkit-0.4.5 → xrefkit-0.4.7}/PKG-INFO +1 -1
- {xrefkit-0.4.5 → xrefkit-0.4.7}/pyproject.toml +1 -1
- xrefkit-0.4.7/tests/test_skills_sync.py +55 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit/__init__.py +1 -1
- {xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit/cli.py +5 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit/skillrun.py +198 -10
- xrefkit-0.4.7/xrefkit/skills_sync.py +278 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit.egg-info/PKG-INFO +1 -1
- {xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit.egg-info/SOURCES.txt +2 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/LICENSE +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/README.md +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/setup.cfg +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/tests/test_base_sync_ownership.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/tests/test_boundary_analysis.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/tests/test_calibration_lint.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/tests/test_check_skill_knowledge_xids.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/tests/test_cli.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/tests/test_collect_analyzer_sarif.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/tests/test_convert_to_xrefkit_skill.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/tests/test_cs_scope_probe.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/tests/test_csharp_commonality.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/tests/test_csharp_naming_profile.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/tests/test_ctx.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/tests/test_cutover_readiness.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/tests/test_dashboard.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/tests/test_error_policy_audit.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/tests/test_error_policy_locator.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/tests/test_fm_multiroot.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/tests/test_gate.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/tests/test_goal_desired_state.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/tests/test_instruction_workflow.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/tests/test_knowledge_relations_validator.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/tests/test_mcp_setup.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/tests/test_ownership.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/tests/test_packmeta.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/tests/test_project_quality_baseline.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/tests/test_resource_provider.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/tests/test_runtime_contracts.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/tests/test_sarif_to_locator.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/tests/test_skill_runtime_audit.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/tests/test_skillmeta.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/tests/test_structure_catalog.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/tests/test_xref.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/tests/test_xrefkit_instance.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/tests/test_xrefkit_tools.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/tests/test_xrefkit_v2_discovery.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/tests/test_xrefkit_v2_models.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/tests/test_xrefkit_v2_pipeline.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit/__main__.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit/boundary_analysis.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit/catalog_cli.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit/contracts.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit/ctx.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit/dashboard.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit/discovery.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit/gate.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit/goalstate.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit/hashing.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit/import_skill.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit/instance.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit/loaders.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit/mcp/__init__.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit/mcp/audit.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit/mcp/bootstrap.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit/mcp/catalog.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit/mcp/cli.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit/mcp/client_cache.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit/mcp/context_registry.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit/mcp/context_token.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit/mcp/contracts.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit/mcp/dist.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit/mcp/ownership.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit/mcp/repository.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit/mcp/schemas.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit/mcp/server.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit/mcp/setup.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit/mcp/startup_contract_pack.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit/mcp_tools.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit/models/__init__.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit/models/common.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit/models/effective_bundle.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit/models/local_manifest.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit/models/package_manifest.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit/models/run_log.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit/models/server_config.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit/models/skill_definition.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit/operations_cli.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit/ownership.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit/packmeta.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit/registry.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit/resolver.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit/resource_provider.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit/resources/base/contracts.json +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit/resources/base/current.json +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit/resources/base/generations/7a682a5272907354/contracts.json +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit/resources/base/generations/7a682a5272907354/model_body.md +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit/resources/base/generations/9929294385ccb7b0/contracts.json +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit/resources/base/generations/9929294385ccb7b0/model_body.md +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit/resources/base/model_body.md +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit/runlog.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit/skillmeta.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit/structure_catalog.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit/tools/__init__.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit/tools/__main__.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit/v2_cli.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit/workspace.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit/xref.py +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit.egg-info/dependency_links.txt +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit.egg-info/entry_points.txt +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit.egg-info/requires.txt +0 -0
- {xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import io
|
|
4
|
+
import json
|
|
5
|
+
import zipfile
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from xrefkit.skills_sync import sync_bundle
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _zip_bytes() -> bytes:
|
|
14
|
+
payload = io.BytesIO()
|
|
15
|
+
with zipfile.ZipFile(payload, "w") as archive:
|
|
16
|
+
archive.writestr("xrefkit-skills-demo/skills/demo/meta.md", "<!-- xid: skill-demo -->\n# Demo\n")
|
|
17
|
+
archive.writestr("xrefkit-skills-demo/skills/demo/SKILL.md", "<!-- xid: skill-demo-body -->\n# Demo\n")
|
|
18
|
+
archive.writestr("xrefkit-skills-demo/knowledge/demo.md", "<!-- xid: knowledge-demo -->\n# Knowledge\n")
|
|
19
|
+
return payload.getvalue()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_sync_bundle_extracts_skill_and_knowledge(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
|
23
|
+
release = {
|
|
24
|
+
"tag_name": "skills-2026.07.30",
|
|
25
|
+
"assets": [{"name": "xrefkit-skills-demo-1.0.0.zip", "browser_download_url": "https://example.test/demo.zip"}],
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
def fake_json(_url: str) -> dict:
|
|
29
|
+
return release
|
|
30
|
+
|
|
31
|
+
def fake_download(_asset: dict) -> tuple[str, bytes]:
|
|
32
|
+
return "xrefkit-skills-demo-1.0.0.zip", _zip_bytes()
|
|
33
|
+
|
|
34
|
+
monkeypatch.setattr("xrefkit.skills_sync._github_json", fake_json)
|
|
35
|
+
monkeypatch.setattr("xrefkit.skills_sync._download_asset", fake_download)
|
|
36
|
+
|
|
37
|
+
result = sync_bundle(repo=tmp_path, source_repository="owner/repo", bundle="demo")
|
|
38
|
+
|
|
39
|
+
assert result.release == "skills-2026.07.30"
|
|
40
|
+
assert (tmp_path / "skills/demo/SKILL.md").is_file()
|
|
41
|
+
assert (tmp_path / "knowledge/demo.md").is_file()
|
|
42
|
+
state = json.loads((tmp_path / ".xrefkit/skill-sync/demo.json").read_text(encoding="utf-8"))
|
|
43
|
+
assert state["files"] == ["knowledge/demo.md", "skills/demo/SKILL.md", "skills/demo/meta.md"]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_sync_bundle_refuses_unmanaged_collision(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
|
47
|
+
release = {"tag_name": "v1", "assets": [{"name": "xrefkit-skills-demo-1.0.0.zip", "browser_download_url": "https://example.test/demo.zip"}]}
|
|
48
|
+
monkeypatch.setattr("xrefkit.skills_sync._github_json", lambda _url: release)
|
|
49
|
+
monkeypatch.setattr("xrefkit.skills_sync._download_asset", lambda _asset: ("demo.zip", _zip_bytes()))
|
|
50
|
+
target = tmp_path / "skills/demo/SKILL.md"
|
|
51
|
+
target.parent.mkdir(parents=True)
|
|
52
|
+
target.write_text("local", encoding="utf-8")
|
|
53
|
+
|
|
54
|
+
with pytest.raises(RuntimeError, match="not owned by sync state"):
|
|
55
|
+
sync_bundle(repo=tmp_path, source_repository="owner/repo", bundle="demo")
|
|
@@ -27,6 +27,7 @@ def _print_help() -> None:
|
|
|
27
27
|
" dashboard inspect runtime state\n"
|
|
28
28
|
" analysis generate proposal-only observation reports\n"
|
|
29
29
|
" mcp start the integrated MCP server\n"
|
|
30
|
+
" skills synchronize released Skill and Knowledge bundles\n"
|
|
30
31
|
" package inspect installed Skill packages\n"
|
|
31
32
|
" show show effective Skill bundles"
|
|
32
33
|
)
|
|
@@ -67,6 +68,10 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
67
68
|
from .mcp import main as mcp_main
|
|
68
69
|
|
|
69
70
|
return mcp_main(args[1:])
|
|
71
|
+
if command == "skills":
|
|
72
|
+
from .skills_sync import main as skills_main
|
|
73
|
+
|
|
74
|
+
return skills_main(args[1:])
|
|
70
75
|
|
|
71
76
|
print(f"xrefkit: unknown command: {command}", file=sys.stderr)
|
|
72
77
|
_print_help()
|
|
@@ -434,6 +434,10 @@ def _render_log(
|
|
|
434
434
|
worklist_lines = "\n".join(
|
|
435
435
|
f"- [ ] {name}: {description}" for name, description in WORKLIST_ROWS
|
|
436
436
|
)
|
|
437
|
+
worklist_report_lines = "\n".join(
|
|
438
|
+
f"| [{name}](#{_safe_slug(name.lower())}) | {description} | Phase checklist | not_checked | [{name}](#{_safe_slug(name.lower())}) |"
|
|
439
|
+
for name, description in WORKLIST_ROWS
|
|
440
|
+
)
|
|
437
441
|
handoff_source_lines = "\n".join(
|
|
438
442
|
f"- source_log: `{source['source_log']}` skill_id=`{source['skill_id']}` closure=`{source['closure']}` handoff=`{source['handoff']}`"
|
|
439
443
|
for source in handoff_sources
|
|
@@ -494,6 +498,8 @@ def _render_log(
|
|
|
494
498
|
- meta: `{meta_path.as_posix()}`
|
|
495
499
|
- skill_doc: `{skill_doc.as_posix()}`
|
|
496
500
|
- task: {task}
|
|
501
|
+
- report_language: `user_language`
|
|
502
|
+
- language_rule: `human-facing report prose follows the user's language; runtime keys, status enums, IDs, paths, and commands remain stable`
|
|
497
503
|
|
|
498
504
|
## Skill Load Gate
|
|
499
505
|
|
|
@@ -602,6 +608,36 @@ def _render_log(
|
|
|
602
608
|
|
|
603
609
|
## Worklist
|
|
604
610
|
|
|
611
|
+
### Report
|
|
612
|
+
|
|
613
|
+
### Status
|
|
614
|
+
pending
|
|
615
|
+
|
|
616
|
+
### Reason
|
|
617
|
+
The workflow protocol has not started; phase results are not yet available.
|
|
618
|
+
|
|
619
|
+
### Result
|
|
620
|
+
The workflow protocol worklist has not started.
|
|
621
|
+
|
|
622
|
+
### Checks Performed
|
|
623
|
+
|
|
624
|
+
| Check ID | What was checked | Target / Scope | Result | Evidence / Details |
|
|
625
|
+
| --- | --- | --- | --- | --- |
|
|
626
|
+
{worklist_report_lines}
|
|
627
|
+
|
|
628
|
+
### Evidence
|
|
629
|
+
- Phase checklist and phase sections in this Run Log
|
|
630
|
+
- Phase events recorded below
|
|
631
|
+
|
|
632
|
+
### Open Items
|
|
633
|
+
- All workflow phases are not_checked.
|
|
634
|
+
|
|
635
|
+
### Handoff
|
|
636
|
+
- Next owner: executor
|
|
637
|
+
- Next action: complete Startup and Planning.
|
|
638
|
+
|
|
639
|
+
### Phase Checklist
|
|
640
|
+
|
|
605
641
|
{worklist_lines}
|
|
606
642
|
|
|
607
643
|
## Concrete Work Items
|
|
@@ -631,6 +667,33 @@ def _render_log(
|
|
|
631
667
|
- policy: `{quality_policy}`
|
|
632
668
|
- rule: declare acceptance check items as `check`-kind artifacts at planning; an independent quality reviewer sets each to `done` (pass) or `blocked` (fail) with `xrefkit skill artifact`; domain reviews run as separate review Skills orchestrated by the main session and linked here. Required when model_tier is `standard` or `heavy`; optional otherwise
|
|
633
669
|
|
|
670
|
+
### Report
|
|
671
|
+
|
|
672
|
+
### Status
|
|
673
|
+
pending
|
|
674
|
+
|
|
675
|
+
### Reason
|
|
676
|
+
Quality acceptance checks have not yet been recorded.
|
|
677
|
+
|
|
678
|
+
### Result
|
|
679
|
+
No quality checklist items have been recorded yet.
|
|
680
|
+
|
|
681
|
+
### Checks Performed
|
|
682
|
+
|
|
683
|
+
| Check ID | What was checked | Target / Scope | Result | Evidence / Details |
|
|
684
|
+
| --- | --- | --- | --- | --- |
|
|
685
|
+
| none | No check artifact recorded | Quality Gate | not_checked | [Runtime Artifacts](#runtime-artifacts) |
|
|
686
|
+
|
|
687
|
+
### Evidence
|
|
688
|
+
- Check-kind artifacts in Runtime Artifacts
|
|
689
|
+
|
|
690
|
+
### Open Items
|
|
691
|
+
- Quality acceptance checks are pending.
|
|
692
|
+
|
|
693
|
+
### Handoff
|
|
694
|
+
- Next owner: quality_reviewer
|
|
695
|
+
- Next action: record each acceptance check as a check-kind artifact.
|
|
696
|
+
|
|
634
697
|
## Unknowns And Risks
|
|
635
698
|
|
|
636
699
|
- status: `pending`
|
|
@@ -675,12 +738,14 @@ def _append_phase_event(text: str, *, phase: str, status: str, role: str | None,
|
|
|
675
738
|
|
|
676
739
|
|
|
677
740
|
def _section_status(text: str, section: str) -> str | None:
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
if
|
|
741
|
+
heading = re.compile(rf"^## {re.escape(section)}\s*$", re.MULTILINE)
|
|
742
|
+
match = heading.search(text)
|
|
743
|
+
if match is None:
|
|
681
744
|
return None
|
|
745
|
+
start = match.start()
|
|
682
746
|
|
|
683
|
-
|
|
747
|
+
next_heading = re.compile(r"^## (?!#).*$", re.MULTILINE).search(text, match.end())
|
|
748
|
+
next_section = next_heading.start() if next_heading else -1
|
|
684
749
|
body = text[start:] if next_section == -1 else text[start:next_section]
|
|
685
750
|
prefix = "- status: `"
|
|
686
751
|
status_start = body.find(prefix)
|
|
@@ -694,12 +759,13 @@ def _section_status(text: str, section: str) -> str | None:
|
|
|
694
759
|
|
|
695
760
|
|
|
696
761
|
def _section_body(text: str, section: str) -> tuple[str | None, int, int]:
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
if
|
|
762
|
+
heading = re.compile(rf"^## {re.escape(section)}\s*$", re.MULTILINE)
|
|
763
|
+
match = heading.search(text)
|
|
764
|
+
if match is None:
|
|
700
765
|
return None, -1, -1
|
|
701
|
-
|
|
702
|
-
|
|
766
|
+
start = match.start()
|
|
767
|
+
next_heading = re.compile(r"^## (?!#).*$", re.MULTILINE).search(text, match.end())
|
|
768
|
+
end = len(text) if next_heading is None else next_heading.start()
|
|
703
769
|
return text[start:end], start, end
|
|
704
770
|
|
|
705
771
|
|
|
@@ -801,7 +867,128 @@ def _set_phase_status(text: str, *, phase: str, status: str) -> str:
|
|
|
801
867
|
text, changed = _replace_line(text, old_status, new_status)
|
|
802
868
|
if changed:
|
|
803
869
|
break
|
|
804
|
-
return text
|
|
870
|
+
return _refresh_quality_report(_refresh_worklist_report(text))
|
|
871
|
+
|
|
872
|
+
|
|
873
|
+
def _refresh_worklist_report(text: str) -> str:
|
|
874
|
+
"""Keep the human-facing worklist report aligned with phase checkboxes."""
|
|
875
|
+
body, _, _ = _section_body(text, "Worklist")
|
|
876
|
+
if body is None or "### Phase Checklist\n" not in body:
|
|
877
|
+
return text
|
|
878
|
+
|
|
879
|
+
rows: list[tuple[str, str, str]] = []
|
|
880
|
+
for name, _description in WORKLIST_ROWS:
|
|
881
|
+
match = re.search(
|
|
882
|
+
rf"^- \[(?P<checkbox>[ x!])\] {re.escape(name)}: (?P<description>.+)$",
|
|
883
|
+
body,
|
|
884
|
+
flags=re.MULTILINE,
|
|
885
|
+
)
|
|
886
|
+
if match:
|
|
887
|
+
checkbox = match.group("checkbox")
|
|
888
|
+
result = "pass" if checkbox == "x" else "fail" if checkbox == "!" else "not_checked"
|
|
889
|
+
rows.append((name, match.group("description"), result))
|
|
890
|
+
|
|
891
|
+
if not rows:
|
|
892
|
+
return text
|
|
893
|
+
completed = sum(result == "pass" for _, _, result in rows)
|
|
894
|
+
if completed == len(rows):
|
|
895
|
+
status = "done"
|
|
896
|
+
elif any(result == "fail" for _, _, result in rows):
|
|
897
|
+
status = "blocked"
|
|
898
|
+
elif completed:
|
|
899
|
+
status = "partial"
|
|
900
|
+
else:
|
|
901
|
+
status = "pending"
|
|
902
|
+
open_items = [name for name, _, result in rows if result != "pass"] or ["なし"]
|
|
903
|
+
next_phase = next((name for name, _, result in rows if result != "pass"), "なし")
|
|
904
|
+
table = "\n".join(
|
|
905
|
+
f"| {name} | {description} | {result} | Phase checklist |"
|
|
906
|
+
for name, description, result in rows
|
|
907
|
+
)
|
|
908
|
+
report = (
|
|
909
|
+
"### Report\n\n"
|
|
910
|
+
f"### Status\n{status}\n\n"
|
|
911
|
+
+ "### Reason\n"
|
|
912
|
+
+ (
|
|
913
|
+
"All workflow phases are complete."
|
|
914
|
+
if status == "done"
|
|
915
|
+
else f"The workflow remains {status}; incomplete phases: {', '.join(open_items)}."
|
|
916
|
+
)
|
|
917
|
+
+ "\n\n"
|
|
918
|
+
f"### Result\n{completed} of {len(rows)} workflow protocol phases are complete.\n\n"
|
|
919
|
+
"### Checks Performed\n\n"
|
|
920
|
+
"| Check ID | What was checked | Target / Scope | Result | Evidence / Details |\n"
|
|
921
|
+
"| --- | --- | --- | --- | --- |\n"
|
|
922
|
+
+ "\n".join(
|
|
923
|
+
f"| [{name}](#{_safe_slug(name.lower())}) | {description} | Phase checklist | {result} | [{name}](#{_safe_slug(name.lower())}) |"
|
|
924
|
+
for name, description, result in rows
|
|
925
|
+
)
|
|
926
|
+
+ "\n\n"
|
|
927
|
+
"### Evidence\n"
|
|
928
|
+
"- Phase checklist and phase sections in this Run Log\n"
|
|
929
|
+
"- Phase events recorded below\n\n"
|
|
930
|
+
"### Open Items\n"
|
|
931
|
+
+ "\n".join(f"- {item}" for item in open_items)
|
|
932
|
+
+ "\n\n### Handoff\n"
|
|
933
|
+
f"- Next owner: executor\n- Next action: advance {next_phase}.\n"
|
|
934
|
+
)
|
|
935
|
+
updated = re.sub(
|
|
936
|
+
r"(?ms)### Report\n\n.*?(?=\n### Phase Checklist\n)",
|
|
937
|
+
report.rstrip(),
|
|
938
|
+
body,
|
|
939
|
+
count=1,
|
|
940
|
+
)
|
|
941
|
+
start = text.find(body)
|
|
942
|
+
return text if start == -1 else text[:start] + updated + text[start + len(body):]
|
|
943
|
+
|
|
944
|
+
|
|
945
|
+
def _refresh_quality_report(text: str) -> str:
|
|
946
|
+
"""Render quality check artifacts as the common checklist report."""
|
|
947
|
+
body, start, end = _section_body(text, "Quality Gate")
|
|
948
|
+
if body is None or "### Report\n" not in body:
|
|
949
|
+
return text
|
|
950
|
+
checks = [artifact for artifact in _parse_artifacts(text) if artifact["kind"] == "check"]
|
|
951
|
+
status = _section_status(text, "Quality Gate") or "pending"
|
|
952
|
+
if checks:
|
|
953
|
+
rows = []
|
|
954
|
+
for check in checks:
|
|
955
|
+
result = {
|
|
956
|
+
"done": "pass",
|
|
957
|
+
"blocked": "fail",
|
|
958
|
+
"na": "not_applicable",
|
|
959
|
+
"pending": "not_checked",
|
|
960
|
+
}.get(check["status"], check["status"])
|
|
961
|
+
rows.append(
|
|
962
|
+
f"| {check['artifact_id']} | {check['note']} | {check['target']} | {result} | [{check['artifact_id']}](#runtime-artifacts) |"
|
|
963
|
+
)
|
|
964
|
+
result_text = f"{len(checks)} quality checklist item(s) are recorded."
|
|
965
|
+
open_items = [check["artifact_id"] for check in checks if check["status"] not in {"done", "na"}] or ["なし"]
|
|
966
|
+
else:
|
|
967
|
+
rows = ["| none | No check artifact recorded | Quality Gate | not_checked | [Runtime Artifacts](#runtime-artifacts) |"]
|
|
968
|
+
result_text = "No quality checklist items have been recorded yet."
|
|
969
|
+
open_items = ["Quality acceptance checks are pending."]
|
|
970
|
+
report = (
|
|
971
|
+
"### Report\n\n"
|
|
972
|
+
f"### Status\n{status}\n\n"
|
|
973
|
+
+ "### Reason\n"
|
|
974
|
+
+ (
|
|
975
|
+
"All quality checks are complete."
|
|
976
|
+
if status == "done"
|
|
977
|
+
else f"The quality gate remains {status}; open checks: {', '.join(open_items)}."
|
|
978
|
+
)
|
|
979
|
+
+ "\n\n"
|
|
980
|
+
f"### Result\n{result_text}\n\n"
|
|
981
|
+
"### Checks Performed\n\n"
|
|
982
|
+
"| Check ID | What was checked | Target / Scope | Result | Evidence / Details |\n"
|
|
983
|
+
"| --- | --- | --- | --- | --- |\n"
|
|
984
|
+
+ "\n".join(rows)
|
|
985
|
+
+ "\n\n### Evidence\n- Check-kind artifacts in Runtime Artifacts\n\n"
|
|
986
|
+
"### Open Items\n"
|
|
987
|
+
+ "\n".join(f"- {item}" for item in open_items)
|
|
988
|
+
+ "\n\n### Handoff\n- Next owner: quality_reviewer\n- Next action: resolve open checklist items.\n"
|
|
989
|
+
)
|
|
990
|
+
updated = re.sub(r"(?ms)### Report\n\n.*?(?=\n## Unknowns And Risks\n)", report.rstrip(), body, count=1)
|
|
991
|
+
return text[:start] + updated + text[end:]
|
|
805
992
|
|
|
806
993
|
|
|
807
994
|
def _workitem_checkbox(status: str) -> str:
|
|
@@ -1116,6 +1303,7 @@ def update_artifact(args) -> SkillRunResult:
|
|
|
1116
1303
|
)
|
|
1117
1304
|
|
|
1118
1305
|
text = _replace_runtime_artifacts_section(text, artifacts)
|
|
1306
|
+
text = _refresh_quality_report(text)
|
|
1119
1307
|
text = _append_phase_event(text, phase=f"artifact:{artifact_id}", status=status, role=role, note=note or target)
|
|
1120
1308
|
_atomic_write_text(log_path, text)
|
|
1121
1309
|
return SkillRunResult(ok=True, skill_id=None, skill_doc=None, run_log=str(log_path), errors=[], artifacts=artifacts)
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"""Synchronize released Skill/Knowledge bundles into an XRefKit repository."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
import re
|
|
9
|
+
import tempfile
|
|
10
|
+
import urllib.error
|
|
11
|
+
import urllib.parse
|
|
12
|
+
import urllib.request
|
|
13
|
+
import zipfile
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from pathlib import Path, PurePosixPath
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
DEFAULT_SOURCE_REPOSITORY = "synthaicode/XRefKit"
|
|
19
|
+
DEFAULT_GITHUB_API = "https://api.github.com"
|
|
20
|
+
ASSET_PREFIX = "xrefkit-skills-"
|
|
21
|
+
VERSION_SUFFIX_RE = re.compile(r"^(?P<bundle>.+)-(?P<version>\d+\.\d+(?:\.\d+)?(?:[-+][0-9A-Za-z.-]+)?)$")
|
|
22
|
+
ALLOWED_ROOTS = {"skills", "knowledge", "review_axes", "schemas"}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class SyncResult:
|
|
27
|
+
bundle: str
|
|
28
|
+
release: str
|
|
29
|
+
asset: str
|
|
30
|
+
sha256: str
|
|
31
|
+
files: tuple[str, ...]
|
|
32
|
+
changed: bool
|
|
33
|
+
dry_run: bool = False
|
|
34
|
+
|
|
35
|
+
def to_dict(self) -> dict[str, object]:
|
|
36
|
+
return {
|
|
37
|
+
"bundle": self.bundle,
|
|
38
|
+
"release": self.release,
|
|
39
|
+
"asset": self.asset,
|
|
40
|
+
"sha256": self.sha256,
|
|
41
|
+
"files": list(self.files),
|
|
42
|
+
"changed": self.changed,
|
|
43
|
+
"dry_run": self.dry_run,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _github_json(url: str) -> dict[str, object]:
|
|
48
|
+
request = urllib.request.Request(
|
|
49
|
+
url,
|
|
50
|
+
headers={
|
|
51
|
+
"Accept": "application/vnd.github+json",
|
|
52
|
+
"User-Agent": "xrefkit-skill-sync",
|
|
53
|
+
},
|
|
54
|
+
)
|
|
55
|
+
try:
|
|
56
|
+
with urllib.request.urlopen(request) as response: # nosec B310 - URL is GitHub API HTTPS
|
|
57
|
+
payload = json.loads(response.read().decode("utf-8"))
|
|
58
|
+
except (urllib.error.URLError, urllib.error.HTTPError) as exc:
|
|
59
|
+
raise RuntimeError(f"could not read GitHub release metadata: {url}") from exc
|
|
60
|
+
if not isinstance(payload, dict):
|
|
61
|
+
raise RuntimeError(f"GitHub release metadata was not an object: {url}")
|
|
62
|
+
return payload
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _release(source_repository: str, release: str) -> dict[str, object]:
|
|
66
|
+
encoded_repo = urllib.parse.quote(source_repository, safe="/")
|
|
67
|
+
if release == "latest":
|
|
68
|
+
url = f"{DEFAULT_GITHUB_API}/repos/{encoded_repo}/releases/latest"
|
|
69
|
+
else:
|
|
70
|
+
encoded_release = urllib.parse.quote(release, safe="")
|
|
71
|
+
url = f"{DEFAULT_GITHUB_API}/repos/{encoded_repo}/releases/tags/{encoded_release}"
|
|
72
|
+
return _github_json(url)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _assets(release: dict[str, object]) -> list[dict[str, object]]:
|
|
76
|
+
assets = release.get("assets")
|
|
77
|
+
if not isinstance(assets, list):
|
|
78
|
+
raise RuntimeError("GitHub release has no assets list")
|
|
79
|
+
return [item for item in assets if isinstance(item, dict)]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _asset_for_bundle(release: dict[str, object], bundle: str) -> dict[str, object]:
|
|
83
|
+
expected_prefix = f"{ASSET_PREFIX}{bundle}-"
|
|
84
|
+
candidates = [
|
|
85
|
+
asset
|
|
86
|
+
for asset in _assets(release)
|
|
87
|
+
if isinstance(asset.get("name"), str)
|
|
88
|
+
and asset["name"].startswith(expected_prefix)
|
|
89
|
+
and asset["name"].endswith(".zip")
|
|
90
|
+
]
|
|
91
|
+
if len(candidates) != 1:
|
|
92
|
+
names = ", ".join(str(item.get("name")) for item in candidates) or "none"
|
|
93
|
+
raise RuntimeError(f"expected one release asset for bundle {bundle}; found: {names}")
|
|
94
|
+
return candidates[0]
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _download_asset(asset: dict[str, object]) -> tuple[str, bytes]:
|
|
98
|
+
name = asset.get("name")
|
|
99
|
+
url = asset.get("browser_download_url")
|
|
100
|
+
if not isinstance(name, str) or not isinstance(url, str):
|
|
101
|
+
raise RuntimeError("GitHub release asset has no usable name or download URL")
|
|
102
|
+
request = urllib.request.Request(url, headers={"User-Agent": "xrefkit-skill-sync"})
|
|
103
|
+
try:
|
|
104
|
+
with urllib.request.urlopen(request) as response: # nosec B310 - URL comes from GitHub release metadata
|
|
105
|
+
return name, response.read()
|
|
106
|
+
except (urllib.error.URLError, urllib.error.HTTPError) as exc:
|
|
107
|
+
raise RuntimeError(f"could not download release asset: {name}") from exc
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _safe_zip_extract(data: bytes, destination: Path) -> None:
|
|
111
|
+
with zipfile.ZipFile(__import__("io").BytesIO(data)) as archive:
|
|
112
|
+
for member in archive.infolist():
|
|
113
|
+
relative = PurePosixPath(member.filename)
|
|
114
|
+
if relative.is_absolute() or ".." in relative.parts:
|
|
115
|
+
raise RuntimeError(f"unsafe path in Skill bundle: {member.filename}")
|
|
116
|
+
target = (destination / Path(*relative.parts)).resolve()
|
|
117
|
+
target.relative_to(destination.resolve())
|
|
118
|
+
if member.is_dir():
|
|
119
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
120
|
+
continue
|
|
121
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
122
|
+
target.write_bytes(archive.read(member))
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _bundle_root(extracted: Path) -> Path:
|
|
126
|
+
if any((extracted / name).is_dir() for name in ALLOWED_ROOTS):
|
|
127
|
+
return extracted
|
|
128
|
+
children = [path for path in extracted.iterdir() if path.is_dir()]
|
|
129
|
+
if len(children) == 1 and any((children[0] / name).is_dir() for name in ALLOWED_ROOTS):
|
|
130
|
+
return children[0]
|
|
131
|
+
raise RuntimeError("Skill bundle must contain skills/ or knowledge/ at its root")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _bundle_files(root: Path) -> list[tuple[Path, Path]]:
|
|
135
|
+
files: list[tuple[Path, Path]] = []
|
|
136
|
+
for dirname in sorted(ALLOWED_ROOTS):
|
|
137
|
+
base = root / dirname
|
|
138
|
+
if not base.is_dir():
|
|
139
|
+
continue
|
|
140
|
+
for source in sorted(path for path in base.rglob("*") if path.is_file()):
|
|
141
|
+
files.append((source, Path(dirname) / source.relative_to(base)))
|
|
142
|
+
manifest = root / "package_manifest.yaml"
|
|
143
|
+
if manifest.is_file():
|
|
144
|
+
files.append((manifest, Path("package_manifest.yaml")))
|
|
145
|
+
if not files:
|
|
146
|
+
raise RuntimeError("Skill bundle contains no files under skills/ or knowledge/")
|
|
147
|
+
return files
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _state_path(repo: Path, bundle: str) -> Path:
|
|
151
|
+
return repo / ".xrefkit" / "skill-sync" / f"{bundle}.json"
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def sync_bundle(
|
|
155
|
+
*,
|
|
156
|
+
repo: Path,
|
|
157
|
+
source_repository: str,
|
|
158
|
+
bundle: str,
|
|
159
|
+
release: str = "latest",
|
|
160
|
+
force: bool = False,
|
|
161
|
+
dry_run: bool = False,
|
|
162
|
+
) -> SyncResult:
|
|
163
|
+
repo = repo.resolve()
|
|
164
|
+
if not repo.is_dir():
|
|
165
|
+
raise FileNotFoundError(f"XRefKit repository not found: {repo}")
|
|
166
|
+
release_payload = _release(source_repository, release)
|
|
167
|
+
asset = _asset_for_bundle(release_payload, bundle)
|
|
168
|
+
asset_name, data = _download_asset(asset)
|
|
169
|
+
digest = hashlib.sha256(data).hexdigest()
|
|
170
|
+
release_name = str(release_payload.get("tag_name") or release)
|
|
171
|
+
|
|
172
|
+
with tempfile.TemporaryDirectory(prefix="xrefkit-skill-sync-") as temp_dir:
|
|
173
|
+
extracted = Path(temp_dir) / "bundle"
|
|
174
|
+
extracted.mkdir()
|
|
175
|
+
_safe_zip_extract(data, extracted)
|
|
176
|
+
root = _bundle_root(extracted)
|
|
177
|
+
files = _bundle_files(root)
|
|
178
|
+
relative_files = tuple(sorted(destination.as_posix() for _source, destination in files))
|
|
179
|
+
|
|
180
|
+
previous: dict[str, object] = {}
|
|
181
|
+
state = _state_path(repo, bundle)
|
|
182
|
+
if state.is_file():
|
|
183
|
+
try:
|
|
184
|
+
loaded = json.loads(state.read_text(encoding="utf-8"))
|
|
185
|
+
if isinstance(loaded, dict):
|
|
186
|
+
previous = loaded
|
|
187
|
+
except json.JSONDecodeError:
|
|
188
|
+
raise RuntimeError(f"invalid sync state: {state}")
|
|
189
|
+
managed = {str(item) for item in previous.get("files", []) if isinstance(item, str)}
|
|
190
|
+
conflicts = [
|
|
191
|
+
str(destination)
|
|
192
|
+
for _source, destination in files
|
|
193
|
+
if (repo / destination).exists() and destination.as_posix() not in managed and not force
|
|
194
|
+
]
|
|
195
|
+
if conflicts:
|
|
196
|
+
raise RuntimeError(
|
|
197
|
+
"refusing to overwrite existing files not owned by sync state; "
|
|
198
|
+
f"use --force after review: {', '.join(sorted(conflicts))}"
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
if not dry_run:
|
|
202
|
+
for source, destination in files:
|
|
203
|
+
target = repo / destination
|
|
204
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
205
|
+
target.write_bytes(source.read_bytes())
|
|
206
|
+
state.parent.mkdir(parents=True, exist_ok=True)
|
|
207
|
+
state.write_text(
|
|
208
|
+
json.dumps(
|
|
209
|
+
{
|
|
210
|
+
"source_repository": source_repository,
|
|
211
|
+
"release": release_name,
|
|
212
|
+
"asset": asset_name,
|
|
213
|
+
"sha256": digest,
|
|
214
|
+
"files": list(relative_files),
|
|
215
|
+
},
|
|
216
|
+
ensure_ascii=False,
|
|
217
|
+
indent=2,
|
|
218
|
+
)
|
|
219
|
+
+ "\n",
|
|
220
|
+
encoding="utf-8",
|
|
221
|
+
)
|
|
222
|
+
return SyncResult(bundle, release_name, asset_name, digest, relative_files, True, dry_run)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _print(payload: object, *, as_json: bool) -> None:
|
|
226
|
+
if as_json:
|
|
227
|
+
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
228
|
+
return
|
|
229
|
+
results = payload if isinstance(payload, list) else [payload]
|
|
230
|
+
for item in results:
|
|
231
|
+
print(f"synced: {item['bundle']} ({item['release']})")
|
|
232
|
+
print(f"asset: {item['asset']}")
|
|
233
|
+
print(f"files: {len(item['files'])}")
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def main(argv: list[str] | None = None) -> int:
|
|
237
|
+
parser = argparse.ArgumentParser(prog="xrefkit skills")
|
|
238
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
239
|
+
sync = sub.add_parser("sync", help="download and register Skill/Knowledge bundles from GitHub Releases")
|
|
240
|
+
sync.add_argument("--repo", default=".", help="Target XRefKit repository; defaults to the current directory")
|
|
241
|
+
sync.add_argument("--source-repository", default=DEFAULT_SOURCE_REPOSITORY, help="GitHub owner/repository containing releases")
|
|
242
|
+
sync.add_argument("--bundle", action="append", help="Bundle name, for example csharp; repeat for multiple bundles")
|
|
243
|
+
sync.add_argument("--all", action="store_true", help="Synchronize every xrefkit-skills-*.zip asset in the release")
|
|
244
|
+
sync.add_argument("--release", default="latest", help="Release tag, or latest (default)")
|
|
245
|
+
sync.add_argument("--force", action="store_true", help="Allow overwriting files not owned by a previous sync")
|
|
246
|
+
sync.add_argument("--dry-run", action="store_true", help="Download and validate without writing the repository")
|
|
247
|
+
sync.add_argument("--json", action="store_true")
|
|
248
|
+
args = parser.parse_args(argv)
|
|
249
|
+
if not args.bundle and not args.all:
|
|
250
|
+
parser.error("one of --bundle or --all is required")
|
|
251
|
+
release_payload = _release(args.source_repository, args.release)
|
|
252
|
+
bundles = list(args.bundle or [])
|
|
253
|
+
if args.all:
|
|
254
|
+
for asset in _assets(release_payload):
|
|
255
|
+
name = asset.get("name")
|
|
256
|
+
if not isinstance(name, str) or not name.startswith(ASSET_PREFIX) or not name.endswith(".zip"):
|
|
257
|
+
continue
|
|
258
|
+
stem = name[:-4][len(ASSET_PREFIX) :]
|
|
259
|
+
match = VERSION_SUFFIX_RE.match(stem)
|
|
260
|
+
bundles.append(match.group("bundle") if match else stem)
|
|
261
|
+
bundles = list(dict.fromkeys(bundles))
|
|
262
|
+
results = [
|
|
263
|
+
sync_bundle(
|
|
264
|
+
repo=Path(args.repo),
|
|
265
|
+
source_repository=args.source_repository,
|
|
266
|
+
bundle=bundle,
|
|
267
|
+
release=args.release,
|
|
268
|
+
force=args.force,
|
|
269
|
+
dry_run=args.dry_run,
|
|
270
|
+
).to_dict()
|
|
271
|
+
for bundle in bundles
|
|
272
|
+
]
|
|
273
|
+
_print(results, as_json=args.json)
|
|
274
|
+
return 0
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
if __name__ == "__main__":
|
|
278
|
+
raise SystemExit(main())
|
|
@@ -30,6 +30,7 @@ tests/test_runtime_contracts.py
|
|
|
30
30
|
tests/test_sarif_to_locator.py
|
|
31
31
|
tests/test_skill_runtime_audit.py
|
|
32
32
|
tests/test_skillmeta.py
|
|
33
|
+
tests/test_skills_sync.py
|
|
33
34
|
tests/test_structure_catalog.py
|
|
34
35
|
tests/test_xref.py
|
|
35
36
|
tests/test_xrefkit_instance.py
|
|
@@ -62,6 +63,7 @@ xrefkit/resource_provider.py
|
|
|
62
63
|
xrefkit/runlog.py
|
|
63
64
|
xrefkit/skillmeta.py
|
|
64
65
|
xrefkit/skillrun.py
|
|
66
|
+
xrefkit/skills_sync.py
|
|
65
67
|
xrefkit/structure_catalog.py
|
|
66
68
|
xrefkit/v2_cli.py
|
|
67
69
|
xrefkit/workspace.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit/resources/base/generations/7a682a5272907354/contracts.json
RENAMED
|
File without changes
|
{xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit/resources/base/generations/7a682a5272907354/model_body.md
RENAMED
|
File without changes
|
{xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit/resources/base/generations/9929294385ccb7b0/contracts.json
RENAMED
|
File without changes
|
{xrefkit-0.4.5 → xrefkit-0.4.7}/xrefkit/resources/base/generations/9929294385ccb7b0/model_body.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|