@christang/keel 5.77.0 → 5.79.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/assets/bootstrap/AGENTS.md +1 -1
- package/bin/keel.js +62 -16
- package/npm-shrinkwrap.json +194 -320
- package/package.json +1 -1
- package/plugins/keel/.claude-plugin/plugin.json +1 -1
- package/plugins/keel/.codex-plugin/plugin.json +1 -1
- package/plugins/keel/scripts/session-start.js +51 -4
- package/scripts/validate_plugin.py +175 -27
- package/src/core/context.js +4 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "keel",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.79.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.79.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",
|
|
@@ -137,6 +137,44 @@ function pluginManifest() {
|
|
|
137
137
|
return { version: null, remedy: HOST_UPDATE };
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
// On Claude the plugin is the published package, cached at
|
|
141
|
+
// <plugins>/cache/<marketplace>/<plugin>/<version>, and the host records what
|
|
142
|
+
// it installed in <plugins>/installed_plugins.json (#172). Another install of
|
|
143
|
+
// this same plugin at a different version means the update already happened
|
|
144
|
+
// and this session has not reloaded it. Located from this file's own path, like
|
|
145
|
+
// the manifest; anything unexpected is no pending install, which leaves the
|
|
146
|
+
// report exactly as it was.
|
|
147
|
+
function pendingInstall(loaded) {
|
|
148
|
+
try {
|
|
149
|
+
const root = fs.realpathSync(path.resolve(__dirname, "..", "..", ".."));
|
|
150
|
+
const record = path.join(root, "..", "..", "..", "..", "installed_plugins.json");
|
|
151
|
+
const plugins = JSON.parse(fs.readFileSync(record, "utf8")).plugins;
|
|
152
|
+
const versions = new Set();
|
|
153
|
+
for (const entries of Object.values(plugins || {})) {
|
|
154
|
+
for (const entry of Array.isArray(entries) ? entries : []) {
|
|
155
|
+
if (!entry || typeof entry.installPath !== "string") continue;
|
|
156
|
+
// Compared through the real path: node loads this file through its
|
|
157
|
+
// symlinks resolved, while the host records whatever path it chose.
|
|
158
|
+
const installPath = path.resolve(entry.installPath);
|
|
159
|
+
let parent;
|
|
160
|
+
try {
|
|
161
|
+
parent = fs.realpathSync(path.dirname(installPath));
|
|
162
|
+
} catch {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (parent !== path.dirname(root)) continue;
|
|
166
|
+
if (path.join(parent, path.basename(installPath)) === root) continue;
|
|
167
|
+
if (typeof entry.version === "string") versions.add(entry.version.trim());
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (versions.size !== 1) return null;
|
|
171
|
+
const [version] = versions;
|
|
172
|
+
return version && version !== loaded ? version : null;
|
|
173
|
+
} catch {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
140
178
|
// The repository states which protocol it runs in the managed block the
|
|
141
179
|
// installer wrote. AGENTS.md is the canonical carrier; CLAUDE.md is read second
|
|
142
180
|
// because a repository may carry only the target-native file.
|
|
@@ -193,16 +231,25 @@ function versionReport(cwd, cli, pathCli = null) {
|
|
|
193
231
|
// The host puts the user's PATH ahead of plugin `bin/` directories, so a
|
|
194
232
|
// global install is what the agent's own `keel` commands run even though
|
|
195
233
|
// this hook ran the plugin's. Both remedies are named; neither is run.
|
|
196
|
-
const
|
|
234
|
+
const pending = pendingInstall(plugin.version);
|
|
235
|
+
// With an installed update pending, the PATH copy is judged against what
|
|
236
|
+
// the reload will load: one that matches it shadows nothing afterwards, and
|
|
237
|
+
// aligning with the loaded plugin would be a downgrade (#172).
|
|
238
|
+
const target = pending || cli;
|
|
239
|
+
const shadow = pathCli && pathCli !== target
|
|
197
240
|
? ` The \`keel\` on PATH (${pathCli}) shadows this plugin's CLI for the `
|
|
198
241
|
+ "agent's commands, because the host puts your PATH first: remove it "
|
|
199
242
|
+ "with `npm rm -g @christang/keel`, since the plugin carries its own, "
|
|
200
|
-
+ `or align it with \`npm i -g @christang/keel@${
|
|
243
|
+
+ `or align it with \`npm i -g @christang/keel@${target}\`.`
|
|
201
244
|
: "";
|
|
245
|
+
// An update the host already installed needs only the reload (#172), so
|
|
246
|
+
// naming the update command there sends the reader to what already happened.
|
|
247
|
+
const remedy = pending
|
|
248
|
+
? `The host has already installed plugin ${pending}, so nothing needs updating.`
|
|
249
|
+
: `Updating is ${plugin.remedy}, which Keel names and does not run.`;
|
|
202
250
|
return `runtime versions disagree: ${named}${missing}. A session's hooks are `
|
|
203
251
|
+ "fixed when it loads the plugin, so an updated plugin applies after "
|
|
204
|
-
+
|
|
205
|
-
+ `${plugin.remedy}, which Keel names and does not run.${shadow}`;
|
|
252
|
+
+ `\`/reload-plugins\` or at the next session start. ${remedy}${shadow}`;
|
|
206
253
|
}
|
|
207
254
|
|
|
208
255
|
// The `keel` a bare command resolves, asked only when this hook ran its own
|
|
@@ -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.79.0"
|
|
42
|
+
PROTOCOL_VERSION = "5.79.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
|
|
@@ -1242,13 +1242,12 @@ def validate_target_surface_scenario() -> int:
|
|
|
1242
1242
|
report((codex_init.stderr or codex_init.stdout).strip())
|
|
1243
1243
|
return 1
|
|
1244
1244
|
codex_doctor = run_keel(codex_repo, "--doctor", "--target", "codex", env=env)
|
|
1245
|
-
codex_prompt_dir = str(codex_home / "prompts")
|
|
1246
1245
|
if (
|
|
1247
1246
|
codex_doctor.returncode != 0
|
|
1248
1247
|
or "OpenSpec commands: ok" not in codex_doctor.stdout
|
|
1249
|
-
or
|
|
1248
|
+
or "OpenSpec 1.13 surfaces Codex's workflows" not in codex_doctor.stdout
|
|
1250
1249
|
or "OpenSpec action skills: ok" not in codex_doctor.stdout
|
|
1251
|
-
or ".
|
|
1250
|
+
or ".agents/skills" not in posix_paths(codex_doctor.stdout)
|
|
1252
1251
|
or "bootstrap: ok" not in codex_doctor.stdout
|
|
1253
1252
|
or "native plugin runtime: manual" not in codex_doctor.stdout
|
|
1254
1253
|
or "Target capabilities (codex):" not in codex_doctor.stdout
|
|
@@ -1265,16 +1264,15 @@ def validate_target_surface_scenario() -> int:
|
|
|
1265
1264
|
report("target-surface scenario Codex init created Claude-only paths.")
|
|
1266
1265
|
return 1
|
|
1267
1266
|
|
|
1268
|
-
|
|
1269
|
-
|
|
1267
|
+
# OpenSpec 1.13 writes Codex's workflows as skills only (#169), so the
|
|
1268
|
+
# surface that can go missing is a skill.
|
|
1269
|
+
shutil.rmtree(codex_repo / ".agents/skills/openspec-propose")
|
|
1270
1270
|
codex_missing = run_keel(codex_repo, "--doctor", "--target", "codex", env=env)
|
|
1271
1271
|
if (
|
|
1272
1272
|
codex_missing.returncode != 0
|
|
1273
|
-
or "OpenSpec
|
|
1274
|
-
or "keel --init --target codex" not in codex_missing.stdout
|
|
1275
|
-
or "openspec update --force" not in codex_missing.stdout
|
|
1273
|
+
or "OpenSpec action skills: missing" not in codex_missing.stdout
|
|
1276
1274
|
):
|
|
1277
|
-
report("target-surface scenario Codex doctor did not report missing
|
|
1275
|
+
report("target-surface scenario Codex doctor did not report a missing skill.")
|
|
1278
1276
|
report((codex_missing.stderr or codex_missing.stdout).strip())
|
|
1279
1277
|
return 1
|
|
1280
1278
|
|
|
@@ -2649,15 +2647,11 @@ def openspec_overlay_files(
|
|
|
2649
2647
|
}
|
|
2650
2648
|
if target == "codex":
|
|
2651
2649
|
assert codex_home is not None
|
|
2650
|
+
# OpenSpec 1.13, which Keel pins, writes Codex's workflows as skills
|
|
2651
|
+
# under `.agents/skills` and no command files (#169).
|
|
2652
2652
|
return {
|
|
2653
|
-
"apply": [
|
|
2654
|
-
|
|
2655
|
-
codex_home / "prompts/opsx-apply.md",
|
|
2656
|
-
],
|
|
2657
|
-
"archive": [
|
|
2658
|
-
repo / ".codex/skills/openspec-archive-change/SKILL.md",
|
|
2659
|
-
codex_home / "prompts/opsx-archive.md",
|
|
2660
|
-
],
|
|
2653
|
+
"apply": [repo / ".agents/skills/openspec-apply-change/SKILL.md"],
|
|
2654
|
+
"archive": [repo / ".agents/skills/openspec-archive-change/SKILL.md"],
|
|
2661
2655
|
}
|
|
2662
2656
|
return {
|
|
2663
2657
|
"apply": [
|
|
@@ -2777,8 +2771,7 @@ def expected_overlay_surfaces(
|
|
|
2777
2771
|
surfaces.append(repo / f".claude/commands/opsx/{action}.md")
|
|
2778
2772
|
elif target == "codex":
|
|
2779
2773
|
assert codex_home is not None
|
|
2780
|
-
surfaces.append(repo / f".
|
|
2781
|
-
surfaces.append(codex_home / f"prompts/opsx-{action}.md")
|
|
2774
|
+
surfaces.append(repo / f".agents/skills/{skill}/SKILL.md")
|
|
2782
2775
|
else:
|
|
2783
2776
|
surfaces.append(repo / f".opencode/skills/{skill}/SKILL.md")
|
|
2784
2777
|
surfaces.append(repo / f".opencode/commands/opsx-{action}.md")
|
|
@@ -3022,7 +3015,7 @@ def validate_sync_surface_overlay_scenario() -> int:
|
|
|
3022
3015
|
".claude/skills/openspec-sync-specs/SKILL.md",
|
|
3023
3016
|
],
|
|
3024
3017
|
"codex": [
|
|
3025
|
-
".
|
|
3018
|
+
".agents/skills/openspec-sync-specs/SKILL.md",
|
|
3026
3019
|
],
|
|
3027
3020
|
}
|
|
3028
3021
|
|
|
@@ -3236,13 +3229,12 @@ def validate_openspec_surface_overlay_scenario() -> int:
|
|
|
3236
3229
|
if (
|
|
3237
3230
|
codex_doctor.returncode != 0
|
|
3238
3231
|
or "Keel apply/archive/sync overlay: ok" not in codex_doctor.stdout
|
|
3239
|
-
or str(codex_home / "prompts") not in codex_doctor.stdout
|
|
3240
3232
|
):
|
|
3241
3233
|
report("openspec-surface-overlay scenario Codex doctor missed overlay health.")
|
|
3242
3234
|
report((codex_doctor.stderr or codex_doctor.stdout).strip())
|
|
3243
3235
|
return 1
|
|
3244
3236
|
|
|
3245
|
-
codex_apply_prompt =
|
|
3237
|
+
codex_apply_prompt = codex_repo / ".agents/skills/openspec-apply-change/SKILL.md"
|
|
3246
3238
|
original_prompt = codex_apply_prompt.read_text(encoding="utf-8")
|
|
3247
3239
|
outdated_prompt = re.sub(
|
|
3248
3240
|
r"<!--\s*keel:openspec-surface-overlay(?:\s+[^>]*)?\s*-->"
|
|
@@ -3269,6 +3261,33 @@ def validate_openspec_surface_overlay_scenario() -> int:
|
|
|
3269
3261
|
report("openspec-surface-overlay scenario Codex install did not refresh overlay idempotently.")
|
|
3270
3262
|
return 1
|
|
3271
3263
|
|
|
3264
|
+
# M6 — a repository set up under OpenSpec 1.6 keeps its layout: skills
|
|
3265
|
+
# under `.codex/skills` and commands as CODEX_HOME prompts (#169).
|
|
3266
|
+
legacy_repo = tmp / "codex-legacy"
|
|
3267
|
+
legacy_home = tmp / "codex-legacy-home"
|
|
3268
|
+
legacy_env = os.environ.copy()
|
|
3269
|
+
legacy_env["CODEX_HOME"] = str(legacy_home)
|
|
3270
|
+
legacy_surfaces = []
|
|
3271
|
+
for action, skill in OVERLAY_ACTION_SKILLS.items():
|
|
3272
|
+
for surface in (
|
|
3273
|
+
legacy_repo / f".codex/skills/{skill}/SKILL.md",
|
|
3274
|
+
legacy_home / f"prompts/opsx-{action}.md",
|
|
3275
|
+
):
|
|
3276
|
+
write_text(surface, f"---\nname: {skill}\n---\n\n# {action}\n")
|
|
3277
|
+
legacy_surfaces.append(surface)
|
|
3278
|
+
run_keel(legacy_repo, "--install", "--target", "codex", env=legacy_env)
|
|
3279
|
+
lost = [
|
|
3280
|
+
str(surface)
|
|
3281
|
+
for surface in legacy_surfaces
|
|
3282
|
+
if surface.read_text(encoding="utf-8").count(OPENSPEC_SURFACE_OVERLAY_START) != 1
|
|
3283
|
+
]
|
|
3284
|
+
if lost:
|
|
3285
|
+
report(
|
|
3286
|
+
"openspec-surface-overlay scenario 1.6-layout Codex surfaces lost "
|
|
3287
|
+
f"their overlay: {lost!r}"
|
|
3288
|
+
)
|
|
3289
|
+
return 1
|
|
3290
|
+
|
|
3272
3291
|
opencode_repo = tmp / "opencode"
|
|
3273
3292
|
opencode_repo.mkdir()
|
|
3274
3293
|
opencode_init = run_keel(opencode_repo, "--init", "--target", "opencode")
|
|
@@ -15983,6 +16002,135 @@ def validate_plugin_runs_its_own_cli_scenario() -> int:
|
|
|
15983
16002
|
return 0
|
|
15984
16003
|
|
|
15985
16004
|
|
|
16005
|
+
def validate_drift_names_a_pending_reload_scenario() -> int:
|
|
16006
|
+
"""Issue #172: an update the host already installed needs only a reload.
|
|
16007
|
+
|
|
16008
|
+
The session keeps the hooks it loaded, and `/clear` does not reload them, so
|
|
16009
|
+
a plugin the host has already updated keeps reporting drift until the
|
|
16010
|
+
reader reloads. Naming the host's update command there sends them to an
|
|
16011
|
+
action that already happened, and aligning the PATH copy with the loaded
|
|
16012
|
+
plugin is a downgrade.
|
|
16013
|
+
"""
|
|
16014
|
+
label = "drift-names-a-pending-reload"
|
|
16015
|
+
event = {"hook_event_name": "SessionStart", "source": "clear"}
|
|
16016
|
+
with tempfile.TemporaryDirectory(
|
|
16017
|
+
prefix="keel-pending-reload-", ignore_cleanup_errors=True
|
|
16018
|
+
) as raw_tmp:
|
|
16019
|
+
tmp = Path(raw_tmp)
|
|
16020
|
+
repo = tmp / "repo"
|
|
16021
|
+
write_text(repo / "openspec/changes/demo/tasks.md", task_contract_fixture())
|
|
16022
|
+
write_text(
|
|
16023
|
+
repo / "AGENTS.md",
|
|
16024
|
+
"# Keel v5.81.0 Agent Protocol\n\n"
|
|
16025
|
+
"<!-- keel:start version=5.81.0 -->\n## Session Start\n"
|
|
16026
|
+
"<!-- keel:end -->\n",
|
|
16027
|
+
)
|
|
16028
|
+
# The host's layout: <plugins>/cache/<marketplace>/<plugin>/<version>,
|
|
16029
|
+
# with its install record in <plugins>. The loaded copy is 5.80.0.
|
|
16030
|
+
plugins_dir = tmp / "plugins"
|
|
16031
|
+
package = plugins_dir / "cache/mkt/keel/5.80.0"
|
|
16032
|
+
write_text(
|
|
16033
|
+
package / "package.json",
|
|
16034
|
+
json.dumps({"name": "@christang/keel", "version": "5.80.0"}) + "\n",
|
|
16035
|
+
)
|
|
16036
|
+
fake_keel_cli(package / "bin/keel.js", "5.80.0")
|
|
16037
|
+
plugin = plant_session_start_plugin(package / "plugins/keel", "5.80.0")
|
|
16038
|
+
record = plugins_dir / "installed_plugins.json"
|
|
16039
|
+
|
|
16040
|
+
def write_record(entries: dict) -> None:
|
|
16041
|
+
write_text(record, json.dumps({"version": 2, "plugins": entries}) + "\n")
|
|
16042
|
+
|
|
16043
|
+
installed = str(plugins_dir / "cache/mkt/keel/5.81.0")
|
|
16044
|
+
write_record(
|
|
16045
|
+
{
|
|
16046
|
+
"keel@mkt": [
|
|
16047
|
+
{"scope": "user", "installPath": installed, "version": "5.81.0"}
|
|
16048
|
+
]
|
|
16049
|
+
}
|
|
16050
|
+
)
|
|
16051
|
+
path_cli = Path(fake_keel_cli(tmp / "path-cli.js", "5.81.0").split('"')[1])
|
|
16052
|
+
path_dir = plant_path_keel(tmp / "path", path_cli)
|
|
16053
|
+
env = {"PATH": str(path_dir) + os.pathsep + os.environ.get("PATH", "")}
|
|
16054
|
+
|
|
16055
|
+
def channels() -> dict[str, str]:
|
|
16056
|
+
run = run_session_start_hook(
|
|
16057
|
+
repo, event, keel_cli=None, plugin_root=plugin, extra_env=env
|
|
16058
|
+
)
|
|
16059
|
+
return {
|
|
16060
|
+
"additionalContext": session_start_context(run) or "",
|
|
16061
|
+
"systemMessage": session_start_message(run) or "",
|
|
16062
|
+
}
|
|
16063
|
+
|
|
16064
|
+
pending = channels()
|
|
16065
|
+
for channel, text in pending.items():
|
|
16066
|
+
lowered = text.lower()
|
|
16067
|
+
# M1 — the update is named as installed, and the reload as the remedy.
|
|
16068
|
+
if "claude plugin update" in lowered:
|
|
16069
|
+
report(
|
|
16070
|
+
f"{label} M1 {channel} named claude plugin update for an "
|
|
16071
|
+
f"update the host already installed: {text!r}"
|
|
16072
|
+
)
|
|
16073
|
+
return 1
|
|
16074
|
+
if "already installed plugin 5.81.0" not in lowered:
|
|
16075
|
+
report(
|
|
16076
|
+
f"{label} M1 {channel} does not say the host already "
|
|
16077
|
+
f"installed plugin 5.81.0: {text!r}"
|
|
16078
|
+
)
|
|
16079
|
+
return 1
|
|
16080
|
+
if "/reload-plugins" not in lowered:
|
|
16081
|
+
report(f"{label} M1 {channel} does not name /reload-plugins: {text!r}")
|
|
16082
|
+
return 1
|
|
16083
|
+
# M2 — a PATH copy at the installed version is not a shadow.
|
|
16084
|
+
if "shadows" in lowered or "@christang/keel@5.80.0" in lowered:
|
|
16085
|
+
report(
|
|
16086
|
+
f"{label} M2 {channel} called a PATH keel at the installed "
|
|
16087
|
+
f"version a shadow: {text!r}"
|
|
16088
|
+
)
|
|
16089
|
+
return 1
|
|
16090
|
+
|
|
16091
|
+
# M3 — another plugin's install is not this one's, and no record at
|
|
16092
|
+
# all is the same as a record that names nothing of ours. Its directory
|
|
16093
|
+
# exists, as the host's would, so the entry is excluded by being
|
|
16094
|
+
# another plugin's and not by being unreadable.
|
|
16095
|
+
(plugins_dir / "cache/mkt/other/9.9.9").mkdir(parents=True)
|
|
16096
|
+
write_record(
|
|
16097
|
+
{
|
|
16098
|
+
"other@mkt": [
|
|
16099
|
+
{
|
|
16100
|
+
"scope": "user",
|
|
16101
|
+
"installPath": str(plugins_dir / "cache/mkt/other/9.9.9"),
|
|
16102
|
+
"version": "9.9.9",
|
|
16103
|
+
}
|
|
16104
|
+
]
|
|
16105
|
+
}
|
|
16106
|
+
)
|
|
16107
|
+
unrelated = channels()
|
|
16108
|
+
for channel, text in unrelated.items():
|
|
16109
|
+
lowered = text.lower()
|
|
16110
|
+
if "already installed" in lowered:
|
|
16111
|
+
report(
|
|
16112
|
+
f"{label} M3 {channel} read another plugin's install as "
|
|
16113
|
+
f"this one's: {text!r}"
|
|
16114
|
+
)
|
|
16115
|
+
return 1
|
|
16116
|
+
if "claude plugin update" not in lowered:
|
|
16117
|
+
report(
|
|
16118
|
+
f"{label} M3 {channel} dropped the update command with no "
|
|
16119
|
+
f"install of this plugin recorded: {text!r}"
|
|
16120
|
+
)
|
|
16121
|
+
return 1
|
|
16122
|
+
record.unlink()
|
|
16123
|
+
absent = channels()
|
|
16124
|
+
if absent != unrelated:
|
|
16125
|
+
report(
|
|
16126
|
+
f"{label} M3 a missing install record changed the report:\n"
|
|
16127
|
+
f" no record: {absent!r}\n other plugin: {unrelated!r}"
|
|
16128
|
+
)
|
|
16129
|
+
return 1
|
|
16130
|
+
report(f"{label} scenario passed.")
|
|
16131
|
+
return 0
|
|
16132
|
+
|
|
16133
|
+
|
|
15986
16134
|
def stage_claude_market_under_test(tmp: Path) -> tuple[Path, str, str] | str:
|
|
15987
16135
|
"""A Claude marketplace that installs this tree rather than the registry.
|
|
15988
16136
|
|
|
@@ -16617,8 +16765,7 @@ def validate_authoring_alignment_overlay_scenario() -> int:
|
|
|
16617
16765
|
report((codex_init.stderr or codex_init.stdout).strip())
|
|
16618
16766
|
return 1
|
|
16619
16767
|
codex_surfaces = (
|
|
16620
|
-
codex_repo / ".
|
|
16621
|
-
codex_home / "prompts/opsx-propose.md",
|
|
16768
|
+
codex_repo / ".agents/skills/openspec-propose/SKILL.md",
|
|
16622
16769
|
)
|
|
16623
16770
|
for surface in codex_surfaces:
|
|
16624
16771
|
content = surface.read_text(encoding="utf-8")
|
|
@@ -16648,7 +16795,7 @@ def validate_authoring_alignment_overlay_scenario() -> int:
|
|
|
16648
16795
|
return 1
|
|
16649
16796
|
|
|
16650
16797
|
apply_skill = (
|
|
16651
|
-
codex_repo / ".
|
|
16798
|
+
codex_repo / ".agents/skills/openspec-apply-change/SKILL.md"
|
|
16652
16799
|
).read_text(encoding="utf-8")
|
|
16653
16800
|
if (
|
|
16654
16801
|
"rerun `keel-align-expectations`" not in apply_skill
|
|
@@ -31428,6 +31575,7 @@ SCENARIOS: tuple = (
|
|
|
31428
31575
|
("native-plugin-session-start", validate_native_plugin_session_start_scenario),
|
|
31429
31576
|
("runtime-version-drift", validate_runtime_version_drift_scenario),
|
|
31430
31577
|
("plugin-runs-its-own-cli", validate_plugin_runs_its_own_cli_scenario),
|
|
31578
|
+
("drift-names-a-pending-reload", validate_drift_names_a_pending_reload_scenario),
|
|
31431
31579
|
("init-declares-plugin-auto-update", validate_init_declares_plugin_auto_update_scenario),
|
|
31432
31580
|
("context-names-the-protocol-refresh", validate_context_names_the_protocol_refresh_scenario),
|
|
31433
31581
|
("init-never-downgrades-openspec", validate_init_never_downgrades_openspec_scenario),
|
package/src/core/context.js
CHANGED
|
@@ -741,9 +741,10 @@ function stampedProtocol(repo) {
|
|
|
741
741
|
|
|
742
742
|
// The target the installer left behind. `CLAUDE.md` carries the managed
|
|
743
743
|
// import only on the Claude target, and only OpenCode writes project commands
|
|
744
|
-
// under `.opencode/`. A Codex install writes neither
|
|
745
|
-
//
|
|
746
|
-
// surface beside it is what a Codex install
|
|
744
|
+
// under `.opencode/`. A Codex install writes neither: OpenSpec 1.13 puts its
|
|
745
|
+
// skills under `.agents/skills`, and 1.6 put its commands in CODEX_HOME, so a
|
|
746
|
+
// managed `AGENTS.md` with neither surface beside it is what a Codex install
|
|
747
|
+
// leaves.
|
|
747
748
|
function installedTarget(repo) {
|
|
748
749
|
try {
|
|
749
750
|
if (/<!--\s*keel:start/.test(fs.readFileSync(path.join(repo, "CLAUDE.md"), "utf8"))) {
|