@christang/keel 5.76.0 → 5.78.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.
@@ -1,4 +1,4 @@
1
- <!-- keel:start version=5.76.0 -->
1
+ <!-- keel:start version=5.78.0 -->
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/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
- const openspecTools = openspecToolsForTarget(options.target);
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 --init --target ${target} to bring the `
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"
@@ -1196,9 +1277,34 @@ function skillRootForTarget(target) {
1196
1277
  return path.join(".opencode", "skills");
1197
1278
  }
1198
1279
 
1199
- function openspecSkillRootForTarget(target) {
1280
+ // Which OpenSpec layout a Codex repository carries (#169). OpenSpec 1.6 wrote
1281
+ // Codex's skills under `.codex/skills` and its commands as prompts in
1282
+ // CODEX_HOME; 1.13, which Keel pins, writes skills under `.agents/skills` and
1283
+ // no command files. A repository set up under 1.6 keeps its layout, and every
1284
+ // other one — a fresh repository included — gets the one the pin writes.
1285
+ function codexOpenSpecLayout(repo) {
1286
+ const holdsOpenSpec = (root) => {
1287
+ try {
1288
+ return fs
1289
+ .readdirSync(path.join(repo, root))
1290
+ .some((entry) => entry.startsWith("openspec-"));
1291
+ } catch {
1292
+ return false;
1293
+ }
1294
+ };
1295
+ return holdsOpenSpec(path.join(".codex", "skills"))
1296
+ && !holdsOpenSpec(path.join(".agents", "skills"))
1297
+ ? "legacy"
1298
+ : "agents";
1299
+ }
1300
+
1301
+ function openspecSkillRootForTarget(target, repo) {
1200
1302
  if (target === "claude") return path.join(".claude", "skills");
1201
- if (target === "codex") return path.join(".codex", "skills");
1303
+ if (target === "codex") {
1304
+ return codexOpenSpecLayout(repo) === "legacy"
1305
+ ? path.join(".codex", "skills")
1306
+ : path.join(".agents", "skills");
1307
+ }
1202
1308
  return path.join(".opencode", "skills");
1203
1309
  }
1204
1310
 
@@ -1213,6 +1319,16 @@ function commandSurfaceForTarget(target, repo) {
1213
1319
  remediation: "run keel --init --target claude or openspec update --force",
1214
1320
  };
1215
1321
  }
1322
+ if (target === "codex" && codexOpenSpecLayout(repo) === "agents") {
1323
+ // OpenSpec 1.13 surfaces Codex's workflows as the skills counted on the
1324
+ // line above and writes no command file, so there is nothing to count here.
1325
+ return {
1326
+ location: path.join(".agents", "skills"),
1327
+ paths: [],
1328
+ remediation: "run keel --init --target codex or openspec update --force",
1329
+ skillsOnly: true,
1330
+ };
1331
+ }
1216
1332
  if (target === "codex") {
1217
1333
  const home = codexHome();
1218
1334
  const location = path.join(home, "prompts");
@@ -1239,13 +1355,15 @@ function commandPathForAction(target, repo, action) {
1239
1355
  return path.join(repo, ".claude", "commands", "opsx", `${action}.md`);
1240
1356
  }
1241
1357
  if (target === "codex") {
1242
- return path.join(codexHome(), "prompts", `opsx-${action}.md`);
1358
+ return codexOpenSpecLayout(repo) === "legacy"
1359
+ ? path.join(codexHome(), "prompts", `opsx-${action}.md`)
1360
+ : null;
1243
1361
  }
1244
1362
  return path.join(repo, ".opencode", "commands", `opsx-${action}.md`);
1245
1363
  }
1246
1364
 
1247
1365
  function openspecOverlaySurfacesForTarget(target, repo) {
1248
- const skillRoot = openspecSkillRootForTarget(target);
1366
+ const skillRoot = openspecSkillRootForTarget(target, repo);
1249
1367
  return OPENSPEC_OVERLAY_ACTIONS.flatMap((action) => {
1250
1368
  if (action === "propose" && target === "opencode") {
1251
1369
  return [];
@@ -1265,7 +1383,7 @@ function openspecOverlaySurfacesForTarget(target, repo) {
1265
1383
  action,
1266
1384
  path: commandPathForAction(target, repo, action),
1267
1385
  },
1268
- ];
1386
+ ].filter((surface) => surface.path !== null);
1269
1387
  });
1270
1388
  }
1271
1389
 
@@ -1618,7 +1736,7 @@ function printTargetSurface(repo, target) {
1618
1736
  }
1619
1737
  }
1620
1738
 
1621
- const openspecSkillRoot = openspecSkillRootForTarget(target);
1739
+ const openspecSkillRoot = openspecSkillRootForTarget(target, repo);
1622
1740
  const openspecSkillPaths = OPENSPEC_SKILLS.map((skill) =>
1623
1741
  path.join(repo, openspecSkillRoot, skill, "SKILL.md")
1624
1742
  );
@@ -1641,16 +1759,25 @@ function printTargetSurface(repo, target) {
1641
1759
  );
1642
1760
 
1643
1761
  const commands = commandSurfaceForTarget(target, repo);
1644
- const commandCounts = countExisting(commands.paths);
1645
- const commandDetail =
1646
- surfaceStatus(commandCounts) === "ok"
1647
- ? formatCount(commandCounts, commands.location)
1648
- : `${formatCount(commandCounts, commands.location)}; ${commands.remediation}`;
1649
- printDoctorLine(
1650
- "OpenSpec commands",
1651
- surfaceStatus(commandCounts),
1652
- commandDetail
1653
- );
1762
+ if (commands.skillsOnly) {
1763
+ printDoctorLine(
1764
+ "OpenSpec commands",
1765
+ "ok",
1766
+ `none; OpenSpec 1.13 surfaces Codex's workflows as the skills under `
1767
+ + `${commands.location}`
1768
+ );
1769
+ } else {
1770
+ const commandCounts = countExisting(commands.paths);
1771
+ const commandDetail =
1772
+ surfaceStatus(commandCounts) === "ok"
1773
+ ? formatCount(commandCounts, commands.location)
1774
+ : `${formatCount(commandCounts, commands.location)}; ${commands.remediation}`;
1775
+ printDoctorLine(
1776
+ "OpenSpec commands",
1777
+ surfaceStatus(commandCounts),
1778
+ commandDetail
1779
+ );
1780
+ }
1654
1781
 
1655
1782
  const overlayPaths = openspecOverlaySurfacesForTarget(target, repo).map(
1656
1783
  (surface) => surface.path
@@ -1752,6 +1879,22 @@ function runDoctor(options) {
1752
1879
  );
1753
1880
  }
1754
1881
 
1882
+ const surfaces = surfaceGeneratorVersion(repo);
1883
+ const running = openspec ? openspecReportedVersion(openspec) : null;
1884
+ if (surfaces && running) {
1885
+ const newer = compareVersions(surfaces, running) > 0;
1886
+ printDoctorLine(
1887
+ "OpenSpec surfaces",
1888
+ newer ? "warning" : "ok",
1889
+ newer
1890
+ ? `written by OpenSpec ${surfaces}, newer than the OpenSpec Keel runs `
1891
+ + `(${running}); keel --init leaves them unrewritten rather than `
1892
+ + `downgrading them, and keel --install --target ${options.target} `
1893
+ + "refreshes the protocol without touching them"
1894
+ : `written by OpenSpec ${surfaces}; Keel runs ${running}`
1895
+ );
1896
+ }
1897
+
1755
1898
  printProtocolVersionDrift(repo, options.target);
1756
1899
 
1757
1900
  process.stdout.write("\nProject status:\n");