litcodex-ai 0.3.6 → 0.3.8

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.
@@ -9,7 +9,7 @@
9
9
  // store PLAN_MISSING→3, PLAN_CORRUPT→4, WRITE_FAILED→5; unknown subcommand/usage→1; bad args→2;
10
10
  // not-found→3. The `doctor` route delegates to the canonical M11 6-check doctor (A3 C5) and ALWAYS
11
11
  // resolves exit 0 (a diagnostic, never a gate).
12
- import { renderDoctorJson, renderDoctorText, runLoopDoctor } from "./loop-doctor.js";
12
+ import { hookRegisteredInstalled, renderDoctorJson, renderDoctorText, runLoopDoctor } from "./loop-doctor.js";
13
13
  import { exitCodeForLoop, LitLoopError } from "./loop-errors.js";
14
14
  import { handleCheckpoint, handleCreate, handleRecordEvidence, handleRun, handleStatus, hasFlag, } from "./loop-handlers.js";
15
15
  import { resolveLoopScope } from "./state-store.js";
@@ -59,7 +59,10 @@ export async function loopCommand(argv, io, clock) {
59
59
  const scope = resolveLoopScope({ argv: rest, env: process.env });
60
60
  if (head === "doctor") {
61
61
  // A3 C5: delegate to the canonical M11 6-check doctor; ALWAYS exit 0 (diagnostic, not gate).
62
- const report = await runLoopDoctor({ repoRoot, scope });
62
+ // Use the INSTALL-AWARE hook probe: a real `litcodex loop doctor` runs from an arbitrary cwd
63
+ // (no dev-tree manifest), so the pure repoRoot-only probe falsely warns "hook not registered".
64
+ // hookRegisteredInstalled also checks where the installed plugin actually lives (Codex cache).
65
+ const report = await runLoopDoctor({ repoRoot, scope }, { hookRegistered: (r) => hookRegisteredInstalled(r) });
63
66
  stdout.write(json ? renderDoctorJson(report) : renderDoctorText(report));
64
67
  return 0;
65
68
  }
@@ -12,6 +12,19 @@ export declare const TERMINAL_LEDGER_KINDS: {
12
12
  * catch (→ warn). Never executes any command from the manifest.
13
13
  */
14
14
  export declare function hookRegistered(repoRoot: string): Promise<boolean>;
15
+ /**
16
+ * Install-aware hook probe (production default the global CLI injects). The dev-tree manifest
17
+ * `<repoRoot>/plugins/litcodex/hooks/hooks.json` ONLY exists when run from the repo; a real install
18
+ * (`litcodex loop doctor` from any cwd) has no such file, so the pure `hookRegistered` falsely warns
19
+ * "not registered". This trusts the repoRoot manifest when present (dev/test), and otherwise looks
20
+ * where the INSTALLED plugin actually lives: the Codex plugin cache
21
+ * `<CODEX_HOME>/plugins/cache/.../hooks/hooks.json` (the global CLI runs the bundled lit-loop, so the
22
+ * plugin's aggregate manifest only exists in that cache). Pure-read, never executes a command;
23
+ * `opts.codexHome` is injectable so tests stay hermetic.
24
+ */
25
+ export declare function hookRegisteredInstalled(repoRoot: string, opts?: {
26
+ codexHome?: string;
27
+ }): Promise<boolean>;
15
28
  /** Scan the ledger tail backward for the last terminal entry; null when none usable. */
16
29
  export declare function latestCheckpointFromLedger(entries: ReadonlyArray<Record<string, unknown>>): LoopCheckpointRef | null;
17
30
  /**
@@ -11,7 +11,8 @@
11
11
  //
12
12
  // Imports flat siblings `./state-store.js`/`./state-paths.js`/`./loop-model.js`/`./loop-types.js`
13
13
  // (A3 C9 — NEVER `../state/...`). No `node:fs` import: existence-probing is M08 `statExists` only.
14
- import { readFile } from "node:fs/promises";
14
+ import { readdir, readFile } from "node:fs/promises";
15
+ import { homedir } from "node:os";
15
16
  import { join } from "node:path";
16
17
  import { sanitizeId } from "./loop-doctor-render.js";
17
18
  import { summarizePlan } from "./loop-model.js";
@@ -52,6 +53,58 @@ export async function hookRegistered(repoRoot) {
52
53
  const parsed = JSON.parse(raw);
53
54
  return scanForCommand(parsed);
54
55
  }
56
+ /** Read + scan a manifest at an absolute path; ENOENT/parse-error ⇒ false (never throws). */
57
+ async function scanManifestAt(manifestAbs) {
58
+ try {
59
+ if (!(await statExists(manifestAbs)))
60
+ return false;
61
+ return scanForCommand(JSON.parse(await readFile(manifestAbs, "utf8")));
62
+ }
63
+ catch {
64
+ return false;
65
+ }
66
+ }
67
+ /** Walk `<cacheRoot>/<marketplace>/<plugin>/<version>/hooks/hooks.json` (bounded; a few installs). */
68
+ async function scanCodexPluginCache(cacheRoot) {
69
+ const listDirs = async (dir) => {
70
+ try {
71
+ return await readdir(dir);
72
+ }
73
+ catch {
74
+ return [];
75
+ }
76
+ };
77
+ for (const mp of await listDirs(cacheRoot)) {
78
+ for (const plugin of await listDirs(join(cacheRoot, mp))) {
79
+ for (const ver of await listDirs(join(cacheRoot, mp, plugin))) {
80
+ if (await scanManifestAt(join(cacheRoot, mp, plugin, ver, "hooks", "hooks.json")))
81
+ return true;
82
+ }
83
+ }
84
+ }
85
+ return false;
86
+ }
87
+ /**
88
+ * Install-aware hook probe (production default the global CLI injects). The dev-tree manifest
89
+ * `<repoRoot>/plugins/litcodex/hooks/hooks.json` ONLY exists when run from the repo; a real install
90
+ * (`litcodex loop doctor` from any cwd) has no such file, so the pure `hookRegistered` falsely warns
91
+ * "not registered". This trusts the repoRoot manifest when present (dev/test), and otherwise looks
92
+ * where the INSTALLED plugin actually lives: the Codex plugin cache
93
+ * `<CODEX_HOME>/plugins/cache/.../hooks/hooks.json` (the global CLI runs the bundled lit-loop, so the
94
+ * plugin's aggregate manifest only exists in that cache). Pure-read, never executes a command;
95
+ * `opts.codexHome` is injectable so tests stay hermetic.
96
+ */
97
+ export async function hookRegisteredInstalled(repoRoot, opts) {
98
+ // 1. Dev tree — authoritative when present (preserves dev/test behavior and keeps the real-fs cache
99
+ // walk below from ever running inside the repo's own hermetic unit tests).
100
+ const devManifest = join(repoRoot, ...HOOK_MANIFEST_RELPATH.split("/"));
101
+ if (await statExists(devManifest)) {
102
+ return scanManifestAt(devManifest);
103
+ }
104
+ // 2. Codex plugin cache — the real install context for a global-CLI `litcodex loop doctor`.
105
+ const codexHome = opts?.codexHome ?? (process.env["CODEX_HOME"]?.trim() || join(homedir(), ".codex"));
106
+ return scanCodexPluginCache(join(codexHome, "plugins", "cache"));
107
+ }
55
108
  /** Recursively scan every string value under a `command` key for ALL three fragments. */
56
109
  function scanForCommand(node) {
57
110
  if (Array.isArray(node)) {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@litcodex/lit-loop",
3
- "version": "0.3.6",
3
+ "version": "0.3.8",
4
4
  "description": "LitCodex Lit-Loop runtime: durable repo-native multi-goal orchestration with embedded success criteria and observable evidence audit.",
5
5
  "type": "module",
6
6
  "bin": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "litcodex-ai",
3
- "version": "0.3.6",
3
+ "version": "0.3.8",
4
4
  "description": "Codex loop harness installer. Run `npx litcodex-ai install` to set up the LitCodex Codex platform: the bare `lit` hook and the durable lit-loop runtime.",
5
5
  "keywords": ["codex", "litcodex", "lit-loop", "ai-agents", "orchestration"],
6
6
  "author": "LitCodex Authors",
@@ -15,7 +15,7 @@
15
15
  },
16
16
  "files": ["bin", "dist", "model-catalog.json", "README.md", "LICENSE"],
17
17
  "dependencies": {
18
- "@litcodex/lit-loop": "0.3.6"
18
+ "@litcodex/lit-loop": "0.3.8"
19
19
  },
20
20
  "bundledDependencies": ["@litcodex/lit-loop"],
21
21
  "scripts": {