@inerrata-corporation/errata 2.0.6-dev.2752 → 2.0.6-dev.2766

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/README.txt CHANGED
@@ -34,9 +34,12 @@ errata link flows also refresh detected Codex hooks; --skip-hooks opts out of
34
34
  either automatic install. The installer preserves other config and writes its
35
35
  Node relay into .codex/hooks/errata-relay.mjs. No curl or Bash dependency.
36
36
 
37
- Restart/resume Codex after installing, and review/trust the hooks with /hooks.
38
- If your Codex build gates hooks, enable [features] codex_hooks = true. Update
39
- older Codex builds if UserPromptSubmit or tool hooks are unsupported.
37
+ Codex desktop: open Settings > Hooks, select the project, expand each event,
38
+ and review/Trust its Errata hook. Use Reload hooks after installation. No
39
+ standalone Codex CLI is required. CLI users can use /hooks instead.
40
+ Restarting or reinstalling does not grant trust. Run errata doctor to check
41
+ activation; it discovers the Windows desktop runtime even without a CLI on PATH.
42
+ Respect explicitly disabled hooks ([features] hooks = false).
40
43
 
41
44
  SessionStart injects .errata/context.md, including startup/resume/compaction.
42
45
  UserPromptSubmit and PostToolUse inject it again only when it changes. Tool
package/errata.mjs CHANGED
@@ -17038,6 +17038,11 @@ var init_context_file = __esm({
17038
17038
  "Live agent context for this checkout \u2014 priors, open problems, the capture",
17039
17039
  "protocol \u2014 is maintained by the errata daemon at `.errata/context.md`.",
17040
17040
  "",
17041
+ "At the beginning of each task and each new user turn, read that file before",
17042
+ "substantive work. Use the current file in preference to an earlier snapshot.",
17043
+ "If it is absent, continue normally. In Codex, an `@` mention alone does not",
17044
+ "load the file; read it explicitly.",
17045
+ "",
17041
17046
  "It is gitignored ON PURPOSE: it is this machine's runtime state, not repo",
17042
17047
  "content. Committing it would prime your teammates with your graph.",
17043
17048
  "",
@@ -71187,9 +71192,9 @@ var init_report_render = __esm({
71187
71192
 
71188
71193
  // src/cli.ts
71189
71194
  init_src6();
71190
- import { closeSync as closeSync2, existsSync as existsSync34, openSync as openSync2, readFileSync as readFileSync35, renameSync as renameSync4, statSync as statSync8 } from "node:fs";
71191
- import { join as join40, resolve as pathResolve2 } from "node:path";
71192
- import { spawn as spawn4 } from "node:child_process";
71195
+ import { closeSync as closeSync2, existsSync as existsSync34, openSync as openSync2, readFileSync as readFileSync35, renameSync as renameSync4, statSync as statSync9 } from "node:fs";
71196
+ import { join as join41, resolve as pathResolve2 } from "node:path";
71197
+ import { spawn as spawn5 } from "node:child_process";
71193
71198
 
71194
71199
  // src/daemon.ts
71195
71200
  import { existsSync as existsSync26, writeFileSync as writeFileSync22 } from "node:fs";
@@ -73721,6 +73726,7 @@ init_src10();
73721
73726
  init_review2();
73722
73727
 
73723
73728
  // src/turn.ts
73729
+ init_checkout_root();
73724
73730
  import { closeSync, existsSync as existsSync16, fstatSync, openSync, readdirSync as readdirSync6, readSync, realpathSync, statSync as statSync5 } from "node:fs";
73725
73731
  import { basename as basename5, delimiter, dirname as dirname10, join as join19, normalize as normalize3 } from "node:path";
73726
73732
  import { homedir as homedir4 } from "node:os";
@@ -74205,6 +74211,8 @@ function codexRolloutsForCwd(cwd, opts = {}) {
74205
74211
  const sinceMs = opts.sinceMs ?? 0;
74206
74212
  const limit = opts.limit ?? 25;
74207
74213
  const out2 = [];
74214
+ const owner = checkoutRootOf(cwd);
74215
+ const owners = /* @__PURE__ */ new Map();
74208
74216
  for (const root of codexSessionRoots(opts.env, opts.home)) {
74209
74217
  if (!existsSync16(root)) continue;
74210
74218
  const stack = [{ dir: root, depth: 0 }];
@@ -74231,7 +74239,14 @@ function codexRolloutsForCwd(cwd, opts = {}) {
74231
74239
  if (depth < 3) continue;
74232
74240
  if (!name2.startsWith("rollout-") || !name2.endsWith(".jsonl")) continue;
74233
74241
  if (st.mtimeMs < sinceMs) continue;
74234
- if (readCodexRolloutCwd(full) !== cwd) continue;
74242
+ const sessionCwd = readCodexRolloutCwd(full);
74243
+ if (sessionCwd === null) continue;
74244
+ let sessionOwner = owners.get(sessionCwd);
74245
+ if (sessionOwner === void 0) {
74246
+ sessionOwner = checkoutRootOf(sessionCwd);
74247
+ owners.set(sessionCwd, sessionOwner);
74248
+ }
74249
+ if (sessionOwner !== owner) continue;
74235
74250
  out2.push({ path: full, sessionId: `codex:${basename5(name2, ".jsonl")}`, mtimeMs: st.mtimeMs });
74236
74251
  }
74237
74252
  }
@@ -79907,7 +79922,7 @@ function createWatchBreaker(deps) {
79907
79922
  }
79908
79923
 
79909
79924
  // src/engine.ts
79910
- var DAEMON_VERSION = true ? "2.0.6-dev.2752" : "2.0.0-alpha.0";
79925
+ var DAEMON_VERSION = true ? "2.0.6-dev.2766" : "2.0.0-alpha.0";
79911
79926
  var IGNORED_PATH = /[\\/](?:\.git|node_modules|\.errata|\.claude|\.codex|\.cursor|\.turbo|\.next|dist|coverage|test-results|playwright-report|__pycache__)(?:[\\/]|$)/;
79912
79927
  var IGNORED_NOISE = /castalia\.db|eventlog\.sqlite|turn-cursors/;
79913
79928
  var GIT_OP_MUTE_MS = 4e3;
@@ -85577,6 +85592,7 @@ var SessionAffinity = class {
85577
85592
  };
85578
85593
 
85579
85594
  // src/multi.ts
85595
+ init_checkout_root();
85580
85596
  var PROCESS_STARTED_AT = Date.now();
85581
85597
  var SELF_ENTRY = process.argv[1] ? pathResolve(process.argv[1]) : "";
85582
85598
  var LOCAL_BUILD_ENTRY = SELF_ENTRY && !isInstalledEntry(SELF_ENTRY) ? SELF_ENTRY : void 0;
@@ -86124,6 +86140,19 @@ async function startMultiDaemon(opts = {}) {
86124
86140
  if (owner) break;
86125
86141
  owner = ownerOf(records, p);
86126
86142
  }
86143
+ if (!owner) {
86144
+ for (const p of [primary, ...others]) {
86145
+ if (!p) continue;
86146
+ const gitRoot = nearestGitRoot(p);
86147
+ const main2 = gitRoot ? mainCheckoutOfWorktree(gitRoot) : null;
86148
+ if (!main2) continue;
86149
+ owner = ownerOf(records, main2);
86150
+ if (!owner && listWorkspaceEntries().some((e) => pickOwnerRoot([e.path], main2))) {
86151
+ if (attachWorkspace(main2).attached) owner = ownerOf(records, main2);
86152
+ }
86153
+ if (owner) break;
86154
+ }
86155
+ }
86127
86156
  if (!owner && others.length > 0) {
86128
86157
  const registryRoots = listWorkspaceEntries().map((e) => e.path);
86129
86158
  const registered = others.map((p) => pickOwnerRoot(registryRoots, p)).find((r) => r != null);
@@ -87552,9 +87581,9 @@ function deriveAuthBase(verificationUri) {
87552
87581
  }
87553
87582
  function openInBrowser(url2) {
87554
87583
  try {
87555
- void import("node:child_process").then(({ spawn: spawn5 }) => {
87584
+ void import("node:child_process").then(({ spawn: spawn6 }) => {
87556
87585
  const [cmd2, args2] = process.platform === "win32" ? ["cmd", ["/c", "start", "", url2]] : process.platform === "darwin" ? ["open", [url2]] : ["xdg-open", [url2]];
87557
- spawn5(cmd2, args2, { detached: true, stdio: "ignore" }).unref();
87586
+ spawn6(cmd2, args2, { detached: true, stdio: "ignore" }).unref();
87558
87587
  });
87559
87588
  } catch {
87560
87589
  }
@@ -87622,9 +87651,41 @@ function shouldUseOAuthLogin(flags2, env2 = process.env) {
87622
87651
 
87623
87652
  // src/doctor.ts
87624
87653
  import { spawnSync } from "node:child_process";
87654
+
87655
+ // src/codex-runtime.ts
87656
+ import { readdirSync as readdirSync12, statSync as statSync8 } from "node:fs";
87657
+ import { join as join39 } from "node:path";
87658
+ function resolveCodexExecutable(options = {}) {
87659
+ const platform = options.platform ?? process.platform;
87660
+ const env2 = options.env ?? process.env;
87661
+ if (platform !== "win32") return "codex";
87662
+ const fileTime = (path2) => {
87663
+ try {
87664
+ const s = statSync8(path2);
87665
+ return s.isFile() ? s.mtimeMs : null;
87666
+ } catch {
87667
+ return null;
87668
+ }
87669
+ };
87670
+ for (const dir of (env2["PATH"] ?? env2["Path"] ?? "").split(";")) {
87671
+ if (!dir.trim()) continue;
87672
+ const path2 = join39(dir.trim().replace(/^"|"$/g, ""), "codex.exe");
87673
+ if (fileTime(path2) !== null) return path2;
87674
+ }
87675
+ if (!env2["LOCALAPPDATA"]) return "codex";
87676
+ const root = join39(env2["LOCALAPPDATA"], "OpenAI", "Codex", "bin");
87677
+ try {
87678
+ const candidates = readdirSync12(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => join39(root, entry.name, "codex.exe")).map((path2) => ({ path: path2, mtime: fileTime(path2) })).filter((candidate) => candidate.mtime !== null).sort((a, b) => b.mtime - a.mtime || a.path.localeCompare(b.path));
87679
+ return candidates[0]?.path ?? "codex";
87680
+ } catch {
87681
+ return "codex";
87682
+ }
87683
+ }
87684
+
87685
+ // src/doctor.ts
87625
87686
  function installedHarnessVersion(bin) {
87626
87687
  try {
87627
- const r = spawnSync(bin, ["--version"], { encoding: "utf8", timeout: 3e3 });
87688
+ const r = spawnSync(bin === "codex" ? resolveCodexExecutable() : bin, ["--version"], { encoding: "utf8", timeout: 3e3, windowsHide: true });
87628
87689
  if (r.status !== 0 && !r.stdout) return null;
87629
87690
  const m = /(\d+\.\d+\.\d+)/.exec(`${r.stdout ?? ""}${r.stderr ?? ""}`);
87630
87691
  return m ? m[1] : null;
@@ -87634,6 +87695,7 @@ function installedHarnessVersion(bin) {
87634
87695
  }
87635
87696
  async function diagnoseInstallation(cfg, dependencies) {
87636
87697
  const checks = [];
87698
+ if (dependencies.codexHooks) checks.push(...await dependencies.codexHooks());
87637
87699
  const profileName = cfg.activeInstallationProfile;
87638
87700
  const profile = profileName ? cfg.installationProfiles[profileName] : void 0;
87639
87701
  checks.push(
@@ -87727,75 +87789,14 @@ async function diagnoseInstallation(cfg, dependencies) {
87727
87789
  };
87728
87790
  }
87729
87791
 
87730
- // src/whoami.ts
87731
- async function resolveWhoami(client, actingHandle) {
87732
- return actingHandle ? client.contextAs(actingHandle) : client.context();
87733
- }
87734
- function targetLabel(context) {
87735
- const target = context.default_write_target;
87736
- if (!target) return "read-only";
87737
- if (target.visibility === "public") return "public graph";
87738
- if (target.visibility === "org") return `organization graph (${target.org_id})`;
87739
- return `private Team graph (${target.team_id})`;
87740
- }
87741
- function formatWhoami(response, localProfile) {
87742
- const { context, identity } = response;
87743
- return [
87744
- `inErrata context \u2014 ${context.profile_name ?? localProfile ?? "unbound credential"}`,
87745
- ` workspace: ${context.workspace_kind} \xB7 ${context.org_id}`,
87746
- ` team: ${context.team_id ?? "(none)"}`,
87747
- ` agent: ${context.agent_id ?? "(default identity)"}`,
87748
- ` installation: ${context.installation_id ?? "(unbound)"}`,
87749
- ` environment: ${context.environment} \xB7 ${context.client_kind}`,
87750
- ` reads: ${context.read_visibilities.join(" + ")}`,
87751
- ` contributes: ${targetLabel(context)}`,
87752
- ` policy: ${context.policy_version ?? "(legacy)"}`,
87753
- ` credential: ${identity.credential_type} \xB7 ${identity.credential_id}`,
87754
- ` actor: ${identity.actor_type} \xB7 ${identity.actor_id}`
87755
- ];
87756
- }
87792
+ // src/codex-doctor.ts
87793
+ import { spawn as spawn4 } from "node:child_process";
87794
+ import { createInterface } from "node:readline";
87757
87795
 
87758
- // src/consolidation-trigger.ts
87759
- function effectiveMomentumThreshold(policy, liveNodes) {
87760
- return Math.max(policy.momentumThreshold, Math.ceil(liveNodes * policy.momentumRatio));
87761
- }
87762
- function nextMomentumThreshold(args2) {
87763
- const { current, consumed, landmarkFlips, floor, cap } = args2;
87764
- const next = landmarkFlips > 0 ? Math.min(current, Math.round(consumed / 2)) : Math.max(current, Math.round(consumed * 1.5));
87765
- return Math.max(floor, Math.min(cap, next));
87766
- }
87767
- var DEFAULT_CONSOLIDATION_POLICY = {
87768
- baseFloorMs: 6e4,
87769
- dutyFactor: 20,
87770
- // ≤5% worst-case duty at any graph size (crater guard)
87771
- momentumThreshold: 500,
87772
- // POLICY freshness bound — the gate never demands less
87773
- momentumRatio: 0.01,
87774
- // cold-start PRIOR only; calibration replaces it after the first pass
87775
- momentumRatioCap: 0.05,
87776
- // POLICY staleness bound — refresh by 5% churn no matter what
87777
- quiescenceMs: 6e4
87778
- // sim preferred 60s over 90s (fresher, esp. heavy-code)
87779
- };
87780
- function numEnv5(env2, key, fallback) {
87781
- const raw2 = env2[key];
87782
- if (raw2 == null || raw2.trim() === "") return fallback;
87783
- const n = Number(raw2);
87784
- return Number.isFinite(n) && n >= 0 ? n : fallback;
87785
- }
87786
- function consolidationPolicyFromEnv(env2 = process.env) {
87787
- return {
87788
- baseFloorMs: numEnv5(env2, "ERRATA_CONSOLIDATE_BASE_FLOOR_MS", DEFAULT_CONSOLIDATION_POLICY.baseFloorMs),
87789
- dutyFactor: numEnv5(env2, "ERRATA_CONSOLIDATE_DUTY_FACTOR", DEFAULT_CONSOLIDATION_POLICY.dutyFactor),
87790
- momentumThreshold: numEnv5(env2, "ERRATA_CONSOLIDATE_MOMENTUM", DEFAULT_CONSOLIDATION_POLICY.momentumThreshold),
87791
- momentumRatio: numEnv5(env2, "ERRATA_CONSOLIDATE_MOMENTUM_RATIO", DEFAULT_CONSOLIDATION_POLICY.momentumRatio),
87792
- momentumRatioCap: numEnv5(env2, "ERRATA_CONSOLIDATE_MOMENTUM_RATIO_CAP", DEFAULT_CONSOLIDATION_POLICY.momentumRatioCap),
87793
- quiescenceMs: numEnv5(env2, "ERRATA_CONSOLIDATE_QUIESCENCE_MS", DEFAULT_CONSOLIDATION_POLICY.quiescenceMs)
87794
- };
87795
- }
87796
- function consolidationGapMs(lastPassMs, policy) {
87797
- return Math.max(policy.baseFloorMs, lastPassMs * policy.dutyFactor);
87798
- }
87796
+ // src/codex-hooks.ts
87797
+ import { existsSync as existsSync33, mkdirSync as mkdirSync9, readFileSync as readFileSync34, writeFileSync as writeFileSync25 } from "node:fs";
87798
+ import { homedir as homedir7 } from "node:os";
87799
+ import { join as join40 } from "node:path";
87799
87800
 
87800
87801
  // src/hook-commands.ts
87801
87802
  function hookCurlCommand(port, path2 = "/api/hook") {
@@ -87819,17 +87820,12 @@ function buildCodexHooksToml(port, options) {
87819
87820
  };
87820
87821
  if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error("Invalid hook port");
87821
87822
  const command = quote(options.nodePath) + " " + quote(options.relayPath) + " " + port;
87822
- const handlers = ["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse"].map(
87823
- (event) => "[[hooks." + event + "]]\n" + (event === "UserPromptSubmit" ? "" : 'matcher = ".*"\n') + "\n[[hooks." + event + '.hooks]]\ntype = "command"\ncommand = ' + JSON.stringify(command) + "\ntimeout = 10\nadditionalContextLimit = 5000\n"
87823
+ const handlers = ["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse", "Stop"].map(
87824
+ (event) => "[[hooks." + event + "]]\n" + (["UserPromptSubmit", "Stop"].includes(event) ? "" : 'matcher = ".*"\n') + "\n[[hooks." + event + '.hooks]]\ntype = "command"\ncommand = ' + JSON.stringify(command) + "\ntimeout = 10\nadditionalContextLimit = 5000\n"
87824
87825
  );
87825
87826
  return CODEX_HOOKS_BEGIN + "\n" + handlers.join("\n") + CODEX_HOOKS_END + "\n";
87826
87827
  }
87827
87828
 
87828
- // src/codex-hooks.ts
87829
- import { existsSync as existsSync33, mkdirSync as mkdirSync9, readFileSync as readFileSync34, writeFileSync as writeFileSync25 } from "node:fs";
87830
- import { homedir as homedir7 } from "node:os";
87831
- import { join as join39 } from "node:path";
87832
-
87833
87829
  // src/codex-hook-relay.ts
87834
87830
  var CODEX_RELAY_SOURCE = String.raw`// errata-managed: regenerated by errata install-hooks codex
87835
87831
  import { readFileSync } from 'node:fs';
@@ -87853,7 +87849,8 @@ try {
87853
87849
  } catch { /* An unavailable daemon must never block the agent. */ }
87854
87850
  };
87855
87851
  if (event === 'PreToolUse' || event === 'PostToolUse') await post('hook', true);
87856
- if (event === 'PostToolUse') await post('turn');
87852
+ // Stop sees the final assistant message, including turns with no tools.
87853
+ if (event === 'PostToolUse' || event === 'Stop') await post('turn');
87857
87854
  if (['SessionStart', 'UserPromptSubmit', 'PostToolUse'].includes(event)) {
87858
87855
  // Run after capture, so changes already rendered by the daemon arrive now.
87859
87856
  // Background extraction finishing later arrives at the next tool boundary.
@@ -87868,8 +87865,9 @@ try {
87868
87865
  `;
87869
87866
 
87870
87867
  // src/codex-hooks.ts
87871
- function hasCodexConfig(root, codexHome = process.env["CODEX_HOME"] || join39(homedir7(), ".codex")) {
87872
- return existsSync33(join39(root, ".codex")) || existsSync33(codexHome);
87868
+ var CODEX_HOOK_REVIEW_HELP = "Codex desktop: Settings > Hooks, select this project, expand each event and Trust the Errata hook. CLI users can use /hooks.";
87869
+ function hasCodexConfig(root, codexHome = process.env["CODEX_HOME"] || join40(homedir7(), ".codex")) {
87870
+ return existsSync33(join40(root, ".codex")) || existsSync33(codexHome);
87873
87871
  }
87874
87872
  function replaceCodexHooks(existing, block) {
87875
87873
  const begin = existing.indexOf(CODEX_HOOKS_BEGIN);
@@ -87885,17 +87883,176 @@ function replaceCodexHooks(existing, block) {
87885
87883
  return existing.trimEnd() + (existing.trim() ? "\n\n" : "") + block;
87886
87884
  }
87887
87885
  function writeCodexHooks(root, port, nodePath = process.execPath) {
87888
- const dir = join39(root, ".codex");
87889
- const file2 = join39(dir, "config.toml");
87890
- const relay = join39(dir, "hooks", "errata-relay.mjs");
87886
+ const dir = join40(root, ".codex");
87887
+ const file2 = join40(dir, "config.toml");
87888
+ const relay = join40(dir, "hooks", "errata-relay.mjs");
87891
87889
  const block = buildCodexHooksToml(port, { nodePath, relayPath: relay });
87892
87890
  const final = replaceCodexHooks(existsSync33(file2) ? readFileSync34(file2, "utf8") : "", block);
87893
- mkdirSync9(join39(dir, "hooks"), { recursive: true });
87891
+ mkdirSync9(join40(dir, "hooks"), { recursive: true });
87894
87892
  writeFileSync25(relay, CODEX_RELAY_SOURCE, "utf8");
87895
87893
  writeFileSync25(file2, final, "utf8");
87896
87894
  return file2;
87897
87895
  }
87898
87896
 
87897
+ // src/codex-doctor.ts
87898
+ function readCodexHookState(cwd, options = {}) {
87899
+ return new Promise((resolve7, reject) => {
87900
+ const child = spawn4(options.bin ?? resolveCodexExecutable(), options.args ?? ["app-server", "--stdio"], {
87901
+ cwd,
87902
+ windowsHide: true,
87903
+ stdio: ["pipe", "pipe", "pipe"]
87904
+ });
87905
+ const lines = createInterface({ input: child.stdout });
87906
+ let settled = false;
87907
+ let size = 0;
87908
+ let hooks;
87909
+ let features;
87910
+ const finish = (error48) => {
87911
+ if (settled) return;
87912
+ settled = true;
87913
+ clearTimeout(timer);
87914
+ lines.close();
87915
+ child.kill();
87916
+ if (error48) reject(error48);
87917
+ else resolve7({ ...hooks, features });
87918
+ };
87919
+ const timer = setTimeout(() => finish(new Error("Codex hook inspection timed out")), options.timeoutMs ?? 1e4);
87920
+ const send = (id, method, params) => {
87921
+ child.stdin.write(JSON.stringify({ id, method, params }) + "\n");
87922
+ };
87923
+ child.on("error", () => finish(new Error(`Codex could not be started for hook inspection. ${CODEX_HOOK_REVIEW_HELP}`)));
87924
+ child.on("exit", () => finish(new Error("Codex exited before returning its hook state")));
87925
+ child.stdin.on("error", () => finish(new Error("Codex hook inspection connection closed")));
87926
+ child.stderr.resume();
87927
+ child.stdout.on("data", (chunk) => {
87928
+ size += chunk.length;
87929
+ if (size > 2e6) finish(new Error("Codex hook inspection exceeded its response budget"));
87930
+ });
87931
+ lines.on("line", (line) => {
87932
+ if (settled) return;
87933
+ let message2;
87934
+ try {
87935
+ message2 = JSON.parse(line);
87936
+ } catch {
87937
+ return;
87938
+ }
87939
+ if (![1, 2, 3].includes(message2.id)) return;
87940
+ if (message2.error) return finish(new Error(`Codex does not support hook inspection. ${CODEX_HOOK_REVIEW_HELP}`));
87941
+ if (message2.id === 1) {
87942
+ child.stdin.write(JSON.stringify({ method: "initialized" }) + "\n");
87943
+ send(2, "hooks/list", { cwds: [cwd] });
87944
+ send(3, "config/read", { cwd, includeLayers: false });
87945
+ } else if (message2.id === 2) {
87946
+ const entry = message2.result?.data?.[0];
87947
+ if (!Array.isArray(entry?.hooks)) return finish(new Error("Codex returned no hook inspection result"));
87948
+ hooks = { hooks: entry.hooks, errors: entry.errors };
87949
+ } else {
87950
+ if (!message2.result?.config) return finish(new Error("Codex returned no effective configuration"));
87951
+ features = message2.result.config.features ?? {};
87952
+ }
87953
+ if (hooks && features) finish();
87954
+ });
87955
+ send(1, "initialize", {
87956
+ clientInfo: { name: "errata_doctor", version: "1.0.0" },
87957
+ capabilities: { experimentalApi: true }
87958
+ });
87959
+ });
87960
+ }
87961
+ function assessCodexHooks(state) {
87962
+ const check2 = (status, detail) => [{ id: "codex_hooks", status, detail }];
87963
+ if (state.errors?.length) return check2("fail", `Codex could not load hook configuration. ${CODEX_HOOK_REVIEW_HELP}`);
87964
+ if ((state.features?.hooks ?? state.features?.codex_hooks) === false) {
87965
+ return check2("fail", "Codex hooks are explicitly disabled; left unchanged");
87966
+ }
87967
+ const hooks = state.hooks.filter((h) => h.command?.includes("errata-relay.mjs"));
87968
+ const required2 = ["sessionStart", "userPromptSubmit", "preToolUse", "postToolUse", "stop"];
87969
+ const missing = required2.filter((event) => !hooks.some((h) => h.eventName === event));
87970
+ const checks = [];
87971
+ if (missing.length) checks.push(...check2("fail", `Errata hooks missing or outdated in this checkout (${missing.join(", ")}); run errata install-hooks codex here`));
87972
+ if (hooks.some((h) => !h.enabled)) checks.push(...check2("fail", `Errata hooks are disabled in Codex; left unchanged. ${CODEX_HOOK_REVIEW_HELP}`));
87973
+ if (hooks.some((h) => !["trusted", "managed"].includes(h.trustStatus))) {
87974
+ checks.push(...check2("fail", `Errata hooks need review/trust before they can run; project trust is separate. ${CODEX_HOOK_REVIEW_HELP}`));
87975
+ }
87976
+ return checks.length ? checks : check2("pass", "Codex discovers enabled, trusted Errata hooks; verify live context and capture in a session");
87977
+ }
87978
+ async function diagnoseCodexHooks(cwd) {
87979
+ try {
87980
+ return assessCodexHooks(await readCodexHookState(cwd));
87981
+ } catch (error48) {
87982
+ return [{ id: "codex_hooks", status: "fail", detail: error48 instanceof Error ? error48.message : "Cannot verify Codex hooks" }];
87983
+ }
87984
+ }
87985
+
87986
+ // src/whoami.ts
87987
+ async function resolveWhoami(client, actingHandle) {
87988
+ return actingHandle ? client.contextAs(actingHandle) : client.context();
87989
+ }
87990
+ function targetLabel(context) {
87991
+ const target = context.default_write_target;
87992
+ if (!target) return "read-only";
87993
+ if (target.visibility === "public") return "public graph";
87994
+ if (target.visibility === "org") return `organization graph (${target.org_id})`;
87995
+ return `private Team graph (${target.team_id})`;
87996
+ }
87997
+ function formatWhoami(response, localProfile) {
87998
+ const { context, identity } = response;
87999
+ return [
88000
+ `inErrata context \u2014 ${context.profile_name ?? localProfile ?? "unbound credential"}`,
88001
+ ` workspace: ${context.workspace_kind} \xB7 ${context.org_id}`,
88002
+ ` team: ${context.team_id ?? "(none)"}`,
88003
+ ` agent: ${context.agent_id ?? "(default identity)"}`,
88004
+ ` installation: ${context.installation_id ?? "(unbound)"}`,
88005
+ ` environment: ${context.environment} \xB7 ${context.client_kind}`,
88006
+ ` reads: ${context.read_visibilities.join(" + ")}`,
88007
+ ` contributes: ${targetLabel(context)}`,
88008
+ ` policy: ${context.policy_version ?? "(legacy)"}`,
88009
+ ` credential: ${identity.credential_type} \xB7 ${identity.credential_id}`,
88010
+ ` actor: ${identity.actor_type} \xB7 ${identity.actor_id}`
88011
+ ];
88012
+ }
88013
+
88014
+ // src/consolidation-trigger.ts
88015
+ function effectiveMomentumThreshold(policy, liveNodes) {
88016
+ return Math.max(policy.momentumThreshold, Math.ceil(liveNodes * policy.momentumRatio));
88017
+ }
88018
+ function nextMomentumThreshold(args2) {
88019
+ const { current, consumed, landmarkFlips, floor, cap } = args2;
88020
+ const next = landmarkFlips > 0 ? Math.min(current, Math.round(consumed / 2)) : Math.max(current, Math.round(consumed * 1.5));
88021
+ return Math.max(floor, Math.min(cap, next));
88022
+ }
88023
+ var DEFAULT_CONSOLIDATION_POLICY = {
88024
+ baseFloorMs: 6e4,
88025
+ dutyFactor: 20,
88026
+ // ≤5% worst-case duty at any graph size (crater guard)
88027
+ momentumThreshold: 500,
88028
+ // POLICY freshness bound — the gate never demands less
88029
+ momentumRatio: 0.01,
88030
+ // cold-start PRIOR only; calibration replaces it after the first pass
88031
+ momentumRatioCap: 0.05,
88032
+ // POLICY staleness bound — refresh by 5% churn no matter what
88033
+ quiescenceMs: 6e4
88034
+ // sim preferred 60s over 90s (fresher, esp. heavy-code)
88035
+ };
88036
+ function numEnv5(env2, key, fallback) {
88037
+ const raw2 = env2[key];
88038
+ if (raw2 == null || raw2.trim() === "") return fallback;
88039
+ const n = Number(raw2);
88040
+ return Number.isFinite(n) && n >= 0 ? n : fallback;
88041
+ }
88042
+ function consolidationPolicyFromEnv(env2 = process.env) {
88043
+ return {
88044
+ baseFloorMs: numEnv5(env2, "ERRATA_CONSOLIDATE_BASE_FLOOR_MS", DEFAULT_CONSOLIDATION_POLICY.baseFloorMs),
88045
+ dutyFactor: numEnv5(env2, "ERRATA_CONSOLIDATE_DUTY_FACTOR", DEFAULT_CONSOLIDATION_POLICY.dutyFactor),
88046
+ momentumThreshold: numEnv5(env2, "ERRATA_CONSOLIDATE_MOMENTUM", DEFAULT_CONSOLIDATION_POLICY.momentumThreshold),
88047
+ momentumRatio: numEnv5(env2, "ERRATA_CONSOLIDATE_MOMENTUM_RATIO", DEFAULT_CONSOLIDATION_POLICY.momentumRatio),
88048
+ momentumRatioCap: numEnv5(env2, "ERRATA_CONSOLIDATE_MOMENTUM_RATIO_CAP", DEFAULT_CONSOLIDATION_POLICY.momentumRatioCap),
88049
+ quiescenceMs: numEnv5(env2, "ERRATA_CONSOLIDATE_QUIESCENCE_MS", DEFAULT_CONSOLIDATION_POLICY.quiescenceMs)
88050
+ };
88051
+ }
88052
+ function consolidationGapMs(lastPassMs, policy) {
88053
+ return Math.max(policy.baseFloorMs, lastPassMs * policy.dutyFactor);
88054
+ }
88055
+
87899
88056
  // src/opencode-plugin.ts
87900
88057
  var OPENCODE_PLUGIN_MARKER = "// @inerrata opencode plugin \u2014 errata-managed (errata install-hooks opencode)";
87901
88058
  var OPENCODE_D1_CONTRACT = "Behavioral contract: (1) On ANY error, call search() BEFORE debugging \u2014 costs ~400 tokens vs 5,000\u201350,000 cold. (2) After solving non-trivial problems, contribute back: if your harness emits typed nodes (canonicalId + label + attrs), use ingest \u2014 the typed door; otherwise call contribute(), the prose fallback. (3) Before web search, try search() first \u2014 graph is 2\u201315\xD7 cheaper. (4) At session end, review what you solved and contribute anything novel.";
@@ -88302,13 +88459,13 @@ async function cmdInit() {
88302
88459
  console.log("");
88303
88460
  console.log("installing harness hooks...");
88304
88461
  const { existsSync: existsSync35 } = await import("node:fs");
88305
- const { join: join41 } = await import("node:path");
88462
+ const { join: join42 } = await import("node:path");
88306
88463
  try {
88307
88464
  await installClaudeHooks(port);
88308
88465
  } catch (err2) {
88309
88466
  console.warn(` \u26A0\uFE0F Claude hook install failed: ${err2 instanceof Error ? err2.message : err2}`);
88310
88467
  }
88311
- if (existsSync35(join41(ROOT, ".cursor"))) {
88468
+ if (existsSync35(join42(ROOT, ".cursor"))) {
88312
88469
  try {
88313
88470
  await installCursorMcpConfig();
88314
88471
  } catch (err2) {
@@ -88752,6 +88909,7 @@ async function cmdWhoami(args2) {
88752
88909
  async function cmdDoctor(args2) {
88753
88910
  const cfg = loadConfig();
88754
88911
  const report = await diagnoseInstallation(cfg, {
88912
+ ...hasCodexConfig(process.cwd()) ? { codexHooks: () => diagnoseCodexHooks(process.cwd()) } : {},
88755
88913
  inspect: () => inspectCloudEndpoint(cfg.cloudUrl),
88756
88914
  authenticate: async () => {
88757
88915
  const me = await authedCloudClient(cfg).me();
@@ -90135,8 +90293,8 @@ async function cmdReport(args2) {
90135
90293
  const outDir = workspacePaths(ROOT).configDir;
90136
90294
  mkdirSync10(outDir, { recursive: true });
90137
90295
  const files = renderReport2(data, { includeFutureVerbs });
90138
- for (const f of files) writeFileSync26(join40(outDir, f.name), f.html, "utf8");
90139
- const indexPath = join40(outDir, "report.html");
90296
+ for (const f of files) writeFileSync26(join41(outDir, f.name), f.html, "utf8");
90297
+ const indexPath = join41(outDir, "report.html");
90140
90298
  console.log(`report \u2192 ${indexPath}`);
90141
90299
  console.log(
90142
90300
  ` ${data.repos.length} repo(s) \xB7 ${files.length} file(s) \xB7 ${data.rollup.nodes.toLocaleString("en-US")} nodes`
@@ -90191,13 +90349,13 @@ function spawnDaemonDetached(options = {}) {
90191
90349
  try {
90192
90350
  const logPath = daemonLogPath();
90193
90351
  try {
90194
- if (statSync8(logPath).size > 5 * 1024 * 1024) renameSync4(logPath, `${logPath}.1`);
90352
+ if (statSync9(logPath).size > 5 * 1024 * 1024) renameSync4(logPath, `${logPath}.1`);
90195
90353
  } catch {
90196
90354
  }
90197
90355
  out2 = openSync2(logPath, "a");
90198
90356
  } catch {
90199
90357
  }
90200
- spawn4(cmd2, args2, {
90358
+ spawn5(cmd2, args2, {
90201
90359
  detached: true,
90202
90360
  stdio: ["ignore", out2, out2],
90203
90361
  windowsHide: true,
@@ -90257,10 +90415,10 @@ async function cmdInstallHooks(args2) {
90257
90415
  }
90258
90416
  async function installOpencodePlugin(port) {
90259
90417
  const { mkdirSync: mkdirSync10, existsSync: existsSync35, readFileSync: readFileSync36, writeFileSync: writeFileSync26 } = await import("node:fs");
90260
- const { join: join41 } = await import("node:path");
90261
- const dir = join41(ROOT, ".opencode", "plugin");
90418
+ const { join: join42 } = await import("node:path");
90419
+ const dir = join42(ROOT, ".opencode", "plugin");
90262
90420
  if (!existsSync35(dir)) mkdirSync10(dir, { recursive: true });
90263
- const file2 = join41(dir, "inerrata.js");
90421
+ const file2 = join42(dir, "inerrata.js");
90264
90422
  if (existsSync35(file2)) {
90265
90423
  const head2 = readFileSync36(file2, "utf8").split("\n")[0] ?? "";
90266
90424
  if (head2.trim() !== OPENCODE_PLUGIN_MARKER) {
@@ -90269,7 +90427,7 @@ async function installOpencodePlugin(port) {
90269
90427
  }
90270
90428
  }
90271
90429
  writeFileSync26(file2, buildOpencodePluginSource(port, { version: DAEMON_VERSION }), "utf8");
90272
- const cfgFile = join41(ROOT, "opencode.json");
90430
+ const cfgFile = join42(ROOT, "opencode.json");
90273
90431
  let cfg = { $schema: "https://opencode.ai/config.json" };
90274
90432
  if (existsSync35(cfgFile)) {
90275
90433
  try {
@@ -90305,10 +90463,10 @@ function errataMcpInvocation() {
90305
90463
  }
90306
90464
  async function installClaudeHooks(port) {
90307
90465
  const { mkdirSync: mkdirSync10, existsSync: existsSync35, readFileSync: readFileSync36, writeFileSync: writeFileSync26 } = await import("node:fs");
90308
- const { join: join41 } = await import("node:path");
90309
- const dir = join41(ROOT, ".claude");
90466
+ const { join: join42 } = await import("node:path");
90467
+ const dir = join42(ROOT, ".claude");
90310
90468
  if (!existsSync35(dir)) mkdirSync10(dir, { recursive: true });
90311
- const file2 = join41(dir, "settings.json");
90469
+ const file2 = join42(dir, "settings.json");
90312
90470
  let settings = {};
90313
90471
  if (existsSync35(file2)) {
90314
90472
  try {
@@ -90361,7 +90519,7 @@ async function installClaudeHooks(port) {
90361
90519
  writeFileSync26(file2, JSON.stringify(settings, null, 2) + "\n", "utf8");
90362
90520
  console.log(`installed Claude Code hooks \u2192 ${file2}`);
90363
90521
  await installClaudeMcpConfig();
90364
- const claudeMd = join41(ROOT, "CLAUDE.md");
90522
+ const claudeMd = join42(ROOT, "CLAUDE.md");
90365
90523
  const recall = writeManagedBlock(claudeMd, { body: RECALL_FIRST_BLOCK });
90366
90524
  if (recall.kind === "collision") {
90367
90525
  console.warn(
@@ -90374,8 +90532,8 @@ async function installClaudeHooks(port) {
90374
90532
  }
90375
90533
  async function installClaudeMcpConfig() {
90376
90534
  const { mkdirSync: mkdirSync10, existsSync: existsSync35, readFileSync: readFileSync36, writeFileSync: writeFileSync26 } = await import("node:fs");
90377
- const { join: join41, dirname: dirname13 } = await import("node:path");
90378
- const file2 = join41(ROOT, ".mcp.json");
90535
+ const { join: join42, dirname: dirname13 } = await import("node:path");
90536
+ const file2 = join42(ROOT, ".mcp.json");
90379
90537
  const dir = dirname13(file2);
90380
90538
  if (!existsSync35(dir)) mkdirSync10(dir, { recursive: true });
90381
90539
  let cfg = {};
@@ -90396,10 +90554,10 @@ async function installClaudeMcpConfig() {
90396
90554
  }
90397
90555
  async function installCursorMcpConfig() {
90398
90556
  const { mkdirSync: mkdirSync10, existsSync: existsSync35, readFileSync: readFileSync36, writeFileSync: writeFileSync26 } = await import("node:fs");
90399
- const { join: join41 } = await import("node:path");
90400
- const dir = join41(ROOT, ".cursor");
90557
+ const { join: join42 } = await import("node:path");
90558
+ const dir = join42(ROOT, ".cursor");
90401
90559
  if (!existsSync35(dir)) mkdirSync10(dir, { recursive: true });
90402
- const file2 = join41(dir, "mcp.json");
90560
+ const file2 = join42(dir, "mcp.json");
90403
90561
  let cfg = {};
90404
90562
  if (existsSync35(file2)) {
90405
90563
  try {
@@ -90419,12 +90577,26 @@ async function installCursorMcpConfig() {
90419
90577
  console.log(` \u26A0\uFE0F reload Cursor (Cmd/Ctrl-Shift-P \u2192 "Reload Window") to pick it up.`);
90420
90578
  }
90421
90579
  async function installCodexHooks(port) {
90422
- const file2 = writeCodexHooks(ROOT, port);
90580
+ const gitRoot = nearestGitRoot(process.cwd());
90581
+ const hookRoot = gitRoot && mainCheckoutOfWorktree(gitRoot) ? gitRoot : ROOT;
90582
+ const file2 = writeCodexHooks(hookRoot, port);
90583
+ const instructions = writeManagedBlock(join41(hookRoot, "AGENTS.md"), {
90584
+ body: AGENTS_POINTER_BODY,
90585
+ stable: true
90586
+ });
90423
90587
  console.log(`installed Codex hooks \u2192 ${file2}`);
90588
+ if (instructions.kind === "collision") {
90589
+ console.warn(" AGENTS.md's Errata block was edited; preserved it. Add an explicit instruction to read .errata/context.md each turn.");
90590
+ } else {
90591
+ console.log(" AGENTS.md now tells Codex to read live context at the start of each turn.");
90592
+ }
90424
90593
  console.log(" Context loads on SessionStart/UserPromptSubmit and refreshes after tools when changed.");
90425
- console.log(" Tool capture and /api/turn harvesting remain enabled.");
90426
- console.log(" Restart/resume Codex to reload config, then review and trust the hooks with /hooks.");
90427
- console.log(" If your Codex version gates hooks, enable [features] codex_hooks = true.");
90594
+ console.log(" Tool and final-reply capture are configured; hooks must be trusted before they run.");
90595
+ console.log(` ${CODEX_HOOK_REVIEW_HELP}`);
90596
+ console.log(" Use Reload hooks in that screen after installation. Restarting alone does not grant trust.");
90597
+ console.log(" No standalone Codex CLI is required for desktop setup.");
90598
+ console.log(" Run `errata doctor` here to check hook discovery and trust; installation alone is not activation.");
90599
+ console.log(" Respect an explicit [features] hooks = false setting (older builds use codex_hooks).");
90428
90600
  console.log(" Older Codex versions may not support every event; update if hooks do not fire.");
90429
90601
  }
90430
90602
  async function refreshLinkedCodexHooks(args2) {
@@ -90812,7 +90984,7 @@ async function cmdDash(args2) {
90812
90984
  await yieldToLoop2();
90813
90985
  try {
90814
90986
  const items = selectDurableMemory(handle2.sharedStore, r.engine.profile);
90815
- const res = bleedRules(join40(r.root, ".claude", "rules"), items);
90987
+ const res = bleedRules(join41(r.root, ".claude", "rules"), items);
90816
90988
  if (res.written || res.pruned) {
90817
90989
  console.log(
90818
90990
  `[rules:${r.entry.name}] ${res.written} collective principle(s) \u2192 .claude/rules` + (res.pruned ? `, ${res.pruned} pruned` : "")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inerrata-corporation/errata",
3
- "version": "2.0.6-dev.2752",
3
+ "version": "2.0.6-dev.2766",
4
4
  "description": "errata - local-first observation engine for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {
package/pass-worker.mjs CHANGED
@@ -27255,6 +27255,11 @@ var AGENTS_POINTER_BODY = [
27255
27255
  "Live agent context for this checkout \u2014 priors, open problems, the capture",
27256
27256
  "protocol \u2014 is maintained by the errata daemon at `.errata/context.md`.",
27257
27257
  "",
27258
+ "At the beginning of each task and each new user turn, read that file before",
27259
+ "substantive work. Use the current file in preference to an earlier snapshot.",
27260
+ "If it is absent, continue normally. In Codex, an `@` mention alone does not",
27261
+ "load the file; read it explicitly.",
27262
+ "",
27258
27263
  "It is gitignored ON PURPOSE: it is this machine's runtime state, not repo",
27259
27264
  "content. Committing it would prime your teammates with your graph.",
27260
27265
  "",