@christang/keel 5.1.2 → 5.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +114 -158
- package/README.zh-CN.md +118 -197
- package/assets/bootstrap/AGENTS.md +1 -1
- package/{plugins/keel/skills/keel-align-expectations/references → assets/lenses}/hardware-dsl.md +4 -2
- package/{plugins/keel/skills/keel-align-expectations/references → assets/lenses}/hardware.md +4 -2
- package/{plugins/keel/skills/keel-align-expectations/references → assets/lenses}/web.md +4 -2
- package/assets/openspec/schemas/keel-spec-driven/schema.yaml +11 -5
- package/assets/openspec/schemas/keel-spec-driven/templates/design.md +1 -1
- package/assets/openspec/schemas/keel-spec-driven/templates/tasks.md +13 -4
- package/bin/keel.js +221 -18
- package/package.json +1 -1
- package/plugins/keel/.claude-plugin/plugin.json +1 -1
- package/plugins/keel/.codex-plugin/plugin.json +1 -1
- package/plugins/keel/skills/keel-align-expectations/SKILL.md +2 -6
- package/plugins/keel/skills/keel-debug-failure/SKILL.md +2 -2
- package/plugins/keel/skills/keel-review-checklist/SKILL.md +2 -2
- package/plugins/keel/skills/keel-tdd-or-test-first/SKILL.md +2 -2
- package/scripts/bump_version.js +140 -0
- package/scripts/install_to_repo.py +3 -73
- package/scripts/validate_plugin.py +558 -107
- package/src/core/context.js +10 -3
- package/src/core/gates.js +40 -10
- package/src/core/task-contract.js +21 -0
|
@@ -37,8 +37,8 @@ REQUIRED_SCRIPTS = [
|
|
|
37
37
|
"scripts/validate_plugin.py",
|
|
38
38
|
]
|
|
39
39
|
|
|
40
|
-
PACKAGE_VERSION = "5.1
|
|
41
|
-
PROTOCOL_VERSION = "5.1
|
|
40
|
+
PACKAGE_VERSION = "5.2.1"
|
|
41
|
+
PROTOCOL_VERSION = "5.2.1"
|
|
42
42
|
LEGACY_MANAGED_START = "<!-- keel:start version=2.1 -->"
|
|
43
43
|
OPENSPEC_SCHEMA_NAME = "keel-spec-driven"
|
|
44
44
|
OPENSPEC_CONFIG_PATH = Path("openspec/config.yaml")
|
|
@@ -188,9 +188,9 @@ SKILL_DOC_REQUIREMENTS = [
|
|
|
188
188
|
"one material decision at a time",
|
|
189
189
|
"recommended answer",
|
|
190
190
|
"Silence does not",
|
|
191
|
-
"
|
|
192
|
-
"
|
|
193
|
-
"
|
|
191
|
+
"keel/lenses/",
|
|
192
|
+
"Applies when",
|
|
193
|
+
"keel lenses add",
|
|
194
194
|
"no separate alignment ledger",
|
|
195
195
|
],
|
|
196
196
|
},
|
|
@@ -220,31 +220,34 @@ SKILL_DOC_REQUIREMENTS = [
|
|
|
220
220
|
],
|
|
221
221
|
},
|
|
222
222
|
{
|
|
223
|
-
# Structure only ("
|
|
224
|
-
#
|
|
225
|
-
# domain-keyword scoping across these three
|
|
226
|
-
# per-domain keywords are not duplicated here.
|
|
227
|
-
"name": "
|
|
228
|
-
"path": "
|
|
223
|
+
# Structure only ("Applies when" self-describing header, "Material risk
|
|
224
|
+
# surface", "Execution and review checks"); the expectation-alignment-skill
|
|
225
|
+
# scenario owns domain-keyword scoping across these three lens templates,
|
|
226
|
+
# so the per-domain keywords are not duplicated here.
|
|
227
|
+
"name": "web lens template",
|
|
228
|
+
"path": "assets/lenses/web.md",
|
|
229
229
|
"required": [
|
|
230
|
+
"Applies when",
|
|
230
231
|
"Material risk surface",
|
|
231
|
-
"
|
|
232
|
+
"Execution and review checks",
|
|
232
233
|
],
|
|
233
234
|
},
|
|
234
235
|
{
|
|
235
|
-
"name": "
|
|
236
|
-
"path": "
|
|
236
|
+
"name": "hardware lens template",
|
|
237
|
+
"path": "assets/lenses/hardware.md",
|
|
237
238
|
"required": [
|
|
239
|
+
"Applies when",
|
|
238
240
|
"Material risk surface",
|
|
239
|
-
"
|
|
241
|
+
"Execution and review checks",
|
|
240
242
|
],
|
|
241
243
|
},
|
|
242
244
|
{
|
|
243
|
-
"name": "
|
|
244
|
-
"path": "
|
|
245
|
+
"name": "hardware-dsl lens template",
|
|
246
|
+
"path": "assets/lenses/hardware-dsl.md",
|
|
245
247
|
"required": [
|
|
248
|
+
"Applies when",
|
|
246
249
|
"Material risk surface",
|
|
247
|
-
"
|
|
250
|
+
"Execution and review checks",
|
|
248
251
|
],
|
|
249
252
|
},
|
|
250
253
|
]
|
|
@@ -394,6 +397,7 @@ def validate_npm_package(errors: list[str]) -> None:
|
|
|
394
397
|
"--doctor",
|
|
395
398
|
"capabilities",
|
|
396
399
|
"gate",
|
|
400
|
+
"lenses",
|
|
397
401
|
"--profile",
|
|
398
402
|
"--version",
|
|
399
403
|
"--help",
|
|
@@ -640,6 +644,9 @@ def validate_openspec_schema(errors: list[str]) -> None:
|
|
|
640
644
|
"provenance and license",
|
|
641
645
|
"positive and negative trigger cases",
|
|
642
646
|
"real-task evidence",
|
|
647
|
+
"one of pass, passed, complete, completed, ok, or done.",
|
|
648
|
+
"## Expectation Coverage",
|
|
649
|
+
"carry a durable owner",
|
|
643
650
|
):
|
|
644
651
|
if required not in schema:
|
|
645
652
|
errors.append(f"OpenSpec schema.yaml missing required language: {required}")
|
|
@@ -655,12 +662,15 @@ def validate_openspec_schema(errors: list[str]) -> None:
|
|
|
655
662
|
"Strategy:",
|
|
656
663
|
"M1:",
|
|
657
664
|
"Evidence:",
|
|
658
|
-
"Contract: pending
|
|
665
|
+
"- Contract: pending",
|
|
659
666
|
"Review:",
|
|
660
667
|
"Status: pending",
|
|
668
|
+
"Status: one of pass, passed, complete, completed, ok, done",
|
|
661
669
|
"Blocker: none",
|
|
662
670
|
"Mode: diagnose-only",
|
|
663
671
|
"Requires modifying files outside Touch.",
|
|
672
|
+
"## Expectation Coverage",
|
|
673
|
+
"Discard reason:",
|
|
664
674
|
):
|
|
665
675
|
if required not in tasks_template:
|
|
666
676
|
errors.append(
|
|
@@ -1540,7 +1550,7 @@ def validate_expectation_completion_gates_scenario() -> int:
|
|
|
1540
1550
|
"- Acceptance check:",
|
|
1541
1551
|
"- Scope check:",
|
|
1542
1552
|
"- Findings:",
|
|
1543
|
-
"
|
|
1553
|
+
"Discard rationale:",
|
|
1544
1554
|
]
|
|
1545
1555
|
handoff_skill_snippets = [
|
|
1546
1556
|
"keel-handoff/v1",
|
|
@@ -1606,13 +1616,13 @@ def validate_authoring_continuity_scenario() -> int:
|
|
|
1606
1616
|
"keel-align-expectations",
|
|
1607
1617
|
"risk-triggered deep path",
|
|
1608
1618
|
"hidden-knowledge risk",
|
|
1609
|
-
"domain
|
|
1619
|
+
"domain lenses",
|
|
1610
1620
|
"Missing authority returns to OpenSpec authoring",
|
|
1611
1621
|
]
|
|
1612
1622
|
schema_snippets = [
|
|
1613
1623
|
"risk-triggered deep",
|
|
1614
1624
|
"hidden-knowledge assumptions",
|
|
1615
|
-
"domain
|
|
1625
|
+
"domain lens",
|
|
1616
1626
|
]
|
|
1617
1627
|
design_template_snippets = [
|
|
1618
1628
|
"Hidden Knowledge / Assumptions",
|
|
@@ -1621,7 +1631,7 @@ def validate_authoring_continuity_scenario() -> int:
|
|
|
1621
1631
|
]
|
|
1622
1632
|
task_template_snippets = [
|
|
1623
1633
|
"hidden-knowledge assumption",
|
|
1624
|
-
"domain
|
|
1634
|
+
"domain lens",
|
|
1625
1635
|
]
|
|
1626
1636
|
|
|
1627
1637
|
checks: list[tuple[str, Path, list[str]]] = [
|
|
@@ -1740,24 +1750,23 @@ def validate_authoring_continuity_scenario() -> int:
|
|
|
1740
1750
|
return 0
|
|
1741
1751
|
|
|
1742
1752
|
|
|
1743
|
-
def
|
|
1753
|
+
def validate_domain_lenses_scenario() -> int:
|
|
1744
1754
|
plugin_skills_root = ROOT / PLUGIN_ROOT / "skills"
|
|
1745
|
-
|
|
1746
|
-
"
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
if not (plugin_skills_root / expected).is_file():
|
|
1755
|
+
if not (plugin_skills_root / "keel-align-expectations/SKILL.md").is_file():
|
|
1756
|
+
report("domain-lenses scenario plugin misses the alignment skill")
|
|
1757
|
+
return 1
|
|
1758
|
+
lenses_root = ROOT / "assets/lenses"
|
|
1759
|
+
for template in ("web.md", "hardware.md", "hardware-dsl.md"):
|
|
1760
|
+
if not (lenses_root / template).is_file():
|
|
1752
1761
|
report(
|
|
1753
|
-
"domain-
|
|
1754
|
-
f"
|
|
1762
|
+
"domain-lenses scenario misses the shipped lens template: "
|
|
1763
|
+
f"{template}"
|
|
1755
1764
|
)
|
|
1756
1765
|
return 1
|
|
1757
1766
|
for legacy_skill in LEGACY_PROFILE_SKILLS:
|
|
1758
1767
|
if (plugin_skills_root / legacy_skill).exists():
|
|
1759
1768
|
report(
|
|
1760
|
-
f"domain-
|
|
1769
|
+
f"domain-lenses scenario plugin packages a legacy profile: {legacy_skill}"
|
|
1761
1770
|
)
|
|
1762
1771
|
return 1
|
|
1763
1772
|
|
|
@@ -1767,20 +1776,20 @@ def validate_domain_profiles_scenario() -> int:
|
|
|
1767
1776
|
repo.mkdir()
|
|
1768
1777
|
install = run_keel(repo, "--install", "--target", "codex")
|
|
1769
1778
|
if install.returncode != 0:
|
|
1770
|
-
report("domain-
|
|
1779
|
+
report("domain-lenses scenario default install failed:")
|
|
1771
1780
|
report((install.stderr or install.stdout).strip())
|
|
1772
1781
|
return 1
|
|
1773
1782
|
if (repo / TARGET_SKILL_ROOTS["codex"]).exists():
|
|
1774
1783
|
report(
|
|
1775
|
-
"domain-
|
|
1784
|
+
"domain-lenses scenario thin install copied Keel skill trees; "
|
|
1776
1785
|
"skills are plugin-owned in v4."
|
|
1777
1786
|
)
|
|
1778
1787
|
return 1
|
|
1779
1788
|
|
|
1780
1789
|
rejected = run_keel(repo, "--install", "--target", "codex", "--profile", "web")
|
|
1781
1790
|
rejected_text = (rejected.stderr or "") + (rejected.stdout or "")
|
|
1782
|
-
if rejected.returncode == 0 or "
|
|
1783
|
-
report("domain-
|
|
1791
|
+
if rejected.returncode == 0 or "keel/lenses" not in rejected_text:
|
|
1792
|
+
report("domain-lenses scenario still accepts --profile.")
|
|
1784
1793
|
report(rejected_text.strip())
|
|
1785
1794
|
return 1
|
|
1786
1795
|
|
|
@@ -1792,7 +1801,7 @@ def validate_domain_profiles_scenario() -> int:
|
|
|
1792
1801
|
or "Keel profiles" in doctor_text
|
|
1793
1802
|
):
|
|
1794
1803
|
report(
|
|
1795
|
-
"domain-
|
|
1804
|
+
"domain-lenses scenario doctor still reports profile state or "
|
|
1796
1805
|
"misses the native plugin surface."
|
|
1797
1806
|
)
|
|
1798
1807
|
report(doctor_text.strip())
|
|
@@ -1805,11 +1814,11 @@ def validate_domain_profiles_scenario() -> int:
|
|
|
1805
1814
|
else ""
|
|
1806
1815
|
)
|
|
1807
1816
|
if uninstall.returncode != 0 or "keel:start" in agents_text:
|
|
1808
|
-
report("domain-
|
|
1817
|
+
report("domain-lenses scenario uninstall left the managed bootstrap.")
|
|
1809
1818
|
report((uninstall.stderr or uninstall.stdout).strip())
|
|
1810
1819
|
return 1
|
|
1811
1820
|
|
|
1812
|
-
report("domain-
|
|
1821
|
+
report("domain-lenses scenario passed.")
|
|
1813
1822
|
return 0
|
|
1814
1823
|
|
|
1815
1824
|
|
|
@@ -2387,6 +2396,42 @@ def validate_update_pack_install_scenario() -> int:
|
|
|
2387
2396
|
return 0
|
|
2388
2397
|
|
|
2389
2398
|
|
|
2399
|
+
def validate_update_default_registry_scenario() -> int:
|
|
2400
|
+
with tempfile.TemporaryDirectory(prefix="keel-update-default-") as raw_tmp:
|
|
2401
|
+
tmp = Path(raw_tmp)
|
|
2402
|
+
update = run_keel(tmp, "--update", "--dry-run")
|
|
2403
|
+
if update.returncode != 0:
|
|
2404
|
+
report("update-default-registry scenario keel --update --dry-run failed:")
|
|
2405
|
+
report((update.stderr or update.stdout).strip())
|
|
2406
|
+
return 1
|
|
2407
|
+
|
|
2408
|
+
pack_plan = (
|
|
2409
|
+
"would run npm pack" in update.stdout
|
|
2410
|
+
or "would run npm.cmd pack" in update.stdout
|
|
2411
|
+
)
|
|
2412
|
+
if not pack_plan:
|
|
2413
|
+
report("update-default-registry scenario did not report a pack plan.")
|
|
2414
|
+
report(update.stdout.strip())
|
|
2415
|
+
return 1
|
|
2416
|
+
if "@christang/keel" not in update.stdout:
|
|
2417
|
+
report(
|
|
2418
|
+
"update-default-registry scenario default source is not the "
|
|
2419
|
+
"published registry package @christang/keel."
|
|
2420
|
+
)
|
|
2421
|
+
report(update.stdout.strip())
|
|
2422
|
+
return 1
|
|
2423
|
+
if "github:" in update.stdout:
|
|
2424
|
+
report(
|
|
2425
|
+
"update-default-registry scenario default source is a git-type "
|
|
2426
|
+
"spec; self-update must default to the registry package."
|
|
2427
|
+
)
|
|
2428
|
+
report(update.stdout.strip())
|
|
2429
|
+
return 1
|
|
2430
|
+
|
|
2431
|
+
report("update-default-registry scenario passed.")
|
|
2432
|
+
return 0
|
|
2433
|
+
|
|
2434
|
+
|
|
2390
2435
|
def run_keel_hook(repo: Path, event: dict) -> subprocess.CompletedProcess[str]:
|
|
2391
2436
|
env = dict(os.environ)
|
|
2392
2437
|
env["KEEL_CLI"] = str(ROOT / "bin/keel.js")
|
|
@@ -2633,7 +2678,7 @@ def validate_cli_scenario() -> int:
|
|
|
2633
2678
|
if (
|
|
2634
2679
|
update.returncode != 0
|
|
2635
2680
|
or not pack_plan
|
|
2636
|
-
or "
|
|
2681
|
+
or "@christang/keel" not in update.stdout
|
|
2637
2682
|
or not install_plan
|
|
2638
2683
|
):
|
|
2639
2684
|
report("cli scenario keel --update did not report global CLI update plan.")
|
|
@@ -4508,11 +4553,23 @@ def validate_core_gates_scenario() -> int:
|
|
|
4508
4553
|
"1.1",
|
|
4509
4554
|
"--json",
|
|
4510
4555
|
)
|
|
4556
|
+
handoff_payload = json.loads(handoff_owner.stdout)
|
|
4557
|
+
finding_owner_message = " ".join(
|
|
4558
|
+
problem.get("message", "")
|
|
4559
|
+
for problem in handoff_payload.get("problems", [])
|
|
4560
|
+
if problem.get("code") == "finding-owner"
|
|
4561
|
+
)
|
|
4511
4562
|
if (
|
|
4512
4563
|
handoff_owner.returncode != 3
|
|
4513
|
-
or
|
|
4564
|
+
or handoff_payload.get("status") != "fail"
|
|
4565
|
+
or "Discard reason" not in finding_owner_message
|
|
4566
|
+
or "keel/archive" not in finding_owner_message
|
|
4567
|
+
or "openspec/changes" not in finding_owner_message
|
|
4514
4568
|
):
|
|
4515
|
-
report(
|
|
4569
|
+
report(
|
|
4570
|
+
"core-gates scenario accepted HANDOFF as finding owner or the "
|
|
4571
|
+
"finding-owner error did not enumerate the accepted forms."
|
|
4572
|
+
)
|
|
4516
4573
|
report((handoff_owner.stderr or handoff_owner.stdout).strip())
|
|
4517
4574
|
return 1
|
|
4518
4575
|
|
|
@@ -4543,6 +4600,54 @@ def validate_core_gates_scenario() -> int:
|
|
|
4543
4600
|
report((owned_finding.stderr or owned_finding.stdout).strip())
|
|
4544
4601
|
return 1
|
|
4545
4602
|
|
|
4603
|
+
write_text(completion_tasks, completion_task("passed", "done"))
|
|
4604
|
+
done_status = run_keel(
|
|
4605
|
+
completion_repo,
|
|
4606
|
+
"gate",
|
|
4607
|
+
"task-complete",
|
|
4608
|
+
"--change",
|
|
4609
|
+
"demo",
|
|
4610
|
+
"--task",
|
|
4611
|
+
"1.1",
|
|
4612
|
+
"--json",
|
|
4613
|
+
)
|
|
4614
|
+
if (
|
|
4615
|
+
done_status.returncode != 0
|
|
4616
|
+
or json.loads(done_status.stdout).get("status") != "pass"
|
|
4617
|
+
):
|
|
4618
|
+
report("core-gates scenario rejected an accepted `done` Review Status.")
|
|
4619
|
+
report((done_status.stderr or done_status.stdout).strip())
|
|
4620
|
+
return 1
|
|
4621
|
+
|
|
4622
|
+
write_text(completion_tasks, completion_task("passed", "reviewed"))
|
|
4623
|
+
bad_status = run_keel(
|
|
4624
|
+
completion_repo,
|
|
4625
|
+
"gate",
|
|
4626
|
+
"task-complete",
|
|
4627
|
+
"--change",
|
|
4628
|
+
"demo",
|
|
4629
|
+
"--task",
|
|
4630
|
+
"1.1",
|
|
4631
|
+
"--json",
|
|
4632
|
+
)
|
|
4633
|
+
bad_status_payload = json.loads(bad_status.stdout)
|
|
4634
|
+
bad_status_message = " ".join(
|
|
4635
|
+
problem.get("message", "")
|
|
4636
|
+
for problem in bad_status_payload.get("problems", [])
|
|
4637
|
+
)
|
|
4638
|
+
if (
|
|
4639
|
+
bad_status.returncode != 4
|
|
4640
|
+
or bad_status_payload.get("status") != "needs-review"
|
|
4641
|
+
or "Status" not in bad_status_message
|
|
4642
|
+
or "done" not in bad_status_message
|
|
4643
|
+
):
|
|
4644
|
+
report(
|
|
4645
|
+
"core-gates scenario semantic-review did not name Status and list "
|
|
4646
|
+
"accepted tokens including done."
|
|
4647
|
+
)
|
|
4648
|
+
report((bad_status.stderr or bad_status.stdout).strip())
|
|
4649
|
+
return 1
|
|
4650
|
+
|
|
4546
4651
|
write_text(completion_tasks, completion_task("passed", "pass"))
|
|
4547
4652
|
write_text(completion_repo / "src/outside.js", "outside\n")
|
|
4548
4653
|
subprocess.run(
|
|
@@ -5115,6 +5220,107 @@ def validate_core_gates_scenario() -> int:
|
|
|
5115
5220
|
return 0
|
|
5116
5221
|
|
|
5117
5222
|
|
|
5223
|
+
def validate_scope_rename_attribution_scenario() -> int:
|
|
5224
|
+
rename_task = (
|
|
5225
|
+
"# Tasks\n\n"
|
|
5226
|
+
"- [ ] 1.1 Complete behavior\n"
|
|
5227
|
+
" - Owner: keel-agent\n"
|
|
5228
|
+
" - Mode: implementation\n"
|
|
5229
|
+
" - Covers:\n"
|
|
5230
|
+
" - E1: public behavior\n"
|
|
5231
|
+
" - Read:\n"
|
|
5232
|
+
" - README.md\n"
|
|
5233
|
+
" - Touch:\n"
|
|
5234
|
+
" - src/renamed-from.js\n"
|
|
5235
|
+
" - src/renamed-to.js\n"
|
|
5236
|
+
" - openspec/changes/demo/tasks.md\n"
|
|
5237
|
+
" - Commands:\n"
|
|
5238
|
+
" - M1: node test.js\n"
|
|
5239
|
+
" - Acceptance:\n"
|
|
5240
|
+
" - Public behavior passes.\n"
|
|
5241
|
+
" - Autonomy boundary:\n"
|
|
5242
|
+
" - Default: hard-stop\n"
|
|
5243
|
+
" - Pre-authorized fallback: none\n"
|
|
5244
|
+
" - Coupling: none\n"
|
|
5245
|
+
" - Candidate Boundary:\n"
|
|
5246
|
+
" - One complete candidate reaches M1.\n"
|
|
5247
|
+
" - Stop Rules:\n"
|
|
5248
|
+
" - Stop on final assertion failure.\n"
|
|
5249
|
+
" - Evidence:\n"
|
|
5250
|
+
" - M1: passed\n"
|
|
5251
|
+
" - Review:\n"
|
|
5252
|
+
" - Status: pass\n"
|
|
5253
|
+
" - Acceptance check: public behavior reviewed\n"
|
|
5254
|
+
" - Scope check: Touch reviewed semantically\n"
|
|
5255
|
+
" - Findings: none\n"
|
|
5256
|
+
" - Blocker: none\n"
|
|
5257
|
+
" - Stop if:\n"
|
|
5258
|
+
" - Requires files outside Touch.\n"
|
|
5259
|
+
" - Report:\n"
|
|
5260
|
+
" - Summary\n"
|
|
5261
|
+
)
|
|
5262
|
+
with tempfile.TemporaryDirectory(prefix="keel-scope-rename-") as raw_tmp:
|
|
5263
|
+
repo = Path(raw_tmp)
|
|
5264
|
+
write_text(repo / "openspec/changes/demo/tasks.md", rename_task)
|
|
5265
|
+
write_text(repo / "src/renamed-from.js", "module.exports = 1;\n")
|
|
5266
|
+
write_text(repo / "README.md", "readme\n")
|
|
5267
|
+
for args in (
|
|
5268
|
+
["init", "--quiet"],
|
|
5269
|
+
["config", "user.email", "keel@example.invalid"],
|
|
5270
|
+
["config", "user.name", "Keel Fixture"],
|
|
5271
|
+
["add", "."],
|
|
5272
|
+
["commit", "--quiet", "-m", "baseline"],
|
|
5273
|
+
):
|
|
5274
|
+
result = subprocess.run(
|
|
5275
|
+
["git", *args], cwd=repo, capture_output=True, text=True
|
|
5276
|
+
)
|
|
5277
|
+
if result.returncode != 0:
|
|
5278
|
+
report("scope-rename scenario git setup failed:")
|
|
5279
|
+
report((result.stderr or result.stdout).strip())
|
|
5280
|
+
return 1
|
|
5281
|
+
# A staged rename whose old and new paths are both in Touch must not be a
|
|
5282
|
+
# false outside-Touch failure (git reports it as one `old -> new` entry).
|
|
5283
|
+
moved = subprocess.run(
|
|
5284
|
+
["git", "mv", "src/renamed-from.js", "src/renamed-to.js"],
|
|
5285
|
+
cwd=repo,
|
|
5286
|
+
capture_output=True,
|
|
5287
|
+
text=True,
|
|
5288
|
+
)
|
|
5289
|
+
if moved.returncode != 0:
|
|
5290
|
+
report("scope-rename scenario git mv failed:")
|
|
5291
|
+
report((moved.stderr or moved.stdout).strip())
|
|
5292
|
+
return 1
|
|
5293
|
+
completed = run_keel(
|
|
5294
|
+
repo,
|
|
5295
|
+
"gate",
|
|
5296
|
+
"task-complete",
|
|
5297
|
+
"--change",
|
|
5298
|
+
"demo",
|
|
5299
|
+
"--task",
|
|
5300
|
+
"1.1",
|
|
5301
|
+
"--base",
|
|
5302
|
+
"HEAD",
|
|
5303
|
+
"--json",
|
|
5304
|
+
)
|
|
5305
|
+
payload = json.loads(completed.stdout or "{}")
|
|
5306
|
+
outside = " ".join(
|
|
5307
|
+
problem.get("message", "") for problem in payload.get("problems", [])
|
|
5308
|
+
)
|
|
5309
|
+
if (
|
|
5310
|
+
completed.returncode != 0
|
|
5311
|
+
or payload.get("status") != "pass"
|
|
5312
|
+
or "outside" in outside.lower()
|
|
5313
|
+
):
|
|
5314
|
+
report(
|
|
5315
|
+
"scope-rename scenario reported a false outside-touch failure for a "
|
|
5316
|
+
"git mv rename whose old and new paths are both in Touch."
|
|
5317
|
+
)
|
|
5318
|
+
report((completed.stderr or completed.stdout).strip())
|
|
5319
|
+
return 1
|
|
5320
|
+
report("scope-rename scenario passed.")
|
|
5321
|
+
return 0
|
|
5322
|
+
|
|
5323
|
+
|
|
5118
5324
|
def validate_target_capability_adapters_scenario() -> int:
|
|
5119
5325
|
capability_keys = (
|
|
5120
5326
|
"continuity.start",
|
|
@@ -5397,27 +5603,24 @@ def validate_expectation_alignment_skill_scenario() -> int:
|
|
|
5397
5603
|
)
|
|
5398
5604
|
return 1
|
|
5399
5605
|
|
|
5400
|
-
# Structure and behavior:
|
|
5401
|
-
#
|
|
5606
|
+
# Structure and behavior: the skill routes to user-authored keel/lenses/,
|
|
5607
|
+
# the shipped lens templates exist, and each template stays scoped to its
|
|
5608
|
+
# own domain (cross-file keyword pairs).
|
|
5609
|
+
if "keel/lenses/" not in skill:
|
|
5610
|
+
report("expectation-alignment-skill does not route to keel/lenses/")
|
|
5611
|
+
return 1
|
|
5402
5612
|
for domain in ("web", "hardware", "hardware-dsl"):
|
|
5403
|
-
|
|
5404
|
-
|
|
5405
|
-
|
|
5406
|
-
|
|
5407
|
-
|
|
5408
|
-
|
|
5409
|
-
|
|
5410
|
-
|
|
5411
|
-
|
|
5412
|
-
|
|
5413
|
-
|
|
5414
|
-
).read_text(encoding="utf-8")
|
|
5415
|
-
hardware_reference = (
|
|
5416
|
-
ROOT / "src/skills/keel-align-expectations/references/hardware.md"
|
|
5417
|
-
).read_text(encoding="utf-8")
|
|
5418
|
-
dsl_reference = (
|
|
5419
|
-
ROOT / "src/skills/keel-align-expectations/references/hardware-dsl.md"
|
|
5420
|
-
).read_text(encoding="utf-8")
|
|
5613
|
+
template_path = ROOT / "assets/lenses" / f"{domain}.md"
|
|
5614
|
+
if not template_path.is_file():
|
|
5615
|
+
report(f"expectation-alignment-skill lens template is missing: {domain}.md")
|
|
5616
|
+
return 1
|
|
5617
|
+
web_reference = (ROOT / "assets/lenses/web.md").read_text(encoding="utf-8")
|
|
5618
|
+
hardware_reference = (ROOT / "assets/lenses/hardware.md").read_text(
|
|
5619
|
+
encoding="utf-8"
|
|
5620
|
+
)
|
|
5621
|
+
dsl_reference = (ROOT / "assets/lenses/hardware-dsl.md").read_text(
|
|
5622
|
+
encoding="utf-8"
|
|
5623
|
+
)
|
|
5421
5624
|
for content, expected, unexpected in (
|
|
5422
5625
|
(web_reference, "accessibility", "valid-ready"),
|
|
5423
5626
|
(hardware_reference, "valid-ready", "browser"),
|
|
@@ -5425,7 +5628,7 @@ def validate_expectation_alignment_skill_scenario() -> int:
|
|
|
5425
5628
|
):
|
|
5426
5629
|
if expected not in content or unexpected in content:
|
|
5427
5630
|
report(
|
|
5428
|
-
"expectation-alignment-skill
|
|
5631
|
+
"expectation-alignment-skill lens templates are not scoped to "
|
|
5429
5632
|
"their own domain."
|
|
5430
5633
|
)
|
|
5431
5634
|
return 1
|
|
@@ -5563,11 +5766,10 @@ def validate_native_plugin_manifests_scenario() -> int:
|
|
|
5563
5766
|
f"source: {skill_name}"
|
|
5564
5767
|
)
|
|
5565
5768
|
return 1
|
|
5566
|
-
|
|
5567
|
-
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
report(f"native-plugin-manifests plugin misses reference: {reference}")
|
|
5769
|
+
lenses_root = ROOT / "assets/lenses"
|
|
5770
|
+
for template in ("web.md", "hardware.md", "hardware-dsl.md"):
|
|
5771
|
+
if not (lenses_root / template).is_file():
|
|
5772
|
+
report(f"native-plugin-manifests misses lens template: {template}")
|
|
5571
5773
|
return 1
|
|
5572
5774
|
|
|
5573
5775
|
codex_market_path = ROOT / ".agents/plugins/marketplace.json"
|
|
@@ -6180,7 +6382,7 @@ def validate_compact_task_authoring_scenario() -> int:
|
|
|
6180
6382
|
"- Strategy: <strategy>",
|
|
6181
6383
|
"- M1:",
|
|
6182
6384
|
"- Evidence:",
|
|
6183
|
-
"- Contract: pending
|
|
6385
|
+
"- Contract: pending",
|
|
6184
6386
|
"- Review:",
|
|
6185
6387
|
"- Blocker: none",
|
|
6186
6388
|
"- Mode: diagnose-only",
|
|
@@ -9299,42 +9501,51 @@ def validate_domain_execution_references_scenario() -> int:
|
|
|
9299
9501
|
"keel-debug-failure",
|
|
9300
9502
|
"keel-review-checklist",
|
|
9301
9503
|
)
|
|
9302
|
-
|
|
9504
|
+
templates = ("web.md", "hardware.md", "hardware-dsl.md")
|
|
9303
9505
|
roots = (ROOT / "src/skills", ROOT / PLUGIN_ROOT / "skills")
|
|
9304
9506
|
|
|
9305
9507
|
for root in roots:
|
|
9306
9508
|
for skill in consuming_skills:
|
|
9307
9509
|
text = (root / skill / "SKILL.md").read_text(encoding="utf-8")
|
|
9308
9510
|
for needle in (
|
|
9309
|
-
"keel
|
|
9511
|
+
"keel/lenses/",
|
|
9310
9512
|
"Execution and review checks",
|
|
9311
|
-
"only
|
|
9312
|
-
"no
|
|
9513
|
+
"load only that one",
|
|
9514
|
+
"no lens matches",
|
|
9313
9515
|
):
|
|
9314
9516
|
if needle not in text:
|
|
9315
9517
|
report(
|
|
9316
9518
|
f"domain-execution-references: {root.name}/{skill} lacks "
|
|
9317
|
-
f"the on-demand consult step ({needle!r})."
|
|
9519
|
+
f"the on-demand lens consult step ({needle!r})."
|
|
9318
9520
|
)
|
|
9319
9521
|
return 1
|
|
9320
|
-
|
|
9321
|
-
|
|
9322
|
-
|
|
9522
|
+
|
|
9523
|
+
lenses_root = ROOT / "assets/lenses"
|
|
9524
|
+
for template in templates:
|
|
9525
|
+
ref_path = lenses_root / template
|
|
9526
|
+
if not ref_path.is_file():
|
|
9527
|
+
report(f"domain-execution-references: lens template is missing: {template}")
|
|
9528
|
+
return 1
|
|
9529
|
+
text = ref_path.read_text(encoding="utf-8")
|
|
9530
|
+
if "## Execution and review checks" not in text:
|
|
9531
|
+
report(
|
|
9532
|
+
f"domain-execution-references: {template} lacks the "
|
|
9533
|
+
"execution and review section."
|
|
9323
9534
|
)
|
|
9324
|
-
|
|
9325
|
-
|
|
9326
|
-
|
|
9327
|
-
|
|
9328
|
-
|
|
9329
|
-
|
|
9330
|
-
|
|
9331
|
-
|
|
9332
|
-
|
|
9333
|
-
|
|
9334
|
-
|
|
9335
|
-
|
|
9336
|
-
|
|
9337
|
-
|
|
9535
|
+
return 1
|
|
9536
|
+
if "Applies when" not in text:
|
|
9537
|
+
report(
|
|
9538
|
+
f"domain-execution-references: {template} lacks the "
|
|
9539
|
+
"self-describing 'Applies when' header."
|
|
9540
|
+
)
|
|
9541
|
+
return 1
|
|
9542
|
+
size = ref_path.stat().st_size
|
|
9543
|
+
if size > 3072:
|
|
9544
|
+
report(
|
|
9545
|
+
f"domain-execution-references: {template} exceeds the 3072-"
|
|
9546
|
+
f"byte budget ({size} bytes); move depth to dedicated skills."
|
|
9547
|
+
)
|
|
9548
|
+
return 1
|
|
9338
9549
|
|
|
9339
9550
|
for skill in consuming_skills:
|
|
9340
9551
|
src_text = (roots[0] / skill / "SKILL.md").read_bytes()
|
|
@@ -9345,19 +9556,6 @@ def validate_domain_execution_references_scenario() -> int:
|
|
|
9345
9556
|
"from the canonical source."
|
|
9346
9557
|
)
|
|
9347
9558
|
return 1
|
|
9348
|
-
for reference in references:
|
|
9349
|
-
src_ref = (
|
|
9350
|
-
roots[0] / "keel-align-expectations" / "references" / reference
|
|
9351
|
-
).read_bytes()
|
|
9352
|
-
shipped_ref = (
|
|
9353
|
-
roots[1] / "keel-align-expectations" / "references" / reference
|
|
9354
|
-
).read_bytes()
|
|
9355
|
-
if src_ref != shipped_ref:
|
|
9356
|
-
report(
|
|
9357
|
-
f"domain-execution-references: shipped {reference} copy diverges "
|
|
9358
|
-
"from the canonical source."
|
|
9359
|
-
)
|
|
9360
|
-
return 1
|
|
9361
9559
|
|
|
9362
9560
|
readme = (ROOT / "README.zh-CN.md").read_text(encoding="utf-8")
|
|
9363
9561
|
if "Execution and review checks" not in readme:
|
|
@@ -9377,6 +9575,129 @@ def validate_domain_execution_references_scenario() -> int:
|
|
|
9377
9575
|
return 0
|
|
9378
9576
|
|
|
9379
9577
|
|
|
9578
|
+
def validate_domain_lens_scaffold_scenario() -> int:
|
|
9579
|
+
shipped = {"web", "hardware", "hardware-dsl"}
|
|
9580
|
+
lenses_root = ROOT / "assets/lenses"
|
|
9581
|
+
for name in shipped:
|
|
9582
|
+
if not (lenses_root / f"{name}.md").is_file():
|
|
9583
|
+
report(f"domain-lens-scaffold: shipped template is missing: {name}")
|
|
9584
|
+
return 1
|
|
9585
|
+
|
|
9586
|
+
with tempfile.TemporaryDirectory(prefix="keel-lenses-") as raw_tmp:
|
|
9587
|
+
repo = Path(raw_tmp) / "repo"
|
|
9588
|
+
repo.mkdir()
|
|
9589
|
+
|
|
9590
|
+
listing = run_keel(repo, "lenses", "list")
|
|
9591
|
+
if listing.returncode != 0:
|
|
9592
|
+
report("domain-lens-scaffold: keel lenses list failed.")
|
|
9593
|
+
report((listing.stderr or listing.stdout).strip())
|
|
9594
|
+
return 1
|
|
9595
|
+
for name in shipped:
|
|
9596
|
+
if name not in listing.stdout:
|
|
9597
|
+
report(
|
|
9598
|
+
f"domain-lens-scaffold: keel lenses list omitted the "
|
|
9599
|
+
f"{name} template."
|
|
9600
|
+
)
|
|
9601
|
+
report(listing.stdout.strip())
|
|
9602
|
+
return 1
|
|
9603
|
+
if "keel/lenses/" not in listing.stdout:
|
|
9604
|
+
report(
|
|
9605
|
+
"domain-lens-scaffold: keel lenses list did not name the "
|
|
9606
|
+
"keel/lenses/ install location."
|
|
9607
|
+
)
|
|
9608
|
+
return 1
|
|
9609
|
+
|
|
9610
|
+
add = run_keel(repo, "lenses", "add", "web")
|
|
9611
|
+
installed = repo / "keel/lenses/web.md"
|
|
9612
|
+
if add.returncode != 0 or not installed.is_file():
|
|
9613
|
+
report("domain-lens-scaffold: keel lenses add web did not scaffold.")
|
|
9614
|
+
report((add.stderr or add.stdout).strip())
|
|
9615
|
+
return 1
|
|
9616
|
+
if "Applies when:" not in installed.read_text(encoding="utf-8"):
|
|
9617
|
+
report(
|
|
9618
|
+
"domain-lens-scaffold: scaffolded lens lost its 'Applies when:' "
|
|
9619
|
+
"header."
|
|
9620
|
+
)
|
|
9621
|
+
return 1
|
|
9622
|
+
|
|
9623
|
+
clobber = run_keel(repo, "lenses", "add", "web")
|
|
9624
|
+
if clobber.returncode == 0:
|
|
9625
|
+
report(
|
|
9626
|
+
"domain-lens-scaffold: a second keel lenses add web must refuse "
|
|
9627
|
+
"without --force."
|
|
9628
|
+
)
|
|
9629
|
+
return 1
|
|
9630
|
+
|
|
9631
|
+
forced = run_keel(repo, "lenses", "add", "web", "--force")
|
|
9632
|
+
if forced.returncode != 0:
|
|
9633
|
+
report("domain-lens-scaffold: keel lenses add web --force failed.")
|
|
9634
|
+
report((forced.stderr or forced.stdout).strip())
|
|
9635
|
+
return 1
|
|
9636
|
+
|
|
9637
|
+
after_add = run_keel(repo, "lenses", "list")
|
|
9638
|
+
if "web (installed)" not in after_add.stdout:
|
|
9639
|
+
report(
|
|
9640
|
+
"domain-lens-scaffold: keel lenses list did not mark web as "
|
|
9641
|
+
"installed after add."
|
|
9642
|
+
)
|
|
9643
|
+
report(after_add.stdout.strip())
|
|
9644
|
+
return 1
|
|
9645
|
+
|
|
9646
|
+
report("domain-lens-scaffold scenario passed.")
|
|
9647
|
+
return 0
|
|
9648
|
+
|
|
9649
|
+
|
|
9650
|
+
def validate_domain_lens_doctor_scenario() -> int:
|
|
9651
|
+
with tempfile.TemporaryDirectory(
|
|
9652
|
+
prefix="keel-lens-doctor-", ignore_cleanup_errors=True
|
|
9653
|
+
) as raw_tmp:
|
|
9654
|
+
repo = Path(raw_tmp) / "repo"
|
|
9655
|
+
repo.mkdir()
|
|
9656
|
+
|
|
9657
|
+
doctor = run_keel(repo, "--doctor")
|
|
9658
|
+
out = doctor.stdout
|
|
9659
|
+
if "Domain lens surface:" not in out:
|
|
9660
|
+
report("domain-lens-doctor: --doctor lacks the Domain lens surface section.")
|
|
9661
|
+
report((doctor.stderr or out).strip())
|
|
9662
|
+
return 1
|
|
9663
|
+
for name in ("web", "hardware", "hardware-dsl"):
|
|
9664
|
+
if name not in out:
|
|
9665
|
+
report(f"domain-lens-doctor: --doctor did not report the {name} template.")
|
|
9666
|
+
report(out.strip())
|
|
9667
|
+
return 1
|
|
9668
|
+
if "lens templates: ok" not in out:
|
|
9669
|
+
report("domain-lens-doctor: --doctor did not report the shipped templates as ok.")
|
|
9670
|
+
report(out.strip())
|
|
9671
|
+
return 1
|
|
9672
|
+
|
|
9673
|
+
legacy = repo / ".claude/skills/keel-profile-web/SKILL.md"
|
|
9674
|
+
legacy.parent.mkdir(parents=True)
|
|
9675
|
+
legacy.write_text("legacy profile bytes\n", encoding="utf-8")
|
|
9676
|
+
before = legacy.read_bytes()
|
|
9677
|
+
|
|
9678
|
+
doctor2 = run_keel(repo, "--doctor")
|
|
9679
|
+
out2 = doctor2.stdout
|
|
9680
|
+
if "legacy profiles: migrate" not in out2 or "keel-profile-web" not in out2:
|
|
9681
|
+
report(
|
|
9682
|
+
"domain-lens-doctor: --doctor did not warn about the legacy "
|
|
9683
|
+
"keel-profile-web skill."
|
|
9684
|
+
)
|
|
9685
|
+
report(out2.strip())
|
|
9686
|
+
return 1
|
|
9687
|
+
if "not active state" not in out2:
|
|
9688
|
+
report(
|
|
9689
|
+
"domain-lens-doctor: the legacy warning must state it is not "
|
|
9690
|
+
"active state."
|
|
9691
|
+
)
|
|
9692
|
+
return 1
|
|
9693
|
+
if legacy.read_bytes() != before:
|
|
9694
|
+
report("domain-lens-doctor: --doctor modified the legacy skill bytes.")
|
|
9695
|
+
return 1
|
|
9696
|
+
|
|
9697
|
+
report("domain-lens-doctor scenario passed.")
|
|
9698
|
+
return 0
|
|
9699
|
+
|
|
9700
|
+
|
|
9380
9701
|
def validate_precompact_probe_scenario() -> int:
|
|
9381
9702
|
hooks_config = json.loads(
|
|
9382
9703
|
(ROOT / PLUGIN_ROOT / "hooks/hooks.json").read_text(encoding="utf-8")
|
|
@@ -9719,9 +10040,54 @@ def validate_validation_runner_scenario() -> int:
|
|
|
9719
10040
|
|
|
9720
10041
|
# The one ordered scenario registry: --scenario dispatch, the --all runner,
|
|
9721
10042
|
# and registration assertions all read this list and nothing else.
|
|
10043
|
+
def validate_doctor_openspec_honesty_scenario() -> int:
|
|
10044
|
+
cli = (ROOT / "bin/keel.js").read_text(encoding="utf-8")
|
|
10045
|
+
if "is not on PATH" not in cli:
|
|
10046
|
+
report(
|
|
10047
|
+
"doctor-openspec-honesty scenario: bin/keel.js is missing the "
|
|
10048
|
+
"openspec PATH-reachability warning branch."
|
|
10049
|
+
)
|
|
10050
|
+
return 1
|
|
10051
|
+
with tempfile.TemporaryDirectory(prefix="keel-doctor-openspec-") as raw:
|
|
10052
|
+
root = Path(raw)
|
|
10053
|
+
repo = root / "repo"
|
|
10054
|
+
repo.mkdir()
|
|
10055
|
+
stub_dir = root / "stub"
|
|
10056
|
+
stub_dir.mkdir()
|
|
10057
|
+
if os.name == "nt":
|
|
10058
|
+
(stub_dir / "openspec.cmd").write_text(
|
|
10059
|
+
"@echo off\necho 1.6.0\n", encoding="utf-8"
|
|
10060
|
+
)
|
|
10061
|
+
else:
|
|
10062
|
+
stub = stub_dir / "openspec"
|
|
10063
|
+
stub.write_text("#!/bin/sh\necho 1.6.0\n", encoding="utf-8")
|
|
10064
|
+
stub.chmod(0o755)
|
|
10065
|
+
env = dict(os.environ)
|
|
10066
|
+
env["PATH"] = str(stub_dir) + os.pathsep + env.get("PATH", "")
|
|
10067
|
+
on_path = run_keel(repo, "--doctor", env=env)
|
|
10068
|
+
openspec_line = next(
|
|
10069
|
+
(
|
|
10070
|
+
line
|
|
10071
|
+
for line in on_path.stdout.splitlines()
|
|
10072
|
+
if line.startswith("openspec:")
|
|
10073
|
+
),
|
|
10074
|
+
"",
|
|
10075
|
+
)
|
|
10076
|
+
if not openspec_line.startswith("openspec: ok"):
|
|
10077
|
+
report(
|
|
10078
|
+
"doctor-openspec-honesty scenario: doctor did not report `ok` "
|
|
10079
|
+
"when a bare openspec is on PATH."
|
|
10080
|
+
)
|
|
10081
|
+
report(on_path.stdout.strip())
|
|
10082
|
+
return 1
|
|
10083
|
+
report("doctor-openspec-honesty scenario passed.")
|
|
10084
|
+
return 0
|
|
10085
|
+
|
|
10086
|
+
|
|
9722
10087
|
SCENARIOS: tuple = (
|
|
9723
10088
|
("stateless-continuity", validate_stateless_continuity_scenario),
|
|
9724
10089
|
("core-gates", validate_core_gates_scenario),
|
|
10090
|
+
("scope-rename-attribution", validate_scope_rename_attribution_scenario),
|
|
9725
10091
|
("target-capability-adapters", validate_target_capability_adapters_scenario),
|
|
9726
10092
|
("native-runtime-projection", validate_native_runtime_projection_scenario),
|
|
9727
10093
|
("target-surface", validate_target_surface_scenario),
|
|
@@ -9729,13 +10095,15 @@ SCENARIOS: tuple = (
|
|
|
9729
10095
|
("expectation-slice-gates", validate_expectation_slice_gates_scenario),
|
|
9730
10096
|
("expectation-completion-gates", validate_expectation_completion_gates_scenario),
|
|
9731
10097
|
("authoring-continuity", validate_authoring_continuity_scenario),
|
|
9732
|
-
("domain-
|
|
10098
|
+
("domain-lenses", validate_domain_lenses_scenario),
|
|
9733
10099
|
("skill-portability-policy", validate_skill_portability_policy_scenario),
|
|
9734
10100
|
("version-alignment", validate_version_alignment_scenario),
|
|
9735
10101
|
("openspec-surface-overlay", validate_openspec_surface_overlay_scenario),
|
|
9736
10102
|
("uninstall", validate_uninstall_scenario),
|
|
9737
10103
|
("cli", validate_cli_scenario),
|
|
10104
|
+
("doctor-openspec-honesty", validate_doctor_openspec_honesty_scenario),
|
|
9738
10105
|
("update-pack-install", validate_update_pack_install_scenario),
|
|
10106
|
+
("update-default-registry", validate_update_default_registry_scenario),
|
|
9739
10107
|
("task-contract-core", validate_task_contract_core_scenario),
|
|
9740
10108
|
("task-capsule", validate_task_capsule_scenario),
|
|
9741
10109
|
("task-verification-strategies", validate_task_verification_strategies_scenario),
|
|
@@ -9768,6 +10136,8 @@ SCENARIOS: tuple = (
|
|
|
9768
10136
|
("plugin-compaction-continuity", validate_plugin_compaction_continuity_scenario),
|
|
9769
10137
|
("precompact-probe", validate_precompact_probe_scenario),
|
|
9770
10138
|
("domain-execution-references", validate_domain_execution_references_scenario),
|
|
10139
|
+
("domain-lens-scaffold", validate_domain_lens_scaffold_scenario),
|
|
10140
|
+
("domain-lens-doctor", validate_domain_lens_doctor_scenario),
|
|
9771
10141
|
("plan-funnel-guidance", validate_plan_funnel_guidance_scenario),
|
|
9772
10142
|
("native-tasks-view", validate_native_tasks_view_scenario),
|
|
9773
10143
|
("validation-runner", validate_validation_runner_scenario),
|
|
@@ -9800,6 +10170,83 @@ def run_scenario_processes(names: list, jobs: int) -> list:
|
|
|
9800
10170
|
return [(name, *results[name]) for name in names]
|
|
9801
10171
|
|
|
9802
10172
|
|
|
10173
|
+
def validate_review_status_single_source(errors: list[str]) -> None:
|
|
10174
|
+
contract = (ROOT / "src/core/task-contract.js").read_text(encoding="utf-8")
|
|
10175
|
+
if "ACCEPTED_REVIEW_STATUSES" not in contract:
|
|
10176
|
+
errors.append(
|
|
10177
|
+
"src/core/task-contract.js must define the shared "
|
|
10178
|
+
"ACCEPTED_REVIEW_STATUSES constant."
|
|
10179
|
+
)
|
|
10180
|
+
if '"done"' not in contract:
|
|
10181
|
+
errors.append(
|
|
10182
|
+
"ACCEPTED_REVIEW_STATUSES must include the `done` Review Status."
|
|
10183
|
+
)
|
|
10184
|
+
literal = "pass|passed|complete|completed|ok"
|
|
10185
|
+
for relative in ("src/core/gates.js", "src/core/context.js"):
|
|
10186
|
+
content = (ROOT / relative).read_text(encoding="utf-8")
|
|
10187
|
+
if "ACCEPTED_REVIEW_STATUSES" not in content:
|
|
10188
|
+
errors.append(
|
|
10189
|
+
f"{relative} must consume the shared ACCEPTED_REVIEW_STATUSES "
|
|
10190
|
+
"constant rather than an inline Review Status vocabulary."
|
|
10191
|
+
)
|
|
10192
|
+
if literal in content:
|
|
10193
|
+
errors.append(
|
|
10194
|
+
f"{relative} still hard-codes the Review Status vocabulary "
|
|
10195
|
+
f"`{literal}`; consume the shared constant instead."
|
|
10196
|
+
)
|
|
10197
|
+
|
|
10198
|
+
|
|
10199
|
+
def validate_install_honesty(errors: list[str]) -> None:
|
|
10200
|
+
installer = (ROOT / "scripts/install_to_repo.py").read_text(encoding="utf-8")
|
|
10201
|
+
if "def skill_actions" in installer or "dist_asset(target_name, \"skills\")" in installer:
|
|
10202
|
+
errors.append(
|
|
10203
|
+
"install_to_repo.py must not carry the dead keel-* skill_actions "
|
|
10204
|
+
"path; keel-* skills are plugin-delivered."
|
|
10205
|
+
)
|
|
10206
|
+
cli = (ROOT / "bin/keel.js").read_text(encoding="utf-8")
|
|
10207
|
+
if "delivered by the installed Keel plugin" not in cli:
|
|
10208
|
+
errors.append(
|
|
10209
|
+
"keel --help/--doctor must state that keel-* skills are delivered "
|
|
10210
|
+
"by the installed Keel plugin, not installed by the CLI."
|
|
10211
|
+
)
|
|
10212
|
+
if ".claude/skills/keel-*." in cli:
|
|
10213
|
+
errors.append(
|
|
10214
|
+
"keel --help must not claim the CLI installs skills under "
|
|
10215
|
+
".claude/skills/keel-*."
|
|
10216
|
+
)
|
|
10217
|
+
|
|
10218
|
+
|
|
10219
|
+
def validate_openspec_invocable(errors: list[str]) -> None:
|
|
10220
|
+
cli = (ROOT / "bin/keel.js").read_text(encoding="utf-8")
|
|
10221
|
+
if 'options.action === "openspec"' not in cli:
|
|
10222
|
+
errors.append("bin/keel.js must implement the `keel openspec` passthrough.")
|
|
10223
|
+
if cli.count("Invoke OpenSpec through") < 2:
|
|
10224
|
+
errors.append(
|
|
10225
|
+
"the apply and archive overlays must direct agents to `keel openspec` "
|
|
10226
|
+
"in place of a bare openspec command."
|
|
10227
|
+
)
|
|
10228
|
+
|
|
10229
|
+
|
|
10230
|
+
def validate_archive_overlay_hygiene(errors: list[str]) -> None:
|
|
10231
|
+
cli = (ROOT / "bin/keel.js").read_text(encoding="utf-8")
|
|
10232
|
+
if "--skip-specs" not in cli:
|
|
10233
|
+
errors.append(
|
|
10234
|
+
"the archive overlay must direct archive to pass --skip-specs "
|
|
10235
|
+
"after /opsx:sync has promoted the delta."
|
|
10236
|
+
)
|
|
10237
|
+
if "drop the change's guard manifest" not in cli:
|
|
10238
|
+
errors.append(
|
|
10239
|
+
"the archive overlay must remind the agent to run keel guard clear "
|
|
10240
|
+
"after archiving."
|
|
10241
|
+
)
|
|
10242
|
+
gates = (ROOT / "src/core/gates.js").read_text(encoding="utf-8")
|
|
10243
|
+
if "clearGuard" in gates:
|
|
10244
|
+
errors.append(
|
|
10245
|
+
"gates must stay read-only: gate code must not clear the guard "
|
|
10246
|
+
"manifest (guard clear is explicit via keel guard clear)."
|
|
10247
|
+
)
|
|
10248
|
+
|
|
10249
|
+
|
|
9803
10250
|
def run_baseline() -> int:
|
|
9804
10251
|
errors: list[str] = []
|
|
9805
10252
|
validate_manifest(errors)
|
|
@@ -9808,6 +10255,10 @@ def run_baseline() -> int:
|
|
|
9808
10255
|
validate_resident_blocks(errors)
|
|
9809
10256
|
validate_templates(errors)
|
|
9810
10257
|
validate_openspec_schema(errors)
|
|
10258
|
+
validate_review_status_single_source(errors)
|
|
10259
|
+
validate_install_honesty(errors)
|
|
10260
|
+
validate_openspec_invocable(errors)
|
|
10261
|
+
validate_archive_overlay_hygiene(errors)
|
|
9811
10262
|
validate_skill_docs(errors)
|
|
9812
10263
|
validate_skill_portability(ROOT, errors)
|
|
9813
10264
|
validate_scripts_use_stdlib(errors)
|