@christang/keel 5.1.2 → 5.2.0
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/tasks.md +13 -4
- package/bin/keel.js +218 -15
- 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 +0 -70
- package/scripts/validate_plugin.py +408 -96
- package/src/core/context.js +10 -3
- package/src/core/gates.js +30 -9
- 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.
|
|
41
|
-
PROTOCOL_VERSION = "5.
|
|
40
|
+
PACKAGE_VERSION = "5.2.0"
|
|
41
|
+
PROTOCOL_VERSION = "5.2.0"
|
|
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]]] = [
|
|
@@ -1742,16 +1752,15 @@ def validate_authoring_continuity_scenario() -> int:
|
|
|
1742
1752
|
|
|
1743
1753
|
def validate_domain_profiles_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-profiles 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-profiles scenario
|
|
1754
|
-
f"
|
|
1762
|
+
"domain-profiles scenario misses the shipped lens template: "
|
|
1763
|
+
f"{template}"
|
|
1755
1764
|
)
|
|
1756
1765
|
return 1
|
|
1757
1766
|
for legacy_skill in LEGACY_PROFILE_SKILLS:
|
|
@@ -4508,11 +4517,23 @@ def validate_core_gates_scenario() -> int:
|
|
|
4508
4517
|
"1.1",
|
|
4509
4518
|
"--json",
|
|
4510
4519
|
)
|
|
4520
|
+
handoff_payload = json.loads(handoff_owner.stdout)
|
|
4521
|
+
finding_owner_message = " ".join(
|
|
4522
|
+
problem.get("message", "")
|
|
4523
|
+
for problem in handoff_payload.get("problems", [])
|
|
4524
|
+
if problem.get("code") == "finding-owner"
|
|
4525
|
+
)
|
|
4511
4526
|
if (
|
|
4512
4527
|
handoff_owner.returncode != 3
|
|
4513
|
-
or
|
|
4528
|
+
or handoff_payload.get("status") != "fail"
|
|
4529
|
+
or "Discard reason" not in finding_owner_message
|
|
4530
|
+
or "keel/archive" not in finding_owner_message
|
|
4531
|
+
or "openspec/changes" not in finding_owner_message
|
|
4514
4532
|
):
|
|
4515
|
-
report(
|
|
4533
|
+
report(
|
|
4534
|
+
"core-gates scenario accepted HANDOFF as finding owner or the "
|
|
4535
|
+
"finding-owner error did not enumerate the accepted forms."
|
|
4536
|
+
)
|
|
4516
4537
|
report((handoff_owner.stderr or handoff_owner.stdout).strip())
|
|
4517
4538
|
return 1
|
|
4518
4539
|
|
|
@@ -4543,6 +4564,54 @@ def validate_core_gates_scenario() -> int:
|
|
|
4543
4564
|
report((owned_finding.stderr or owned_finding.stdout).strip())
|
|
4544
4565
|
return 1
|
|
4545
4566
|
|
|
4567
|
+
write_text(completion_tasks, completion_task("passed", "done"))
|
|
4568
|
+
done_status = run_keel(
|
|
4569
|
+
completion_repo,
|
|
4570
|
+
"gate",
|
|
4571
|
+
"task-complete",
|
|
4572
|
+
"--change",
|
|
4573
|
+
"demo",
|
|
4574
|
+
"--task",
|
|
4575
|
+
"1.1",
|
|
4576
|
+
"--json",
|
|
4577
|
+
)
|
|
4578
|
+
if (
|
|
4579
|
+
done_status.returncode != 0
|
|
4580
|
+
or json.loads(done_status.stdout).get("status") != "pass"
|
|
4581
|
+
):
|
|
4582
|
+
report("core-gates scenario rejected an accepted `done` Review Status.")
|
|
4583
|
+
report((done_status.stderr or done_status.stdout).strip())
|
|
4584
|
+
return 1
|
|
4585
|
+
|
|
4586
|
+
write_text(completion_tasks, completion_task("passed", "reviewed"))
|
|
4587
|
+
bad_status = run_keel(
|
|
4588
|
+
completion_repo,
|
|
4589
|
+
"gate",
|
|
4590
|
+
"task-complete",
|
|
4591
|
+
"--change",
|
|
4592
|
+
"demo",
|
|
4593
|
+
"--task",
|
|
4594
|
+
"1.1",
|
|
4595
|
+
"--json",
|
|
4596
|
+
)
|
|
4597
|
+
bad_status_payload = json.loads(bad_status.stdout)
|
|
4598
|
+
bad_status_message = " ".join(
|
|
4599
|
+
problem.get("message", "")
|
|
4600
|
+
for problem in bad_status_payload.get("problems", [])
|
|
4601
|
+
)
|
|
4602
|
+
if (
|
|
4603
|
+
bad_status.returncode != 4
|
|
4604
|
+
or bad_status_payload.get("status") != "needs-review"
|
|
4605
|
+
or "Status" not in bad_status_message
|
|
4606
|
+
or "done" not in bad_status_message
|
|
4607
|
+
):
|
|
4608
|
+
report(
|
|
4609
|
+
"core-gates scenario semantic-review did not name Status and list "
|
|
4610
|
+
"accepted tokens including done."
|
|
4611
|
+
)
|
|
4612
|
+
report((bad_status.stderr or bad_status.stdout).strip())
|
|
4613
|
+
return 1
|
|
4614
|
+
|
|
4546
4615
|
write_text(completion_tasks, completion_task("passed", "pass"))
|
|
4547
4616
|
write_text(completion_repo / "src/outside.js", "outside\n")
|
|
4548
4617
|
subprocess.run(
|
|
@@ -5397,27 +5466,24 @@ def validate_expectation_alignment_skill_scenario() -> int:
|
|
|
5397
5466
|
)
|
|
5398
5467
|
return 1
|
|
5399
5468
|
|
|
5400
|
-
# Structure and behavior:
|
|
5401
|
-
#
|
|
5469
|
+
# Structure and behavior: the skill routes to user-authored keel/lenses/,
|
|
5470
|
+
# the shipped lens templates exist, and each template stays scoped to its
|
|
5471
|
+
# own domain (cross-file keyword pairs).
|
|
5472
|
+
if "keel/lenses/" not in skill:
|
|
5473
|
+
report("expectation-alignment-skill does not route to keel/lenses/")
|
|
5474
|
+
return 1
|
|
5402
5475
|
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")
|
|
5476
|
+
template_path = ROOT / "assets/lenses" / f"{domain}.md"
|
|
5477
|
+
if not template_path.is_file():
|
|
5478
|
+
report(f"expectation-alignment-skill lens template is missing: {domain}.md")
|
|
5479
|
+
return 1
|
|
5480
|
+
web_reference = (ROOT / "assets/lenses/web.md").read_text(encoding="utf-8")
|
|
5481
|
+
hardware_reference = (ROOT / "assets/lenses/hardware.md").read_text(
|
|
5482
|
+
encoding="utf-8"
|
|
5483
|
+
)
|
|
5484
|
+
dsl_reference = (ROOT / "assets/lenses/hardware-dsl.md").read_text(
|
|
5485
|
+
encoding="utf-8"
|
|
5486
|
+
)
|
|
5421
5487
|
for content, expected, unexpected in (
|
|
5422
5488
|
(web_reference, "accessibility", "valid-ready"),
|
|
5423
5489
|
(hardware_reference, "valid-ready", "browser"),
|
|
@@ -5425,7 +5491,7 @@ def validate_expectation_alignment_skill_scenario() -> int:
|
|
|
5425
5491
|
):
|
|
5426
5492
|
if expected not in content or unexpected in content:
|
|
5427
5493
|
report(
|
|
5428
|
-
"expectation-alignment-skill
|
|
5494
|
+
"expectation-alignment-skill lens templates are not scoped to "
|
|
5429
5495
|
"their own domain."
|
|
5430
5496
|
)
|
|
5431
5497
|
return 1
|
|
@@ -5563,11 +5629,10 @@ def validate_native_plugin_manifests_scenario() -> int:
|
|
|
5563
5629
|
f"source: {skill_name}"
|
|
5564
5630
|
)
|
|
5565
5631
|
return 1
|
|
5566
|
-
|
|
5567
|
-
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
report(f"native-plugin-manifests plugin misses reference: {reference}")
|
|
5632
|
+
lenses_root = ROOT / "assets/lenses"
|
|
5633
|
+
for template in ("web.md", "hardware.md", "hardware-dsl.md"):
|
|
5634
|
+
if not (lenses_root / template).is_file():
|
|
5635
|
+
report(f"native-plugin-manifests misses lens template: {template}")
|
|
5571
5636
|
return 1
|
|
5572
5637
|
|
|
5573
5638
|
codex_market_path = ROOT / ".agents/plugins/marketplace.json"
|
|
@@ -6180,7 +6245,7 @@ def validate_compact_task_authoring_scenario() -> int:
|
|
|
6180
6245
|
"- Strategy: <strategy>",
|
|
6181
6246
|
"- M1:",
|
|
6182
6247
|
"- Evidence:",
|
|
6183
|
-
"- Contract: pending
|
|
6248
|
+
"- Contract: pending",
|
|
6184
6249
|
"- Review:",
|
|
6185
6250
|
"- Blocker: none",
|
|
6186
6251
|
"- Mode: diagnose-only",
|
|
@@ -9299,42 +9364,51 @@ def validate_domain_execution_references_scenario() -> int:
|
|
|
9299
9364
|
"keel-debug-failure",
|
|
9300
9365
|
"keel-review-checklist",
|
|
9301
9366
|
)
|
|
9302
|
-
|
|
9367
|
+
templates = ("web.md", "hardware.md", "hardware-dsl.md")
|
|
9303
9368
|
roots = (ROOT / "src/skills", ROOT / PLUGIN_ROOT / "skills")
|
|
9304
9369
|
|
|
9305
9370
|
for root in roots:
|
|
9306
9371
|
for skill in consuming_skills:
|
|
9307
9372
|
text = (root / skill / "SKILL.md").read_text(encoding="utf-8")
|
|
9308
9373
|
for needle in (
|
|
9309
|
-
"keel
|
|
9374
|
+
"keel/lenses/",
|
|
9310
9375
|
"Execution and review checks",
|
|
9311
|
-
"only
|
|
9312
|
-
"no
|
|
9376
|
+
"load only that one",
|
|
9377
|
+
"no lens matches",
|
|
9313
9378
|
):
|
|
9314
9379
|
if needle not in text:
|
|
9315
9380
|
report(
|
|
9316
9381
|
f"domain-execution-references: {root.name}/{skill} lacks "
|
|
9317
|
-
f"the on-demand consult step ({needle!r})."
|
|
9382
|
+
f"the on-demand lens consult step ({needle!r})."
|
|
9318
9383
|
)
|
|
9319
9384
|
return 1
|
|
9320
|
-
|
|
9321
|
-
|
|
9322
|
-
|
|
9385
|
+
|
|
9386
|
+
lenses_root = ROOT / "assets/lenses"
|
|
9387
|
+
for template in templates:
|
|
9388
|
+
ref_path = lenses_root / template
|
|
9389
|
+
if not ref_path.is_file():
|
|
9390
|
+
report(f"domain-execution-references: lens template is missing: {template}")
|
|
9391
|
+
return 1
|
|
9392
|
+
text = ref_path.read_text(encoding="utf-8")
|
|
9393
|
+
if "## Execution and review checks" not in text:
|
|
9394
|
+
report(
|
|
9395
|
+
f"domain-execution-references: {template} lacks the "
|
|
9396
|
+
"execution and review section."
|
|
9323
9397
|
)
|
|
9324
|
-
|
|
9325
|
-
|
|
9326
|
-
|
|
9327
|
-
|
|
9328
|
-
|
|
9329
|
-
|
|
9330
|
-
|
|
9331
|
-
|
|
9332
|
-
|
|
9333
|
-
|
|
9334
|
-
|
|
9335
|
-
|
|
9336
|
-
|
|
9337
|
-
|
|
9398
|
+
return 1
|
|
9399
|
+
if "Applies when" not in text:
|
|
9400
|
+
report(
|
|
9401
|
+
f"domain-execution-references: {template} lacks the "
|
|
9402
|
+
"self-describing 'Applies when' header."
|
|
9403
|
+
)
|
|
9404
|
+
return 1
|
|
9405
|
+
size = ref_path.stat().st_size
|
|
9406
|
+
if size > 3072:
|
|
9407
|
+
report(
|
|
9408
|
+
f"domain-execution-references: {template} exceeds the 3072-"
|
|
9409
|
+
f"byte budget ({size} bytes); move depth to dedicated skills."
|
|
9410
|
+
)
|
|
9411
|
+
return 1
|
|
9338
9412
|
|
|
9339
9413
|
for skill in consuming_skills:
|
|
9340
9414
|
src_text = (roots[0] / skill / "SKILL.md").read_bytes()
|
|
@@ -9345,19 +9419,6 @@ def validate_domain_execution_references_scenario() -> int:
|
|
|
9345
9419
|
"from the canonical source."
|
|
9346
9420
|
)
|
|
9347
9421
|
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
9422
|
|
|
9362
9423
|
readme = (ROOT / "README.zh-CN.md").read_text(encoding="utf-8")
|
|
9363
9424
|
if "Execution and review checks" not in readme:
|
|
@@ -9377,6 +9438,129 @@ def validate_domain_execution_references_scenario() -> int:
|
|
|
9377
9438
|
return 0
|
|
9378
9439
|
|
|
9379
9440
|
|
|
9441
|
+
def validate_domain_lens_scaffold_scenario() -> int:
|
|
9442
|
+
shipped = {"web", "hardware", "hardware-dsl"}
|
|
9443
|
+
lenses_root = ROOT / "assets/lenses"
|
|
9444
|
+
for name in shipped:
|
|
9445
|
+
if not (lenses_root / f"{name}.md").is_file():
|
|
9446
|
+
report(f"domain-lens-scaffold: shipped template is missing: {name}")
|
|
9447
|
+
return 1
|
|
9448
|
+
|
|
9449
|
+
with tempfile.TemporaryDirectory(prefix="keel-lenses-") as raw_tmp:
|
|
9450
|
+
repo = Path(raw_tmp) / "repo"
|
|
9451
|
+
repo.mkdir()
|
|
9452
|
+
|
|
9453
|
+
listing = run_keel(repo, "lenses", "list")
|
|
9454
|
+
if listing.returncode != 0:
|
|
9455
|
+
report("domain-lens-scaffold: keel lenses list failed.")
|
|
9456
|
+
report((listing.stderr or listing.stdout).strip())
|
|
9457
|
+
return 1
|
|
9458
|
+
for name in shipped:
|
|
9459
|
+
if name not in listing.stdout:
|
|
9460
|
+
report(
|
|
9461
|
+
f"domain-lens-scaffold: keel lenses list omitted the "
|
|
9462
|
+
f"{name} template."
|
|
9463
|
+
)
|
|
9464
|
+
report(listing.stdout.strip())
|
|
9465
|
+
return 1
|
|
9466
|
+
if "keel/lenses/" not in listing.stdout:
|
|
9467
|
+
report(
|
|
9468
|
+
"domain-lens-scaffold: keel lenses list did not name the "
|
|
9469
|
+
"keel/lenses/ install location."
|
|
9470
|
+
)
|
|
9471
|
+
return 1
|
|
9472
|
+
|
|
9473
|
+
add = run_keel(repo, "lenses", "add", "web")
|
|
9474
|
+
installed = repo / "keel/lenses/web.md"
|
|
9475
|
+
if add.returncode != 0 or not installed.is_file():
|
|
9476
|
+
report("domain-lens-scaffold: keel lenses add web did not scaffold.")
|
|
9477
|
+
report((add.stderr or add.stdout).strip())
|
|
9478
|
+
return 1
|
|
9479
|
+
if "Applies when:" not in installed.read_text(encoding="utf-8"):
|
|
9480
|
+
report(
|
|
9481
|
+
"domain-lens-scaffold: scaffolded lens lost its 'Applies when:' "
|
|
9482
|
+
"header."
|
|
9483
|
+
)
|
|
9484
|
+
return 1
|
|
9485
|
+
|
|
9486
|
+
clobber = run_keel(repo, "lenses", "add", "web")
|
|
9487
|
+
if clobber.returncode == 0:
|
|
9488
|
+
report(
|
|
9489
|
+
"domain-lens-scaffold: a second keel lenses add web must refuse "
|
|
9490
|
+
"without --force."
|
|
9491
|
+
)
|
|
9492
|
+
return 1
|
|
9493
|
+
|
|
9494
|
+
forced = run_keel(repo, "lenses", "add", "web", "--force")
|
|
9495
|
+
if forced.returncode != 0:
|
|
9496
|
+
report("domain-lens-scaffold: keel lenses add web --force failed.")
|
|
9497
|
+
report((forced.stderr or forced.stdout).strip())
|
|
9498
|
+
return 1
|
|
9499
|
+
|
|
9500
|
+
after_add = run_keel(repo, "lenses", "list")
|
|
9501
|
+
if "web (installed)" not in after_add.stdout:
|
|
9502
|
+
report(
|
|
9503
|
+
"domain-lens-scaffold: keel lenses list did not mark web as "
|
|
9504
|
+
"installed after add."
|
|
9505
|
+
)
|
|
9506
|
+
report(after_add.stdout.strip())
|
|
9507
|
+
return 1
|
|
9508
|
+
|
|
9509
|
+
report("domain-lens-scaffold scenario passed.")
|
|
9510
|
+
return 0
|
|
9511
|
+
|
|
9512
|
+
|
|
9513
|
+
def validate_domain_lens_doctor_scenario() -> int:
|
|
9514
|
+
with tempfile.TemporaryDirectory(
|
|
9515
|
+
prefix="keel-lens-doctor-", ignore_cleanup_errors=True
|
|
9516
|
+
) as raw_tmp:
|
|
9517
|
+
repo = Path(raw_tmp) / "repo"
|
|
9518
|
+
repo.mkdir()
|
|
9519
|
+
|
|
9520
|
+
doctor = run_keel(repo, "--doctor")
|
|
9521
|
+
out = doctor.stdout
|
|
9522
|
+
if "Domain lens surface:" not in out:
|
|
9523
|
+
report("domain-lens-doctor: --doctor lacks the Domain lens surface section.")
|
|
9524
|
+
report((doctor.stderr or out).strip())
|
|
9525
|
+
return 1
|
|
9526
|
+
for name in ("web", "hardware", "hardware-dsl"):
|
|
9527
|
+
if name not in out:
|
|
9528
|
+
report(f"domain-lens-doctor: --doctor did not report the {name} template.")
|
|
9529
|
+
report(out.strip())
|
|
9530
|
+
return 1
|
|
9531
|
+
if "lens templates: ok" not in out:
|
|
9532
|
+
report("domain-lens-doctor: --doctor did not report the shipped templates as ok.")
|
|
9533
|
+
report(out.strip())
|
|
9534
|
+
return 1
|
|
9535
|
+
|
|
9536
|
+
legacy = repo / ".claude/skills/keel-profile-web/SKILL.md"
|
|
9537
|
+
legacy.parent.mkdir(parents=True)
|
|
9538
|
+
legacy.write_text("legacy profile bytes\n", encoding="utf-8")
|
|
9539
|
+
before = legacy.read_bytes()
|
|
9540
|
+
|
|
9541
|
+
doctor2 = run_keel(repo, "--doctor")
|
|
9542
|
+
out2 = doctor2.stdout
|
|
9543
|
+
if "legacy profiles: migrate" not in out2 or "keel-profile-web" not in out2:
|
|
9544
|
+
report(
|
|
9545
|
+
"domain-lens-doctor: --doctor did not warn about the legacy "
|
|
9546
|
+
"keel-profile-web skill."
|
|
9547
|
+
)
|
|
9548
|
+
report(out2.strip())
|
|
9549
|
+
return 1
|
|
9550
|
+
if "not active state" not in out2:
|
|
9551
|
+
report(
|
|
9552
|
+
"domain-lens-doctor: the legacy warning must state it is not "
|
|
9553
|
+
"active state."
|
|
9554
|
+
)
|
|
9555
|
+
return 1
|
|
9556
|
+
if legacy.read_bytes() != before:
|
|
9557
|
+
report("domain-lens-doctor: --doctor modified the legacy skill bytes.")
|
|
9558
|
+
return 1
|
|
9559
|
+
|
|
9560
|
+
report("domain-lens-doctor scenario passed.")
|
|
9561
|
+
return 0
|
|
9562
|
+
|
|
9563
|
+
|
|
9380
9564
|
def validate_precompact_probe_scenario() -> int:
|
|
9381
9565
|
hooks_config = json.loads(
|
|
9382
9566
|
(ROOT / PLUGIN_ROOT / "hooks/hooks.json").read_text(encoding="utf-8")
|
|
@@ -9719,6 +9903,50 @@ def validate_validation_runner_scenario() -> int:
|
|
|
9719
9903
|
|
|
9720
9904
|
# The one ordered scenario registry: --scenario dispatch, the --all runner,
|
|
9721
9905
|
# and registration assertions all read this list and nothing else.
|
|
9906
|
+
def validate_doctor_openspec_honesty_scenario() -> int:
|
|
9907
|
+
cli = (ROOT / "bin/keel.js").read_text(encoding="utf-8")
|
|
9908
|
+
if "is not on PATH" not in cli:
|
|
9909
|
+
report(
|
|
9910
|
+
"doctor-openspec-honesty scenario: bin/keel.js is missing the "
|
|
9911
|
+
"openspec PATH-reachability warning branch."
|
|
9912
|
+
)
|
|
9913
|
+
return 1
|
|
9914
|
+
with tempfile.TemporaryDirectory(prefix="keel-doctor-openspec-") as raw:
|
|
9915
|
+
root = Path(raw)
|
|
9916
|
+
repo = root / "repo"
|
|
9917
|
+
repo.mkdir()
|
|
9918
|
+
stub_dir = root / "stub"
|
|
9919
|
+
stub_dir.mkdir()
|
|
9920
|
+
if os.name == "nt":
|
|
9921
|
+
(stub_dir / "openspec.cmd").write_text(
|
|
9922
|
+
"@echo off\necho 1.6.0\n", encoding="utf-8"
|
|
9923
|
+
)
|
|
9924
|
+
else:
|
|
9925
|
+
stub = stub_dir / "openspec"
|
|
9926
|
+
stub.write_text("#!/bin/sh\necho 1.6.0\n", encoding="utf-8")
|
|
9927
|
+
stub.chmod(0o755)
|
|
9928
|
+
env = dict(os.environ)
|
|
9929
|
+
env["PATH"] = str(stub_dir) + os.pathsep + env.get("PATH", "")
|
|
9930
|
+
on_path = run_keel(repo, "--doctor", env=env)
|
|
9931
|
+
openspec_line = next(
|
|
9932
|
+
(
|
|
9933
|
+
line
|
|
9934
|
+
for line in on_path.stdout.splitlines()
|
|
9935
|
+
if line.startswith("openspec:")
|
|
9936
|
+
),
|
|
9937
|
+
"",
|
|
9938
|
+
)
|
|
9939
|
+
if not openspec_line.startswith("openspec: ok"):
|
|
9940
|
+
report(
|
|
9941
|
+
"doctor-openspec-honesty scenario: doctor did not report `ok` "
|
|
9942
|
+
"when a bare openspec is on PATH."
|
|
9943
|
+
)
|
|
9944
|
+
report(on_path.stdout.strip())
|
|
9945
|
+
return 1
|
|
9946
|
+
report("doctor-openspec-honesty scenario passed.")
|
|
9947
|
+
return 0
|
|
9948
|
+
|
|
9949
|
+
|
|
9722
9950
|
SCENARIOS: tuple = (
|
|
9723
9951
|
("stateless-continuity", validate_stateless_continuity_scenario),
|
|
9724
9952
|
("core-gates", validate_core_gates_scenario),
|
|
@@ -9735,6 +9963,7 @@ SCENARIOS: tuple = (
|
|
|
9735
9963
|
("openspec-surface-overlay", validate_openspec_surface_overlay_scenario),
|
|
9736
9964
|
("uninstall", validate_uninstall_scenario),
|
|
9737
9965
|
("cli", validate_cli_scenario),
|
|
9966
|
+
("doctor-openspec-honesty", validate_doctor_openspec_honesty_scenario),
|
|
9738
9967
|
("update-pack-install", validate_update_pack_install_scenario),
|
|
9739
9968
|
("task-contract-core", validate_task_contract_core_scenario),
|
|
9740
9969
|
("task-capsule", validate_task_capsule_scenario),
|
|
@@ -9768,6 +9997,8 @@ SCENARIOS: tuple = (
|
|
|
9768
9997
|
("plugin-compaction-continuity", validate_plugin_compaction_continuity_scenario),
|
|
9769
9998
|
("precompact-probe", validate_precompact_probe_scenario),
|
|
9770
9999
|
("domain-execution-references", validate_domain_execution_references_scenario),
|
|
10000
|
+
("domain-lens-scaffold", validate_domain_lens_scaffold_scenario),
|
|
10001
|
+
("domain-lens-doctor", validate_domain_lens_doctor_scenario),
|
|
9771
10002
|
("plan-funnel-guidance", validate_plan_funnel_guidance_scenario),
|
|
9772
10003
|
("native-tasks-view", validate_native_tasks_view_scenario),
|
|
9773
10004
|
("validation-runner", validate_validation_runner_scenario),
|
|
@@ -9800,6 +10031,83 @@ def run_scenario_processes(names: list, jobs: int) -> list:
|
|
|
9800
10031
|
return [(name, *results[name]) for name in names]
|
|
9801
10032
|
|
|
9802
10033
|
|
|
10034
|
+
def validate_review_status_single_source(errors: list[str]) -> None:
|
|
10035
|
+
contract = (ROOT / "src/core/task-contract.js").read_text(encoding="utf-8")
|
|
10036
|
+
if "ACCEPTED_REVIEW_STATUSES" not in contract:
|
|
10037
|
+
errors.append(
|
|
10038
|
+
"src/core/task-contract.js must define the shared "
|
|
10039
|
+
"ACCEPTED_REVIEW_STATUSES constant."
|
|
10040
|
+
)
|
|
10041
|
+
if '"done"' not in contract:
|
|
10042
|
+
errors.append(
|
|
10043
|
+
"ACCEPTED_REVIEW_STATUSES must include the `done` Review Status."
|
|
10044
|
+
)
|
|
10045
|
+
literal = "pass|passed|complete|completed|ok"
|
|
10046
|
+
for relative in ("src/core/gates.js", "src/core/context.js"):
|
|
10047
|
+
content = (ROOT / relative).read_text(encoding="utf-8")
|
|
10048
|
+
if "ACCEPTED_REVIEW_STATUSES" not in content:
|
|
10049
|
+
errors.append(
|
|
10050
|
+
f"{relative} must consume the shared ACCEPTED_REVIEW_STATUSES "
|
|
10051
|
+
"constant rather than an inline Review Status vocabulary."
|
|
10052
|
+
)
|
|
10053
|
+
if literal in content:
|
|
10054
|
+
errors.append(
|
|
10055
|
+
f"{relative} still hard-codes the Review Status vocabulary "
|
|
10056
|
+
f"`{literal}`; consume the shared constant instead."
|
|
10057
|
+
)
|
|
10058
|
+
|
|
10059
|
+
|
|
10060
|
+
def validate_install_honesty(errors: list[str]) -> None:
|
|
10061
|
+
installer = (ROOT / "scripts/install_to_repo.py").read_text(encoding="utf-8")
|
|
10062
|
+
if "def skill_actions" in installer or "dist_asset(target_name, \"skills\")" in installer:
|
|
10063
|
+
errors.append(
|
|
10064
|
+
"install_to_repo.py must not carry the dead keel-* skill_actions "
|
|
10065
|
+
"path; keel-* skills are plugin-delivered."
|
|
10066
|
+
)
|
|
10067
|
+
cli = (ROOT / "bin/keel.js").read_text(encoding="utf-8")
|
|
10068
|
+
if "delivered by the installed Keel plugin" not in cli:
|
|
10069
|
+
errors.append(
|
|
10070
|
+
"keel --help/--doctor must state that keel-* skills are delivered "
|
|
10071
|
+
"by the installed Keel plugin, not installed by the CLI."
|
|
10072
|
+
)
|
|
10073
|
+
if ".claude/skills/keel-*." in cli:
|
|
10074
|
+
errors.append(
|
|
10075
|
+
"keel --help must not claim the CLI installs skills under "
|
|
10076
|
+
".claude/skills/keel-*."
|
|
10077
|
+
)
|
|
10078
|
+
|
|
10079
|
+
|
|
10080
|
+
def validate_openspec_invocable(errors: list[str]) -> None:
|
|
10081
|
+
cli = (ROOT / "bin/keel.js").read_text(encoding="utf-8")
|
|
10082
|
+
if 'options.action === "openspec"' not in cli:
|
|
10083
|
+
errors.append("bin/keel.js must implement the `keel openspec` passthrough.")
|
|
10084
|
+
if cli.count("Invoke OpenSpec through") < 2:
|
|
10085
|
+
errors.append(
|
|
10086
|
+
"the apply and archive overlays must direct agents to `keel openspec` "
|
|
10087
|
+
"in place of a bare openspec command."
|
|
10088
|
+
)
|
|
10089
|
+
|
|
10090
|
+
|
|
10091
|
+
def validate_archive_overlay_hygiene(errors: list[str]) -> None:
|
|
10092
|
+
cli = (ROOT / "bin/keel.js").read_text(encoding="utf-8")
|
|
10093
|
+
if "--skip-specs" not in cli:
|
|
10094
|
+
errors.append(
|
|
10095
|
+
"the archive overlay must direct archive to pass --skip-specs "
|
|
10096
|
+
"after /opsx:sync has promoted the delta."
|
|
10097
|
+
)
|
|
10098
|
+
if "drop the change's guard manifest" not in cli:
|
|
10099
|
+
errors.append(
|
|
10100
|
+
"the archive overlay must remind the agent to run keel guard clear "
|
|
10101
|
+
"after archiving."
|
|
10102
|
+
)
|
|
10103
|
+
gates = (ROOT / "src/core/gates.js").read_text(encoding="utf-8")
|
|
10104
|
+
if "clearGuard" in gates:
|
|
10105
|
+
errors.append(
|
|
10106
|
+
"gates must stay read-only: gate code must not clear the guard "
|
|
10107
|
+
"manifest (guard clear is explicit via keel guard clear)."
|
|
10108
|
+
)
|
|
10109
|
+
|
|
10110
|
+
|
|
9803
10111
|
def run_baseline() -> int:
|
|
9804
10112
|
errors: list[str] = []
|
|
9805
10113
|
validate_manifest(errors)
|
|
@@ -9808,6 +10116,10 @@ def run_baseline() -> int:
|
|
|
9808
10116
|
validate_resident_blocks(errors)
|
|
9809
10117
|
validate_templates(errors)
|
|
9810
10118
|
validate_openspec_schema(errors)
|
|
10119
|
+
validate_review_status_single_source(errors)
|
|
10120
|
+
validate_install_honesty(errors)
|
|
10121
|
+
validate_openspec_invocable(errors)
|
|
10122
|
+
validate_archive_overlay_hygiene(errors)
|
|
9811
10123
|
validate_skill_docs(errors)
|
|
9812
10124
|
validate_skill_portability(ROOT, errors)
|
|
9813
10125
|
validate_scripts_use_stdlib(errors)
|