@christang/keel 5.76.0 → 5.77.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/bin/keel.js
CHANGED
|
@@ -755,6 +755,42 @@ function openspecReportedVersion(command) {
|
|
|
755
755
|
return match ? match[0] : null;
|
|
756
756
|
}
|
|
757
757
|
|
|
758
|
+
// The OpenSpec that wrote this repository's surfaces: the highest
|
|
759
|
+
// `generatedBy` stamp among its OpenSpec skills (#168). Every target's root is
|
|
760
|
+
// read, including `.agents/skills`, where OpenSpec 1.13 writes Codex's.
|
|
761
|
+
const OPENSPEC_SURFACE_ROOTS = [
|
|
762
|
+
path.join(".claude", "skills"),
|
|
763
|
+
path.join(".codex", "skills"),
|
|
764
|
+
path.join(".agents", "skills"),
|
|
765
|
+
path.join(".opencode", "skills"),
|
|
766
|
+
];
|
|
767
|
+
|
|
768
|
+
function surfaceGeneratorVersion(repo) {
|
|
769
|
+
let newest = null;
|
|
770
|
+
for (const root of OPENSPEC_SURFACE_ROOTS) {
|
|
771
|
+
let entries = [];
|
|
772
|
+
try {
|
|
773
|
+
entries = fs.readdirSync(path.join(repo, root));
|
|
774
|
+
} catch {
|
|
775
|
+
continue;
|
|
776
|
+
}
|
|
777
|
+
for (const entry of entries) {
|
|
778
|
+
if (!entry.startsWith("openspec-")) continue;
|
|
779
|
+
let text;
|
|
780
|
+
try {
|
|
781
|
+
text = fs.readFileSync(path.join(repo, root, entry, "SKILL.md"), "utf8");
|
|
782
|
+
} catch {
|
|
783
|
+
continue;
|
|
784
|
+
}
|
|
785
|
+
const match = text.match(/generatedBy:\s*"?(\d+\.\d+\.\d+)/);
|
|
786
|
+
if (match && (!newest || compareVersions(match[1], newest) > 0)) {
|
|
787
|
+
newest = match[1];
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
return newest;
|
|
792
|
+
}
|
|
793
|
+
|
|
758
794
|
// What the project declares about Keel plugin auto-update (#164). Claude reads
|
|
759
795
|
// it first from `autoUpdate` on the marketplace's `extraKnownMarketplaces`
|
|
760
796
|
// entry, and `keel --install --target claude` writes it. The host reports no
|
|
@@ -774,6 +810,30 @@ function pluginAutoUpdateDeclaration(repo) {
|
|
|
774
810
|
entry = undefined;
|
|
775
811
|
}
|
|
776
812
|
if (entry && entry.autoUpdate === true) {
|
|
813
|
+
// A declaration in a file Git does not track exists in this checkout only:
|
|
814
|
+
// every other clone of the project starts with auto-update off (#168).
|
|
815
|
+
const inWorkTree = spawnSync("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
816
|
+
cwd: repo,
|
|
817
|
+
encoding: "utf8",
|
|
818
|
+
});
|
|
819
|
+
const tracked = spawnSync(
|
|
820
|
+
"git",
|
|
821
|
+
["ls-files", "--error-unmatch", ".claude/settings.json"],
|
|
822
|
+
{ cwd: repo, encoding: "utf8" }
|
|
823
|
+
);
|
|
824
|
+
if (
|
|
825
|
+
!inWorkTree.error
|
|
826
|
+
&& inWorkTree.status === 0
|
|
827
|
+
&& String(inWorkTree.stdout).trim() === "true"
|
|
828
|
+
&& tracked.status !== 0
|
|
829
|
+
) {
|
|
830
|
+
return [
|
|
831
|
+
"warning",
|
|
832
|
+
".claude/settings.json declares keel-marketplace autoUpdate: true, "
|
|
833
|
+
+ "but Git does not track the file, so only this checkout declares "
|
|
834
|
+
+ "it; commit it with `git add .claude/settings.json`",
|
|
835
|
+
];
|
|
836
|
+
}
|
|
777
837
|
return [
|
|
778
838
|
"ok",
|
|
779
839
|
`.claude/settings.json declares keel-marketplace autoUpdate: true; ${observation}`,
|
|
@@ -1066,7 +1126,25 @@ function runProjectInit(options) {
|
|
|
1066
1126
|
fs.mkdirSync(repo, { recursive: true });
|
|
1067
1127
|
}
|
|
1068
1128
|
|
|
1069
|
-
|
|
1129
|
+
// Never downgrade (#168): surfaces a newer OpenSpec wrote keep their
|
|
1130
|
+
// content. The protocol and Keel's overlays still move; only OpenSpec's own
|
|
1131
|
+
// `--force` rewrite is skipped, and the reason is printed.
|
|
1132
|
+
const surfaces = surfaceGeneratorVersion(repo);
|
|
1133
|
+
const resolvedCommand = findOpenSpecCommand();
|
|
1134
|
+
const resolved = resolvedCommand ? openspecReportedVersion(resolvedCommand) : null;
|
|
1135
|
+
const keepSurfaces = Boolean(
|
|
1136
|
+
surfaces && resolved && compareVersions(surfaces, resolved) > 0
|
|
1137
|
+
);
|
|
1138
|
+
if (keepSurfaces) {
|
|
1139
|
+
process.stdout.write(
|
|
1140
|
+
`keel: OpenSpec surfaces were written by OpenSpec ${surfaces}, newer `
|
|
1141
|
+
+ `than the ${resolved} Keel runs; skipped \`openspec init --force\` `
|
|
1142
|
+
+ "and `openspec update --force`, which would downgrade them (#168). "
|
|
1143
|
+
+ "The protocol and Keel's overlays are still refreshed.\n"
|
|
1144
|
+
);
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
const openspecTools = keepSurfaces ? null : openspecToolsForTarget(options.target);
|
|
1070
1148
|
if (openspecTools) {
|
|
1071
1149
|
const initStatus = runOpenSpec(
|
|
1072
1150
|
["init", "--tools", openspecTools, "--force"],
|
|
@@ -1088,6 +1166,8 @@ function runProjectInit(options) {
|
|
|
1088
1166
|
if (updateStatus !== 0) {
|
|
1089
1167
|
return updateStatus;
|
|
1090
1168
|
}
|
|
1169
|
+
}
|
|
1170
|
+
if (openspecTools || keepSurfaces) {
|
|
1091
1171
|
return refreshOpenSpecSurfaceOverlay(repo, options.target, {
|
|
1092
1172
|
dryRun: options.dryRun,
|
|
1093
1173
|
}).status;
|
|
@@ -1164,8 +1244,9 @@ function printProtocolVersionDrift(repo, target) {
|
|
|
1164
1244
|
"warning",
|
|
1165
1245
|
order < 0
|
|
1166
1246
|
? `repo declares ${declared}, this CLI is ${running} — the repository is `
|
|
1167
|
-
+ `behind its install; run keel --
|
|
1168
|
-
+ "protocol forward"
|
|
1247
|
+
+ `behind its install; run keel --install --target ${target} to bring `
|
|
1248
|
+
+ "the protocol forward; it leaves OpenSpec's surfaces as they are "
|
|
1249
|
+
+ "(#168)"
|
|
1169
1250
|
: `repo declares ${declared}, this CLI is ${running} — the install is `
|
|
1170
1251
|
+ "behind the repository, which carries a protocol this CLI cannot "
|
|
1171
1252
|
+ "enforce; update the Keel package"
|
|
@@ -1752,6 +1833,22 @@ function runDoctor(options) {
|
|
|
1752
1833
|
);
|
|
1753
1834
|
}
|
|
1754
1835
|
|
|
1836
|
+
const surfaces = surfaceGeneratorVersion(repo);
|
|
1837
|
+
const running = openspec ? openspecReportedVersion(openspec) : null;
|
|
1838
|
+
if (surfaces && running) {
|
|
1839
|
+
const newer = compareVersions(surfaces, running) > 0;
|
|
1840
|
+
printDoctorLine(
|
|
1841
|
+
"OpenSpec surfaces",
|
|
1842
|
+
newer ? "warning" : "ok",
|
|
1843
|
+
newer
|
|
1844
|
+
? `written by OpenSpec ${surfaces}, newer than the OpenSpec Keel runs `
|
|
1845
|
+
+ `(${running}); keel --init leaves them unrewritten rather than `
|
|
1846
|
+
+ `downgrading them, and keel --install --target ${options.target} `
|
|
1847
|
+
+ "refreshes the protocol without touching them"
|
|
1848
|
+
: `written by OpenSpec ${surfaces}; Keel runs ${running}`
|
|
1849
|
+
);
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1755
1852
|
printProtocolVersionDrift(repo, options.target);
|
|
1756
1853
|
|
|
1757
1854
|
process.stdout.write("\nProject status:\n");
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@christang/keel",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.77.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@christang/keel",
|
|
9
|
-
"version": "5.
|
|
9
|
+
"version": "5.77.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@fission-ai/openspec": "^1.4.1"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "keel",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.77.0",
|
|
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.
|
|
3
|
+
"version": "5.77.0",
|
|
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",
|
|
@@ -38,8 +38,8 @@ REQUIRED_SCRIPTS = [
|
|
|
38
38
|
"scripts/validate_plugin.py",
|
|
39
39
|
]
|
|
40
40
|
|
|
41
|
-
PACKAGE_VERSION = "5.
|
|
42
|
-
PROTOCOL_VERSION = "5.
|
|
41
|
+
PACKAGE_VERSION = "5.77.0"
|
|
42
|
+
PROTOCOL_VERSION = "5.77.0"
|
|
43
43
|
LEGACY_MANAGED_START = "<!-- keel:start version=2.1 -->"
|
|
44
44
|
OPENSPEC_SCHEMA_NAME = "keel-spec-driven"
|
|
45
45
|
# Mirrors KEEL_PACKAGE_NAME in scripts/install_to_repo.py, one of the two
|
|
@@ -16337,6 +16337,114 @@ def validate_context_names_the_protocol_refresh_scenario() -> int:
|
|
|
16337
16337
|
return 0
|
|
16338
16338
|
|
|
16339
16339
|
|
|
16340
|
+
# Issue #168: 5.74.0's shrinkwrap pinned OpenSpec 1.6.0 for every install, and
|
|
16341
|
+
# `keel --init` ran `openspec update --force` over surfaces 1.13.2 had written,
|
|
16342
|
+
# deleting ~1000 lines in a consumer while doctor said `openspec: ok`.
|
|
16343
|
+
DOWNGRADE_LABEL = "init-never-downgrades-openspec"
|
|
16344
|
+
DOWNGRADE_SENTINEL = "keel-downgrade-sentinel: this line exists only in the planted surface"
|
|
16345
|
+
|
|
16346
|
+
|
|
16347
|
+
def _restamp_surfaces(repo: Path, version: str) -> Path:
|
|
16348
|
+
"""Stamp every OpenSpec skill `generatedBy: version` and plant a sentinel."""
|
|
16349
|
+
skills = sorted((repo / ".claude/skills").glob("openspec-*/SKILL.md"))
|
|
16350
|
+
for skill in skills:
|
|
16351
|
+
text = skill.read_text(encoding="utf-8")
|
|
16352
|
+
skill.write_text(
|
|
16353
|
+
re.sub(r'generatedBy: "[^"]*"', f'generatedBy: "{version}"', text),
|
|
16354
|
+
encoding="utf-8",
|
|
16355
|
+
)
|
|
16356
|
+
target = repo / ".claude/skills/openspec-propose/SKILL.md"
|
|
16357
|
+
head, sep, body = target.read_text(encoding="utf-8").partition("\n---\n")
|
|
16358
|
+
target.write_text(head + sep + DOWNGRADE_SENTINEL + "\n" + body, encoding="utf-8")
|
|
16359
|
+
return target
|
|
16360
|
+
|
|
16361
|
+
|
|
16362
|
+
def _downgrade_m1(tmp: Path) -> str | None:
|
|
16363
|
+
repo = tmp / "newer"
|
|
16364
|
+
repo.mkdir()
|
|
16365
|
+
run_keel(repo, "--init", "--target", "claude")
|
|
16366
|
+
planted = _restamp_surfaces(repo, "99.0.0")
|
|
16367
|
+
again = run_keel(repo, "--init", "--target", "claude")
|
|
16368
|
+
output = (again.stdout or "") + (again.stderr or "")
|
|
16369
|
+
if DOWNGRADE_SENTINEL not in planted.read_text(encoding="utf-8"):
|
|
16370
|
+
return (
|
|
16371
|
+
"M1 keel --init rewrote OpenSpec surfaces written by a newer OpenSpec "
|
|
16372
|
+
"(99.0.0): the planted sentinel is gone."
|
|
16373
|
+
)
|
|
16374
|
+
if "99.0.0" not in output or "skip" not in output.lower():
|
|
16375
|
+
return (
|
|
16376
|
+
"M1 keel --init kept the surfaces but did not say it skipped the rewrite "
|
|
16377
|
+
f"of OpenSpec surfaces written by a newer OpenSpec: {output[-600:]!r}"
|
|
16378
|
+
)
|
|
16379
|
+
return None
|
|
16380
|
+
|
|
16381
|
+
|
|
16382
|
+
def _downgrade_m2(tmp: Path) -> str | None:
|
|
16383
|
+
repo = tmp / "doctor-newer"
|
|
16384
|
+
repo.mkdir()
|
|
16385
|
+
run_keel(repo, "--init", "--target", "claude")
|
|
16386
|
+
_restamp_surfaces(repo, "99.0.0")
|
|
16387
|
+
running = run_keel(repo, "openspec", "--version").stdout.strip()
|
|
16388
|
+
doctor = run_keel(repo, "--doctor").stdout or ""
|
|
16389
|
+
lines = [l for l in doctor.splitlines() if l.startswith("OpenSpec surfaces:")]
|
|
16390
|
+
if len(lines) != 1 or not lines[0].startswith("OpenSpec surfaces: warning"):
|
|
16391
|
+
return f"M2 doctor printed no OpenSpec surfaces warning for 99.0.0 surfaces: {lines!r}"
|
|
16392
|
+
for needle in ("99.0.0", running, "keel --install --target claude"):
|
|
16393
|
+
if needle not in lines[0]:
|
|
16394
|
+
return f"M2 doctor printed no OpenSpec surfaces warning carrying {needle!r}: {lines[0]!r}"
|
|
16395
|
+
return None
|
|
16396
|
+
|
|
16397
|
+
|
|
16398
|
+
def _downgrade_m3(tmp: Path) -> str | None:
|
|
16399
|
+
repo = tmp / "older"
|
|
16400
|
+
repo.mkdir()
|
|
16401
|
+
run_keel(repo, "--init", "--target", "claude")
|
|
16402
|
+
planted = _restamp_surfaces(repo, "1.0.0")
|
|
16403
|
+
run_keel(repo, "--init", "--target", "claude")
|
|
16404
|
+
if DOWNGRADE_SENTINEL in planted.read_text(encoding="utf-8"):
|
|
16405
|
+
return (
|
|
16406
|
+
"M3 keel --init skipped the OpenSpec refresh for surfaces older than "
|
|
16407
|
+
"the OpenSpec it runs (1.0.0); only newer surfaces are protected."
|
|
16408
|
+
)
|
|
16409
|
+
return None
|
|
16410
|
+
|
|
16411
|
+
|
|
16412
|
+
def _downgrade_m4(tmp: Path) -> str | None:
|
|
16413
|
+
repo = tmp / "git"
|
|
16414
|
+
repo.mkdir()
|
|
16415
|
+
git = ["git", "-c", "user.name=keel", "-c", "user.email=keel@example.invalid"]
|
|
16416
|
+
subprocess.run(["git", "init", "-q"], cwd=repo, check=True)
|
|
16417
|
+
run_keel(repo, "--install", "--target", "claude")
|
|
16418
|
+
|
|
16419
|
+
def line() -> str:
|
|
16420
|
+
doctor = run_keel(repo, "--doctor", "--target", "claude").stdout or ""
|
|
16421
|
+
return next((l for l in doctor.splitlines() if l.startswith("plugin auto-update:")), "")
|
|
16422
|
+
|
|
16423
|
+
untracked = line()
|
|
16424
|
+
if not untracked.startswith("plugin auto-update: warning") or "git add .claude/settings.json" not in untracked:
|
|
16425
|
+
return f"M4 doctor did not say .claude/settings.json is untracked: {untracked!r}"
|
|
16426
|
+
subprocess.run([*git, "add", ".claude/settings.json"], cwd=repo, check=True)
|
|
16427
|
+
subprocess.run([*git, "commit", "-q", "-m", "declare"], cwd=repo, check=True)
|
|
16428
|
+
tracked = line()
|
|
16429
|
+
if not tracked.startswith("plugin auto-update: ok"):
|
|
16430
|
+
return f"M4 doctor did not say .claude/settings.json is untracked only while it was: {tracked!r}"
|
|
16431
|
+
return None
|
|
16432
|
+
|
|
16433
|
+
|
|
16434
|
+
def validate_init_never_downgrades_openspec_scenario() -> int:
|
|
16435
|
+
with tempfile.TemporaryDirectory(
|
|
16436
|
+
prefix="keel-downgrade-", ignore_cleanup_errors=True
|
|
16437
|
+
) as raw:
|
|
16438
|
+
tmp = Path(raw)
|
|
16439
|
+
for check in (_downgrade_m1, _downgrade_m2, _downgrade_m3, _downgrade_m4):
|
|
16440
|
+
problem = check(tmp)
|
|
16441
|
+
if problem:
|
|
16442
|
+
report(f"{DOWNGRADE_LABEL} {problem}")
|
|
16443
|
+
return 1
|
|
16444
|
+
report(f"{DOWNGRADE_LABEL} scenario passed.")
|
|
16445
|
+
return 0
|
|
16446
|
+
|
|
16447
|
+
|
|
16340
16448
|
def validate_native_plugin_marketplaces_scenario() -> int:
|
|
16341
16449
|
codex = shutil.which("codex")
|
|
16342
16450
|
claude = claude_cli()
|
|
@@ -26150,7 +26258,15 @@ def validate_marker_version_is_read_scenario() -> int:
|
|
|
26150
26258
|
f"older protocol did not warn; got {line!r}."
|
|
26151
26259
|
)
|
|
26152
26260
|
return 1
|
|
26153
|
-
|
|
26261
|
+
# #168: `keel --init` also runs OpenSpec's `--force` rewrite, which is
|
|
26262
|
+
# what downgraded a consumer's surfaces. The protocol refresh is install.
|
|
26263
|
+
if "keel --init" in line:
|
|
26264
|
+
report(
|
|
26265
|
+
"the-marker-version-is-read scenario: a repository behind its "
|
|
26266
|
+
f"install was told to run keel --init; got {line!r}."
|
|
26267
|
+
)
|
|
26268
|
+
return 1
|
|
26269
|
+
for needed in ("5.14.0", PACKAGE_VERSION, "keel --install"):
|
|
26154
26270
|
if needed not in line:
|
|
26155
26271
|
report(
|
|
26156
26272
|
"the-marker-version-is-read scenario: the behind-repository "
|
|
@@ -31314,6 +31430,7 @@ SCENARIOS: tuple = (
|
|
|
31314
31430
|
("plugin-runs-its-own-cli", validate_plugin_runs_its_own_cli_scenario),
|
|
31315
31431
|
("init-declares-plugin-auto-update", validate_init_declares_plugin_auto_update_scenario),
|
|
31316
31432
|
("context-names-the-protocol-refresh", validate_context_names_the_protocol_refresh_scenario),
|
|
31433
|
+
("init-never-downgrades-openspec", validate_init_never_downgrades_openspec_scenario),
|
|
31317
31434
|
("native-plugin-marketplaces", validate_native_plugin_marketplaces_scenario),
|
|
31318
31435
|
("native-plugin-install-matrix", validate_native_plugin_install_matrix_scenario),
|
|
31319
31436
|
("native-goal-projection", validate_native_goal_projection_scenario),
|