@christang/keel 5.2.0 → 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/assets/bootstrap/AGENTS.md +1 -1
- package/assets/openspec/schemas/keel-spec-driven/templates/design.md +1 -1
- package/bin/keel.js +3 -3
- package/package.json +1 -1
- package/plugins/keel/.claude-plugin/plugin.json +1 -1
- package/plugins/keel/.codex-plugin/plugin.json +1 -1
- package/scripts/install_to_repo.py +3 -3
- package/scripts/validate_plugin.py +154 -15
- package/src/core/gates.js +10 -1
|
@@ -21,7 +21,7 @@ Ordinary narrative stays unnumbered. -->
|
|
|
21
21
|
## Hidden Knowledge / Assumptions
|
|
22
22
|
|
|
23
23
|
<!-- Accepted hidden-knowledge assumptions from risk-triggered grill or domain
|
|
24
|
-
|
|
24
|
+
lenses. Critical assumptions use A<n>, record Basis, and name Resolve by or a
|
|
25
25
|
durable Owner. Put compressed recovery context here when future sessions need
|
|
26
26
|
it; keel/HANDOFF.md should only point to durable owners. Use "None." when empty. -->
|
|
27
27
|
|
package/bin/keel.js
CHANGED
|
@@ -47,7 +47,7 @@ const {
|
|
|
47
47
|
const PACKAGE_ROOT = path.resolve(__dirname, "..");
|
|
48
48
|
const PACKAGE_JSON = require(path.join(PACKAGE_ROOT, "package.json"));
|
|
49
49
|
const INSTALL_SCRIPT = path.join(PACKAGE_ROOT, "scripts", "install_to_repo.py");
|
|
50
|
-
const DEFAULT_UPDATE_SOURCE = "
|
|
50
|
+
const DEFAULT_UPDATE_SOURCE = "@christang/keel";
|
|
51
51
|
const VALID_TARGETS = new Set(["claude", "codex", "opencode", "both"]);
|
|
52
52
|
const KEEL_SKILLS = [
|
|
53
53
|
"keel-align-expectations",
|
|
@@ -378,8 +378,8 @@ function parseArgs(argv) {
|
|
|
378
378
|
if (arg === "--profile" || arg.startsWith("--profile=")) {
|
|
379
379
|
fail(
|
|
380
380
|
"--profile is no longer supported: web, hardware, and hardware-dsl "
|
|
381
|
-
+ "guidance is
|
|
382
|
-
+ "
|
|
381
|
+
+ "guidance is now user-authored lenses in keel/lenses/*.md "
|
|
382
|
+
+ "(scaffold with `keel lenses add`)"
|
|
383
383
|
);
|
|
384
384
|
}
|
|
385
385
|
if (arg === "--repo") {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "keel",
|
|
3
|
-
"version": "5.2.
|
|
3
|
+
"version": "5.2.1",
|
|
4
4
|
"description": "Keel OpenSpec execution discipline: stateless continuity, task capsules, deterministic gates, and expectation alignment for Codex and Claude Code.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "TanglmChris",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "keel",
|
|
3
|
-
"version": "5.2.
|
|
3
|
+
"version": "5.2.1",
|
|
4
4
|
"description": "Keel OpenSpec execution discipline: stateless continuity, task capsules, deterministic gates, and expectation alignment for Codex and Claude Code.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "TanglmChris",
|
|
@@ -1010,7 +1010,7 @@ def main() -> int:
|
|
|
1010
1010
|
parser.add_argument(
|
|
1011
1011
|
"--profile",
|
|
1012
1012
|
action="append",
|
|
1013
|
-
help="Obsolete
|
|
1013
|
+
help="Obsolete; domain guidance is now user-authored lenses in keel/lenses/*.md.",
|
|
1014
1014
|
)
|
|
1015
1015
|
args = parser.parse_args()
|
|
1016
1016
|
|
|
@@ -1019,8 +1019,8 @@ def main() -> int:
|
|
|
1019
1019
|
if args.profile:
|
|
1020
1020
|
print(
|
|
1021
1021
|
"Install failed: --profile is no longer supported; web, hardware, "
|
|
1022
|
-
"and hardware-dsl guidance is
|
|
1023
|
-
"keel
|
|
1022
|
+
"and hardware-dsl guidance is now user-authored lenses in "
|
|
1023
|
+
"keel/lenses/*.md (scaffold with `keel lenses add`)",
|
|
1024
1024
|
file=sys.stderr,
|
|
1025
1025
|
)
|
|
1026
1026
|
return 1
|
|
@@ -37,8 +37,8 @@ REQUIRED_SCRIPTS = [
|
|
|
37
37
|
"scripts/validate_plugin.py",
|
|
38
38
|
]
|
|
39
39
|
|
|
40
|
-
PACKAGE_VERSION = "5.2.
|
|
41
|
-
PROTOCOL_VERSION = "5.2.
|
|
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")
|
|
@@ -1750,23 +1750,23 @@ def validate_authoring_continuity_scenario() -> int:
|
|
|
1750
1750
|
return 0
|
|
1751
1751
|
|
|
1752
1752
|
|
|
1753
|
-
def
|
|
1753
|
+
def validate_domain_lenses_scenario() -> int:
|
|
1754
1754
|
plugin_skills_root = ROOT / PLUGIN_ROOT / "skills"
|
|
1755
1755
|
if not (plugin_skills_root / "keel-align-expectations/SKILL.md").is_file():
|
|
1756
|
-
report("domain-
|
|
1756
|
+
report("domain-lenses scenario plugin misses the alignment skill")
|
|
1757
1757
|
return 1
|
|
1758
1758
|
lenses_root = ROOT / "assets/lenses"
|
|
1759
1759
|
for template in ("web.md", "hardware.md", "hardware-dsl.md"):
|
|
1760
1760
|
if not (lenses_root / template).is_file():
|
|
1761
1761
|
report(
|
|
1762
|
-
"domain-
|
|
1762
|
+
"domain-lenses scenario misses the shipped lens template: "
|
|
1763
1763
|
f"{template}"
|
|
1764
1764
|
)
|
|
1765
1765
|
return 1
|
|
1766
1766
|
for legacy_skill in LEGACY_PROFILE_SKILLS:
|
|
1767
1767
|
if (plugin_skills_root / legacy_skill).exists():
|
|
1768
1768
|
report(
|
|
1769
|
-
f"domain-
|
|
1769
|
+
f"domain-lenses scenario plugin packages a legacy profile: {legacy_skill}"
|
|
1770
1770
|
)
|
|
1771
1771
|
return 1
|
|
1772
1772
|
|
|
@@ -1776,20 +1776,20 @@ def validate_domain_profiles_scenario() -> int:
|
|
|
1776
1776
|
repo.mkdir()
|
|
1777
1777
|
install = run_keel(repo, "--install", "--target", "codex")
|
|
1778
1778
|
if install.returncode != 0:
|
|
1779
|
-
report("domain-
|
|
1779
|
+
report("domain-lenses scenario default install failed:")
|
|
1780
1780
|
report((install.stderr or install.stdout).strip())
|
|
1781
1781
|
return 1
|
|
1782
1782
|
if (repo / TARGET_SKILL_ROOTS["codex"]).exists():
|
|
1783
1783
|
report(
|
|
1784
|
-
"domain-
|
|
1784
|
+
"domain-lenses scenario thin install copied Keel skill trees; "
|
|
1785
1785
|
"skills are plugin-owned in v4."
|
|
1786
1786
|
)
|
|
1787
1787
|
return 1
|
|
1788
1788
|
|
|
1789
1789
|
rejected = run_keel(repo, "--install", "--target", "codex", "--profile", "web")
|
|
1790
1790
|
rejected_text = (rejected.stderr or "") + (rejected.stdout or "")
|
|
1791
|
-
if rejected.returncode == 0 or "
|
|
1792
|
-
report("domain-
|
|
1791
|
+
if rejected.returncode == 0 or "keel/lenses" not in rejected_text:
|
|
1792
|
+
report("domain-lenses scenario still accepts --profile.")
|
|
1793
1793
|
report(rejected_text.strip())
|
|
1794
1794
|
return 1
|
|
1795
1795
|
|
|
@@ -1801,7 +1801,7 @@ def validate_domain_profiles_scenario() -> int:
|
|
|
1801
1801
|
or "Keel profiles" in doctor_text
|
|
1802
1802
|
):
|
|
1803
1803
|
report(
|
|
1804
|
-
"domain-
|
|
1804
|
+
"domain-lenses scenario doctor still reports profile state or "
|
|
1805
1805
|
"misses the native plugin surface."
|
|
1806
1806
|
)
|
|
1807
1807
|
report(doctor_text.strip())
|
|
@@ -1814,11 +1814,11 @@ def validate_domain_profiles_scenario() -> int:
|
|
|
1814
1814
|
else ""
|
|
1815
1815
|
)
|
|
1816
1816
|
if uninstall.returncode != 0 or "keel:start" in agents_text:
|
|
1817
|
-
report("domain-
|
|
1817
|
+
report("domain-lenses scenario uninstall left the managed bootstrap.")
|
|
1818
1818
|
report((uninstall.stderr or uninstall.stdout).strip())
|
|
1819
1819
|
return 1
|
|
1820
1820
|
|
|
1821
|
-
report("domain-
|
|
1821
|
+
report("domain-lenses scenario passed.")
|
|
1822
1822
|
return 0
|
|
1823
1823
|
|
|
1824
1824
|
|
|
@@ -2396,6 +2396,42 @@ def validate_update_pack_install_scenario() -> int:
|
|
|
2396
2396
|
return 0
|
|
2397
2397
|
|
|
2398
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
|
+
|
|
2399
2435
|
def run_keel_hook(repo: Path, event: dict) -> subprocess.CompletedProcess[str]:
|
|
2400
2436
|
env = dict(os.environ)
|
|
2401
2437
|
env["KEEL_CLI"] = str(ROOT / "bin/keel.js")
|
|
@@ -2642,7 +2678,7 @@ def validate_cli_scenario() -> int:
|
|
|
2642
2678
|
if (
|
|
2643
2679
|
update.returncode != 0
|
|
2644
2680
|
or not pack_plan
|
|
2645
|
-
or "
|
|
2681
|
+
or "@christang/keel" not in update.stdout
|
|
2646
2682
|
or not install_plan
|
|
2647
2683
|
):
|
|
2648
2684
|
report("cli scenario keel --update did not report global CLI update plan.")
|
|
@@ -5184,6 +5220,107 @@ def validate_core_gates_scenario() -> int:
|
|
|
5184
5220
|
return 0
|
|
5185
5221
|
|
|
5186
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
|
+
|
|
5187
5324
|
def validate_target_capability_adapters_scenario() -> int:
|
|
5188
5325
|
capability_keys = (
|
|
5189
5326
|
"continuity.start",
|
|
@@ -9950,6 +10087,7 @@ def validate_doctor_openspec_honesty_scenario() -> int:
|
|
|
9950
10087
|
SCENARIOS: tuple = (
|
|
9951
10088
|
("stateless-continuity", validate_stateless_continuity_scenario),
|
|
9952
10089
|
("core-gates", validate_core_gates_scenario),
|
|
10090
|
+
("scope-rename-attribution", validate_scope_rename_attribution_scenario),
|
|
9953
10091
|
("target-capability-adapters", validate_target_capability_adapters_scenario),
|
|
9954
10092
|
("native-runtime-projection", validate_native_runtime_projection_scenario),
|
|
9955
10093
|
("target-surface", validate_target_surface_scenario),
|
|
@@ -9957,7 +10095,7 @@ SCENARIOS: tuple = (
|
|
|
9957
10095
|
("expectation-slice-gates", validate_expectation_slice_gates_scenario),
|
|
9958
10096
|
("expectation-completion-gates", validate_expectation_completion_gates_scenario),
|
|
9959
10097
|
("authoring-continuity", validate_authoring_continuity_scenario),
|
|
9960
|
-
("domain-
|
|
10098
|
+
("domain-lenses", validate_domain_lenses_scenario),
|
|
9961
10099
|
("skill-portability-policy", validate_skill_portability_policy_scenario),
|
|
9962
10100
|
("version-alignment", validate_version_alignment_scenario),
|
|
9963
10101
|
("openspec-surface-overlay", validate_openspec_surface_overlay_scenario),
|
|
@@ -9965,6 +10103,7 @@ SCENARIOS: tuple = (
|
|
|
9965
10103
|
("cli", validate_cli_scenario),
|
|
9966
10104
|
("doctor-openspec-honesty", validate_doctor_openspec_honesty_scenario),
|
|
9967
10105
|
("update-pack-install", validate_update_pack_install_scenario),
|
|
10106
|
+
("update-default-registry", validate_update_default_registry_scenario),
|
|
9968
10107
|
("task-contract-core", validate_task_contract_core_scenario),
|
|
9969
10108
|
("task-capsule", validate_task_capsule_scenario),
|
|
9970
10109
|
("task-verification-strategies", validate_task_verification_strategies_scenario),
|
package/src/core/gates.js
CHANGED
|
@@ -237,7 +237,16 @@ function gitPaths(repo) {
|
|
|
237
237
|
return status.stdout
|
|
238
238
|
.split(/\r?\n/)
|
|
239
239
|
.filter(Boolean)
|
|
240
|
-
.
|
|
240
|
+
.flatMap((line) => {
|
|
241
|
+
// A staged rename/copy is one porcelain line, `R old -> new`; attribute
|
|
242
|
+
// both endpoints so a rename whose old and new paths are in Touch is not
|
|
243
|
+
// a false outside-Touch failure. Every other line carries one path.
|
|
244
|
+
const entry = line.slice(3).trim().replace(/\\/g, "/");
|
|
245
|
+
const arrow = entry.indexOf(" -> ");
|
|
246
|
+
return arrow === -1
|
|
247
|
+
? [entry]
|
|
248
|
+
: [entry.slice(0, arrow), entry.slice(arrow + 4)];
|
|
249
|
+
});
|
|
241
250
|
}
|
|
242
251
|
|
|
243
252
|
function touchEntries(task, contract = null) {
|