@christang/keel 5.3.4 → 5.3.5

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.
@@ -1,4 +1,4 @@
1
- <!-- keel:start version=5.3.4 -->
1
+ <!-- keel:start version=5.3.5 -->
2
2
  ## Keel Bootstrap
3
3
 
4
4
  - Start every session with `keel context`; OpenSpec artifacts and Git are the only durable authority — never native memory, goals, or transcripts.
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@christang/keel",
3
3
  "displayName": "Keel",
4
4
  "description": "Keel OpenSpec execution discipline CLI for Claude Code, Codex, and OpenCode.",
5
- "version": "5.3.4",
5
+ "version": "5.3.5",
6
6
  "license": "MIT",
7
7
  "repository": {
8
8
  "type": "git",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "keel",
3
- "version": "5.3.4",
3
+ "version": "5.3.5",
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.4",
3
+ "version": "5.3.5",
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",
@@ -37,10 +37,13 @@ REQUIRED_SCRIPTS = [
37
37
  "scripts/validate_plugin.py",
38
38
  ]
39
39
 
40
- PACKAGE_VERSION = "5.3.4"
41
- PROTOCOL_VERSION = "5.3.4"
40
+ PACKAGE_VERSION = "5.3.5"
41
+ PROTOCOL_VERSION = "5.3.5"
42
42
  LEGACY_MANAGED_START = "<!-- keel:start version=2.1 -->"
43
43
  OPENSPEC_SCHEMA_NAME = "keel-spec-driven"
44
+ # Mirrors KEEL_PACKAGE_NAME in scripts/install_to_repo.py, one of the two
45
+ # signals is_keel_source_repo reads.
46
+ KEEL_PACKAGE_NAME = "@christang/keel"
44
47
  OPENSPEC_CONFIG_PATH = Path("openspec/config.yaml")
45
48
  OPENSPEC_SCHEMA_ROOT = Path("openspec/schemas") / OPENSPEC_SCHEMA_NAME
46
49
  OPENSPEC_SURFACE_OVERLAY_START = (
@@ -417,6 +420,17 @@ def validate_npm_package(errors: list[str]) -> None:
417
420
  # Path expressions rooted at a tree the retirement check above requires to be
418
421
  # absent. `src/core` and `src/skills` are live, so only the retired `src`
419
422
  # children are listed.
423
+ # Keel subcommands that write. A scenario may point read-only ones at the
424
+ # repository root; these need a fixture.
425
+ MUTATING_KEEL_COMMANDS = (
426
+ "--install",
427
+ "--init",
428
+ "--uninstall",
429
+ "--clear",
430
+ "--update",
431
+ "--with-git-hooks",
432
+ )
433
+
420
434
  RETIRED_PATH_EXPRESSIONS = (
421
435
  r'ROOT\s*/\s*"dist"',
422
436
  r'ROOT\s*/\s*"src"\s*/\s*"assets"',
@@ -458,6 +472,29 @@ def validate_paths(errors: list[str]) -> None:
458
472
  f"retired custom distribution path must be removed: {retired}"
459
473
  )
460
474
 
475
+ validator_source = (ROOT / "scripts" / "validate_plugin.py").read_text(
476
+ encoding="utf-8"
477
+ )
478
+
479
+ # A scenario that writes to the repository it validates can satisfy the very
480
+ # condition another check asserts, and a check whose input its own run
481
+ # produces cannot fail. Reads against ROOT are fine and common; writes are
482
+ # not. Keyed on the mutating subcommand rather than on ROOT itself, so
483
+ # `--version`, `--doctor`, and the gates stay legal.
484
+ for line_number, line in enumerate(validator_source.splitlines(), start=1):
485
+ invocation = re.search(r"run_(?:keel|install)\(\s*ROOT\s*,([^)]*)", line)
486
+ if not invocation:
487
+ continue
488
+ if any(
489
+ re.search(rf'"{command}"', invocation.group(1))
490
+ for command in MUTATING_KEEL_COMMANDS
491
+ ):
492
+ errors.append(
493
+ "a scenario must not run a mutating Keel command against the "
494
+ "repository it validates; build a fixture instead: "
495
+ f"scripts/validate_plugin.py:{line_number}: {line.strip()}"
496
+ )
497
+
461
498
  # Every Keel marker that carries a version is a shipped claim about which
462
499
  # version this is. Derive the set from the markers that exist rather than a
463
500
  # fixed list, because a fixed list is the next thing to fall behind — which
@@ -484,9 +521,6 @@ def validate_paths(errors: list[str]) -> None:
484
521
  # directory yields no error, so the check reports success forever. Naming a
485
522
  # retired tree in a string literal is fine — that is how the checks above
486
523
  # state what must not exist; building a Path into one is not.
487
- validator_source = (ROOT / "scripts" / "validate_plugin.py").read_text(
488
- encoding="utf-8"
489
- )
490
524
  for line_number, line in enumerate(validator_source.splitlines(), start=1):
491
525
  if any(re.search(pattern, line) for pattern in RETIRED_PATH_EXPRESSIONS):
492
526
  errors.append(
@@ -5004,29 +5038,56 @@ def validate_source_repo_bootstrap_skip_scenario() -> int:
5004
5038
  found = managed.search(path.read_text(encoding="utf-8"))
5005
5039
  return found.group(0) if found else ""
5006
5040
 
5007
- own_agents = ROOT / "AGENTS.md"
5008
- before = block(own_agents)
5009
- if not before:
5041
+ # `is_keel_source_repo` reads exactly two signals — the package name and a
5042
+ # plugins/keel directory — so a fixture carrying both exercises the same
5043
+ # branch. Running this against the real repository used to work and used to
5044
+ # rewrite the .claude/ overlay markers as a side effect, which is how the
5045
+ # marker check ended up green on that side for the wrong reason.
5046
+ if not (ROOT / "AGENTS.md").is_file() or not block(ROOT / "AGENTS.md"):
5010
5047
  report("source-repo-bootstrap-skip: Keel's AGENTS.md has no managed block.")
5011
5048
  return 1
5012
- result = run_keel(ROOT, "--install", "--target", "claude")
5013
- if result.returncode != 0:
5014
- report("source-repo-bootstrap-skip: keel --install failed in Keel's repo.")
5015
- report((result.stderr or result.stdout).strip())
5016
- return 1
5017
- if block(own_agents) != before:
5018
- report(
5019
- "source-repo-bootstrap-skip: keel --install rewrote Keel's own "
5020
- "AGENTS.md managed block."
5021
- )
5022
- return 1
5023
- if "skip AGENTS.md" not in (result.stdout or ""):
5024
- report(
5025
- "source-repo-bootstrap-skip: the skip was silent; it must be "
5026
- "reported explicitly."
5027
- )
5028
- report((result.stdout or "").strip())
5029
- return 1
5049
+
5050
+ with tempfile.TemporaryDirectory(prefix="keel-source-repo-") as raw:
5051
+ fixture = Path(raw) / "keel"
5052
+ write_text(fixture / "package.json", json.dumps({"name": KEEL_PACKAGE_NAME}))
5053
+ write_text(fixture / "plugins/keel/.keep", "")
5054
+ own_agents = fixture / "AGENTS.md"
5055
+ write_text(own_agents, (ROOT / "AGENTS.md").read_text(encoding="utf-8"))
5056
+ before_tree = snapshot_files(fixture)
5057
+ before = block(own_agents)
5058
+
5059
+ result = run_keel(fixture, "--install", "--target", "claude")
5060
+ if result.returncode != 0:
5061
+ report("source-repo-bootstrap-skip: keel --install failed in Keel's repo.")
5062
+ report((result.stderr or result.stdout).strip())
5063
+ return 1
5064
+ if block(own_agents) != before:
5065
+ report(
5066
+ "source-repo-bootstrap-skip: keel --install rewrote Keel's own "
5067
+ "AGENTS.md managed block."
5068
+ )
5069
+ return 1
5070
+ if "skip AGENTS.md" not in (result.stdout or ""):
5071
+ report(
5072
+ "source-repo-bootstrap-skip: the skip was silent; it must be "
5073
+ "reported explicitly."
5074
+ )
5075
+ report((result.stdout or "").strip())
5076
+ return 1
5077
+ # The original defect was the missing assertion, not only the wrong
5078
+ # repository: name what the install must not have rewritten.
5079
+ rewritten = [
5080
+ name
5081
+ for name, text in before_tree.items()
5082
+ if (fixture / name).is_file()
5083
+ and (fixture / name).read_text(encoding="utf-8") != text
5084
+ ]
5085
+ if rewritten:
5086
+ report(
5087
+ "source-repo-bootstrap-skip: keel --install rewrote files it "
5088
+ "did not announce: " + ", ".join(sorted(rewritten))
5089
+ )
5090
+ return 1
5030
5091
  # A consuming project must still receive the bootstrap.
5031
5092
  with tempfile.TemporaryDirectory(prefix="keel-bootstrap-consumer-") as raw:
5032
5093
  consumer = Path(raw)