xrefkit 0.4.1__tar.gz → 0.4.2__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 (106) hide show
  1. {xrefkit-0.4.1 → xrefkit-0.4.2}/PKG-INFO +4 -1
  2. {xrefkit-0.4.1 → xrefkit-0.4.2}/README.md +3 -0
  3. {xrefkit-0.4.1 → xrefkit-0.4.2}/pyproject.toml +1 -1
  4. {xrefkit-0.4.1 → xrefkit-0.4.2}/tests/test_cli.py +22 -10
  5. {xrefkit-0.4.1 → xrefkit-0.4.2}/tests/test_dashboard.py +2 -0
  6. {xrefkit-0.4.1 → xrefkit-0.4.2}/tests/test_instruction_workflow.py +52 -0
  7. {xrefkit-0.4.1 → xrefkit-0.4.2}/tests/test_skill_runtime_audit.py +4 -1
  8. {xrefkit-0.4.1 → xrefkit-0.4.2}/xrefkit/__init__.py +1 -1
  9. {xrefkit-0.4.1 → xrefkit-0.4.2}/xrefkit/operations_cli.py +2 -0
  10. {xrefkit-0.4.1 → xrefkit-0.4.2}/xrefkit/skillrun.py +45 -5
  11. {xrefkit-0.4.1 → xrefkit-0.4.2}/xrefkit.egg-info/PKG-INFO +4 -1
  12. {xrefkit-0.4.1 → xrefkit-0.4.2}/LICENSE +0 -0
  13. {xrefkit-0.4.1 → xrefkit-0.4.2}/setup.cfg +0 -0
  14. {xrefkit-0.4.1 → xrefkit-0.4.2}/tests/test_base_sync_ownership.py +0 -0
  15. {xrefkit-0.4.1 → xrefkit-0.4.2}/tests/test_boundary_analysis.py +0 -0
  16. {xrefkit-0.4.1 → xrefkit-0.4.2}/tests/test_calibration_lint.py +0 -0
  17. {xrefkit-0.4.1 → xrefkit-0.4.2}/tests/test_check_skill_knowledge_xids.py +0 -0
  18. {xrefkit-0.4.1 → xrefkit-0.4.2}/tests/test_collect_analyzer_sarif.py +0 -0
  19. {xrefkit-0.4.1 → xrefkit-0.4.2}/tests/test_convert_to_xrefkit_skill.py +0 -0
  20. {xrefkit-0.4.1 → xrefkit-0.4.2}/tests/test_cs_scope_probe.py +0 -0
  21. {xrefkit-0.4.1 → xrefkit-0.4.2}/tests/test_csharp_commonality.py +0 -0
  22. {xrefkit-0.4.1 → xrefkit-0.4.2}/tests/test_csharp_naming_profile.py +0 -0
  23. {xrefkit-0.4.1 → xrefkit-0.4.2}/tests/test_ctx.py +0 -0
  24. {xrefkit-0.4.1 → xrefkit-0.4.2}/tests/test_cutover_readiness.py +0 -0
  25. {xrefkit-0.4.1 → xrefkit-0.4.2}/tests/test_error_policy_audit.py +0 -0
  26. {xrefkit-0.4.1 → xrefkit-0.4.2}/tests/test_error_policy_locator.py +0 -0
  27. {xrefkit-0.4.1 → xrefkit-0.4.2}/tests/test_fm_multiroot.py +0 -0
  28. {xrefkit-0.4.1 → xrefkit-0.4.2}/tests/test_gate.py +0 -0
  29. {xrefkit-0.4.1 → xrefkit-0.4.2}/tests/test_goal_desired_state.py +0 -0
  30. {xrefkit-0.4.1 → xrefkit-0.4.2}/tests/test_knowledge_relations_validator.py +0 -0
  31. {xrefkit-0.4.1 → xrefkit-0.4.2}/tests/test_ownership.py +0 -0
  32. {xrefkit-0.4.1 → xrefkit-0.4.2}/tests/test_packmeta.py +0 -0
  33. {xrefkit-0.4.1 → xrefkit-0.4.2}/tests/test_project_quality_baseline.py +0 -0
  34. {xrefkit-0.4.1 → xrefkit-0.4.2}/tests/test_resource_provider.py +0 -0
  35. {xrefkit-0.4.1 → xrefkit-0.4.2}/tests/test_runtime_contracts.py +0 -0
  36. {xrefkit-0.4.1 → xrefkit-0.4.2}/tests/test_sarif_to_locator.py +0 -0
  37. {xrefkit-0.4.1 → xrefkit-0.4.2}/tests/test_skillmeta.py +0 -0
  38. {xrefkit-0.4.1 → xrefkit-0.4.2}/tests/test_structure_catalog.py +0 -0
  39. {xrefkit-0.4.1 → xrefkit-0.4.2}/tests/test_xref.py +0 -0
  40. {xrefkit-0.4.1 → xrefkit-0.4.2}/tests/test_xrefkit_instance.py +0 -0
  41. {xrefkit-0.4.1 → xrefkit-0.4.2}/tests/test_xrefkit_tools.py +0 -0
  42. {xrefkit-0.4.1 → xrefkit-0.4.2}/tests/test_xrefkit_v2_discovery.py +0 -0
  43. {xrefkit-0.4.1 → xrefkit-0.4.2}/tests/test_xrefkit_v2_models.py +0 -0
  44. {xrefkit-0.4.1 → xrefkit-0.4.2}/tests/test_xrefkit_v2_pipeline.py +0 -0
  45. {xrefkit-0.4.1 → xrefkit-0.4.2}/xrefkit/__main__.py +0 -0
  46. {xrefkit-0.4.1 → xrefkit-0.4.2}/xrefkit/boundary_analysis.py +0 -0
  47. {xrefkit-0.4.1 → xrefkit-0.4.2}/xrefkit/catalog_cli.py +0 -0
  48. {xrefkit-0.4.1 → xrefkit-0.4.2}/xrefkit/cli.py +0 -0
  49. {xrefkit-0.4.1 → xrefkit-0.4.2}/xrefkit/contracts.py +0 -0
  50. {xrefkit-0.4.1 → xrefkit-0.4.2}/xrefkit/ctx.py +0 -0
  51. {xrefkit-0.4.1 → xrefkit-0.4.2}/xrefkit/dashboard.py +0 -0
  52. {xrefkit-0.4.1 → xrefkit-0.4.2}/xrefkit/discovery.py +0 -0
  53. {xrefkit-0.4.1 → xrefkit-0.4.2}/xrefkit/gate.py +0 -0
  54. {xrefkit-0.4.1 → xrefkit-0.4.2}/xrefkit/goalstate.py +0 -0
  55. {xrefkit-0.4.1 → xrefkit-0.4.2}/xrefkit/hashing.py +0 -0
  56. {xrefkit-0.4.1 → xrefkit-0.4.2}/xrefkit/import_skill.py +0 -0
  57. {xrefkit-0.4.1 → xrefkit-0.4.2}/xrefkit/instance.py +0 -0
  58. {xrefkit-0.4.1 → xrefkit-0.4.2}/xrefkit/loaders.py +0 -0
  59. {xrefkit-0.4.1 → xrefkit-0.4.2}/xrefkit/mcp/__init__.py +0 -0
  60. {xrefkit-0.4.1 → xrefkit-0.4.2}/xrefkit/mcp/audit.py +0 -0
  61. {xrefkit-0.4.1 → xrefkit-0.4.2}/xrefkit/mcp/bootstrap.py +0 -0
  62. {xrefkit-0.4.1 → xrefkit-0.4.2}/xrefkit/mcp/catalog.py +0 -0
  63. {xrefkit-0.4.1 → xrefkit-0.4.2}/xrefkit/mcp/cli.py +0 -0
  64. {xrefkit-0.4.1 → xrefkit-0.4.2}/xrefkit/mcp/client_cache.py +0 -0
  65. {xrefkit-0.4.1 → xrefkit-0.4.2}/xrefkit/mcp/context_registry.py +0 -0
  66. {xrefkit-0.4.1 → xrefkit-0.4.2}/xrefkit/mcp/contracts.py +0 -0
  67. {xrefkit-0.4.1 → xrefkit-0.4.2}/xrefkit/mcp/dist.py +0 -0
  68. {xrefkit-0.4.1 → xrefkit-0.4.2}/xrefkit/mcp/ownership.py +0 -0
  69. {xrefkit-0.4.1 → xrefkit-0.4.2}/xrefkit/mcp/repository.py +0 -0
  70. {xrefkit-0.4.1 → xrefkit-0.4.2}/xrefkit/mcp/schemas.py +0 -0
  71. {xrefkit-0.4.1 → xrefkit-0.4.2}/xrefkit/mcp/server.py +0 -0
  72. {xrefkit-0.4.1 → xrefkit-0.4.2}/xrefkit/mcp/startup_contract_pack.py +0 -0
  73. {xrefkit-0.4.1 → xrefkit-0.4.2}/xrefkit/mcp_tools.py +0 -0
  74. {xrefkit-0.4.1 → xrefkit-0.4.2}/xrefkit/models/__init__.py +0 -0
  75. {xrefkit-0.4.1 → xrefkit-0.4.2}/xrefkit/models/common.py +0 -0
  76. {xrefkit-0.4.1 → xrefkit-0.4.2}/xrefkit/models/effective_bundle.py +0 -0
  77. {xrefkit-0.4.1 → xrefkit-0.4.2}/xrefkit/models/local_manifest.py +0 -0
  78. {xrefkit-0.4.1 → xrefkit-0.4.2}/xrefkit/models/package_manifest.py +0 -0
  79. {xrefkit-0.4.1 → xrefkit-0.4.2}/xrefkit/models/run_log.py +0 -0
  80. {xrefkit-0.4.1 → xrefkit-0.4.2}/xrefkit/models/server_config.py +0 -0
  81. {xrefkit-0.4.1 → xrefkit-0.4.2}/xrefkit/models/skill_definition.py +0 -0
  82. {xrefkit-0.4.1 → xrefkit-0.4.2}/xrefkit/ownership.py +0 -0
  83. {xrefkit-0.4.1 → xrefkit-0.4.2}/xrefkit/packmeta.py +0 -0
  84. {xrefkit-0.4.1 → xrefkit-0.4.2}/xrefkit/registry.py +0 -0
  85. {xrefkit-0.4.1 → xrefkit-0.4.2}/xrefkit/resolver.py +0 -0
  86. {xrefkit-0.4.1 → xrefkit-0.4.2}/xrefkit/resource_provider.py +0 -0
  87. {xrefkit-0.4.1 → xrefkit-0.4.2}/xrefkit/resources/base/contracts.json +0 -0
  88. {xrefkit-0.4.1 → xrefkit-0.4.2}/xrefkit/resources/base/current.json +0 -0
  89. {xrefkit-0.4.1 → xrefkit-0.4.2}/xrefkit/resources/base/generations/7a682a5272907354/contracts.json +0 -0
  90. {xrefkit-0.4.1 → xrefkit-0.4.2}/xrefkit/resources/base/generations/7a682a5272907354/model_body.md +0 -0
  91. {xrefkit-0.4.1 → xrefkit-0.4.2}/xrefkit/resources/base/generations/9929294385ccb7b0/contracts.json +0 -0
  92. {xrefkit-0.4.1 → xrefkit-0.4.2}/xrefkit/resources/base/generations/9929294385ccb7b0/model_body.md +0 -0
  93. {xrefkit-0.4.1 → xrefkit-0.4.2}/xrefkit/resources/base/model_body.md +0 -0
  94. {xrefkit-0.4.1 → xrefkit-0.4.2}/xrefkit/runlog.py +0 -0
  95. {xrefkit-0.4.1 → xrefkit-0.4.2}/xrefkit/skillmeta.py +0 -0
  96. {xrefkit-0.4.1 → xrefkit-0.4.2}/xrefkit/structure_catalog.py +0 -0
  97. {xrefkit-0.4.1 → xrefkit-0.4.2}/xrefkit/tools/__init__.py +0 -0
  98. {xrefkit-0.4.1 → xrefkit-0.4.2}/xrefkit/tools/__main__.py +0 -0
  99. {xrefkit-0.4.1 → xrefkit-0.4.2}/xrefkit/v2_cli.py +0 -0
  100. {xrefkit-0.4.1 → xrefkit-0.4.2}/xrefkit/workspace.py +0 -0
  101. {xrefkit-0.4.1 → xrefkit-0.4.2}/xrefkit/xref.py +0 -0
  102. {xrefkit-0.4.1 → xrefkit-0.4.2}/xrefkit.egg-info/SOURCES.txt +0 -0
  103. {xrefkit-0.4.1 → xrefkit-0.4.2}/xrefkit.egg-info/dependency_links.txt +0 -0
  104. {xrefkit-0.4.1 → xrefkit-0.4.2}/xrefkit.egg-info/entry_points.txt +0 -0
  105. {xrefkit-0.4.1 → xrefkit-0.4.2}/xrefkit.egg-info/requires.txt +0 -0
  106. {xrefkit-0.4.1 → xrefkit-0.4.2}/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.1
3
+ Version: 0.4.2
4
4
  Summary: Portable XID, Skill, Knowledge, workflow, and MCP runtime for XRefKit
5
5
  Author: synthaicode
6
6
  License: MIT License
@@ -120,6 +120,9 @@ xrefkit workflow run --task "Perform the requested procedure" `
120
120
 
121
121
  `verify` and `close` determine procedural completion only. Output quality is
122
122
  recorded separately after human acceptance with the existing feedback record.
123
+ Each work item also requires its own completion criterion; if that criterion is
124
+ not yet definable, record the item as unknown, blocked, or escalated with a
125
+ reason instead of inventing a criterion.
123
126
 
124
127
  ## Quick Start
125
128
 
@@ -81,6 +81,9 @@ xrefkit workflow run --task "Perform the requested procedure" `
81
81
 
82
82
  `verify` and `close` determine procedural completion only. Output quality is
83
83
  recorded separately after human acceptance with the existing feedback record.
84
+ Each work item also requires its own completion criterion; if that criterion is
85
+ not yet definable, record the item as unknown, blocked, or escalated with a
86
+ reason instead of inventing a criterion.
84
87
 
85
88
  ## Quick Start
86
89
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "xrefkit"
7
- version = "0.4.1"
7
+ version = "0.4.2"
8
8
  description = "Portable XID, Skill, Knowledge, workflow, and MCP runtime for XRefKit"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -82,6 +82,8 @@ class CliTests(unittest.TestCase):
82
82
  "WI-001",
83
83
  "--text",
84
84
  "Implement controlled output",
85
+ "--completion-criterion",
86
+ "output is written and validated",
85
87
  "--status",
86
88
  "done",
87
89
  "--role",
@@ -739,6 +741,8 @@ class CliTests(unittest.TestCase):
739
741
  "WI-001",
740
742
  "--text",
741
743
  "Implement controlled output",
744
+ "--completion-criterion",
745
+ "output is written and validated",
742
746
  "--status",
743
747
  "in_progress",
744
748
  "--role",
@@ -765,6 +769,8 @@ class CliTests(unittest.TestCase):
765
769
  "WI-001",
766
770
  "--status",
767
771
  "done",
772
+ "--completion-criterion",
773
+ "output is written and validated",
768
774
  "--role",
769
775
  "sample_skill:executor",
770
776
  ]
@@ -772,7 +778,7 @@ class CliTests(unittest.TestCase):
772
778
  )
773
779
 
774
780
  text = out.read_text(encoding="utf-8")
775
- self.assertIn("- [x] WI-001 status=`done` role=`sample_skill:executor`: Implement controlled output", text)
781
+ self.assertIn("WI-001 status=`done` role=`sample_skill:executor` criterion=`output is written and validated`", text)
776
782
 
777
783
  def test_main_skill_close_rejects_pending_concrete_work_item(self) -> None:
778
784
  with tempfile.TemporaryDirectory() as tmp:
@@ -808,9 +814,11 @@ class CliTests(unittest.TestCase):
808
814
  str(out),
809
815
  "--item",
810
816
  "WI-001",
811
- "--text",
812
- "Implement controlled output",
813
- "--status",
817
+ "--text",
818
+ "Implement controlled output",
819
+ "--completion-criterion",
820
+ "output is written and validated",
821
+ "--status",
814
822
  "pending",
815
823
  "--role",
816
824
  "sample_skill:executor",
@@ -1065,9 +1073,11 @@ class CliTests(unittest.TestCase):
1065
1073
  str(out),
1066
1074
  "--item",
1067
1075
  "WI-001",
1068
- "--text",
1069
- "Implement controlled output",
1070
- "--status",
1076
+ "--text",
1077
+ "Implement controlled output",
1078
+ "--completion-criterion",
1079
+ "output is written and validated",
1080
+ "--status",
1071
1081
  "done",
1072
1082
  "--role",
1073
1083
  "sample_skill:executor",
@@ -1375,9 +1385,11 @@ class CliTests(unittest.TestCase):
1375
1385
  str(out),
1376
1386
  "--item",
1377
1387
  "WI-001",
1378
- "--text",
1379
- "Implement controlled output",
1380
- "--status",
1388
+ "--text",
1389
+ "Implement controlled output",
1390
+ "--completion-criterion",
1391
+ "output is written and validated",
1392
+ "--status",
1381
1393
  "done",
1382
1394
  "--role",
1383
1395
  "sample_skill:executor",
@@ -118,6 +118,8 @@ class DashboardTests(unittest.TestCase):
118
118
  "WI-001",
119
119
  "--text",
120
120
  "Implement controlled output",
121
+ "--completion-criterion",
122
+ "output is written and validated",
121
123
  "--status",
122
124
  "done",
123
125
  "--role",
@@ -48,6 +48,8 @@ def test_instruction_workflow_uses_default_conditions_and_shared_protocol(tmp_pa
48
48
  "WI-001",
49
49
  "--text",
50
50
  "Perform the instruction",
51
+ "--completion-criterion",
52
+ "instruction result is recorded and verified",
51
53
  "--status",
52
54
  "done",
53
55
  "--role",
@@ -98,3 +100,53 @@ def test_instruction_workflow_uses_default_conditions_and_shared_protocol(tmp_pa
98
100
  ) == 0
99
101
  assert _run(tmp_path, "skill", "close", "--log", str(out)) == 0
100
102
  assert "## Completion Conditions" in out.read_text(encoding="utf-8")
103
+
104
+
105
+ def test_workitem_requires_criterion_or_explicit_unknown_reason(tmp_path: Path) -> None:
106
+ out = tmp_path / "work" / "sessions" / "run.md"
107
+ assert _run(
108
+ tmp_path,
109
+ "workflow",
110
+ "run",
111
+ "--task",
112
+ "Do work",
113
+ "--out",
114
+ str(out),
115
+ "--use-default-completion-conditions",
116
+ ) == 0
117
+ assert _run(
118
+ tmp_path,
119
+ "skill",
120
+ "workitem",
121
+ "--log",
122
+ str(out),
123
+ "--item",
124
+ "WI-001",
125
+ "--text",
126
+ "Investigate missing requirement",
127
+ "--status",
128
+ "pending",
129
+ "--role",
130
+ "instruction:executor",
131
+ ) == 1
132
+ assert _run(
133
+ tmp_path,
134
+ "skill",
135
+ "workitem",
136
+ "--log",
137
+ str(out),
138
+ "--item",
139
+ "WI-001",
140
+ "--text",
141
+ "Investigate missing requirement",
142
+ "--status",
143
+ "unknown",
144
+ "--criterion-unknown-reason",
145
+ "The business owner has not defined the acceptance outcome",
146
+ "--role",
147
+ "instruction:executor",
148
+ ) == 0
149
+ text = out.read_text(encoding="utf-8")
150
+ assert "criterion=`` reason=`The business owner has not defined the acceptance outcome`" in text
151
+ assert _run(tmp_path, "skill", "phase", "--log", str(out), "--phase", "execution", "--status", "done", "--role", "instruction:executor") == 0
152
+ assert _run(tmp_path, "skill", "verify", "--log", str(out)) == 1
@@ -84,6 +84,8 @@ class SkillRuntimeAuditTests(unittest.TestCase):
84
84
  "WI-001",
85
85
  "--text",
86
86
  "Implement controlled output",
87
+ "--completion-criterion",
88
+ "output is written and validated",
87
89
  "--status",
88
90
  "done",
89
91
  "--role",
@@ -322,6 +324,7 @@ class SkillRuntimeAuditTests(unittest.TestCase):
322
324
  [
323
325
  "skill", "workitem", "--log", str(out), "--item", "WI-001",
324
326
  "--text", "Implement controlled output", "--status", "done",
327
+ "--completion-criterion", "output is written and validated",
325
328
  "--role", "sample_skill:executor",
326
329
  ]
327
330
  ),
@@ -393,7 +396,7 @@ class SkillRuntimeAuditTests(unittest.TestCase):
393
396
  main(
394
397
  [
395
398
  "skill", "workitem", "--log", str(out), "--item", "WI-001",
396
- "--text", "do work", "--status", "done", "--role", "sample_skill:executor",
399
+ "--text", "do work", "--status", "done", "--completion-criterion", "work is verified", "--role", "sample_skill:executor",
397
400
  ]
398
401
  ),
399
402
  )
@@ -2,4 +2,4 @@
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "0.4.1"
5
+ __version__ = "0.4.2"
@@ -497,6 +497,8 @@ def _build_parser() -> argparse.ArgumentParser:
497
497
  p_skill_workitem.add_argument("--log", required=True, help="Skill run log to update")
498
498
  p_skill_workitem.add_argument("--item", required=True, help="Stable work item id, such as WI-001")
499
499
  p_skill_workitem.add_argument("--text", default=None, help="Work item text; required when adding a new item")
500
+ p_skill_workitem.add_argument("--completion-criterion", default=None, help="Observable procedural condition for this work item")
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")
500
502
  p_skill_workitem.add_argument(
501
503
  "--status",
502
504
  required=True,
@@ -179,6 +179,11 @@ 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>[^`]*)`: (?P<text>.*)$"
185
+ )
186
+ LEGACY_WORKITEM_RE = re.compile(
182
187
  r"^- \[(?P<checkbox>[ x!])\] (?P<item_id>[A-Za-z0-9_.-]+) "
183
188
  r"status=`(?P<status>[^`]+)` role=`(?P<role>[^`]+)`: (?P<text>.*)$"
184
189
  )
@@ -825,6 +830,19 @@ def _parse_work_items(text: str) -> list[dict[str, str]]:
825
830
  items: list[dict[str, str]] = []
826
831
  for line in body.splitlines():
827
832
  match = WORKITEM_RE.match(line)
833
+ if match:
834
+ items.append(
835
+ {
836
+ "item_id": match.group("item_id"),
837
+ "status": match.group("status"),
838
+ "role": match.group("role"),
839
+ "criterion": match.group("criterion"),
840
+ "reason": match.group("reason"),
841
+ "text": match.group("text"),
842
+ }
843
+ )
844
+ continue
845
+ match = LEGACY_WORKITEM_RE.match(line)
828
846
  if not match:
829
847
  continue
830
848
  items.append(
@@ -832,14 +850,19 @@ def _parse_work_items(text: str) -> list[dict[str, str]]:
832
850
  "item_id": match.group("item_id"),
833
851
  "status": match.group("status"),
834
852
  "role": match.group("role"),
853
+ "criterion": "",
854
+ "reason": "legacy work item has no recorded completion criterion",
835
855
  "text": match.group("text"),
836
856
  }
837
857
  )
838
858
  return items
839
859
 
840
860
 
841
- def _render_workitem_line(*, item_id: str, status: str, role: str, text: str) -> str:
842
- return f"- [{_workitem_checkbox(status)}] {item_id} status=`{status}` role=`{role}`: {text}"
861
+ def _render_workitem_line(*, item_id: str, status: str, role: str, criterion: str, reason: str, text: str) -> str:
862
+ return (
863
+ f"- [{_workitem_checkbox(status)}] {item_id} status=`{status}` role=`{role}` "
864
+ f"criterion=`{criterion}` reason=`{reason}`: {text}"
865
+ )
843
866
 
844
867
 
845
868
  def _overall_workitem_status(items: list[dict[str, str]]) -> str:
@@ -860,7 +883,7 @@ def _replace_concrete_work_items_section(text: str, items: list[dict[str, str]])
860
883
  insert_at = text.find("\n## Execution Role")
861
884
  if insert_at == -1:
862
885
  insert_at = len(text)
863
- section = "\n\n## Concrete Work Items\n\n- status: `pending`\n- rule: task-specific work items must be added with `xrefkit skill workitem` and closed as `done` or `escalated`\n"
886
+ section = "\n\n## Concrete Work Items\n\n- status: `pending`\n- rule: each work item requires a completion criterion; use unknown, blocked, or escalated with a reason when the criterion cannot yet be defined\n"
864
887
  text = text[:insert_at] + section + text[insert_at:]
865
888
  body, start, end = _section_body(text, "Concrete Work Items")
866
889
  if body is None:
@@ -871,7 +894,7 @@ def _replace_concrete_work_items_section(text: str, items: list[dict[str, str]])
871
894
  "## Concrete Work Items",
872
895
  "",
873
896
  f"- status: `{status}`",
874
- "- rule: task-specific work items must be added with `xrefkit skill workitem` and closed as `done` or `escalated`",
897
+ "- rule: each work item requires a completion criterion; use unknown, blocked, or escalated with a reason when the criterion cannot yet be defined",
875
898
  ]
876
899
  lines.extend(_render_workitem_line(**item) for item in items)
877
900
  new_body = "\n".join(lines) + "\n"
@@ -888,12 +911,18 @@ def update_work_item(args) -> SkillRunResult:
888
911
  status = str(args.status).lower()
889
912
  role = str(args.role).strip()
890
913
  item_text = str(args.text or "").strip()
914
+ criterion = str(getattr(args, "completion_criterion", None) or "").strip().replace("`", "'").replace("\n", " ")
915
+ reason = str(getattr(args, "criterion_unknown_reason", None) or "").strip().replace("`", "'").replace("\n", " ")
891
916
  if not item_id:
892
917
  return SkillRunResult(ok=False, skill_id=None, skill_doc=None, run_log=str(log_path), errors=["missing --item"])
893
918
  if status not in VALID_WORKITEM_STATUSES:
894
919
  return SkillRunResult(ok=False, skill_id=None, skill_doc=None, run_log=str(log_path), errors=[f"invalid work item status: {status}"])
895
920
  if not role:
896
921
  return SkillRunResult(ok=False, skill_id=None, skill_doc=None, run_log=str(log_path), errors=["missing --role"])
922
+ if not criterion and not reason and status in {"unknown", "blocked", "escalated"}:
923
+ return SkillRunResult(ok=False, skill_id=None, skill_doc=None, run_log=str(log_path), errors=["completion criterion is undefined; provide --criterion-unknown-reason for unknown, blocked, or escalated work items"])
924
+ if not criterion and status in {"pending", "in_progress", "done"}:
925
+ return SkillRunResult(ok=False, skill_id=None, skill_doc=None, run_log=str(log_path), errors=["--completion-criterion is required for pending, in_progress, and done work items"])
897
926
 
898
927
  text = log_path.read_text(encoding="utf-8")
899
928
  if not _has_opened_run_gate(text):
@@ -904,12 +933,16 @@ def update_work_item(args) -> SkillRunResult:
904
933
  if existing:
905
934
  existing["status"] = status
906
935
  existing["role"] = role
936
+ if criterion:
937
+ existing["criterion"] = criterion
938
+ if reason:
939
+ existing["reason"] = reason
907
940
  if item_text:
908
941
  existing["text"] = item_text
909
942
  else:
910
943
  if not item_text:
911
944
  return SkillRunResult(ok=False, skill_id=None, skill_doc=None, run_log=str(log_path), errors=["new work item requires --text"])
912
- items.append({"item_id": item_id, "status": status, "role": role, "text": item_text})
945
+ items.append({"item_id": item_id, "status": status, "role": role, "criterion": criterion, "reason": reason, "text": item_text})
913
946
 
914
947
  text = _replace_concrete_work_items_section(text, items)
915
948
  text = _append_phase_event(text, phase=f"workitem:{item_id}", status=status, role=role, note=item_text or None)
@@ -1436,6 +1469,13 @@ def _progression_record_errors(
1436
1469
  if not work_items:
1437
1470
  errors.append("at least one concrete work item is required before closure")
1438
1471
  for item in work_items:
1472
+ if not item.get("criterion"):
1473
+ if item["status"] in {"unknown", "blocked", "escalated"} and item.get("reason"):
1474
+ pass
1475
+ else:
1476
+ errors.append(f"work item {item['item_id']} must record a completion criterion or a reason why it cannot be defined")
1477
+ if item.get("criterion") == "unknown" and item["status"] in {"pending", "in_progress", "done"}:
1478
+ errors.append(f"work item {item['item_id']} cannot use unknown as its completion criterion while executable")
1439
1479
  if item["status"] not in ACCEPTED_CLOSE_STATUSES:
1440
1480
  errors.append(
1441
1481
  f"work item {item['item_id']} must be done or escalated before closure; current={item['status']}"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: xrefkit
3
- Version: 0.4.1
3
+ Version: 0.4.2
4
4
  Summary: Portable XID, Skill, Knowledge, workflow, and MCP runtime for XRefKit
5
5
  Author: synthaicode
6
6
  License: MIT License
@@ -120,6 +120,9 @@ xrefkit workflow run --task "Perform the requested procedure" `
120
120
 
121
121
  `verify` and `close` determine procedural completion only. Output quality is
122
122
  recorded separately after human acceptance with the existing feedback record.
123
+ Each work item also requires its own completion criterion; if that criterion is
124
+ not yet definable, record the item as unknown, blocked, or escalated with a
125
+ reason instead of inventing a criterion.
123
126
 
124
127
  ## Quick Start
125
128
 
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