@christang/keel 5.1.1 → 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.
Files changed (33) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +114 -158
  3. package/README.zh-CN.md +118 -197
  4. package/assets/bootstrap/AGENTS.md +1 -1
  5. package/{plugins/keel/skills/keel-align-expectations/references → assets/lenses}/hardware-dsl.md +4 -2
  6. package/{plugins/keel/skills/keel-align-expectations/references → assets/lenses}/hardware.md +4 -2
  7. package/{plugins/keel/skills/keel-align-expectations/references → assets/lenses}/web.md +4 -2
  8. package/assets/openspec/schemas/keel-spec-driven/schema.yaml +172 -166
  9. package/assets/openspec/schemas/keel-spec-driven/templates/tasks.md +13 -4
  10. package/bin/keel.js +218 -15
  11. package/package.json +1 -1
  12. package/plugins/keel/.claude-plugin/plugin.json +1 -1
  13. package/plugins/keel/.codex-plugin/plugin.json +1 -1
  14. package/plugins/keel/hooks/hooks.json +30 -30
  15. package/plugins/keel/scripts/pretooluse-guard.js +156 -156
  16. package/plugins/keel/scripts/session-start.js +182 -182
  17. package/plugins/keel/skills/keel-align-expectations/SKILL.md +2 -6
  18. package/plugins/keel/skills/keel-debug-failure/SKILL.md +2 -2
  19. package/plugins/keel/skills/keel-review-checklist/SKILL.md +2 -2
  20. package/plugins/keel/skills/keel-tdd-or-test-first/SKILL.md +2 -2
  21. package/scripts/bump_version.js +140 -0
  22. package/scripts/install_to_repo.py +0 -70
  23. package/scripts/run_python.js +63 -63
  24. package/scripts/validate_plugin.py +408 -96
  25. package/src/core/capabilities.js +291 -291
  26. package/src/core/context.js +521 -514
  27. package/src/core/gates.js +664 -643
  28. package/src/core/goal.js +230 -230
  29. package/src/core/guard.js +295 -295
  30. package/src/core/helper.js +319 -319
  31. package/src/core/projection.js +195 -195
  32. package/src/core/task-contract.js +757 -736
  33. package/src/core/tasksview.js +123 -123
@@ -40,13 +40,9 @@ Accepted alignment routes to existing OpenSpec owners; create no separate alignm
40
40
  - specs own observable requirements and positive/negative/edge/failure scenarios.
41
41
  - tasks.md owns Covers, verification strategy and checks, scope, and stop boundaries that reference the accepted authority instead of duplicating chat prose.
42
42
 
43
- ## Domain references
43
+ ## Domain lenses
44
44
 
45
- When the change touches a specific domain, read only the applicable reference before asking domain questions; do not load the others:
46
-
47
- - references/web.md for UI, API, routing, auth/session, persistence, or backend integration work.
48
- - references/hardware.md for Verilog/SystemVerilog interface, protocol, reset, or verification work.
49
- - references/hardware-dsl.md for hardware modeling DSL, generated RTL, or golden/equivalence work.
45
+ When the change signals a specific domain, look in `keel/lenses/` for a lens whose `Applies when:` header matches, and read only that lens before asking domain questions; do not load unrelated lenses. When no lens matches, or the repo defines none, proceed on the domain-agnostic path. Lenses are user-authored; scaffold the bundled starting points with `keel lenses add` (web, hardware, hardware-dsl).
50
46
 
51
47
  ## Boundaries
52
48
 
@@ -12,9 +12,9 @@ Use this skill when a command or validation path fails. Keep the investigation t
12
12
 
13
13
  Read the selected OpenSpec task's Commands, Acceptance, Coupling, Candidate Boundary, Autonomy boundary, Stop Rules, Evidence, Stop if, Read, Touch, and Mode fields. When `Coupling: required`, also read design.md's Coupled Iteration Contract. Read the failed command output and any repository files needed to reproduce or explain the failure.
14
14
 
15
- ## Domain reference
15
+ ## Domain lenses
16
16
 
17
- When the change's artifacts or the failing surface signal a supported domain (web, hardware, hardware-dsl), consult the matching reference's `Execution and review checks` section under `keel-align-expectations/references/` before locking a root-cause hypothesis only the matching reference, never all of them. When no domain signal exists, load nothing.
17
+ When the change's artifacts or the failing surface signal a domain, consult the matching lens's `Execution and review checks` section from `keel/lenses/` — the lens whose `Applies when:` header matches — before locking a root-cause hypothesis, and load only that one. When no lens matches, load nothing.
18
18
 
19
19
  ## Fuse
20
20
 
@@ -35,9 +35,9 @@ Record the current agent's judgment inside the selected task Evidence:
35
35
 
36
36
  The Review remains in tasks.md. A user-facing Report summarizes delivery but is not hidden gate state. Do not let Core or this checklist write evidence automatically.
37
37
 
38
- ## Domain reference
38
+ ## Domain lenses
39
39
 
40
- When the change's artifacts or Touch extensions signal a supported domain (web, hardware, hardware-dsl), consult the matching reference's `Execution and review checks` section under `keel-align-expectations/references/` before concluding the reviewonly the matching reference, never all of them. When no domain signal exists, load nothing.
40
+ When the change's artifacts or Touch extensions signal a domain, consult the matching lens's `Execution and review checks` section from `keel/lenses/` the lens whose `Applies when:` header matches before concluding the review, and load only that one. When no lens matches, load nothing.
41
41
 
42
42
  ## Expectation and follow-up ownership
43
43
 
@@ -24,9 +24,9 @@ Read the selected task's compiled capsule: resolved Acceptance, Verify strategy
24
24
 
25
25
  Red-green strategies (`vertical-tdd`, `regression-first`) must record concrete per-label `.red` and `.green` Evidence entries for the same check; `keel gate task-complete` rejects absent or pending entries.
26
26
 
27
- ## Domain reference
27
+ ## Domain lenses
28
28
 
29
- When the change's proposal/design/specs or the task's Touch extensions signal a supported domain (web, hardware, hardware-dsl), consult the matching reference's `Execution and review checks` section under `keel-align-expectations/references/` before finalizing the strategy and the first check only the matching reference, never all of them. When no domain signal exists, load nothing.
29
+ When the change's proposal/design/specs or the task's Touch extensions signal a domain, consult the matching lens's `Execution and review checks` section from `keel/lenses/` — the lens whose `Applies when:` header matches — before finalizing the strategy and the first check, and load only that one. When no lens matches, load nothing.
30
30
 
31
31
  ## Coupled-task preflight
32
32
 
@@ -0,0 +1,140 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ // One-shot version bump across every place Keel pins its version.
5
+ //
6
+ // The Keel validation suite requires the same version in package.json,
7
+ // package-lock.json, both native plugin manifests, the validator constants,
8
+ // the protocol docs, and the changelog. This script updates all of them
9
+ // together so a release never ships half-aligned.
10
+ //
11
+ // Usage:
12
+ // node scripts/bump_version.js <patch|minor|major|explicit-version>
13
+ //
14
+ // After running: fill in the CHANGELOG entry, then `npm test`, commit,
15
+ // tag `vX.Y.Z`, push, and publish a GitHub Release.
16
+
17
+ const fs = require("fs");
18
+ const path = require("path");
19
+
20
+ const ROOT = path.resolve(__dirname, "..");
21
+ const PKG_PATH = path.join(ROOT, "package.json");
22
+ const LOCK_PATH = path.join(ROOT, "package-lock.json");
23
+ const CHANGELOG_PATH = path.join(ROOT, "keel", "CHANGELOG.md");
24
+ const CHANGELOG_HEADER = "# Keel Changelog\n\n";
25
+ const SEMVER_RE = /^(\d+)\.(\d+)\.(\d+)$/;
26
+
27
+ function fail(message) {
28
+ process.stderr.write(`bump-version: ${message}\n`);
29
+ process.exit(1);
30
+ }
31
+
32
+ function resolveNewVersion(current, target) {
33
+ if (target === "patch" || target === "minor" || target === "major") {
34
+ const [, major, minor, patch] = current.match(SEMVER_RE).map(Number);
35
+ if (target === "major") return `${major + 1}.0.0`;
36
+ if (target === "minor") return `${major}.${minor + 1}.0`;
37
+ return `${major}.${minor}.${patch + 1}`;
38
+ }
39
+ if (!SEMVER_RE.test(target)) {
40
+ fail(`not a patch|minor|major keyword or an X.Y.Z version: ${target}`);
41
+ }
42
+ return target;
43
+ }
44
+
45
+ function writeJson(filePath, data) {
46
+ fs.writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`);
47
+ }
48
+
49
+ function bumpPackageFiles(newVersion) {
50
+ const pkg = JSON.parse(fs.readFileSync(PKG_PATH, "utf8"));
51
+ pkg.version = newVersion;
52
+ writeJson(PKG_PATH, pkg);
53
+ process.stdout.write(" updated package.json\n");
54
+
55
+ const lock = JSON.parse(fs.readFileSync(LOCK_PATH, "utf8"));
56
+ lock.version = newVersion;
57
+ if (lock.packages && lock.packages[""]) {
58
+ lock.packages[""].version = newVersion;
59
+ }
60
+ writeJson(LOCK_PATH, lock);
61
+ process.stdout.write(" updated package-lock.json\n");
62
+ }
63
+
64
+ function replaceInFile(relPath, replacements) {
65
+ const filePath = path.join(ROOT, relPath);
66
+ let content = fs.readFileSync(filePath, "utf8");
67
+ for (const [from, to] of replacements) {
68
+ if (!content.includes(from)) {
69
+ fail(`expected to find ${JSON.stringify(from)} in ${relPath}`);
70
+ }
71
+ content = content.split(from).join(to);
72
+ }
73
+ fs.writeFileSync(filePath, content);
74
+ process.stdout.write(` updated ${relPath}\n`);
75
+ }
76
+
77
+ function prependChangelogEntry(newVersion) {
78
+ let content = fs.readFileSync(CHANGELOG_PATH, "utf8");
79
+ if (content.includes(`## ${newVersion} `) || content.includes(`## ${newVersion}\n`)) {
80
+ process.stdout.write(` keel/CHANGELOG.md already has a ${newVersion} entry\n`);
81
+ return;
82
+ }
83
+ if (!content.startsWith(CHANGELOG_HEADER)) {
84
+ fail("keel/CHANGELOG.md does not start with the expected header");
85
+ }
86
+ const entry =
87
+ `## ${newVersion} - TODO: summarize this release\n\n` +
88
+ "- TODO: describe the change.\n" +
89
+ "- Version alignment: the npm package, both native plugin manifests, " +
90
+ `protocol docs, and this changelog share Keel ${newVersion}; the OpenSpec ` +
91
+ "dependency pin stays `^1.4.1`.\n\n";
92
+ content = CHANGELOG_HEADER + entry + content.slice(CHANGELOG_HEADER.length);
93
+ fs.writeFileSync(CHANGELOG_PATH, content);
94
+ process.stdout.write(" updated keel/CHANGELOG.md (fill in the TODO lines)\n");
95
+ }
96
+
97
+ function main() {
98
+ const target = process.argv[2];
99
+ if (!target) {
100
+ fail("usage: node scripts/bump_version.js <patch|minor|major|explicit-version>");
101
+ }
102
+
103
+ const oldVersion = JSON.parse(fs.readFileSync(PKG_PATH, "utf8")).version;
104
+ if (!SEMVER_RE.test(oldVersion)) {
105
+ fail(`current package.json version is not X.Y.Z: ${oldVersion}`);
106
+ }
107
+ const newVersion = resolveNewVersion(oldVersion, target);
108
+ process.stdout.write(`Bumping ${oldVersion} -> ${newVersion}\n`);
109
+
110
+ bumpPackageFiles(newVersion);
111
+ replaceInFile("plugins/keel/.claude-plugin/plugin.json", [
112
+ [`"version": "${oldVersion}"`, `"version": "${newVersion}"`],
113
+ ]);
114
+ replaceInFile("plugins/keel/.codex-plugin/plugin.json", [
115
+ [`"version": "${oldVersion}"`, `"version": "${newVersion}"`],
116
+ ]);
117
+ replaceInFile("scripts/validate_plugin.py", [
118
+ [`PACKAGE_VERSION = "${oldVersion}"`, `PACKAGE_VERSION = "${newVersion}"`],
119
+ [`PROTOCOL_VERSION = "${oldVersion}"`, `PROTOCOL_VERSION = "${newVersion}"`],
120
+ ]);
121
+ replaceInFile("AGENTS.md", [
122
+ [`v${oldVersion}`, `v${newVersion}`],
123
+ [`version=${oldVersion}`, `version=${newVersion}`],
124
+ ]);
125
+ replaceInFile("assets/bootstrap/AGENTS.md", [
126
+ [`version=${oldVersion}`, `version=${newVersion}`],
127
+ ]);
128
+ prependChangelogEntry(newVersion);
129
+
130
+ process.stdout.write(
131
+ `\nDone. Next:\n` +
132
+ ` 1. Edit keel/CHANGELOG.md ${newVersion} entry.\n` +
133
+ ` 2. npm test\n` +
134
+ ` 3. git commit -am "${newVersion}"\n` +
135
+ ` 4. git tag v${newVersion} && git push --follow-tags && git push origin v${newVersion}\n` +
136
+ ` 5. gh release create v${newVersion} --title v${newVersion} --notes "..."\n`
137
+ );
138
+ }
139
+
140
+ main();
@@ -51,23 +51,11 @@ KEEL_HOOK_NAME = "keel-gate"
51
51
  KEEL_HOOK_ROOT = Path(".claude") / "hooks" / KEEL_HOOK_NAME
52
52
  SUPPORTED_TARGETS = ("claude", "codex", "opencode")
53
53
  AGENT_PROTOCOL_TARGETS = {"codex", "opencode"}
54
- TARGET_SKILL_ROOTS = {
55
- "claude": Path(".claude") / "skills",
56
- "codex": Path(".agents") / "skills",
57
- "opencode": Path(".opencode") / "skills",
58
- }
59
54
  TARGET_ADAPTER_PATHS = {
60
55
  "claude": Path(".claude") / "keel" / "keel-adapter.js",
61
56
  "codex": Path(".agents") / "keel" / "keel-adapter.js",
62
57
  "opencode": Path(".opencode") / "keel" / "keel-adapter.js",
63
58
  }
64
- CORE_KEEL_SKILLS = {
65
- "keel-align-expectations",
66
- "keel-debug-failure",
67
- "keel-handoff",
68
- "keel-review-checklist",
69
- "keel-tdd-or-test-first",
70
- }
71
59
  HANDOFF_FIELDS = {"schema", "owner", "action", "reason"}
72
60
  HANDOFF_ACTIONS = {
73
61
  "discuss",
@@ -158,10 +146,6 @@ def target_set(target: str) -> set[str]:
158
146
  return set(target_names(target))
159
147
 
160
148
 
161
- def core_skill_names() -> set[str]:
162
- return set(CORE_KEEL_SKILLS)
163
-
164
-
165
149
  def file_action(relative_path: str, source_path: Path) -> InstallAction:
166
150
  if not source_path.is_file():
167
151
  raise ValueError(f"missing packaged asset: {source_path}")
@@ -233,31 +217,6 @@ def openspec_schema_actions() -> list[InstallAction]:
233
217
  return actions
234
218
 
235
219
 
236
- def skill_actions(target: str) -> list[InstallAction]:
237
- actions: list[InstallAction] = []
238
- selected_skill_names = core_skill_names()
239
- for target_name in target_names(target):
240
- skill_destination_root = TARGET_SKILL_ROOTS[target_name]
241
- skills_root = dist_asset(target_name, "skills")
242
- if skills_root.is_dir():
243
- for skill in sorted(skills_root.iterdir()):
244
- if not skill.is_dir():
245
- continue
246
- if skill.name not in selected_skill_names:
247
- continue
248
- for skill_file in sorted(skill.rglob("*")):
249
- if not skill_file.is_file():
250
- continue
251
- relative_file = skill_file.relative_to(skill).as_posix()
252
- actions.append(
253
- file_action(
254
- (skill_destination_root / skill.name / relative_file).as_posix(),
255
- skill_file,
256
- )
257
- )
258
- return actions
259
-
260
-
261
220
  def agent_actions(target: str) -> list[InstallAction]:
262
221
  actions: list[InstallAction] = []
263
222
  if "claude" not in target_set(target):
@@ -945,35 +904,6 @@ def rmdir_if_empty_action(relative_dir: str) -> PlannedAction:
945
904
  return PlannedAction("rmdir", Path(relative_dir))
946
905
 
947
906
 
948
- def plan_uninstall_skill_actions(repo: Path, target: str) -> list[PlannedAction]:
949
- actions: list[PlannedAction] = []
950
- for target_name in target_names(target):
951
- skill_root = TARGET_SKILL_ROOTS[target_name]
952
- target_actions = skill_actions(target_name)
953
- for action in target_actions:
954
- if action.source_path is None:
955
- continue
956
- actions.append(
957
- plan_uninstall_packaged_file(
958
- repo,
959
- action.relative_path.as_posix(),
960
- action.source_path,
961
- )
962
- )
963
-
964
- skill_dirs: set[Path] = set()
965
- for action in target_actions:
966
- parent = action.relative_path.parent
967
- while parent != skill_root:
968
- skill_dirs.add(parent)
969
- parent = parent.parent
970
- for skill_dir in sorted(skill_dirs, key=lambda path: len(path.parts), reverse=True):
971
- actions.append(rmdir_if_empty_action(skill_dir.as_posix()))
972
- actions.append(rmdir_if_empty_action(skill_root.as_posix()))
973
- actions.append(rmdir_if_empty_action(skill_root.parent.as_posix()))
974
- return actions
975
-
976
-
977
907
  def plan_uninstall_agent_actions(repo: Path, target: str) -> list[PlannedAction]:
978
908
  actions: list[PlannedAction] = []
979
909
  if "claude" not in target_set(target):
@@ -1,63 +1,63 @@
1
- #!/usr/bin/env node
2
- "use strict";
3
-
4
- const { spawnSync } = require("child_process");
5
-
6
- function pythonCandidates() {
7
- if (process.env.KEEL_PYTHON) {
8
- return [{ command: process.env.KEEL_PYTHON, prefixArgs: [] }];
9
- }
10
-
11
- if (process.platform === "win32") {
12
- return [
13
- { command: "py", prefixArgs: ["-3"] },
14
- { command: "python", prefixArgs: [] },
15
- { command: "python3", prefixArgs: [] },
16
- ];
17
- }
18
-
19
- return [
20
- { command: "python3", prefixArgs: [] },
21
- { command: "python", prefixArgs: [] },
22
- ];
23
- }
24
-
25
- function commandExists(candidate) {
26
- const result = spawnSync(
27
- candidate.command,
28
- [...candidate.prefixArgs, "--version"],
29
- { encoding: "utf8" }
30
- );
31
- return !result.error && result.status === 0;
32
- }
33
-
34
- function main() {
35
- const args = process.argv.slice(2);
36
- if (args.length === 0) {
37
- process.stderr.write("run_python: expected a Python script path.\n");
38
- return 2;
39
- }
40
-
41
- const candidate = pythonCandidates().find(commandExists);
42
- if (!candidate) {
43
- process.stderr.write(
44
- "run_python: could not find Python. Install python3/python or set KEEL_PYTHON.\n"
45
- );
46
- return 127;
47
- }
48
-
49
- const result = spawnSync(
50
- candidate.command,
51
- [...candidate.prefixArgs, ...args],
52
- { stdio: "inherit" }
53
- );
54
-
55
- if (result.error) {
56
- process.stderr.write(`run_python: ${result.error.message}\n`);
57
- return 127;
58
- }
59
-
60
- return result.status ?? 1;
61
- }
62
-
63
- process.exit(main());
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const { spawnSync } = require("child_process");
5
+
6
+ function pythonCandidates() {
7
+ if (process.env.KEEL_PYTHON) {
8
+ return [{ command: process.env.KEEL_PYTHON, prefixArgs: [] }];
9
+ }
10
+
11
+ if (process.platform === "win32") {
12
+ return [
13
+ { command: "py", prefixArgs: ["-3"] },
14
+ { command: "python", prefixArgs: [] },
15
+ { command: "python3", prefixArgs: [] },
16
+ ];
17
+ }
18
+
19
+ return [
20
+ { command: "python3", prefixArgs: [] },
21
+ { command: "python", prefixArgs: [] },
22
+ ];
23
+ }
24
+
25
+ function commandExists(candidate) {
26
+ const result = spawnSync(
27
+ candidate.command,
28
+ [...candidate.prefixArgs, "--version"],
29
+ { encoding: "utf8" }
30
+ );
31
+ return !result.error && result.status === 0;
32
+ }
33
+
34
+ function main() {
35
+ const args = process.argv.slice(2);
36
+ if (args.length === 0) {
37
+ process.stderr.write("run_python: expected a Python script path.\n");
38
+ return 2;
39
+ }
40
+
41
+ const candidate = pythonCandidates().find(commandExists);
42
+ if (!candidate) {
43
+ process.stderr.write(
44
+ "run_python: could not find Python. Install python3/python or set KEEL_PYTHON.\n"
45
+ );
46
+ return 127;
47
+ }
48
+
49
+ const result = spawnSync(
50
+ candidate.command,
51
+ [...candidate.prefixArgs, ...args],
52
+ { stdio: "inherit" }
53
+ );
54
+
55
+ if (result.error) {
56
+ process.stderr.write(`run_python: ${result.error.message}\n`);
57
+ return 127;
58
+ }
59
+
60
+ return result.status ?? 1;
61
+ }
62
+
63
+ process.exit(main());