diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json
index 40a834f..dbd47f5 100644
--- a/.claude-plugin/marketplace.json
+++ b/.claude-plugin/marketplace.json
@@ -9,7 +9,7 @@
"name": "superloopy",
"source": "./",
"description": "Strict evidence loop harness: repo-local loop state, success criteria, append-only ledgers, evidence-gated completion, plus skills (loop, research, frontend, clone) and a read-only/worker crew. Works on Claude Code and Codex from one repo.",
- "version": "0.12.3",
+ "version": "0.12.4",
"license": "MIT",
"keywords": [
"claude-code",
diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json
index 19dac56..00a92be 100644
--- a/.claude-plugin/plugin.json
+++ b/.claude-plugin/plugin.json
@@ -1,6 +1,6 @@
{
"name": "superloopy",
- "version": "0.12.3",
+ "version": "0.12.4",
"description": "Lightweight loop harness with strict evidence gates — Claude Code edition.",
"homepage": "https://github.com/beefiker/superloopy#readme",
"repository": "https://github.com/beefiker/superloopy",
diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json
index 79a343a..fd5f430 100644
--- a/.codex-plugin/plugin.json
+++ b/.codex-plugin/plugin.json
@@ -1,6 +1,6 @@
{
"name": "superloopy",
- "version": "0.12.3",
+ "version": "0.12.4",
"description": "Lightweight loop harness with strict evidence gates.",
"homepage": "https://github.com/beefiker/superloopy#readme",
"repository": "https://github.com/beefiker/superloopy",
diff --git a/README.md b/README.md
index 8882ea9..a0f0e69 100644
--- a/README.md
+++ b/README.md
@@ -90,6 +90,8 @@ Upgrades from pre-managed Superloopy releases are hash-bound: a complete exact l
Resolution finishes before launch, with no post-launch retry or model switch. The TOML pins configure routing, but a host that does not expose `agent_type` plus resolved-model attestation remains `model_unverified`; Superloopy never presents that as a proven GPT-5.6 runtime gate. The policy details are in `docs/superloopy-model-policy.md` (Codex) and `docs/superloopy-model-policy-claude.md` (Claude Code).
+**Installed-plugin truth.** `installedPluginTruth` runs the read-only `codex plugin list --json` authority probe. A confirmed `superloopy@beefiker` version mismatch is informational in source scope, but fails installed scope. Missing Codex, no registered plugin, and invalid authority output stay informational and do not fail doctor. Never infer the installed version from cache directory names or repair without approval.
+
 franky builds it |
@@ -169,12 +171,14 @@ Superloopy checks for updates on `SessionStart`. Marketplace installs are Codex-
Restart Codex after the upgrade. If hooks show up as Modified, approve them; the following approved `SessionStart` automatically reconciles the generated wrapper, all six agents, and model-routing state from the new plugin version. No Superloopy migration command is required. If definitions changed, follow only the Codex restart notice so the host reloads them.
-If the plugin still looks stale or degraded after that, do a repair reinstall from the refreshed marketplace:
+If a confirmed `installedPluginTruth` version mismatch still looks stale or degraded after that, approve a repair reinstall from the refreshed marketplace:
```
-codex plugin add superloopy@beefiker
+codex plugin add superloopy@beefiker --json
```
+Then start a new Codex session.
+
If you installed from a checkout, update the checkout and rerun the installer:
```
diff --git a/docs/superloopy-file-audit.md b/docs/superloopy-file-audit.md
index 6b8d9fd..7941dd2 100644
--- a/docs/superloopy-file-audit.md
+++ b/docs/superloopy-file-audit.md
@@ -446,6 +446,7 @@ Superloopy is its own lightweight loop harness: one small CLI, repo-local `.supe
| `src/help.js` | CLI help text with shortest evidence-backed flow and pass-artifact rule. | Superloopy-specific onboarding surface. |
| `src/hooks.js` | Codex hook runtime for bootstrap, receipts, guide-backed context, continuation, and steering. | SessionStart reuses fresh managed model state without a catalog query and surfaces compatibility or restart instructions when relevant. |
| `src/install-flow.js` | Distinguishes marketplace, checkout, future npx-local snapshot, and unknown install states. | Prevents unsafe npx self-update when Superloopy was installed from marketplace or checkout. |
+| `src/installed-plugin-truth.js` | Bounded read-only Codex installed-plugin authority probe and strict Superloopy version classifier. | Uses only the exact `superloopy@beefiker` JSON entry; never scans or mutates caches, wrappers, or plugin state. |
| `src/installed-model-policy.js` | Read-only doctor check for cached routing, exact legacy/unmanaged fleets, all six managed agent files, and explicit live availability before or after install. | Reads only an explicitly supplied Codex home/state path; sanitizes failures and never writes or launches workers. |
| `src/legacy-agents.js` | Validates legacy manifest data, hashes personal agent files, classifies complete legacy fleets, and resolves the default Codex agent target. | Reads hashes and file kinds only; exact all-six matching is required before migration is authorized. |
| `src/interop.js` | Best-effort, advisory detection of a neighboring Superpowers install for coexistence routing. | Superloopy-native; read-only filesystem probe, env-overridable, never mutates state or fails a hook. |
@@ -504,6 +505,7 @@ Superloopy is its own lightweight loop harness: one small CLI, repo-local `.supe
| `test/host-adapter.test.js` | Unit tests for host-agnostic receipt recovery (direct + transcript fallback, evidence + audit). | Superloopy-native test only. |
| `test/humanize-korean.test.js` | Contract tests for the Korean humanizer audit script's accept and reject paths. | Superloopy-native test for dependency-free safeguards and protected-token gating. |
| `test/interop.test.js` | Tests Superpowers detection (override, filesystem, both hosts), the doctor interop check, and coexistence routing in loop guidance. | Tests Superloopy-native coexistence behavior only. |
+| `test/installed-plugin-truth.test.js` | Unit coverage for authoritative version matching, malformed authority data, bounded subprocess failures, and output sanitization. | Uses injected process results only and never reads or changes the live Codex installation. |
| `test/installed-model-policy.test.js` | Read-only doctor coverage for absent, healthy, stale, mixed, tampered, and explicitly refreshed installed Codex routing. | Uses isolated temporary `CODEX_HOME` fixtures and injected catalog results; never reads or mutates personal installation state. |
| `test/loop-gates.test.js` | Gate, report, trace, check, review, finish, and checkpoint tests. | Covers completion evidence flows. |
| `test/loop.test.js` | Core lifecycle and command-capture unit tests. | Tests Superloopy state semantics. |
diff --git a/docs/superloopy-loop-golden-set.md b/docs/superloopy-loop-golden-set.md
index 7a5c832..fb1735a 100644
--- a/docs/superloopy-loop-golden-set.md
+++ b/docs/superloopy-loop-golden-set.md
@@ -1,10 +1,8 @@
# Superloopy Loop Golden Set
This is Superloopy's own long-running loop-engineering golden set. It scores Superloopy on explicit criteria, artifact-backed proof, strict continuation, local state, append-only history, repository clarity, and reviewable files. Inventory scope: Git-visible repository files from `git ls-files --cached --others --exclude-standard`.
## Strict Rules
-- Each improvement turn must record a score before claiming completion.
-- Each new score must be greater than the previous score.
-- Each accepted score must be greater than the previous accepted score for this golden-set version.
-- If a turn lowers the score, skip that turn's changes and continue from the last accepted state.
+- Each improvement turn must record a score before claiming completion. Each new score must be greater than the previous score.
+- Each accepted score must be greater than the previous accepted score for this golden-set version; if a turn lowers the score, skip that turn's changes and continue from the last accepted state.
- A score is invalid without command evidence, changed-file evidence, and a short next-turn improvement target.
- Pass evidence must be artifact-backed or test-backed. Narrative-only evidence is not enough.
## Threshold Model
@@ -466,6 +464,7 @@ Total: 100 points.
| `src/help.js` | CLI help tests. | Must show the shortest evidence-backed flow and pass-artifact rule. |
| `src/hooks.js` | Hook, model-install, and golden-hook tests. | Must run managed bootstrap at SessionStart, reuse fresh state without a catalog query, surface compatibility/restart status, and keep continuation, steering, and receipt validation fail-closed. |
| `src/install-flow.js` | `test/auto-update.test.js`. | Must distinguish marketplace, checkout, future npx-local snapshot, and unknown install states so unsafe npx updates stay off. |
+| `src/installed-plugin-truth.js` | `test/installed-plugin-truth.test.js`. | Must use only the exact `superloopy@beefiker` JSON entry, bounded read-only Codex subprocess output, and strict version classification without scanning or mutating caches, wrappers, or plugin state. |
| `src/installed-model-policy.js` | `test/installed-model-policy.test.js`, doctor tests. | Must report absent/legacy/unmanaged/preferred/degraded/stale/mixed/tampered status and keep explicit availability refresh read-only before and after install. |
| `src/interop.js` | `test/interop.test.js`. | Must detect a neighboring Superpowers install best-effort across both hosts, honor the `SUPERLOOPY_SUPERPOWERS` override, and never mutate state or fail a hook. |
| `src/loop.js` | Core loop and CLI tests. | Must preserve lifecycle state, ledger appends, evidence recording, review, checkpoint, status, and steering. |
@@ -523,6 +522,7 @@ Total: 100 points.
| `test/host-adapter.test.js` | `npm test`. | Must verify direct and transcript-fallback receipt recovery for both evidence and audit receipts, including trailing-newline, oversized-final-message, tool-use-only, and stale-token cases. |
| `test/humanize-korean.test.js` | `node --test test/humanize-korean.test.js`. | Must prove the Korean humanizer audit script accepts preserved Korean output and rejects non-Korean or token-dropping output. |
| `test/installed-model-policy.test.js` | `npm test`. | Must cover absent, preferred, degraded, stale, mixed, unsupported, missing, and tampered installs plus read-only live refresh with isolated `CODEX_HOME` fixtures. |
+| `test/installed-plugin-truth.test.js` | `npm test`. | Must cover authoritative version matching, malformed authority data, bounded subprocess failures, and output sanitization using injected process results only; never read or change the live Codex installation. |
| `test/interop.test.js` | `npm test`. | Must cover Superpowers detection, the informational doctor interop check, and coexistence routing in loop guidance. |
| `test/loop-gates.test.js` | `npm test`. | Must cover gate, report, trace, check, review, finish, and checkpoint behavior. |
| `test/loop.test.js` | `npm test`. | Must cover core lifecycle, evidence recording, steering, and command capture. |
diff --git a/package-lock.json b/package-lock.json
index 13d398b..16d98ea 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "superloopy",
- "version": "0.12.3",
+ "version": "0.12.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "superloopy",
- "version": "0.12.3",
+ "version": "0.12.4",
"license": "MIT",
"bin": {
"superloopy": "src/cli.js"
diff --git a/package.json b/package.json
index aeca59a..a4d4405 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "superloopy",
- "version": "0.12.3",
+ "version": "0.12.4",
"description": "A lightweight loop harness for Codex and Claude Code with strict evidence gates.",
"type": "module",
"bin": {
diff --git a/skills/superloopy-doctor/SKILL.md b/skills/superloopy-doctor/SKILL.md
index 905e79b..697a3ca 100644
--- a/skills/superloopy-doctor/SKILL.md
+++ b/skills/superloopy-doctor/SKILL.md
@@ -21,6 +21,10 @@ This is a Superloopy health check, not a generic plugin drift audit.
- Do not clone external repos, search issue trackers, or compare unrelated harness layouts unless the user separately asks for research.
- `superloopy doctor` reports the plugin root it checked: by default it uses `source` scope in a recognized Superloopy checkout and `installed` scope elsewhere. Pin a checkout with `superloopy doctor --root --scope source --json`; pin an installed cache with `superloopy doctor --root --scope installed --json`. Source verdicts still return machine-local diagnostics, but `installedModelPolicy` and `wrapper` do not gate source health.
+## Installed-plugin Authority
+
+`installedPluginTruth` runs the read-only `codex plugin list --json` authority probe for `superloopy@beefiker`; it never infers the installed version from cache directory names. A confirmed version mismatch is informational in source scope, but fails installed scope. Missing Codex, no registered plugin, and invalid authority output are informational and do not fail doctor. Report these four states as `current`, `version_mismatch`, `not_registered`, or `authority_unavailable`; never repair from this probe without explicit approval.
+
## Diagnostic Spine
1. Identify the target root: `pwd`, `git rev-parse --show-toplevel` when it is a checkout, and the installed plugin cache path when the wrapper points into one.
@@ -46,7 +50,7 @@ This is a Superloopy health check, not a generic plugin drift audit.
Recommend repair commands only after the read-only report explains the failure:
- Marketplace refresh: `codex plugin marketplace upgrade beefiker`.
-- Repair reinstall: `codex plugin add superloopy@beefiker`.
+- Confirmed `installedPluginTruth` mismatch after approval: `codex plugin add superloopy@beefiker --json`, then start a new Codex session.
- Checkout install: `node src/cli.js install --json`.
- Forced local wrapper/agent overwrite: `node src/cli.js install --force --json`.
diff --git a/src/cli.js b/src/cli.js
index 6459173..39e3ef2 100755
--- a/src/cli.js
+++ b/src/cli.js
@@ -19,6 +19,7 @@ import { beginLoop } from "./begin.js";
import { captureLoop } from "./capture.js";
import { checkLoop, formatCheckResult } from "./check.js";
import { formatDoctor, runDoctor } from "./doctor.js";
+import { queryInstalledPluginTruth } from "./installed-plugin-truth.js";
import { finishLoop } from "./finish.js";
import { formatGuideResult } from "./guide.js";
import { fleetLoop, handoffLoop } from "./fleet.js";
@@ -137,6 +138,7 @@ async function runDoctorCommand(argv, stdout, cwd) {
const result = await runDoctor(selection.root, {
scope: selection.scope,
comparisonPath: parsed.comparisonPath,
+ queryInstalledPluginTruth,
installedModelPolicy: {
env: process.env,
homeDir: homedir(),
diff --git a/src/doctor.js b/src/doctor.js
index a8e8e60..3143586 100644
--- a/src/doctor.js
+++ b/src/doctor.js
@@ -31,6 +31,11 @@ const RUNTIME_IGNORE_SAMPLES = [
const GENERATED_INSTALL_FILES = new Set([
".codex-marketplace-install.json"
]);
+const SOURCE_NON_GATING_CHECKS = new Set([
+ "installedModelPolicy",
+ "installedPluginTruth",
+ "wrapper"
+]);
export async function runDoctor(cwd, options = {}) {
const scope = options.scope ?? "source";
@@ -58,7 +63,15 @@ export async function runDoctor(cwd, options = {}) {
const modelPolicy = await checkModelPolicy(cwd);
const claudeModelPolicy = await checkClaudeModelPolicy(cwd);
const installedModelPolicy = await checkInstalledModelPolicy(cwd, options.installedModelPolicy ?? {});
- const checks = { pluginManifest, hooks, skills, cli, dependencies, runtimeBoundary, fileAudit, gateNotes, designAudit, comparisonSimilarity, reviewability, dispatchCoherence, claudeHostWiring, modelPolicy, claudeModelPolicy, installedModelPolicy, hostContract: checkHostContract(), interop: checkInterop(options), wrapper: checkWrapper({ ...options, diagnosedRoot: cwd }) };
+ const installedPluginTruth = options.queryInstalledPluginTruth === undefined
+ ? {
+ ok: true,
+ informational: true,
+ state: "authority_unavailable",
+ message: "Codex installed-plugin authority is unavailable."
+ }
+ : options.queryInstalledPluginTruth(pluginManifest.ok ? pluginManifest.manifest.version : undefined);
+ const checks = { pluginManifest, hooks, skills, cli, dependencies, runtimeBoundary, fileAudit, gateNotes, designAudit, comparisonSimilarity, reviewability, dispatchCoherence, claudeHostWiring, modelPolicy, claudeModelPolicy, installedModelPolicy, installedPluginTruth, hostContract: checkHostContract(), interop: checkInterop(options), wrapper: checkWrapper({ ...options, diagnosedRoot: cwd }) };
return {
ok: doctorOverallOk(checks, scope),
scope,
@@ -68,20 +81,27 @@ export async function runDoctor(cwd, options = {}) {
}
export function doctorOverallOk(checks, scope) {
- const excluded = scope === "source" ? new Set(["installedModelPolicy", "wrapper"]) : new Set();
- return Object.entries(checks).every(([name, check]) => excluded.has(name) || check.ok);
+ return Object.entries(checks).every(([name, check]) =>
+ scope === "source" && SOURCE_NON_GATING_CHECKS.has(name) ? true : check.ok
+ );
}
export function formatDoctor(result) {
const lines = ["Superloopy doctor", `scope: ${result.scope ?? "source"}`];
for (const [name, check] of Object.entries(result.checks)) {
- const verdict = check.ok ? check.warning === true ? "warn" : "ok" : "fail";
+ const verdict = doctorVerdict(name, check, result.scope ?? "source");
lines.push(`- ${name}: ${verdict}${doctorDetail(name, check)}`);
}
lines.push(`overall: ${result.ok ? "ok" : "fail"}`);
return `${lines.join("\n")}\n`;
}
+function doctorVerdict(name, check, scope) {
+ if (scope === "source" && SOURCE_NON_GATING_CHECKS.has(name) && !check.ok) return "info";
+ if (!check.ok) return "fail";
+ return check.warning === true ? "warn" : "ok";
+}
+
function doctorDetail(name, check) {
if (name === "comparisonSimilarity") return ` - ${comparisonSimilarityDetail(check)}`;
return check.message === undefined ? "" : ` - ${check.message}`;
diff --git a/src/installed-plugin-truth.js b/src/installed-plugin-truth.js
new file mode 100644
index 0000000..a3d3e79
--- /dev/null
+++ b/src/installed-plugin-truth.js
@@ -0,0 +1,99 @@
+import { spawnSync } from "node:child_process";
+
+const PLUGIN_ID = "superloopy@beefiker";
+const PLUGIN_NAME = "superloopy";
+const MARKETPLACE_NAME = "beefiker";
+const DEFAULT_TIMEOUT_MS = 5_000;
+const MAX_OUTPUT_BYTES = 1_000_000;
+const MAX_VERSION_LENGTH = 256;
+const STRICT_SEMVER = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/u;
+const NEXT = "Run `codex plugin add superloopy@beefiker --json`, then start a new Codex session.";
+
+export function queryInstalledPluginTruth(executingVersion, options = {}) {
+ let result;
+ try {
+ if (!isRecord(options)) return unavailable();
+ const spawnSyncImpl = options.spawnSyncImpl ?? spawnSync;
+ if (typeof spawnSyncImpl !== "function") return unavailable();
+ result = spawnSyncImpl("codex", ["plugin", "list", "--json"], {
+ encoding: "utf8",
+ maxBuffer: MAX_OUTPUT_BYTES,
+ shell: false,
+ stdio: ["ignore", "pipe", "pipe"],
+ timeout: DEFAULT_TIMEOUT_MS,
+ windowsHide: true
+ });
+ } catch {
+ return unavailable();
+ }
+ if (result?.status !== 0 || typeof result.stdout !== "string" || Buffer.byteLength(result.stdout, "utf8") > MAX_OUTPUT_BYTES) {
+ return unavailable();
+ }
+ try {
+ return classifyInstalledPluginTruth(executingVersion, JSON.parse(result.stdout));
+ } catch {
+ return unavailable();
+ }
+}
+
+export function classifyInstalledPluginTruth(executingVersion, payload) {
+ if (!validAuthorityVersion(executingVersion) || !isRecord(payload) || !Array.isArray(payload.installed)) {
+ return unavailable();
+ }
+ const matches = payload.installed.filter((entry) => isRecord(entry) && entry.pluginId === PLUGIN_ID);
+ if (matches.length === 0) {
+ return informational("not_registered", "Codex does not report an installed superloopy@beefiker plugin.");
+ }
+ if (matches.length !== 1 || !validExactEntry(matches[0])) return unavailable();
+ const entry = matches[0];
+ if (entry.installed !== true) {
+ return informational("not_registered", "Codex does not report an installed superloopy@beefiker plugin.");
+ }
+ if (entry.version === executingVersion) {
+ return {
+ ok: true,
+ informational: true,
+ state: "current",
+ executingVersion,
+ installedVersion: entry.version,
+ message: `Codex and this doctor both report Superloopy v${executingVersion}.`
+ };
+ }
+ return {
+ ok: false,
+ informational: true,
+ state: "version_mismatch",
+ executingVersion,
+ installedVersion: entry.version,
+ message: `This doctor runs Superloopy v${executingVersion}, but Codex reports installed v${entry.version}.`,
+ next: NEXT
+ };
+}
+
+function validExactEntry(entry) {
+ return entry.pluginId === PLUGIN_ID
+ && entry.name === PLUGIN_NAME
+ && entry.marketplaceName === MARKETPLACE_NAME
+ && validAuthorityVersion(entry.version)
+ && typeof entry.installed === "boolean"
+ && (entry.enabled === undefined || typeof entry.enabled === "boolean");
+}
+
+function validAuthorityVersion(version) {
+ if (typeof version !== "string" || version.length === 0 || version.length > MAX_VERSION_LENGTH) return false;
+ const match = STRICT_SEMVER.exec(version);
+ if (match === null || match[1] === undefined) return match !== null;
+ return match[1].split(".").every((identifier) => !/^\d+$/u.test(identifier) || identifier === "0" || !identifier.startsWith("0"));
+}
+
+function isRecord(value) {
+ return value !== null && typeof value === "object" && !Array.isArray(value);
+}
+
+function informational(state, message) {
+ return { ok: true, informational: true, state, message };
+}
+
+function unavailable() {
+ return informational("authority_unavailable", "Codex installed-plugin authority is unavailable.");
+}
diff --git a/test/docs.test.js b/test/docs.test.js
index 83d0938..dac21d6 100644
--- a/test/docs.test.js
+++ b/test/docs.test.js
@@ -104,7 +104,20 @@ test("public docs describe doctor checks", async () => {
assert.match(modelPolicy, /model_unverified/);
assert.match(modelPolicy, /split-brain/i);
});
-
+test("doctor docs describe authoritative installed-plugin version truth", async () => {
+ for (const content of await Promise.all(["README.md", "skills/superloopy-doctor/SKILL.md"].map((path) => readFile(path, "utf8")))) {
+ assert.match(content, /codex plugin list --json/iu);
+ assert.match(content, /installedPluginTruth/iu);
+ assert.match(content, /source.*informational|informational.*source/isu);
+ assert.match(content, /installed.*fail|fail.*installed/isu);
+ }
+});
+test("doctor docs prescribe the confirmed mismatch remediation", async () => {
+ for (const content of await Promise.all(["README.md", "skills/superloopy-doctor/SKILL.md"].map((path) => readFile(path, "utf8")))) {
+ assert.match(content, /codex plugin add superloopy@beefiker --json/iu);
+ assert.match(content, /codex plugin add superloopy@beefiker --json.*start a new Codex session/isu);
+ }
+});
test("public docs describe real marketplace install and bootstrap", async () => {
const readme = await readFile("README.md", "utf8");
const skill = await readFile("skills/superloopy-loop/SKILL.md", "utf8");
@@ -137,7 +150,6 @@ test("public docs describe real marketplace install and bootstrap", async () =>
assert.match(skill, /first approved `SessionStart` hook/);
assert.match(notes, /one-time SessionStart bootstrap/);
});
-
test("agent install guide gives host-specific marketplace flows", async () => {
const readme = await readFile("README.md", "utf8");
const install = await readFile("installation.md", "utf8");
@@ -153,7 +165,6 @@ test("agent install guide gives host-specific marketplace flows", async () => {
assert.match(install, /Do not add dependencies/);
assert.match(install, /no (?:Superloopy )?migration command is required/i);
});
-
test("README locales are discoverable and do not point at removed PDF manuals", async () => {
const locales = ["README.md", "README.ko.md", "README.zh-CN.md", "README.ja.md", "README.es.md"];
const root = await readFile("README.md", "utf8");
@@ -185,7 +196,6 @@ test("README locales are discoverable and do not point at removed PDF manuals",
assert.match(await readFile("README.ja.md", "utf8"), /loopy 決済モジュールを追加して/);
assert.match(await readFile("README.es.md", "utf8"), /loopy agrega el módulo de pagos/);
});
-
test("README lists the packaged Superloopy skills and their jobs", async () => {
const locales = ["README.md", "README.ko.md", "README.zh-CN.md", "README.ja.md", "README.es.md"];
@@ -214,7 +224,6 @@ test("README lists the packaged Superloopy skills and their jobs", async () => {
assert.match(await readFile("README.md", "utf8"), /Guidance aliases do not mutate state/);
assert.match(await readFile("README.ko.md", "utf8"), /guidance alias는 상태를 바꾸지 않습니다/);
});
-
test("frontend discovery rows publish explicit screen-based scope and claim-shaped evidence", async () => {
const locales = [
{
diff --git a/test/doctor-packed.test.js b/test/doctor-packed.test.js
index e75afff..9830eff 100644
--- a/test/doctor-packed.test.js
+++ b/test/doctor-packed.test.js
@@ -50,6 +50,7 @@ test("doctor accepts an npm-packed install run from an arbitrary cwd", async ()
const result = spawnSync(process.execPath, [join(packed, "src", "cli.js"), "doctor", "--json"], {
cwd: elsewhere,
encoding: "utf8",
+ env: { ...process.env, PATH: "" },
timeout: 30_000
});
@@ -76,6 +77,7 @@ test("doctor accepts an npm-packed install nested inside a parent Git repository
const result = spawnSync(process.execPath, [join(packed, "src", "cli.js"), "doctor", "--json"], {
cwd: parent,
encoding: "utf8",
+ env: { ...process.env, PATH: "" },
timeout: 30_000
});
@@ -127,6 +129,7 @@ test("doctor ignores host .in_use heartbeat files in a packed install", async ()
const result = spawnSync(process.execPath, [join(packed, "src", "cli.js"), "doctor", "--json"], {
cwd: packed,
encoding: "utf8",
+ env: { ...process.env, PATH: "" },
timeout: 30_000
});
@@ -137,6 +140,21 @@ test("doctor ignores host .in_use heartbeat files in a packed install", async ()
assert.deepEqual(parsed.checks.fileAudit.missing, []);
});
+test("packed doctor stays healthy when Codex installed-plugin authority is unavailable", async () => {
+ const packed = await tempNpmPackedCopy();
+ const result = spawnSync(process.execPath, [join(packed, "src", "cli.js"), "doctor", "--json"], {
+ cwd: packed,
+ encoding: "utf8",
+ env: { ...process.env, PATH: "" },
+ timeout: 30_000
+ });
+ assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
+ const parsed = JSON.parse(result.stdout);
+ assert.equal(parsed.scope, "installed");
+ assert.equal(parsed.checks.installedPluginTruth.ok, true);
+ assert.equal(parsed.checks.installedPluginTruth.state, "authority_unavailable");
+});
+
test("source checkout detection falls back safely when git is unavailable", async () => {
const repo = await mkdtemp(join(tmpdir(), "superloopy-source-checkout-"));
const oldPath = process.env.PATH;
diff --git a/test/doctor-review-feedback.test.js b/test/doctor-review-feedback.test.js
index a39ec4b..91fc368 100644
--- a/test/doctor-review-feedback.test.js
+++ b/test/doctor-review-feedback.test.js
@@ -112,11 +112,10 @@ test("doctor CLI falls back to CLI root from an unrelated Codex plugin project",
const result = runCli(["doctor", "--json"], { cwd: project });
- assert.equal(result.status, 0, result.stderr);
const parsed = JSON.parse(result.stdout);
+ assert.equal(result.status, parsed.ok ? 0 : 1, result.stderr);
// Not Superloopy by package identity -> diagnose the installed CLI root, not this project.
assert.equal(parsed.root, process.cwd());
- assert.equal(parsed.ok, true);
});
test("doctor CLI returns a structured skills failure when a SKILL.md is unreadable", async () => {
diff --git a/test/doctor.test.js b/test/doctor.test.js
index 77e0bae..0ea0db3 100644
--- a/test/doctor.test.js
+++ b/test/doctor.test.js
@@ -1,9 +1,9 @@
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { existsSync } from "node:fs";
-import { mkdir, mkdtemp, readFile, realpath, rm, writeFile } from "node:fs/promises";
+import { chmod, copyFile, link, mkdir, mkdtemp, readFile, realpath, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
-import { dirname, join } from "node:path";
+import { delimiter, dirname, join } from "node:path";
import test from "node:test";
import { checkComparisonSimilarity, normalizeComparisonPath } from "../src/comparison-similarity.js";
@@ -34,6 +34,7 @@ test("doctor scope aggregation separates source health from machine-local instal
const checks = {
sourceCheck: { ok: true },
installedModelPolicy: { ok: false },
+ installedPluginTruth: { ok: false, informational: true, state: "version_mismatch" },
wrapper: { ok: false }
};
assert.equal(doctorOverallOk(checks, "source"), true);
@@ -55,6 +56,12 @@ function runCli(args, options = {}) {
});
}
+function assertDoctorExitMatchesResult(result) {
+ const parsed = JSON.parse(result.stdout);
+ assert.equal(result.status, parsed.ok ? 0 : 1, result.stderr);
+ return parsed;
+}
+
async function tempRepoCopy({ initGit = true, prefix = "superloopy-doctor-" } = {}) {
const repo = await mkdtemp(join(tmpdir(), prefix));
const result = spawnSync("git", ["ls-files", "--cached", "--others", "--exclude-standard"], {
@@ -96,25 +103,10 @@ test("doctor --json reports Superloopy packaging, audit, and reviewability check
assert.equal(parsed.scope, "source");
assert.equal(parsed.root, process.cwd());
assert.deepEqual(Object.keys(parsed.checks), [
- "pluginManifest",
- "hooks",
- "skills",
- "cli",
- "dependencies",
- "runtimeBoundary",
- "fileAudit",
- "gateNotes",
- "designAudit",
- "comparisonSimilarity",
- "reviewability",
- "dispatchCoherence",
- "claudeHostWiring",
- "modelPolicy",
- "claudeModelPolicy",
- "installedModelPolicy",
- "hostContract",
- "interop",
- "wrapper"
+ "pluginManifest", "hooks", "skills", "cli", "dependencies", "runtimeBoundary", "fileAudit",
+ "gateNotes", "designAudit", "comparisonSimilarity", "reviewability", "dispatchCoherence",
+ "claudeHostWiring", "modelPolicy", "claudeModelPolicy", "installedModelPolicy",
+ "installedPluginTruth", "hostContract", "interop", "wrapper"
]);
assert.equal(parsed.checks.pluginManifest.ok, true);
assert.equal(parsed.checks.hooks.ok, true);
@@ -178,6 +170,7 @@ test("doctor --json reports Superloopy packaging, audit, and reviewability check
},
{ ok: true, installed: false, degraded: false, restartRequired: false }
);
+ assert.equal(parsed.checks.installedPluginTruth.state, "authority_unavailable");
// Interop is informational: it never fails and does not gate overall health.
assert.equal(parsed.checks.interop.ok, true);
assert.equal(parsed.checks.interop.informational, true);
@@ -190,10 +183,7 @@ test("doctor --json reports Superloopy packaging, audit, and reviewability check
test("doctor CLI falls back to the CLI root outside a Superloopy checkout", async () => {
const project = await mkdtemp(join(tmpdir(), "superloopy-doctor-project-"));
const result = runCli(["doctor", "--json"], { cwd: project });
-
- assert.equal(result.status, 0, result.stderr);
- const parsed = JSON.parse(result.stdout);
- assert.equal(parsed.ok, true);
+ const parsed = assertDoctorExitMatchesResult(result);
assert.equal(parsed.scope, "installed");
assert.equal(parsed.root, process.cwd());
assert.equal(parsed.checks.pluginManifest.ok, true);
@@ -207,9 +197,7 @@ test("doctor CLI defaults a non-Git package root to installed scope", async () =
env: { ...process.env, CODEX_HOME: join(tmpdir(), `superloopy-doctor-empty-cache-${process.pid}`) },
timeout: 10_000
});
-
- assert.equal(result.status, 0, result.stderr);
- const parsed = JSON.parse(result.stdout);
+ const parsed = assertDoctorExitMatchesResult(result);
assert.equal(parsed.scope, "installed");
assert.equal(await realpath(parsed.root), await realpath(repo));
});
@@ -217,7 +205,6 @@ test("doctor CLI defaults a non-Git package root to installed scope", async () =
test("doctor CLI accepts an explicit diagnostic root", async () => {
const repo = await tempRepoCopy();
const result = runCli(["doctor", "--root", repo, "--json"], { cwd: tmpdir() });
-
assert.equal(result.status, 0, result.stderr);
const parsed = JSON.parse(result.stdout);
assert.equal(parsed.ok, true);
@@ -228,8 +215,7 @@ test("doctor CLI accepts an explicit diagnostic root", async () => {
test("doctor CLI supports explicit scope and equals-form selectors", () => {
const installed = runCli(["doctor", "--scope=installed", "--json"]);
- assert.equal(installed.status, 0, installed.stderr);
- assert.equal(JSON.parse(installed.stdout).scope, "installed");
+ assert.equal(assertDoctorExitMatchesResult(installed).scope, "installed");
const source = runCli(["doctor", `--root=${process.cwd()}`, "--scope=source", "--json"], { cwd: tmpdir() });
assert.equal(source.status, 0, source.stderr);
@@ -264,7 +250,6 @@ test("doctor model policy fails when bundled agent defaults drift", async () =>
await writeFile(agentPath, agent.replace('model = "gpt-5.6-luna"', 'model = "gpt-5.5"'), "utf8");
const result = await runDoctor(repo);
-
assert.equal(result.ok, false);
assert.equal(result.checks.modelPolicy.ok, false);
assert.match(result.checks.modelPolicy.message, /nami\.toml model/);
@@ -279,7 +264,6 @@ test("doctor model policy resolves bundled defaults from the model policy data",
await writeFile(policyDataPath, `${JSON.stringify(policyData, null, 2)}\n`, "utf8");
const result = await runDoctor(repo);
-
assert.equal(result.ok, false);
assert.equal(result.checks.modelPolicy.ok, false);
assert.match(result.checks.modelPolicy.message, /nami\.toml model must be gpt-5\.5/);
@@ -293,7 +277,6 @@ test("doctor model policy reports invalid model policy data entries", async () =
await writeFile(policyDataPath, `${JSON.stringify(policyData, null, 2)}\n`, "utf8");
const result = await runDoctor(repo);
-
assert.equal(result.ok, false);
assert.equal(result.checks.modelPolicy.ok, false);
assert.match(result.checks.modelPolicy.message, /model policy data\.codex\.agents\.nami/);
@@ -304,7 +287,6 @@ test("doctor dispatch coherence fails when a dispatched agent is not installed",
await rm(join(repo, ".codex", "agents", "robin.toml"), { force: true });
const result = await runDoctor(repo);
-
assert.equal(result.checks.dispatchCoherence.ok, false);
assert.match(result.checks.dispatchCoherence.message, /robin/);
});
@@ -314,7 +296,6 @@ test("doctor ignores Codex marketplace install metadata in plugin cache", async
await writeFile(join(repo, ".codex-marketplace-install.json"), '{"source":"cache"}\n', "utf8");
const result = await runDoctor(repo);
-
assert.equal(result.ok, true);
assert.equal(result.checks.fileAudit.ok, true);
});
@@ -323,7 +304,6 @@ test("doctor accepts installed plugin caches that are not Git repositories", asy
const repo = await tempRepoCopy({ initGit: false, prefix: "superloopy-cache-" });
const result = await runDoctor(repo);
-
assert.equal(result.ok, true);
assert.equal(result.checks.runtimeBoundary.ok, true);
assert.equal(result.checks.fileAudit.ok, true);
@@ -337,7 +317,6 @@ test("doctor accepts skill frontmatter after CRLF checkout", async () => {
await writeFile(skillPath, toCrLf(skill), "utf8");
const result = await runDoctor(repo);
-
assert.equal(result.ok, true);
assert.equal(result.checks.skills.ok, true);
});
@@ -347,7 +326,6 @@ test("doctor skill check requires the bundled doctor skill", async () => {
await rm(join(repo, "skills", "superloopy-doctor"), { recursive: true, force: true });
const result = await runDoctor(repo);
-
assert.equal(result.ok, false);
assert.equal(result.checks.skills.ok, false);
assert.match(result.checks.skills.message, /superloopy-doctor/);
@@ -385,6 +363,71 @@ test("doctor text renders advisory warning checks as warn", () => {
assert.match(output, /overall: ok/u);
});
+test("doctor renders a source-only installed mismatch as info", () => {
+ const output = formatDoctor({
+ ok: true,
+ scope: "source",
+ checks: {
+ installedPluginTruth: {
+ ok: false,
+ informational: true,
+ state: "version_mismatch",
+ message: "confirmed mismatch"
+ }
+ }
+ });
+ assert.match(output, /- installedPluginTruth: info - confirmed mismatch/u);
+ assert.match(output, /overall: ok/u);
+});
+
+test("runDoctor accepts an injected installed-plugin probe without reading live machine state", async () => {
+ const unavailable = await runDoctor(process.cwd());
+ assert.equal(unavailable.checks.installedPluginTruth.state, "authority_unavailable");
+
+ const mismatch = await runDoctor(process.cwd(), {
+ scope: "installed",
+ queryInstalledPluginTruth: () => ({
+ ok: false,
+ informational: true,
+ state: "version_mismatch",
+ executingVersion: "0.12.4",
+ installedVersion: "0.12.3",
+ message: "confirmed mismatch"
+ })
+ });
+ assert.equal(mismatch.ok, false);
+ assert.equal(mismatch.checks.installedPluginTruth.state, "version_mismatch");
+});
+
+test("doctor CLI gates installed scope through the real Codex authority seam", async () => {
+ const fixture = await mkdtemp(join(tmpdir(), "superloopy-codex-authority-"));
+ const binDir = join(fixture, "bin");
+ await mkdir(binDir);
+ const fakePlugin = "if (JSON.stringify(process.argv.slice(2)) !== JSON.stringify(['list', '--json'])) process.exit(2);\nconst version = process.env.SUPERLOOPY_FAKE_INSTALLED_VERSION;\nprocess.stdout.write(JSON.stringify({ installed: [{ pluginId: 'superloopy@beefiker', name: 'superloopy', marketplaceName: 'beefiker', version, installed: true, enabled: true }] }));\n";
+ await writeFile(join(fixture, "plugin"), fakePlugin, "utf8");
+ const codexPath = join(binDir, process.platform === "win32" ? "codex.exe" : "codex");
+ try {
+ await link(process.execPath, codexPath);
+ } catch {
+ await copyFile(process.execPath, codexPath);
+ if (process.platform !== "win32") await chmod(codexPath, 0o755);
+ }
+ const currentVersion = JSON.parse(await readFile(join(process.cwd(), "package.json"), "utf8")).version;
+ const scenarios = [{ version: currentVersion, state: "current", status: 0 }, { version: "0.0.0", state: "version_mismatch", status: 1 }];
+ for (const scenario of scenarios) {
+ const result = runCli(["doctor", "--scope=installed", "--json"], {
+ cwd: fixture,
+ env: { PATH: `${binDir}${delimiter}${process.env.PATH ?? ""}`, SUPERLOOPY_FAKE_INSTALLED_VERSION: scenario.version }
+ });
+ const parsed = JSON.parse(result.stdout);
+ const failedChecks = Object.entries(parsed.checks).filter(([, check]) => !check.ok).map(([name]) => name);
+ const diagnostic = JSON.stringify({ authority: parsed.checks.installedPluginTruth.state, failedChecks, stderr: result.stderr });
+ assert.equal(result.status, scenario.status, diagnostic);
+ assert.equal(parsed.scope, "installed");
+ assert.equal(parsed.checks.installedPluginTruth.state, scenario.state);
+ }
+});
+
test("doctor comparison scan fails on copied-looking blocks", async () => {
const repo = await tempRepoCopy();
const copiedBlock = [
diff --git a/test/installed-plugin-truth.test.js b/test/installed-plugin-truth.test.js
new file mode 100644
index 0000000..c400def
--- /dev/null
+++ b/test/installed-plugin-truth.test.js
@@ -0,0 +1,147 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+
+import {
+ classifyInstalledPluginTruth,
+ queryInstalledPluginTruth
+} from "../src/installed-plugin-truth.js";
+
+function exact(version, overrides = {}) {
+ return {
+ pluginId: "superloopy@beefiker",
+ name: "superloopy",
+ marketplaceName: "beefiker",
+ version,
+ installed: true,
+ enabled: true,
+ ...overrides
+ };
+}
+
+test("classifier reports matching and mismatching exact installed versions", () => {
+ const current = classifyInstalledPluginTruth("0.12.4", { installed: [exact("0.12.4")] });
+ assert.deepEqual(
+ { ok: current.ok, state: current.state, executingVersion: current.executingVersion, installedVersion: current.installedVersion },
+ { ok: true, state: "current", executingVersion: "0.12.4", installedVersion: "0.12.4" }
+ );
+
+ const mismatch = classifyInstalledPluginTruth("0.12.4", { installed: [exact("0.12.3")] });
+ assert.deepEqual(
+ { ok: mismatch.ok, state: mismatch.state, executingVersion: mismatch.executingVersion, installedVersion: mismatch.installedVersion },
+ { ok: false, state: "version_mismatch", executingVersion: "0.12.4", installedVersion: "0.12.3" }
+ );
+ assert.equal(mismatch.next, "Run `codex plugin add superloopy@beefiker --json`, then start a new Codex session.");
+});
+
+test("classifier ignores unrelated entries and distinguishes absent from duplicate authority", () => {
+ const unrelated = exact("9.9.9", {
+ pluginId: "other@beefiker",
+ name: "other"
+ });
+ assert.equal(classifyInstalledPluginTruth("0.12.4", { installed: [unrelated] }).state, "not_registered");
+ assert.equal(classifyInstalledPluginTruth("0.12.4", { installed: [exact("0.12.4", { installed: false })] }).state, "not_registered");
+ assert.equal(classifyInstalledPluginTruth("0.12.4", { installed: [exact("0.12.4"), exact("0.12.4")] }).state, "authority_unavailable");
+});
+
+test("classifier rejects invalid executing versions, top-level shapes, and exact entry schemas", () => {
+ for (const [version, payload] of [
+ ["not-a-version", { installed: [exact("0.12.4")] }],
+ ["0.12.4", []],
+ ["0.12.4", { installed: "invalid" }],
+ ["0.12.4", { installed: [exact(12)] }],
+ ["0.12.4", { installed: [exact("0.12.4", { installed: "yes" })] }],
+ ["0.12.4", { installed: [exact("0.12.4", { enabled: "yes" })] }]
+ ]) {
+ assert.equal(classifyInstalledPluginTruth(version, payload).state, "authority_unavailable");
+ }
+});
+
+test("classifier rejects unsafe authority versions without returning rejected data", () => {
+ const secret = "do-not-leak";
+ const invalidCases = [
+ [`0.12.4-\u001b[31m${secret}`, "0.12.4"],
+ [` 0.12.4+${secret}`, "0.12.4"],
+ [`0.12.4+${secret} `, "0.12.4"],
+ ["0.12.4", `0.12.3-\u001b[31m${secret}`],
+ ["0.12.4", ` 0.12.3+${secret}`],
+ ["0.12.4", `0.12.3+${secret} `],
+ ["0.12.4", `0.12.3-01.${secret}`],
+ ["0.12.4", `0.12.3-${secret}..1`],
+ ["0.12.4", `0.12.3+${secret}..1`],
+ ["0.12.4", `0.12.3+${"a".repeat(1_024)}.${secret}`]
+ ];
+
+ for (const [executingVersion, installedVersion] of invalidCases) {
+ const result = classifyInstalledPluginTruth(executingVersion, { installed: [exact(installedVersion)] });
+ assert.equal(result.ok, true);
+ assert.equal(result.state, "authority_unavailable");
+ assert.doesNotMatch(JSON.stringify(result), /do-not-leak|\\u001b/iu);
+ }
+});
+
+test("classifier supports strict SemVer prerelease and build strings with exact equality", () => {
+ for (const version of [
+ "0.12.4-alpha.1+build.5",
+ "1.0.0-0.3.7",
+ "1.0.0-x.7.z.92",
+ "1.0.0+21AF26D3----117B344092BD"
+ ]) {
+ const result = classifyInstalledPluginTruth(version, { installed: [exact(version)] });
+ assert.equal(result.state, "current");
+ assert.equal(result.executingVersion, version);
+ assert.equal(result.installedVersion, version);
+ }
+
+ const mismatch = classifyInstalledPluginTruth("0.12.4-alpha+build.1", {
+ installed: [exact("0.12.4-alpha+build.2")]
+ });
+ assert.equal(mismatch.state, "version_mismatch");
+});
+
+test("query invokes Codex directly with bounded options", () => {
+ let invocation;
+ const result = queryInstalledPluginTruth("0.12.4", {
+ spawnSyncImpl(command, args, options) {
+ invocation = { command, args, options };
+ return { status: 0, stdout: JSON.stringify({ installed: [exact("0.12.4")] }), stderr: "" };
+ }
+ });
+
+ assert.equal(result.state, "current");
+ assert.equal(invocation.command, "codex");
+ assert.deepEqual(invocation.args, ["plugin", "list", "--json"]);
+ assert.equal(invocation.options.shell, false);
+ assert.equal(invocation.options.timeout, 5_000);
+ assert.equal(invocation.options.maxBuffer, 1_000_000);
+ assert.deepEqual(invocation.options.stdio, ["ignore", "pipe", "pipe"]);
+});
+
+test("query converts command and payload failures into sanitized unavailable results", () => {
+ const secret = "do-not-leak";
+ const failures = [
+ () => { throw new Error(secret); },
+ () => ({ status: null, signal: "SIGTERM", error: Object.assign(new Error(secret), { code: "ETIMEDOUT" }), stdout: "", stderr: secret }),
+ () => ({ status: 7, stdout: secret, stderr: secret }),
+ () => ({ status: 0, stdout: "{bad", stderr: secret }),
+ () => ({ status: 0, stdout: "x".repeat(1_000_001), stderr: secret })
+ ];
+ for (const spawnSyncImpl of failures) {
+ const result = queryInstalledPluginTruth("0.12.4", { spawnSyncImpl });
+ assert.equal(result.state, "authority_unavailable");
+ assert.doesNotMatch(JSON.stringify(result), /do-not-leak/);
+ }
+});
+
+test("query sanitizes malformed options before resolving an injected process", () => {
+ const throwingGetter = {
+ get spawnSyncImpl() {
+ throw new Error("do-not-leak");
+ }
+ };
+
+ for (const options of [null, throwingGetter]) {
+ const result = queryInstalledPluginTruth("0.12.4", options);
+ assert.equal(result.state, "authority_unavailable");
+ assert.doesNotMatch(JSON.stringify(result), /do-not-leak/);
+ }
+});