xrefkit 0.4.2__tar.gz → 0.4.4__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.
Files changed (108) hide show
  1. {xrefkit-0.4.2 → xrefkit-0.4.4}/PKG-INFO +50 -2
  2. {xrefkit-0.4.2 → xrefkit-0.4.4}/README.md +49 -1
  3. {xrefkit-0.4.2 → xrefkit-0.4.4}/pyproject.toml +1 -1
  4. {xrefkit-0.4.2 → xrefkit-0.4.4}/tests/test_instruction_workflow.py +20 -0
  5. xrefkit-0.4.4/tests/test_mcp_setup.py +72 -0
  6. {xrefkit-0.4.2 → xrefkit-0.4.4}/xrefkit/__init__.py +1 -1
  7. {xrefkit-0.4.2 → xrefkit-0.4.4}/xrefkit/mcp/__init__.py +10 -0
  8. {xrefkit-0.4.2 → xrefkit-0.4.4}/xrefkit/mcp/bootstrap.py +13 -2
  9. xrefkit-0.4.4/xrefkit/mcp/setup.py +234 -0
  10. {xrefkit-0.4.2 → xrefkit-0.4.4}/xrefkit/operations_cli.py +1 -0
  11. {xrefkit-0.4.2 → xrefkit-0.4.4}/xrefkit/skillrun.py +45 -5
  12. {xrefkit-0.4.2 → xrefkit-0.4.4}/xrefkit.egg-info/PKG-INFO +50 -2
  13. {xrefkit-0.4.2 → xrefkit-0.4.4}/xrefkit.egg-info/SOURCES.txt +2 -0
  14. {xrefkit-0.4.2 → xrefkit-0.4.4}/LICENSE +0 -0
  15. {xrefkit-0.4.2 → xrefkit-0.4.4}/setup.cfg +0 -0
  16. {xrefkit-0.4.2 → xrefkit-0.4.4}/tests/test_base_sync_ownership.py +0 -0
  17. {xrefkit-0.4.2 → xrefkit-0.4.4}/tests/test_boundary_analysis.py +0 -0
  18. {xrefkit-0.4.2 → xrefkit-0.4.4}/tests/test_calibration_lint.py +0 -0
  19. {xrefkit-0.4.2 → xrefkit-0.4.4}/tests/test_check_skill_knowledge_xids.py +0 -0
  20. {xrefkit-0.4.2 → xrefkit-0.4.4}/tests/test_cli.py +0 -0
  21. {xrefkit-0.4.2 → xrefkit-0.4.4}/tests/test_collect_analyzer_sarif.py +0 -0
  22. {xrefkit-0.4.2 → xrefkit-0.4.4}/tests/test_convert_to_xrefkit_skill.py +0 -0
  23. {xrefkit-0.4.2 → xrefkit-0.4.4}/tests/test_cs_scope_probe.py +0 -0
  24. {xrefkit-0.4.2 → xrefkit-0.4.4}/tests/test_csharp_commonality.py +0 -0
  25. {xrefkit-0.4.2 → xrefkit-0.4.4}/tests/test_csharp_naming_profile.py +0 -0
  26. {xrefkit-0.4.2 → xrefkit-0.4.4}/tests/test_ctx.py +0 -0
  27. {xrefkit-0.4.2 → xrefkit-0.4.4}/tests/test_cutover_readiness.py +0 -0
  28. {xrefkit-0.4.2 → xrefkit-0.4.4}/tests/test_dashboard.py +0 -0
  29. {xrefkit-0.4.2 → xrefkit-0.4.4}/tests/test_error_policy_audit.py +0 -0
  30. {xrefkit-0.4.2 → xrefkit-0.4.4}/tests/test_error_policy_locator.py +0 -0
  31. {xrefkit-0.4.2 → xrefkit-0.4.4}/tests/test_fm_multiroot.py +0 -0
  32. {xrefkit-0.4.2 → xrefkit-0.4.4}/tests/test_gate.py +0 -0
  33. {xrefkit-0.4.2 → xrefkit-0.4.4}/tests/test_goal_desired_state.py +0 -0
  34. {xrefkit-0.4.2 → xrefkit-0.4.4}/tests/test_knowledge_relations_validator.py +0 -0
  35. {xrefkit-0.4.2 → xrefkit-0.4.4}/tests/test_ownership.py +0 -0
  36. {xrefkit-0.4.2 → xrefkit-0.4.4}/tests/test_packmeta.py +0 -0
  37. {xrefkit-0.4.2 → xrefkit-0.4.4}/tests/test_project_quality_baseline.py +0 -0
  38. {xrefkit-0.4.2 → xrefkit-0.4.4}/tests/test_resource_provider.py +0 -0
  39. {xrefkit-0.4.2 → xrefkit-0.4.4}/tests/test_runtime_contracts.py +0 -0
  40. {xrefkit-0.4.2 → xrefkit-0.4.4}/tests/test_sarif_to_locator.py +0 -0
  41. {xrefkit-0.4.2 → xrefkit-0.4.4}/tests/test_skill_runtime_audit.py +0 -0
  42. {xrefkit-0.4.2 → xrefkit-0.4.4}/tests/test_skillmeta.py +0 -0
  43. {xrefkit-0.4.2 → xrefkit-0.4.4}/tests/test_structure_catalog.py +0 -0
  44. {xrefkit-0.4.2 → xrefkit-0.4.4}/tests/test_xref.py +0 -0
  45. {xrefkit-0.4.2 → xrefkit-0.4.4}/tests/test_xrefkit_instance.py +0 -0
  46. {xrefkit-0.4.2 → xrefkit-0.4.4}/tests/test_xrefkit_tools.py +0 -0
  47. {xrefkit-0.4.2 → xrefkit-0.4.4}/tests/test_xrefkit_v2_discovery.py +0 -0
  48. {xrefkit-0.4.2 → xrefkit-0.4.4}/tests/test_xrefkit_v2_models.py +0 -0
  49. {xrefkit-0.4.2 → xrefkit-0.4.4}/tests/test_xrefkit_v2_pipeline.py +0 -0
  50. {xrefkit-0.4.2 → xrefkit-0.4.4}/xrefkit/__main__.py +0 -0
  51. {xrefkit-0.4.2 → xrefkit-0.4.4}/xrefkit/boundary_analysis.py +0 -0
  52. {xrefkit-0.4.2 → xrefkit-0.4.4}/xrefkit/catalog_cli.py +0 -0
  53. {xrefkit-0.4.2 → xrefkit-0.4.4}/xrefkit/cli.py +0 -0
  54. {xrefkit-0.4.2 → xrefkit-0.4.4}/xrefkit/contracts.py +0 -0
  55. {xrefkit-0.4.2 → xrefkit-0.4.4}/xrefkit/ctx.py +0 -0
  56. {xrefkit-0.4.2 → xrefkit-0.4.4}/xrefkit/dashboard.py +0 -0
  57. {xrefkit-0.4.2 → xrefkit-0.4.4}/xrefkit/discovery.py +0 -0
  58. {xrefkit-0.4.2 → xrefkit-0.4.4}/xrefkit/gate.py +0 -0
  59. {xrefkit-0.4.2 → xrefkit-0.4.4}/xrefkit/goalstate.py +0 -0
  60. {xrefkit-0.4.2 → xrefkit-0.4.4}/xrefkit/hashing.py +0 -0
  61. {xrefkit-0.4.2 → xrefkit-0.4.4}/xrefkit/import_skill.py +0 -0
  62. {xrefkit-0.4.2 → xrefkit-0.4.4}/xrefkit/instance.py +0 -0
  63. {xrefkit-0.4.2 → xrefkit-0.4.4}/xrefkit/loaders.py +0 -0
  64. {xrefkit-0.4.2 → xrefkit-0.4.4}/xrefkit/mcp/audit.py +0 -0
  65. {xrefkit-0.4.2 → xrefkit-0.4.4}/xrefkit/mcp/catalog.py +0 -0
  66. {xrefkit-0.4.2 → xrefkit-0.4.4}/xrefkit/mcp/cli.py +0 -0
  67. {xrefkit-0.4.2 → xrefkit-0.4.4}/xrefkit/mcp/client_cache.py +0 -0
  68. {xrefkit-0.4.2 → xrefkit-0.4.4}/xrefkit/mcp/context_registry.py +0 -0
  69. {xrefkit-0.4.2 → xrefkit-0.4.4}/xrefkit/mcp/contracts.py +0 -0
  70. {xrefkit-0.4.2 → xrefkit-0.4.4}/xrefkit/mcp/dist.py +0 -0
  71. {xrefkit-0.4.2 → xrefkit-0.4.4}/xrefkit/mcp/ownership.py +0 -0
  72. {xrefkit-0.4.2 → xrefkit-0.4.4}/xrefkit/mcp/repository.py +0 -0
  73. {xrefkit-0.4.2 → xrefkit-0.4.4}/xrefkit/mcp/schemas.py +0 -0
  74. {xrefkit-0.4.2 → xrefkit-0.4.4}/xrefkit/mcp/server.py +0 -0
  75. {xrefkit-0.4.2 → xrefkit-0.4.4}/xrefkit/mcp/startup_contract_pack.py +0 -0
  76. {xrefkit-0.4.2 → xrefkit-0.4.4}/xrefkit/mcp_tools.py +0 -0
  77. {xrefkit-0.4.2 → xrefkit-0.4.4}/xrefkit/models/__init__.py +0 -0
  78. {xrefkit-0.4.2 → xrefkit-0.4.4}/xrefkit/models/common.py +0 -0
  79. {xrefkit-0.4.2 → xrefkit-0.4.4}/xrefkit/models/effective_bundle.py +0 -0
  80. {xrefkit-0.4.2 → xrefkit-0.4.4}/xrefkit/models/local_manifest.py +0 -0
  81. {xrefkit-0.4.2 → xrefkit-0.4.4}/xrefkit/models/package_manifest.py +0 -0
  82. {xrefkit-0.4.2 → xrefkit-0.4.4}/xrefkit/models/run_log.py +0 -0
  83. {xrefkit-0.4.2 → xrefkit-0.4.4}/xrefkit/models/server_config.py +0 -0
  84. {xrefkit-0.4.2 → xrefkit-0.4.4}/xrefkit/models/skill_definition.py +0 -0
  85. {xrefkit-0.4.2 → xrefkit-0.4.4}/xrefkit/ownership.py +0 -0
  86. {xrefkit-0.4.2 → xrefkit-0.4.4}/xrefkit/packmeta.py +0 -0
  87. {xrefkit-0.4.2 → xrefkit-0.4.4}/xrefkit/registry.py +0 -0
  88. {xrefkit-0.4.2 → xrefkit-0.4.4}/xrefkit/resolver.py +0 -0
  89. {xrefkit-0.4.2 → xrefkit-0.4.4}/xrefkit/resource_provider.py +0 -0
  90. {xrefkit-0.4.2 → xrefkit-0.4.4}/xrefkit/resources/base/contracts.json +0 -0
  91. {xrefkit-0.4.2 → xrefkit-0.4.4}/xrefkit/resources/base/current.json +0 -0
  92. {xrefkit-0.4.2 → xrefkit-0.4.4}/xrefkit/resources/base/generations/7a682a5272907354/contracts.json +0 -0
  93. {xrefkit-0.4.2 → xrefkit-0.4.4}/xrefkit/resources/base/generations/7a682a5272907354/model_body.md +0 -0
  94. {xrefkit-0.4.2 → xrefkit-0.4.4}/xrefkit/resources/base/generations/9929294385ccb7b0/contracts.json +0 -0
  95. {xrefkit-0.4.2 → xrefkit-0.4.4}/xrefkit/resources/base/generations/9929294385ccb7b0/model_body.md +0 -0
  96. {xrefkit-0.4.2 → xrefkit-0.4.4}/xrefkit/resources/base/model_body.md +0 -0
  97. {xrefkit-0.4.2 → xrefkit-0.4.4}/xrefkit/runlog.py +0 -0
  98. {xrefkit-0.4.2 → xrefkit-0.4.4}/xrefkit/skillmeta.py +0 -0
  99. {xrefkit-0.4.2 → xrefkit-0.4.4}/xrefkit/structure_catalog.py +0 -0
  100. {xrefkit-0.4.2 → xrefkit-0.4.4}/xrefkit/tools/__init__.py +0 -0
  101. {xrefkit-0.4.2 → xrefkit-0.4.4}/xrefkit/tools/__main__.py +0 -0
  102. {xrefkit-0.4.2 → xrefkit-0.4.4}/xrefkit/v2_cli.py +0 -0
  103. {xrefkit-0.4.2 → xrefkit-0.4.4}/xrefkit/workspace.py +0 -0
  104. {xrefkit-0.4.2 → xrefkit-0.4.4}/xrefkit/xref.py +0 -0
  105. {xrefkit-0.4.2 → xrefkit-0.4.4}/xrefkit.egg-info/dependency_links.txt +0 -0
  106. {xrefkit-0.4.2 → xrefkit-0.4.4}/xrefkit.egg-info/entry_points.txt +0 -0
  107. {xrefkit-0.4.2 → xrefkit-0.4.4}/xrefkit.egg-info/requires.txt +0 -0
  108. {xrefkit-0.4.2 → xrefkit-0.4.4}/xrefkit.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: xrefkit
3
- Version: 0.4.2
3
+ Version: 0.4.4
4
4
  Summary: Portable XID, Skill, Knowledge, workflow, and MCP runtime for XRefKit
5
5
  Author: synthaicode
6
6
  License: MIT License
@@ -126,7 +126,36 @@ reason instead of inventing a criterion.
126
126
 
127
127
  ## Quick Start
128
128
 
129
- Install the package and initialize an instance:
129
+ ### Install from PyPI
130
+
131
+ XRefKit requires Python 3.11 or later. For a normal installation, create a
132
+ virtual environment and install the published package from PyPI:
133
+
134
+ ```powershell
135
+ python -m venv .venv
136
+ .\.venv\Scripts\Activate.ps1
137
+ python -m pip install --upgrade pip
138
+ python -m pip install xrefkit
139
+ xrefkit init
140
+ xrefkit --help
141
+ ```
142
+
143
+ If the `xrefkit` command is not available on `PATH`, use the module form:
144
+
145
+ ```powershell
146
+ python -m xrefkit --help
147
+ ```
148
+
149
+ To use the integrated MCP server, install the optional MCP dependencies:
150
+
151
+ ```powershell
152
+ python -m pip install "xrefkit[mcp]"
153
+ xrefkit mcp serve --repo . --transport stdio
154
+ ```
155
+
156
+ ### Install from a checkout
157
+
158
+ For XRefKit development, install the local checkout in editable mode instead:
130
159
 
131
160
  ```powershell
132
161
  python -m pip install -e .
@@ -140,6 +169,25 @@ Start the integrated MCP server over stdio:
140
169
  xrefkit mcp serve --repo . --transport stdio
141
170
  ```
142
171
 
172
+ To import existing Skills and prepare a reviewable VS Code MCP setup, run:
173
+
174
+ ```powershell
175
+ python -m pip install "xrefkit[mcp]"
176
+ xrefkit mcp setup `
177
+ --repo C:\dev\itsm\XRefKit `
178
+ --import C:\work\existing-skills
179
+ ```
180
+
181
+ The command writes `SETUP.md`, `import-report.json`, a VS Code
182
+ `.vscode/mcp.json` example, and reviewed append text for `AGENTS.md` and
183
+ `CLAUDE.md` into a temporary setup folder. Apply those artifacts after review:
184
+
185
+ ```powershell
186
+ xrefkit mcp setup apply `
187
+ --source C:\Users\<user>\AppData\Local\Temp\xrefkit-setup-<id> `
188
+ --repo C:\dev\itsm\XRefKit
189
+ ```
190
+
143
191
  The server writes structured correlation events to
144
192
  `work/mcp/xid_audit.jsonl` by default. After `xrefkit skill run` returns a
145
193
  `run_id`, the client calls MCP `bind_skill_run` and executes the returned
@@ -87,7 +87,36 @@ reason instead of inventing a criterion.
87
87
 
88
88
  ## Quick Start
89
89
 
90
- Install the package and initialize an instance:
90
+ ### Install from PyPI
91
+
92
+ XRefKit requires Python 3.11 or later. For a normal installation, create a
93
+ virtual environment and install the published package from PyPI:
94
+
95
+ ```powershell
96
+ python -m venv .venv
97
+ .\.venv\Scripts\Activate.ps1
98
+ python -m pip install --upgrade pip
99
+ python -m pip install xrefkit
100
+ xrefkit init
101
+ xrefkit --help
102
+ ```
103
+
104
+ If the `xrefkit` command is not available on `PATH`, use the module form:
105
+
106
+ ```powershell
107
+ python -m xrefkit --help
108
+ ```
109
+
110
+ To use the integrated MCP server, install the optional MCP dependencies:
111
+
112
+ ```powershell
113
+ python -m pip install "xrefkit[mcp]"
114
+ xrefkit mcp serve --repo . --transport stdio
115
+ ```
116
+
117
+ ### Install from a checkout
118
+
119
+ For XRefKit development, install the local checkout in editable mode instead:
91
120
 
92
121
  ```powershell
93
122
  python -m pip install -e .
@@ -101,6 +130,25 @@ Start the integrated MCP server over stdio:
101
130
  xrefkit mcp serve --repo . --transport stdio
102
131
  ```
103
132
 
133
+ To import existing Skills and prepare a reviewable VS Code MCP setup, run:
134
+
135
+ ```powershell
136
+ python -m pip install "xrefkit[mcp]"
137
+ xrefkit mcp setup `
138
+ --repo C:\dev\itsm\XRefKit `
139
+ --import C:\work\existing-skills
140
+ ```
141
+
142
+ The command writes `SETUP.md`, `import-report.json`, a VS Code
143
+ `.vscode/mcp.json` example, and reviewed append text for `AGENTS.md` and
144
+ `CLAUDE.md` into a temporary setup folder. Apply those artifacts after review:
145
+
146
+ ```powershell
147
+ xrefkit mcp setup apply `
148
+ --source C:\Users\<user>\AppData\Local\Temp\xrefkit-setup-<id> `
149
+ --repo C:\dev\itsm\XRefKit
150
+ ```
151
+
104
152
  The server writes structured correlation events to
105
153
  `work/mcp/xid_audit.jsonl` by default. After `xrefkit skill run` returns a
106
154
  `run_id`, the client calls MCP `bind_skill_run` and executes the returned
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "xrefkit"
7
- version = "0.4.2"
7
+ version = "0.4.4"
8
8
  description = "Portable XID, Skill, Knowledge, workflow, and MCP runtime for XRefKit"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -150,3 +150,23 @@ def test_workitem_requires_criterion_or_explicit_unknown_reason(tmp_path: Path)
150
150
  assert "criterion=`` reason=`The business owner has not defined the acceptance outcome`" in text
151
151
  assert _run(tmp_path, "skill", "phase", "--log", str(out), "--phase", "execution", "--status", "done", "--role", "instruction:executor") == 0
152
152
  assert _run(tmp_path, "skill", "verify", "--log", str(out)) == 1
153
+
154
+
155
+ def test_workitem_criterion_is_immutable_and_changes_use_supersedes(tmp_path: Path) -> None:
156
+ out = tmp_path / "work" / "sessions" / "run.md"
157
+ assert _run(tmp_path, "workflow", "run", "--task", "Do work", "--out", str(out), "--use-default-completion-conditions") == 0
158
+ base = [
159
+ "skill", "workitem", "--log", str(out), "--item", "WI-001",
160
+ "--text", "Implement original outcome", "--completion-criterion", "original outcome is verified",
161
+ "--status", "pending", "--role", "instruction:executor",
162
+ ]
163
+ assert _run(tmp_path, *base) == 0
164
+ assert _run(tmp_path, *base[:-6], "--completion-criterion", "different outcome is verified", "--status", "pending", "--role", "instruction:executor") == 1
165
+ assert _run(
166
+ tmp_path,
167
+ "skill", "workitem", "--log", str(out), "--item", "WI-002", "--supersedes", "WI-001",
168
+ "--text", "Implement revised outcome", "--completion-criterion", "revised outcome is verified",
169
+ "--status", "pending", "--role", "instruction:executor",
170
+ ) == 0
171
+ text = out.read_text(encoding="utf-8")
172
+ assert "WI-002" in text and "supersedes=`WI-001`" in text
@@ -0,0 +1,72 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ from xrefkit.__main__ import main
7
+
8
+
9
+ def test_mcp_setup_writes_reviewable_workspace(tmp_path: Path, capsys) -> None:
10
+ output = tmp_path / "setup-output"
11
+ root = tmp_path / "repo"
12
+
13
+ assert main(["mcp", "setup", "--repo", str(root), "--output", str(output), "--json"]) == 0
14
+
15
+ payload = json.loads(capsys.readouterr().out)
16
+ assert payload["output"] == str(output.resolve())
17
+ assert (output / "SETUP.md").is_file()
18
+ assert (output / "vscode-mcp.json").is_file()
19
+ assert (output / "AGENTS.md.append.md").is_file()
20
+ assert (output / "CLAUDE.md.append.md").is_file()
21
+ assert (output / "import-report.json").is_file()
22
+
23
+ vscode = json.loads((output / "vscode-mcp.json").read_text(encoding="utf-8"))
24
+ server = vscode["servers"]["xrefkit"]
25
+ assert server["type"] == "stdio"
26
+ assert server["args"][-2:] == ["--transport", "stdio"]
27
+
28
+
29
+ def test_mcp_setup_imports_existing_batch_skill_before_writing_report(tmp_path: Path, capsys) -> None:
30
+ source = tmp_path / "existing"
31
+ skill = source / "skills" / "legacy-review"
32
+ skill.mkdir(parents=True)
33
+ (skill / "SKILL.md").write_text("# Legacy Review\n\nReview the supplied change.\n", encoding="utf-8")
34
+ root = tmp_path / "repo"
35
+ output = tmp_path / "setup-output"
36
+
37
+ exit_code = main(
38
+ [
39
+ "mcp",
40
+ "setup",
41
+ "--repo",
42
+ str(root),
43
+ "--import",
44
+ str(source),
45
+ "--output",
46
+ str(output),
47
+ "--json",
48
+ ]
49
+ )
50
+ payload = json.loads(capsys.readouterr().out)
51
+ assert exit_code in {0, 1}
52
+ assert (root / "skills" / "imported.legacy-review" / "SKILL.md").is_file()
53
+ report = json.loads((output / "import-report.json").read_text(encoding="utf-8"))
54
+ assert report["import"]["converted_skills"][0]["skill_id"] == "imported.legacy-review"
55
+ assert payload["output"] == str(output.resolve())
56
+
57
+
58
+ def test_mcp_setup_apply_copies_config_and_appends_instructions(tmp_path: Path, capsys) -> None:
59
+ output = tmp_path / "setup-output"
60
+ root = tmp_path / "repo"
61
+ root.mkdir()
62
+ assert main(["mcp", "setup", "--repo", str(root), "--output", str(output)]) == 0
63
+ capsys.readouterr()
64
+
65
+ assert main(["mcp", "setup-apply", "--source", str(output), "--repo", str(root)]) == 0
66
+ capsys.readouterr()
67
+ assert (root / ".vscode" / "mcp.json").is_file()
68
+ agents = (root / "AGENTS.md").read_text(encoding="utf-8")
69
+ assert "XRefKit MCP Skill Routing" in agents
70
+
71
+ assert main(["mcp", "setup-apply", "--source", str(output), "--repo", str(root)]) == 0
72
+ assert (root / "AGENTS.md").read_text(encoding="utf-8").count("XRefKit MCP Skill Routing") == 1
@@ -2,4 +2,4 @@
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "0.4.2"
5
+ __version__ = "0.4.4"
@@ -11,6 +11,16 @@ def main(argv: list[str] | None = None) -> int:
11
11
  from .server import main as server_main
12
12
 
13
13
  args = list(argv or [])
14
+ if args and args[0] == "setup":
15
+ from .setup import main as setup_main
16
+
17
+ if len(args) > 1 and args[1] == "apply":
18
+ return setup_main(["setup-apply", *args[2:]])
19
+ return setup_main(args)
20
+ if args and args[0] == "setup-apply":
21
+ from .setup import main as setup_main
22
+
23
+ return setup_main(args)
14
24
  if args and args[0] == "serve":
15
25
  args = args[1:]
16
26
  return server_main(args)
@@ -37,6 +37,7 @@ import sys
37
37
  import urllib.request
38
38
  import zipfile
39
39
  from pathlib import Path
40
+ from urllib.parse import urlsplit
40
41
 
41
42
 
42
43
  DIST_STATE_RELATIVE_PATH = ".xrefkit/dist-state.json"
@@ -50,6 +51,12 @@ class BootstrapError(RuntimeError):
50
51
  pass
51
52
 
52
53
 
54
+ def _validate_http_url(url: str) -> None:
55
+ parsed = urlsplit(url)
56
+ if parsed.scheme not in {"http", "https"} or not parsed.netloc:
57
+ raise BootstrapError("bootstrap endpoints must use an absolute http(s) URL")
58
+
59
+
53
60
  def _ssl_context(ca_file: str | None) -> ssl.SSLContext:
54
61
  if ca_file:
55
62
  return ssl.create_default_context(cafile=ca_file)
@@ -57,8 +64,10 @@ def _ssl_context(ca_file: str | None) -> ssl.SSLContext:
57
64
 
58
65
 
59
66
  def http_get(url: str, ca_file: str | None = None, headers: dict[str, str] | None = None) -> bytes:
67
+ _validate_http_url(url)
60
68
  request = urllib.request.Request(url, headers=headers or {})
61
- with urllib.request.urlopen(request, context=_ssl_context(ca_file)) as response:
69
+ # The URL scheme is validated above; B310 cannot infer that from Request.
70
+ with urllib.request.urlopen(request, context=_ssl_context(ca_file)) as response: # nosec B310
62
71
  return response.read()
63
72
 
64
73
 
@@ -68,6 +77,7 @@ def http_post_json(
68
77
  ca_file: str | None = None,
69
78
  headers: dict[str, str] | None = None,
70
79
  ) -> tuple[bytes, dict[str, str]]:
80
+ _validate_http_url(url)
71
81
  request_headers = {
72
82
  "Content-Type": "application/json",
73
83
  "Accept": "application/json, text/event-stream",
@@ -79,7 +89,8 @@ def http_post_json(
79
89
  headers=request_headers,
80
90
  method="POST",
81
91
  )
82
- with urllib.request.urlopen(request, context=_ssl_context(ca_file)) as response:
92
+ # The URL scheme is validated above; B310 cannot infer that from Request.
93
+ with urllib.request.urlopen(request, context=_ssl_context(ca_file)) as response: # nosec B310
83
94
  return response.read(), {key.lower(): value for key, value in response.headers.items()}
84
95
 
85
96
 
@@ -0,0 +1,234 @@
1
+ """Prepare an XRefKit repository and client configuration for MCP use."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import shutil
8
+ import subprocess
9
+ import sys
10
+ import tempfile
11
+ from pathlib import Path
12
+
13
+
14
+ AGENT_APPEND = """## XRefKit MCP Skill Routing
15
+
16
+ When the XRefKit MCP server is configured and available in this client, use
17
+ its Skill catalog and semantic routing for Skill selection.
18
+
19
+ The VS Code workspace MCP configuration is `.vscode/mcp.json`.
20
+
21
+ Do not manually import or select individual Skill files during normal task
22
+ execution. The administrator manages Skill registration; the client uses the
23
+ MCP catalog to select and execute Skills.
24
+ """
25
+
26
+
27
+ def _vscode_config(root: Path) -> dict[str, object]:
28
+ python_command = "python"
29
+ venv_python = root / ".venv" / "Scripts" / "python.exe"
30
+ if venv_python.is_file():
31
+ python_command = "${workspaceFolder}\\.venv\\Scripts\\python.exe"
32
+ return {
33
+ "servers": {
34
+ "xrefkit": {
35
+ "type": "stdio",
36
+ "command": python_command,
37
+ "args": [
38
+ "-m",
39
+ "xrefkit",
40
+ "mcp",
41
+ "serve",
42
+ "--repo",
43
+ "${workspaceFolder}",
44
+ "--transport",
45
+ "stdio",
46
+ ],
47
+ }
48
+ }
49
+ }
50
+
51
+
52
+ def _run_xref_fix(root: Path) -> dict[str, object]:
53
+ proc = subprocess.run(
54
+ [sys.executable, "-m", "xrefkit", "xref", "fix", "--root", str(root), "--json"],
55
+ capture_output=True,
56
+ text=True,
57
+ check=False,
58
+ )
59
+ try:
60
+ payload: object = json.loads(proc.stdout)
61
+ except json.JSONDecodeError:
62
+ payload = {"stdout": proc.stdout.strip(), "stderr": proc.stderr.strip()}
63
+ return {"returncode": proc.returncode, "result": payload}
64
+
65
+
66
+ def _run_skill_checks(root: Path, metas: list[Path]) -> list[dict[str, object]]:
67
+ results: list[dict[str, object]] = []
68
+ for meta in metas:
69
+ proc = subprocess.run(
70
+ [
71
+ sys.executable,
72
+ "-m",
73
+ "xrefkit",
74
+ "skill",
75
+ "check",
76
+ "--root",
77
+ str(root),
78
+ "--meta",
79
+ str(meta),
80
+ "--level",
81
+ "trial",
82
+ "--json",
83
+ ],
84
+ capture_output=True,
85
+ text=True,
86
+ check=False,
87
+ )
88
+ try:
89
+ result: object = json.loads(proc.stdout)
90
+ except json.JSONDecodeError:
91
+ result = {"stdout": proc.stdout.strip(), "stderr": proc.stderr.strip()}
92
+ results.append({"meta": meta.relative_to(root).as_posix(), "returncode": proc.returncode, "result": result})
93
+ return results
94
+
95
+
96
+ def _write_setup_files(
97
+ output: Path,
98
+ *,
99
+ root: Path,
100
+ import_report: dict[str, object] | None,
101
+ xref_report: dict[str, object],
102
+ checks: list[dict[str, object]],
103
+ ) -> None:
104
+ output.mkdir(parents=True, exist_ok=True)
105
+ (output / "vscode-mcp.json").write_text(
106
+ json.dumps(_vscode_config(root), ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
107
+ )
108
+ (output / "AGENTS.md.append.md").write_text(AGENT_APPEND, encoding="utf-8")
109
+ (output / "CLAUDE.md.append.md").write_text(AGENT_APPEND, encoding="utf-8")
110
+ report = {"root": str(root), "import": import_report, "xref_fix": xref_report, "skill_checks": checks}
111
+ (output / "import-report.json").write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
112
+ guide = f"""# XRefKit MCP setup review
113
+
114
+ Generated setup files:
115
+
116
+ - `vscode-mcp.json`: copy to `{(root / '.vscode' / 'mcp.json').as_posix()}`.
117
+ - `AGENTS.md.append.md`: review and append to `AGENTS.md`.
118
+ - `CLAUDE.md.append.md`: review and append to `CLAUDE.md`.
119
+ - `import-report.json`: import, XID fix, and Skill validation results.
120
+
121
+ The setup command does not modify client instruction files or overwrite an
122
+ existing VS Code MCP configuration. Apply these files only after review.
123
+
124
+ After applying the files, open the repository in VS Code and start the
125
+ `xrefkit` MCP server from the MCP controls.
126
+ """
127
+ (output / "SETUP.md").write_text(guide, encoding="utf-8")
128
+
129
+
130
+ def setup(args: argparse.Namespace) -> int:
131
+ root = Path(args.repo).resolve()
132
+ root.mkdir(parents=True, exist_ok=True)
133
+ output = Path(args.output).resolve() if args.output else Path(tempfile.mkdtemp(prefix="xrefkit-setup-"))
134
+ import_report: dict[str, object] | None = None
135
+ metas: list[Path] = []
136
+
137
+ if args.import_source:
138
+ from xrefkit.import_skill import convert_skill, convert_skill_tree
139
+
140
+ source = Path(args.import_source).resolve()
141
+ if (source / "skills").is_dir() or args.batch:
142
+ result = convert_skill_tree(
143
+ source_root=source,
144
+ repo_root=root,
145
+ skill_id_prefix=args.skill_id_prefix,
146
+ target_skill_root=root / "skills",
147
+ dry_run=False,
148
+ )
149
+ import_report = result.to_dict()
150
+ else:
151
+ if not args.skill_id:
152
+ raise SystemExit("--skill-id is required when --import is a single Skill directory")
153
+ result = convert_skill(
154
+ source_dir=source,
155
+ source_root=source.parent,
156
+ repo_root=root,
157
+ skill_id=args.skill_id,
158
+ target_skill_dir=root / "skills" / args.skill_id,
159
+ dry_run=False,
160
+ )
161
+ import_report = result.to_dict()
162
+
163
+ if isinstance(import_report, dict):
164
+ metas = [root / Path(item["meta_doc"]) for item in import_report.get("converted_skills", []) if item.get("meta_doc")]
165
+ if not metas and import_report.get("meta_doc"):
166
+ metas = [root / Path(import_report["meta_doc"])]
167
+
168
+ xref_report = _run_xref_fix(root)
169
+ checks = _run_skill_checks(root, metas)
170
+ _write_setup_files(output, root=root, import_report=import_report, xref_report=xref_report, checks=checks)
171
+
172
+ payload = {"ok": xref_report["returncode"] == 0 and all(item["returncode"] == 0 for item in checks), "output": str(output), "report": str(output / "import-report.json")}
173
+ if args.json:
174
+ print(json.dumps(payload, ensure_ascii=False, indent=2))
175
+ else:
176
+ print(f"setup workspace: {output}")
177
+ print(f"guide: {output / 'SETUP.md'}")
178
+ print(f"report: {output / 'import-report.json'}")
179
+ return 0 if payload["ok"] else 1
180
+
181
+
182
+ def apply_setup(args: argparse.Namespace) -> int:
183
+ source = Path(args.source).resolve()
184
+ root = Path(args.repo).resolve()
185
+ if not source.is_dir():
186
+ raise SystemExit(f"setup workspace does not exist: {source}")
187
+ destinations = {
188
+ source / "vscode-mcp.json": root / ".vscode" / "mcp.json",
189
+ source / "AGENTS.md.append.md": root / "AGENTS.md",
190
+ source / "CLAUDE.md.append.md": root / "CLAUDE.md",
191
+ }
192
+ for candidate, destination in destinations.items():
193
+ if not candidate.is_file():
194
+ raise SystemExit(f"missing setup artifact: {candidate}")
195
+ if destination.name == "mcp.json":
196
+ destination.parent.mkdir(parents=True, exist_ok=True)
197
+ if destination.exists() and destination.read_bytes() != candidate.read_bytes() and not args.force:
198
+ raise SystemExit(f"refusing to overwrite {destination}; use --force after review")
199
+ if not destination.exists() or destination.read_bytes() != candidate.read_bytes():
200
+ shutil.copyfile(candidate, destination)
201
+ else:
202
+ existing = destination.read_text(encoding="utf-8") if destination.exists() else ""
203
+ addition = candidate.read_text(encoding="utf-8")
204
+ if addition.strip() not in existing:
205
+ destination.write_text(existing.rstrip() + "\n\n" + addition, encoding="utf-8")
206
+ print(f"applied setup artifacts from: {source}")
207
+ return 0
208
+
209
+
210
+ def main(argv: list[str] | None = None) -> int:
211
+ parser = argparse.ArgumentParser(prog="xrefkit mcp")
212
+ sub = parser.add_subparsers(dest="command", required=True)
213
+ setup_parser = sub.add_parser("setup", help="import Skills and generate reviewable MCP setup artifacts")
214
+ setup_parser.add_argument("--repo", required=True)
215
+ setup_parser.add_argument("--import", dest="import_source", default=None, help="Skill directory or batch root")
216
+ setup_parser.add_argument("--batch", action="store_true")
217
+ setup_parser.add_argument("--skill-id-prefix", default="imported")
218
+ setup_parser.add_argument("--skill-id", default=None)
219
+ setup_parser.add_argument("--output", default=None, help="Setup workspace; defaults to a temporary folder")
220
+ setup_parser.add_argument("--json", action="store_true")
221
+ setup_parser.set_defaults(handler=setup)
222
+
223
+ apply_parser = sub.add_parser("setup-apply", help="apply reviewed setup artifacts")
224
+ apply_parser.add_argument("--source", required=True)
225
+ apply_parser.add_argument("--repo", required=True)
226
+ apply_parser.add_argument("--force", action="store_true")
227
+ apply_parser.set_defaults(handler=apply_setup)
228
+
229
+ args = parser.parse_args(argv)
230
+ return args.handler(args)
231
+
232
+
233
+ if __name__ == "__main__":
234
+ raise SystemExit(main())
@@ -499,6 +499,7 @@ def _build_parser() -> argparse.ArgumentParser:
499
499
  p_skill_workitem.add_argument("--text", default=None, help="Work item text; required when adding a new item")
500
500
  p_skill_workitem.add_argument("--completion-criterion", default=None, help="Observable procedural condition for this work item")
501
501
  p_skill_workitem.add_argument("--criterion-unknown-reason", default=None, help="Why the completion criterion cannot yet be defined for unknown/blocked/escalated work")
502
+ p_skill_workitem.add_argument("--supersedes", default=None, help="Existing work item whose criterion is being replaced by this new item")
502
503
  p_skill_workitem.add_argument(
503
504
  "--status",
504
505
  required=True,
@@ -179,6 +179,12 @@ WORKLIST_ROWS = [
179
179
  ("Handoff", "Record outputs, unresolved items, next owner, and human decision points."),
180
180
  ]
181
181
  WORKITEM_RE = re.compile(
182
+ r"^- \[(?P<checkbox>[ x!])\] (?P<item_id>[A-Za-z0-9_.-]+) "
183
+ r"status=`(?P<status>[^`]+)` role=`(?P<role>[^`]+)` "
184
+ r"criterion=`(?P<criterion>[^`]*)` reason=`(?P<reason>[^`]*)` "
185
+ r"supersedes=`(?P<supersedes>[^`]*)`: (?P<text>.*)$"
186
+ )
187
+ WORKITEM_V2_RE = re.compile(
182
188
  r"^- \[(?P<checkbox>[ x!])\] (?P<item_id>[A-Za-z0-9_.-]+) "
183
189
  r"status=`(?P<status>[^`]+)` role=`(?P<role>[^`]+)` "
184
190
  r"criterion=`(?P<criterion>[^`]*)` reason=`(?P<reason>[^`]*)`: (?P<text>.*)$"
@@ -838,6 +844,21 @@ def _parse_work_items(text: str) -> list[dict[str, str]]:
838
844
  "role": match.group("role"),
839
845
  "criterion": match.group("criterion"),
840
846
  "reason": match.group("reason"),
847
+ "supersedes": match.group("supersedes"),
848
+ "text": match.group("text"),
849
+ }
850
+ )
851
+ continue
852
+ match = WORKITEM_V2_RE.match(line)
853
+ if match:
854
+ items.append(
855
+ {
856
+ "item_id": match.group("item_id"),
857
+ "status": match.group("status"),
858
+ "role": match.group("role"),
859
+ "criterion": match.group("criterion"),
860
+ "reason": match.group("reason"),
861
+ "supersedes": "",
841
862
  "text": match.group("text"),
842
863
  }
843
864
  )
@@ -852,16 +873,17 @@ def _parse_work_items(text: str) -> list[dict[str, str]]:
852
873
  "role": match.group("role"),
853
874
  "criterion": "",
854
875
  "reason": "legacy work item has no recorded completion criterion",
876
+ "supersedes": "",
855
877
  "text": match.group("text"),
856
878
  }
857
879
  )
858
880
  return items
859
881
 
860
882
 
861
- def _render_workitem_line(*, item_id: str, status: str, role: str, criterion: str, reason: str, text: str) -> str:
883
+ def _render_workitem_line(*, item_id: str, status: str, role: str, criterion: str, reason: str, supersedes: str, text: str) -> str:
862
884
  return (
863
885
  f"- [{_workitem_checkbox(status)}] {item_id} status=`{status}` role=`{role}` "
864
- f"criterion=`{criterion}` reason=`{reason}`: {text}"
886
+ f"criterion=`{criterion}` reason=`{reason}` supersedes=`{supersedes}`: {text}"
865
887
  )
866
888
 
867
889
 
@@ -913,6 +935,7 @@ def update_work_item(args) -> SkillRunResult:
913
935
  item_text = str(args.text or "").strip()
914
936
  criterion = str(getattr(args, "completion_criterion", None) or "").strip().replace("`", "'").replace("\n", " ")
915
937
  reason = str(getattr(args, "criterion_unknown_reason", None) or "").strip().replace("`", "'").replace("\n", " ")
938
+ supersedes = str(getattr(args, "supersedes", None) or "").strip().replace("`", "'").replace("\n", " ")
916
939
  if not item_id:
917
940
  return SkillRunResult(ok=False, skill_id=None, skill_doc=None, run_log=str(log_path), errors=["missing --item"])
918
941
  if status not in VALID_WORKITEM_STATUSES:
@@ -931,10 +954,21 @@ def update_work_item(args) -> SkillRunResult:
931
954
  items = _parse_work_items(text)
932
955
  existing = next((item for item in items if item["item_id"] == item_id), None)
933
956
  if existing:
957
+ existing_criterion = existing.get("criterion", "")
958
+ if criterion and criterion != existing_criterion:
959
+ return SkillRunResult(
960
+ ok=False,
961
+ skill_id=None,
962
+ skill_doc=None,
963
+ run_log=str(log_path),
964
+ errors=[
965
+ f"completion criterion for {item_id} is immutable; create a new work item with --supersedes {item_id}"
966
+ ],
967
+ )
968
+ if supersedes:
969
+ return SkillRunResult(ok=False, skill_id=None, skill_doc=None, run_log=str(log_path), errors=[f"--supersedes is only valid when creating a new work item, not updating {item_id}"])
934
970
  existing["status"] = status
935
971
  existing["role"] = role
936
- if criterion:
937
- existing["criterion"] = criterion
938
972
  if reason:
939
973
  existing["reason"] = reason
940
974
  if item_text:
@@ -942,7 +976,9 @@ def update_work_item(args) -> SkillRunResult:
942
976
  else:
943
977
  if not item_text:
944
978
  return SkillRunResult(ok=False, skill_id=None, skill_doc=None, run_log=str(log_path), errors=["new work item requires --text"])
945
- items.append({"item_id": item_id, "status": status, "role": role, "criterion": criterion, "reason": reason, "text": item_text})
979
+ if supersedes and not any(item["item_id"] == supersedes for item in items):
980
+ return SkillRunResult(ok=False, skill_id=None, skill_doc=None, run_log=str(log_path), errors=[f"superseded work item not found: {supersedes}"])
981
+ items.append({"item_id": item_id, "status": status, "role": role, "criterion": criterion, "reason": reason, "supersedes": supersedes, "text": item_text})
946
982
 
947
983
  text = _replace_concrete_work_items_section(text, items)
948
984
  text = _append_phase_event(text, phase=f"workitem:{item_id}", status=status, role=role, note=item_text or None)
@@ -1469,6 +1505,10 @@ def _progression_record_errors(
1469
1505
  if not work_items:
1470
1506
  errors.append("at least one concrete work item is required before closure")
1471
1507
  for item in work_items:
1508
+ if item.get("supersedes") and not any(previous["item_id"] == item["supersedes"] for previous in work_items):
1509
+ errors.append(f"work item {item['item_id']} supersedes missing work item {item['supersedes']}")
1510
+ if item.get("supersedes") == item["item_id"]:
1511
+ errors.append(f"work item {item['item_id']} cannot supersede itself")
1472
1512
  if not item.get("criterion"):
1473
1513
  if item["status"] in {"unknown", "blocked", "escalated"} and item.get("reason"):
1474
1514
  pass
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: xrefkit
3
- Version: 0.4.2
3
+ Version: 0.4.4
4
4
  Summary: Portable XID, Skill, Knowledge, workflow, and MCP runtime for XRefKit
5
5
  Author: synthaicode
6
6
  License: MIT License
@@ -126,7 +126,36 @@ reason instead of inventing a criterion.
126
126
 
127
127
  ## Quick Start
128
128
 
129
- Install the package and initialize an instance:
129
+ ### Install from PyPI
130
+
131
+ XRefKit requires Python 3.11 or later. For a normal installation, create a
132
+ virtual environment and install the published package from PyPI:
133
+
134
+ ```powershell
135
+ python -m venv .venv
136
+ .\.venv\Scripts\Activate.ps1
137
+ python -m pip install --upgrade pip
138
+ python -m pip install xrefkit
139
+ xrefkit init
140
+ xrefkit --help
141
+ ```
142
+
143
+ If the `xrefkit` command is not available on `PATH`, use the module form:
144
+
145
+ ```powershell
146
+ python -m xrefkit --help
147
+ ```
148
+
149
+ To use the integrated MCP server, install the optional MCP dependencies:
150
+
151
+ ```powershell
152
+ python -m pip install "xrefkit[mcp]"
153
+ xrefkit mcp serve --repo . --transport stdio
154
+ ```
155
+
156
+ ### Install from a checkout
157
+
158
+ For XRefKit development, install the local checkout in editable mode instead:
130
159
 
131
160
  ```powershell
132
161
  python -m pip install -e .
@@ -140,6 +169,25 @@ Start the integrated MCP server over stdio:
140
169
  xrefkit mcp serve --repo . --transport stdio
141
170
  ```
142
171
 
172
+ To import existing Skills and prepare a reviewable VS Code MCP setup, run:
173
+
174
+ ```powershell
175
+ python -m pip install "xrefkit[mcp]"
176
+ xrefkit mcp setup `
177
+ --repo C:\dev\itsm\XRefKit `
178
+ --import C:\work\existing-skills
179
+ ```
180
+
181
+ The command writes `SETUP.md`, `import-report.json`, a VS Code
182
+ `.vscode/mcp.json` example, and reviewed append text for `AGENTS.md` and
183
+ `CLAUDE.md` into a temporary setup folder. Apply those artifacts after review:
184
+
185
+ ```powershell
186
+ xrefkit mcp setup apply `
187
+ --source C:\Users\<user>\AppData\Local\Temp\xrefkit-setup-<id> `
188
+ --repo C:\dev\itsm\XRefKit
189
+ ```
190
+
143
191
  The server writes structured correlation events to
144
192
  `work/mcp/xid_audit.jsonl` by default. After `xrefkit skill run` returns a
145
193
  `run_id`, the client calls MCP `bind_skill_run` and executes the returned
@@ -21,6 +21,7 @@ tests/test_gate.py
21
21
  tests/test_goal_desired_state.py
22
22
  tests/test_instruction_workflow.py
23
23
  tests/test_knowledge_relations_validator.py
24
+ tests/test_mcp_setup.py
24
25
  tests/test_ownership.py
25
26
  tests/test_packmeta.py
26
27
  tests/test_project_quality_baseline.py
@@ -84,6 +85,7 @@ xrefkit/mcp/ownership.py
84
85
  xrefkit/mcp/repository.py
85
86
  xrefkit/mcp/schemas.py
86
87
  xrefkit/mcp/server.py
88
+ xrefkit/mcp/setup.py
87
89
  xrefkit/mcp/startup_contract_pack.py
88
90
  xrefkit/models/__init__.py
89
91
  xrefkit/models/common.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