@christang/keel 5.1.2 → 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/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 = "github:TanglmChris/keel";
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",
@@ -56,11 +56,6 @@ const KEEL_SKILLS = [
56
56
  "keel-review-checklist",
57
57
  "keel-tdd-or-test-first",
58
58
  ];
59
- const ALIGNMENT_REFERENCES = [
60
- "references/web.md",
61
- "references/hardware.md",
62
- "references/hardware-dsl.md",
63
- ];
64
59
  const OPENSPEC_COMMAND_IDS = ["propose", "explore", "apply", "sync", "archive"];
65
60
  const OPENSPEC_SKILLS = [
66
61
  "openspec-propose",
@@ -86,6 +81,8 @@ Usage:
86
81
  keel project tasks [repo] --target claude [--change name] [--json]
87
82
  keel gate task-start|task-complete|change-close [repo] [--change name] [--task id] [--action sync|archive] [--base git-ref] [--no-guard] [--record] [--json]
88
83
  keel guard start|status|clear [repo] [--change name] [--task id] [--force] [--json]
84
+ keel lenses list|add [name] [repo] [--force]
85
+ keel openspec [args...]
89
86
  keel --init [repo] [--target claude|codex|opencode] [--dry-run] [--force-template-update]
90
87
  keel --install [repo] [--target claude|codex|opencode] [--dry-run] [--force-template-update]
91
88
  keel --clear [repo] [--target claude|codex|opencode] [--dry-run]
@@ -105,9 +102,7 @@ Defaults:
105
102
  Project layout:
106
103
  continuity is recomputed from OpenSpec on every invocation.
107
104
  keel/HANDOFF.md is an optional keel-handoff/v1 pointer override.
108
- Claude skills are installed under .claude/skills/keel-*.
109
- Codex skills are installed under .agents/skills/keel-*.
110
- OpenCode skills are installed under .opencode/skills/keel-*.
105
+ keel-* behavioral skills are delivered by the installed Keel plugin, not by the CLI.
111
106
  repeat keel --install to refresh project protocol files.
112
107
 
113
108
  Examples:
@@ -121,6 +116,9 @@ Examples:
121
116
  keel guard start --change my-change --task 1.1 --json
122
117
  keel guard status --json
123
118
  keel guard clear --json
119
+ keel lenses list
120
+ keel lenses add web
121
+ keel lenses add web --force
124
122
  keel --init
125
123
  keel --install
126
124
  keel --install --target codex
@@ -169,6 +167,9 @@ function parseArgs(argv) {
169
167
  noGuard: false,
170
168
  record: false,
171
169
  guardSubcommand: null,
170
+ lensesSubcommand: null,
171
+ lensName: null,
172
+ openspecArgs: [],
172
173
  force: false,
173
174
  projectionEvent: null,
174
175
  authorizations: [],
@@ -207,6 +208,15 @@ function parseArgs(argv) {
207
208
  parsed.action = "guard";
208
209
  continue;
209
210
  }
211
+ if (arg === "lenses" && parsed.action === null && parsed.repo === null) {
212
+ parsed.action = "lenses";
213
+ continue;
214
+ }
215
+ if (arg === "openspec" && parsed.action === null && parsed.repo === null) {
216
+ parsed.action = "openspec";
217
+ parsed.openspecArgs = argv.slice(index + 1);
218
+ break;
219
+ }
210
220
  if (arg === "--force") {
211
221
  parsed.force = true;
212
222
  continue;
@@ -368,8 +378,8 @@ function parseArgs(argv) {
368
378
  if (arg === "--profile" || arg.startsWith("--profile=")) {
369
379
  fail(
370
380
  "--profile is no longer supported: web, hardware, and hardware-dsl "
371
- + "guidance is bundled with the keel-align-expectations skill as "
372
- + "on-demand references"
381
+ + "guidance is now user-authored lenses in keel/lenses/*.md "
382
+ + "(scaffold with `keel lenses add`)"
373
383
  );
374
384
  }
375
385
  if (arg === "--repo") {
@@ -423,6 +433,18 @@ function parseArgs(argv) {
423
433
  parsed.projectSubcommand = arg;
424
434
  continue;
425
435
  }
436
+ if (parsed.action === "lenses" && parsed.lensesSubcommand === null) {
437
+ parsed.lensesSubcommand = arg;
438
+ continue;
439
+ }
440
+ if (
441
+ parsed.action === "lenses"
442
+ && parsed.lensesSubcommand === "add"
443
+ && parsed.lensName === null
444
+ ) {
445
+ parsed.lensName = arg;
446
+ continue;
447
+ }
426
448
  if (parsed.repo !== null) {
427
449
  fail("repo path was provided more than once");
428
450
  }
@@ -486,8 +508,25 @@ function parseArgs(argv) {
486
508
  if (parsed.action !== "guard" && parsed.guardSubcommand !== null) {
487
509
  fail("guard subcommands apply only to keel guard");
488
510
  }
489
- if (parsed.force && parsed.action !== "guard") {
490
- fail("--force applies only to keel guard start");
511
+ if (
512
+ parsed.force
513
+ && parsed.action !== "guard"
514
+ && !(parsed.action === "lenses" && parsed.lensesSubcommand === "add")
515
+ ) {
516
+ fail("--force applies only to keel guard start or keel lenses add");
517
+ }
518
+ if (parsed.action === "lenses") {
519
+ if (!["list", "add"].includes(parsed.lensesSubcommand || "")) {
520
+ fail("lenses requires list or add");
521
+ }
522
+ if (parsed.lensesSubcommand === "add" && !parsed.lensName) {
523
+ fail("keel lenses add requires a lens name");
524
+ }
525
+ if (parsed.lensesSubcommand === "list" && parsed.lensName) {
526
+ fail("keel lenses list does not take a lens name");
527
+ }
528
+ } else if (parsed.lensesSubcommand !== null || parsed.lensName !== null) {
529
+ fail("lens subcommands apply only to keel lenses");
491
530
  }
492
531
  if (parsed.noGuard && parsed.action !== "gate") {
493
532
  fail("--no-guard applies only to keel gate task-start");
@@ -983,6 +1022,7 @@ function keelOpenSpecOverlay(action) {
983
1022
  "- The current agent reviews all subagent output, command evidence, and diffs before marking any task complete.",
984
1023
  "- When implementation exposes a material expectation, acceptance boundary, or user-owned decision absent from durable authority, stop before implementing that choice, rerun `keel-align-expectations`, and reauthor the affected proposal/design/spec/task authority first.",
985
1024
  "- A discovered repository fact that does not change accepted behavior or scope may be recorded and execution continues inside the existing task boundary without a product interview.",
1025
+ "- Invoke OpenSpec through `keel openspec` (for example `keel openspec validate`); a bare `openspec` command may not be on PATH.",
986
1026
  ]
987
1027
  : [
988
1028
  "- The current agent owns final sync/archive decisions and must verify task evidence, follow-up ownership, and completion gates before proceeding.",
@@ -990,6 +1030,9 @@ function keelOpenSpecOverlay(action) {
990
1030
  "- Target-native subagents may help with bounded assessment or evidence production only; they cannot archive, sync, change acceptance, or bypass completion gates.",
991
1031
  "- The current agent reviews any subagent report before running `openspec-sync-specs`, `/opsx:sync`, or `/opsx:archive`.",
992
1032
  "- Do not treat generic OpenSpec archive delegation language as authority to transfer Keel ownership.",
1033
+ "- Invoke OpenSpec through `keel openspec` (for example `keel openspec validate`); a bare `openspec` command may not be on PATH.",
1034
+ "- When `/opsx:sync` has already promoted the change's spec delta, run the archive with `--skip-specs` so the promoted delta is not re-applied; archive is not idempotent over an already-synced delta.",
1035
+ "- After archiving, run `keel guard clear` to drop the change's guard manifest; the read-only gate never clears it for you.",
993
1036
  ];
994
1037
 
995
1038
  const lines = [
@@ -1174,6 +1217,12 @@ function printTargetSurface(repo, target) {
1174
1217
  formatCount(openspecSkillCounts, openspecSkillRoot)
1175
1218
  );
1176
1219
 
1220
+ printDoctorLine(
1221
+ "Keel behavioral skills",
1222
+ "plugin",
1223
+ "keel-* skills are delivered by the installed Keel plugin (see native plugin status above); install the plugin if it is missing"
1224
+ );
1225
+
1177
1226
  const commands = commandSurfaceForTarget(target, repo);
1178
1227
  const commandCounts = countExisting(commands.paths);
1179
1228
  const commandDetail =
@@ -1226,11 +1275,27 @@ function runDoctor(options) {
1226
1275
  );
1227
1276
 
1228
1277
  const openspec = findOpenSpecCommand();
1229
- printDoctorLine(
1230
- "openspec",
1231
- openspec ? "ok" : "missing",
1232
- openspec || "reinstall keel so npm installs its OpenSpec dependency"
1233
- );
1278
+ if (!openspec) {
1279
+ printDoctorLine(
1280
+ "openspec",
1281
+ "missing",
1282
+ "reinstall keel so npm installs its OpenSpec dependency"
1283
+ );
1284
+ } else {
1285
+ const bareOpenSpecOnPath =
1286
+ !path.isAbsolute(openspec)
1287
+ || runCommand("openspec", ["--version"], {
1288
+ stdio: "ignore",
1289
+ silentNotFound: true,
1290
+ }) === 0;
1291
+ printDoctorLine(
1292
+ "openspec",
1293
+ bareOpenSpecOnPath ? "ok" : "warning",
1294
+ bareOpenSpecOnPath
1295
+ ? openspec
1296
+ : `${openspec} is keel-resolvable but bare \`openspec\` is not on PATH — use \`keel openspec\``
1297
+ );
1298
+ }
1234
1299
 
1235
1300
  process.stdout.write("\nProject status:\n");
1236
1301
  const checkStatus = runPython(
@@ -1257,15 +1322,142 @@ function runDoctor(options) {
1257
1322
  }
1258
1323
 
1259
1324
  printTargetSurface(repo, options.target);
1325
+ printLensSurface(repo, options.target);
1260
1326
 
1261
1327
  return checkStatus;
1262
1328
  }
1263
1329
 
1330
+ const SHIPPED_LENS_DIR = path.join(PACKAGE_ROOT, "assets", "lenses");
1331
+ const EXPECTED_LENS_TEMPLATES = ["web", "hardware", "hardware-dsl"];
1332
+
1333
+ function targetSkillsDir(repo, target) {
1334
+ if (target === "codex") return path.join(repo, ".agents", "skills");
1335
+ if (target === "opencode") return path.join(repo, ".opencode", "skills");
1336
+ return path.join(repo, ".claude", "skills");
1337
+ }
1338
+
1339
+ function legacyProfileSkills(repo, target) {
1340
+ try {
1341
+ return fs
1342
+ .readdirSync(targetSkillsDir(repo, target), { withFileTypes: true })
1343
+ .filter(
1344
+ (entry) => entry.isDirectory() && /^keel-profile-/.test(entry.name)
1345
+ )
1346
+ .map((entry) => entry.name)
1347
+ .sort();
1348
+ } catch (error) {
1349
+ return [];
1350
+ }
1351
+ }
1352
+
1353
+ function printLensSurface(repo, target) {
1354
+ process.stdout.write("\nDomain lens surface:\n");
1355
+ const shipped = lensNames(SHIPPED_LENS_DIR);
1356
+ const missing = EXPECTED_LENS_TEMPLATES.filter(
1357
+ (name) => !shipped.includes(name)
1358
+ );
1359
+ printDoctorLine(
1360
+ "lens templates",
1361
+ missing.length === 0 ? "ok" : "incomplete",
1362
+ missing.length === 0
1363
+ ? `shipped: ${EXPECTED_LENS_TEMPLATES.join(", ")}; scaffold with keel lenses add`
1364
+ : `missing template(s): ${missing.join(", ")}`
1365
+ );
1366
+ const installed = lensNames(path.join(repo, "keel", "lenses"));
1367
+ printDoctorLine(
1368
+ "installed lenses",
1369
+ installed.length > 0 ? "ok" : "none",
1370
+ installed.length > 0
1371
+ ? `keel/lenses/: ${installed.join(", ")}`
1372
+ : "no user lenses yet; keel lenses add scaffolds one"
1373
+ );
1374
+ const legacy = legacyProfileSkills(repo, target);
1375
+ if (legacy.length > 0) {
1376
+ printDoctorLine(
1377
+ "legacy profiles",
1378
+ "migrate",
1379
+ `found ${legacy.join(", ")}; v3 keel-profile-* skills are replaced by `
1380
+ + "pluggable lenses (keel lenses add). Left untouched; not active state."
1381
+ );
1382
+ }
1383
+ }
1384
+
1385
+ function lensNames(dir) {
1386
+ try {
1387
+ return fs
1388
+ .readdirSync(dir)
1389
+ .filter((name) => name.endsWith(".md"))
1390
+ .map((name) => name.slice(0, -3))
1391
+ .sort();
1392
+ } catch (error) {
1393
+ return [];
1394
+ }
1395
+ }
1396
+
1397
+ function runLensesList(repo) {
1398
+ const shipped = lensNames(SHIPPED_LENS_DIR);
1399
+ const installed = lensNames(path.join(repo, "keel", "lenses"));
1400
+ process.stdout.write("Shipped lens templates (assets/lenses/):\n");
1401
+ if (shipped.length === 0) {
1402
+ process.stdout.write(" (none)\n");
1403
+ } else {
1404
+ for (const name of shipped) {
1405
+ const mark = installed.includes(name) ? " (installed)" : "";
1406
+ process.stdout.write(` ${name}${mark}\n`);
1407
+ }
1408
+ }
1409
+ process.stdout.write("\nInstalled lenses (keel/lenses/):\n");
1410
+ if (installed.length === 0) {
1411
+ process.stdout.write(" (none) — run keel lenses add <name>\n");
1412
+ } else {
1413
+ for (const name of installed) {
1414
+ const mark = shipped.includes(name) ? "" : " (custom)";
1415
+ process.stdout.write(` ${name}${mark}\n`);
1416
+ }
1417
+ }
1418
+ return 0;
1419
+ }
1420
+
1421
+ function runLensesAdd(repo, name, force) {
1422
+ const source = path.join(SHIPPED_LENS_DIR, `${name}.md`);
1423
+ if (!fs.existsSync(source)) {
1424
+ const available = lensNames(SHIPPED_LENS_DIR).join(", ") || "(none)";
1425
+ fail(`unknown lens template: ${name}; shipped templates: ${available}`);
1426
+ }
1427
+ const destDir = path.join(repo, "keel", "lenses");
1428
+ const dest = path.join(destDir, `${name}.md`);
1429
+ if (fs.existsSync(dest) && !force) {
1430
+ process.stderr.write(
1431
+ `keel: keel/lenses/${name}.md already exists; pass --force to overwrite\n`
1432
+ );
1433
+ return 3;
1434
+ }
1435
+ fs.mkdirSync(destDir, { recursive: true });
1436
+ fs.copyFileSync(source, dest);
1437
+ process.stdout.write(
1438
+ `keel: wrote keel/lenses/${name}.md from the ${name} template; `
1439
+ + "edit it to fit this repository\n"
1440
+ );
1441
+ return 0;
1442
+ }
1443
+
1264
1444
  function runAction(options) {
1265
1445
  if (options.updateSource !== null && options.action !== "update") {
1266
1446
  fail("--source only applies to --update");
1267
1447
  }
1268
1448
 
1449
+ if (options.action === "openspec") {
1450
+ const openspec = findOpenSpecCommand();
1451
+ if (!openspec) {
1452
+ process.stderr.write(
1453
+ "keel: openspec is not resolvable; reinstall keel so npm installs "
1454
+ + "its OpenSpec dependency\n"
1455
+ );
1456
+ return 1;
1457
+ }
1458
+ return runCommand(openspec, options.openspecArgs, { stdio: "inherit" });
1459
+ }
1460
+
1269
1461
  if (options.action === "context") {
1270
1462
  if (options.dryRun || options.forceTemplateUpdate || options.updateSource) {
1271
1463
  fail("context does not accept install or update options");
@@ -1351,6 +1543,17 @@ function runAction(options) {
1351
1543
  : 3;
1352
1544
  }
1353
1545
 
1546
+ if (options.action === "lenses") {
1547
+ if (options.dryRun || options.forceTemplateUpdate || options.updateSource) {
1548
+ fail("lenses does not accept install or update options");
1549
+ }
1550
+ const repo = path.resolve(options.repo || process.cwd());
1551
+ if (options.lensesSubcommand === "list") {
1552
+ return runLensesList(repo);
1553
+ }
1554
+ return runLensesAdd(repo, options.lensName, options.force);
1555
+ }
1556
+
1354
1557
  if (options.action === "capabilities") {
1355
1558
  if (options.dryRun || options.forceTemplateUpdate || options.updateSource) {
1356
1559
  fail("capabilities does not accept install or update options");
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.1.2",
5
+ "version": "5.2.1",
6
6
  "license": "MIT",
7
7
  "repository": {
8
8
  "type": "git",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "keel",
3
- "version": "5.1.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.1.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",
@@ -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();