@inerrata-corporation/errata 2.0.8-dev.2839 → 2.0.8-dev.2870

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.
Files changed (2) hide show
  1. package/errata.mjs +391 -118
  2. package/package.json +1 -1
package/errata.mjs CHANGED
@@ -29066,6 +29066,92 @@ var init_agent_signals = __esm({
29066
29066
  }
29067
29067
  });
29068
29068
 
29069
+ // src/recall-card.ts
29070
+ function labelName(label) {
29071
+ return LABEL_NAMES[label] ?? label.toUpperCase();
29072
+ }
29073
+ function clip(text, width) {
29074
+ const flat = text.replace(/\s+/g, " ").trim();
29075
+ return flat.length > width ? `${flat.slice(0, width - 1)}\u2026` : flat;
29076
+ }
29077
+ function plural(n, word, many = `${word}s`) {
29078
+ return `${n} ${n === 1 ? word : many}`;
29079
+ }
29080
+ function errorCardHeadline(items) {
29081
+ const fixes = items.filter((it) => it.label === "Solution").length;
29082
+ return fixes > 0 ? `${plural(fixes, "known fix", "known fixes")} for this error` : `${plural(items.length, "prior")} for this error`;
29083
+ }
29084
+ function leadItem(items) {
29085
+ return items.find((it) => it.label === "Solution") ?? items[0];
29086
+ }
29087
+ function renderRecallLine(card) {
29088
+ const lead2 = leadItem(card.items);
29089
+ if (!lead2) return null;
29090
+ const rest2 = card.items.length - 1;
29091
+ return `\u26A1 errata \xB7 ${card.headline} \u2014 ${labelName(lead2.label)}: ${clip(lead2.text, INLINE_TEXT_WIDTH)}` + (lead2.note ? ` ${lead2.note}` : "") + (rest2 > 0 ? ` \xB7 +${rest2} in context` : "");
29092
+ }
29093
+ function labelColor(label) {
29094
+ if (label === "Solution") return GREEN;
29095
+ if (label === "Problem" || label === "AntiPattern") return MADDER;
29096
+ return AMBER;
29097
+ }
29098
+ function renderStatusCard(card) {
29099
+ const ordered = [...card.items].sort(
29100
+ (a, b) => Number(b.label === "Solution") - Number(a.label === "Solution")
29101
+ );
29102
+ const rows = ordered.slice(0, STATUS_ROWS).map((it) => {
29103
+ const label = labelName(it.label).padEnd(LABEL_WIDTH);
29104
+ const note = it.note ? ` ${DIM}${it.note}${RESET}` : "";
29105
+ return `${DIM}\u2502${RESET} ${BOLD}${labelColor(it.label)}${label}${RESET} ${INK}${clip(it.text, STATUS_TEXT_WIDTH)}${RESET}${note}`;
29106
+ });
29107
+ const hidden = card.items.length - STATUS_ROWS;
29108
+ const more = hidden > 0 ? ` ${DIM}\xB7 +${hidden} more in the agent's context${RESET}` : "";
29109
+ return [`${AMBER}\u26A1${RESET} ${BOLD}errata${RESET} ${DIM}\xB7${RESET} ${card.headline}${more}`, ...rows].join("\n");
29110
+ }
29111
+ function rememberCard(sessionId, card, now = Date.now()) {
29112
+ if (!sessionId) return;
29113
+ latestBySession.delete(sessionId);
29114
+ latestBySession.set(sessionId, { card, at: now });
29115
+ if (latestBySession.size > MAX_SESSIONS) {
29116
+ latestBySession.delete(latestBySession.keys().next().value);
29117
+ }
29118
+ }
29119
+ function statuslineText(sessionId, now = Date.now()) {
29120
+ const hit = latestBySession.get(sessionId);
29121
+ if (!hit || now - hit.at > STATUS_CARD_TTL_MS) return IDLE_STATUS;
29122
+ return renderStatusCard(hit.card);
29123
+ }
29124
+ function recallCardEnabled(env2 = process.env) {
29125
+ const v = (env2["ERRATA_RECALL_CARD"] ?? "").trim().toLowerCase();
29126
+ return !(v === "0" || v === "off" || v === "false" || v === "no");
29127
+ }
29128
+ var LABEL_NAMES, INLINE_TEXT_WIDTH, ESC, RESET, DIM, BOLD, AMBER, MADDER, GREEN, INK, STATUS_ROWS, STATUS_TEXT_WIDTH, LABEL_WIDTH, STATUS_CARD_TTL_MS, IDLE_STATUS, latestBySession, MAX_SESSIONS;
29129
+ var init_recall_card = __esm({
29130
+ "src/recall-card.ts"() {
29131
+ "use strict";
29132
+ LABEL_NAMES = {
29133
+ RootCause: "ROOT CAUSE",
29134
+ AntiPattern: "ANTIPATTERN"
29135
+ };
29136
+ INLINE_TEXT_WIDTH = 64;
29137
+ ESC = "\x1B[";
29138
+ RESET = `${ESC}0m`;
29139
+ DIM = `${ESC}2m`;
29140
+ BOLD = `${ESC}1m`;
29141
+ AMBER = `${ESC}38;2;217;164;65m`;
29142
+ MADDER = `${ESC}38;2;210;58;77m`;
29143
+ GREEN = `${ESC}38;2;111;191;138m`;
29144
+ INK = `${ESC}38;2;236;226;221m`;
29145
+ STATUS_ROWS = 2;
29146
+ STATUS_TEXT_WIDTH = 96;
29147
+ LABEL_WIDTH = 10;
29148
+ STATUS_CARD_TTL_MS = 9e4;
29149
+ IDLE_STATUS = `${AMBER}\u26A1${RESET} ${DIM}errata \xB7 watching${RESET}`;
29150
+ latestBySession = /* @__PURE__ */ new Map();
29151
+ MAX_SESSIONS = 256;
29152
+ }
29153
+ });
29154
+
29069
29155
  // src/review.ts
29070
29156
  import { existsSync as existsSync7, readFileSync as readFileSync5, writeFileSync as writeFileSync6 } from "node:fs";
29071
29157
  function loadReviewQueue(paths) {
@@ -51311,8 +51397,11 @@ var init_message_accumulator = __esm({
51311
51397
  // src/webui.ts
51312
51398
  var webui_exports = {};
51313
51399
  __export(webui_exports, {
51400
+ asFailedPostToolUse: () => asFailedPostToolUse,
51314
51401
  buildWebUi: () => buildWebUi,
51315
51402
  decideContextInject: () => decideContextInject,
51403
+ errorRecallTerms: () => errorRecallTerms,
51404
+ fileRecall: () => fileRecall,
51316
51405
  fileUriToFsPath: () => fileUriToFsPath,
51317
51406
  findFileByPath: () => findFileByPath,
51318
51407
  recallForFile: () => recallForFile,
@@ -51327,9 +51416,7 @@ function fileUriToFsPath(raw2) {
51327
51416
  return p;
51328
51417
  }
51329
51418
  function recallForError(store, errorText) {
51330
- const terms = Array.from(
51331
- new Set(errorText.split(/[^A-Za-z0-9_.]+/).filter((t) => t.length >= 5))
51332
- ).slice(0, 3);
51419
+ const terms = errorRecallTerms(errorText);
51333
51420
  if (terms.length === 0) return null;
51334
51421
  const labels = ["Problem", "Solution", "RootCause"];
51335
51422
  const seen = /* @__PURE__ */ new Set();
@@ -51344,15 +51431,46 @@ function recallForError(store, errorText) {
51344
51431
  }
51345
51432
  if (hits.length === 0) return null;
51346
51433
  const lines = hits.slice(0, 3).map((n) => `- [${n.label}] ${(n.description ?? "").replace(/\s+/g, " ").slice(0, 140)}`);
51347
- return `errata recall \u2014 ${hits.length} prior match(es) in the local graph for this error:
51434
+ const items = hits.map((n) => ({ label: n.label, text: n.description ?? "" }));
51435
+ return {
51436
+ context: `errata recall \u2014 ${hits.length} prior match(es) in the local graph for this error:
51348
51437
  ${lines.join("\n")}
51349
- Reuse the prior fix if it applies before re-deriving.`;
51438
+ Reuse the prior fix if it applies before re-deriving.`,
51439
+ card: { headline: errorCardHeadline(items), items }
51440
+ };
51441
+ }
51442
+ function asFailedPostToolUse(body2) {
51443
+ const error48 = typeof body2["error"] === "string" ? body2["error"] : "";
51444
+ const exit = /^Exit code (\d+)/.exec(error48);
51445
+ return {
51446
+ ...body2,
51447
+ hook_event_name: "PostToolUse",
51448
+ is_error: true,
51449
+ tool_response: { stdout: "", stderr: error48, exitCode: exit ? Number(exit[1]) : 1 }
51450
+ };
51451
+ }
51452
+ function errorRecallTerms(errorText) {
51453
+ const seen = /* @__PURE__ */ new Set();
51454
+ const scored = [];
51455
+ errorText.split(/[^A-Za-z0-9_.-]+/).forEach((raw2, at) => {
51456
+ const term = raw2.replace(/^[.-]+|[.-]+$/g, "");
51457
+ const key = term.toLowerCase();
51458
+ if (term.length < 5 || seen.has(key) || GENERIC_ERROR_WORDS.has(key)) return;
51459
+ seen.add(key);
51460
+ const identifier = /[_.\d-]/.test(term) || /[a-z][A-Z]/.test(term) || /^[A-Z]{5,}$/.test(term);
51461
+ scored.push({ term, score: identifier ? 1 : 0, at });
51462
+ });
51463
+ const hasIdentifier = scored.some((s) => s.score > 0);
51464
+ return scored.filter((s) => !hasIdentifier || s.score > 0).sort((a, b) => b.score - a.score || a.at - b.at).slice(0, 3).map((s) => s.term);
51350
51465
  }
51351
51466
  function recallLine(n, token) {
51352
51467
  const desc = (n.description ?? "").replace(/\s+/g, " ").slice(0, 120);
51353
51468
  return `- [${n.label}] ${desc} \u2192 ${token}`;
51354
51469
  }
51355
51470
  function recallForFile(store, relPath) {
51471
+ return fileRecall(store, relPath)?.context ?? null;
51472
+ }
51473
+ function fileRecall(store, relPath) {
51356
51474
  const priors = priorsForFile(store, relPath);
51357
51475
  if (!priors) return null;
51358
51476
  const lines = [];
@@ -51370,9 +51488,16 @@ function recallForFile(store, relPath) {
51370
51488
  lines.push(recallLine(n, `cite with ${TAG_EXAMPLE.prior(n.id)}`));
51371
51489
  }
51372
51490
  const total = priors.openProblems.length + priors.constraints.length + priors.related.length;
51373
- return `errata \u2014 ${total} prior(s) recorded on this file (act only if relevant):
51491
+ const items = [...priors.openProblems, ...priors.constraints, ...priors.related].map((n) => ({
51492
+ label: n.label,
51493
+ text: n.description ?? ""
51494
+ }));
51495
+ return {
51496
+ context: `errata \u2014 ${total} prior(s) recorded on this file (act only if relevant):
51374
51497
  ${lines.join("\n")}
51375
- ` + buildFileRecallInstruction();
51498
+ ` + buildFileRecallInstruction(),
51499
+ card: { headline: `${plural(total, "prior")} on ${relPath.split("/").slice(-2).join("/")}`, items }
51500
+ };
51376
51501
  }
51377
51502
  function workspacePackageIds(store) {
51378
51503
  const hit = wsPackagesCache.get(store);
@@ -51383,7 +51508,11 @@ function workspacePackageIds(store) {
51383
51508
  return ids;
51384
51509
  }
51385
51510
  function recallForTool(store, command, ctx) {
51511
+ return toolRecall(store, command, ctx)?.context ?? null;
51512
+ }
51513
+ function toolRecall(store, command, ctx) {
51386
51514
  const lines = [];
51515
+ const items = [];
51387
51516
  const seen = /* @__PURE__ */ new Set();
51388
51517
  for (const name2 of extractExecutables(command)) {
51389
51518
  if (seen.has(name2)) continue;
@@ -51397,11 +51526,19 @@ function recallForTool(store, command, ctx) {
51397
51526
  fit.node.label === "Problem" ? `${TAG_EXAMPLE.fix(fit.node.id)} if you resolved it \xB7 ${TAG_EXAMPLE.prior(fit.node.id)} to cite` : `cite with ${TAG_EXAMPLE.prior(fit.node.id)}`
51398
51527
  ).slice(2)}`
51399
51528
  );
51529
+ items.push({
51530
+ label: fit.node.label,
51531
+ text: `${name2}: ${fit.node.description ?? ""}`,
51532
+ note: fit.osMismatch ? `[seen on ${fit.observedOn.join("/")}]` : void 0
51533
+ });
51400
51534
  }
51401
51535
  }
51402
51536
  if (lines.length === 0) return null;
51403
- return `errata \u2014 ${lines.length} prior(s) about the tool(s) you're about to run (a \`[platform]\` tag means it was seen on a DIFFERENT OS than this one \u2014 judge it):
51404
- ${lines.join("\n")}`;
51537
+ return {
51538
+ context: `errata \u2014 ${lines.length} prior(s) about the tool(s) you're about to run (a \`[platform]\` tag means it was seen on a DIFFERENT OS than this one \u2014 judge it):
51539
+ ${lines.join("\n")}`,
51540
+ card: { headline: `${plural(items.length, "prior")} before this command runs`, items }
51541
+ };
51405
51542
  }
51406
51543
  function decideContextInject(args2) {
51407
51544
  const { event, sessionId, block, lastInjectedHash } = args2;
@@ -51559,8 +51696,10 @@ function buildWebUi(deps) {
51559
51696
  if (deps.onHook) deps.onHook();
51560
51697
  const tool = String(body2["tool_name"] ?? "");
51561
51698
  const input = body2["tool_input"] ?? {};
51562
- const event = String(body2["hook_event_name"] ?? "PreToolUse");
51563
- let recallCtx = null;
51699
+ const hookEvent = String(body2["hook_event_name"] ?? "PreToolUse");
51700
+ if (hookEvent === "PostToolUseFailure") body2 = asFailedPostToolUse(body2);
51701
+ const event = hookEvent === "PostToolUseFailure" ? "PostToolUse" : hookEvent;
51702
+ let recall = null;
51564
51703
  if (event === "PreToolUse") {
51565
51704
  const virtual = extractVirtualReads(input);
51566
51705
  if (virtual.length > 0) {
@@ -51575,7 +51714,7 @@ function buildWebUi(deps) {
51575
51714
  }
51576
51715
  if (tool === "Bash" && typeof input["command"] === "string") {
51577
51716
  try {
51578
- recallCtx = recallForTool(deps.store, input["command"], {
51717
+ recall = toolRecall(deps.store, input["command"], {
51579
51718
  os: currentOsName(),
51580
51719
  workspacePackageIds: workspacePackageIds(deps.store)
51581
51720
  });
@@ -51629,7 +51768,7 @@ function buildWebUi(deps) {
51629
51768
  deps.onToolsObserved?.(toolSid, observed.nodeIds);
51630
51769
  } catch {
51631
51770
  }
51632
- if (failed) recallCtx = recallForError(deps.store, stderr || combined);
51771
+ if (failed) recall = recallForError(deps.store, stderr || combined);
51633
51772
  }
51634
51773
  if (event === "PostToolUse" && tool && tool !== "Bash" && deps.onToolError) {
51635
51774
  const rawResp = body2["tool_response"];
@@ -51652,11 +51791,11 @@ function buildWebUi(deps) {
51652
51791
  exitCode: numericExit ?? 1,
51653
51792
  ts: Date.now()
51654
51793
  });
51655
- recallCtx = recallForError(deps.store, errText2);
51794
+ recall = recallForError(deps.store, errText2);
51656
51795
  }
51657
51796
  }
51658
51797
  }
51659
- if (event === "PostToolUse" && !recallCtx && rel && (tool === "Read" || tool === "Edit" || tool === "Write" || tool === "NotebookEdit")) {
51798
+ if (event === "PostToolUse" && !recall && rel && (tool === "Read" || tool === "Edit" || tool === "Write" || tool === "NotebookEdit")) {
51660
51799
  const sid = String(body2["session_id"] ?? "");
51661
51800
  let seen = fileRecallSeen.get(sid);
51662
51801
  if (!seen) {
@@ -51664,28 +51803,29 @@ function buildWebUi(deps) {
51664
51803
  fileRecallSeen.set(sid, seen);
51665
51804
  }
51666
51805
  if (!seen.has(rel)) {
51667
- const fileCtx = recallForFile(deps.store, rel);
51668
- if (fileCtx) {
51806
+ const fileHit = fileRecall(deps.store, rel);
51807
+ if (fileHit) {
51669
51808
  seen.add(rel);
51670
- recallCtx = fileCtx;
51809
+ recall = fileHit;
51671
51810
  }
51672
51811
  }
51673
51812
  }
51674
- if (event === "PostToolUse") {
51675
- if (recallCtx) {
51676
- return c.json({
51677
- hookSpecificOutput: { hookEventName: "PostToolUse", additionalContext: recallCtx }
51678
- });
51679
- }
51680
- return c.body(null, 204);
51681
- }
51682
- if (event === "PreToolUse" && recallCtx) {
51813
+ if ((event === "PostToolUse" || event === "PreToolUse") && recall) {
51814
+ const showCard = recallCardEnabled();
51815
+ if (showCard) rememberCard(String(body2["session_id"] ?? ""), recall.card);
51816
+ const line = showCard ? renderRecallLine(recall.card) : null;
51683
51817
  return c.json({
51684
- hookSpecificOutput: { hookEventName: "PreToolUse", additionalContext: recallCtx }
51818
+ ...line ? { systemMessage: line } : {},
51819
+ hookSpecificOutput: { hookEventName: hookEvent, additionalContext: recall.context }
51685
51820
  });
51686
51821
  }
51822
+ if (event === "PostToolUse") return c.body(null, 204);
51687
51823
  return c.json({ ok: true, event, tool, recognizedFile: filePath ?? null });
51688
51824
  });
51825
+ app.post("/api/statusline", async (c) => {
51826
+ const body2 = await c.req.json().catch(() => ({}));
51827
+ return c.text(statuslineText(String(body2["session_id"] ?? "")));
51828
+ });
51689
51829
  app.post("/api/turn", async (c) => {
51690
51830
  let body2;
51691
51831
  try {
@@ -51929,7 +52069,7 @@ function proposalToIngest(p, profile, daemonVersion) {
51929
52069
  payloadDigest: p.id
51930
52070
  };
51931
52071
  }
51932
- var messageAccumulator, wsPackagesCache, WS_PACKAGES_TTL_MS, loggedToolSessions, META_LABELS;
52072
+ var messageAccumulator, GENERIC_ERROR_WORDS, wsPackagesCache, WS_PACKAGES_TTL_MS, loggedToolSessions, META_LABELS;
51933
52073
  var init_webui = __esm({
51934
52074
  "src/webui.ts"() {
51935
52075
  "use strict";
@@ -51937,12 +52077,54 @@ var init_webui = __esm({
51937
52077
  init_src6();
51938
52078
  init_src5();
51939
52079
  init_agent_signals();
52080
+ init_recall_card();
51940
52081
  init_review2();
51941
52082
  init_vfile();
51942
52083
  init_tool_index();
51943
52084
  init_outbox();
51944
52085
  init_message_accumulator();
51945
52086
  messageAccumulator = createMessageAccumulator();
52087
+ GENERIC_ERROR_WORDS = /* @__PURE__ */ new Set([
52088
+ "cannot",
52089
+ "error",
52090
+ "errors",
52091
+ "failed",
52092
+ "failure",
52093
+ "found",
52094
+ "install",
52095
+ "installed",
52096
+ "module",
52097
+ "command",
52098
+ "because",
52099
+ "while",
52100
+ "could",
52101
+ "should",
52102
+ "which",
52103
+ "there",
52104
+ "their",
52105
+ "about",
52106
+ "after",
52107
+ "before",
52108
+ "update",
52109
+ "warning",
52110
+ "expected",
52111
+ "received",
52112
+ "value",
52113
+ "undefined",
52114
+ "unable",
52115
+ "invalid",
52116
+ "missing",
52117
+ "exited",
52118
+ "unknown",
52119
+ "files",
52120
+ "directory",
52121
+ "process",
52122
+ "access",
52123
+ "denied",
52124
+ "permission",
52125
+ "running",
52126
+ "argument"
52127
+ ]);
51946
52128
  wsPackagesCache = /* @__PURE__ */ new WeakMap();
51947
52129
  WS_PACKAGES_TTL_MS = 6e4;
51948
52130
  loggedToolSessions = /* @__PURE__ */ new Set();
@@ -55730,8 +55912,8 @@ var require_webidl = __commonJS({
55730
55912
  return new TypeError(`${message2.header}: ${message2.message}`);
55731
55913
  };
55732
55914
  webidl.errors.conversionFailed = function(context) {
55733
- const plural = context.types.length === 1 ? "" : " one of";
55734
- const message2 = `${context.argument} could not be converted to${plural}: ${context.types.join(", ")}.`;
55915
+ const plural2 = context.types.length === 1 ? "" : " one of";
55916
+ const message2 = `${context.argument} could not be converted to${plural2}: ${context.types.join(", ")}.`;
55735
55917
  return webidl.errors.exception({
55736
55918
  header: context.prefix,
55737
55919
  message: message2
@@ -63095,9 +63277,9 @@ var require_pluralizer = __commonJS({
63095
63277
  this: "these"
63096
63278
  };
63097
63279
  module2.exports = class Pluralizer {
63098
- constructor(singular, plural) {
63280
+ constructor(singular, plural2) {
63099
63281
  this.singular = singular;
63100
- this.plural = plural;
63282
+ this.plural = plural2;
63101
63283
  }
63102
63284
  pluralize(count) {
63103
63285
  const one = count === 1;
@@ -71202,7 +71384,7 @@ var init_report_render = __esm({
71202
71384
  // src/cli.ts
71203
71385
  init_src6();
71204
71386
  import { closeSync as closeSync2, existsSync as existsSync34, openSync as openSync2, readFileSync as readFileSync35, renameSync as renameSync4, statSync as statSync9 } from "node:fs";
71205
- import { join as join41, resolve as pathResolve2 } from "node:path";
71387
+ import { join as join42, resolve as pathResolve2 } from "node:path";
71206
71388
  import { spawn as spawn5 } from "node:child_process";
71207
71389
 
71208
71390
  // src/daemon.ts
@@ -79251,7 +79433,7 @@ function createWorkingFileState() {
79251
79433
  }
79252
79434
 
79253
79435
  // src/tool-run-state.ts
79254
- var MAX_SESSIONS = 64;
79436
+ var MAX_SESSIONS2 = 64;
79255
79437
  var MAX_TOOLS_PER_SESSION = 64;
79256
79438
  function createToolRunState() {
79257
79439
  const bySession = /* @__PURE__ */ new Map();
@@ -79260,7 +79442,7 @@ function createToolRunState() {
79260
79442
  if (!sessionId || toolNodeIds.length === 0) return;
79261
79443
  let set2 = bySession.get(sessionId);
79262
79444
  if (!set2) {
79263
- if (bySession.size >= MAX_SESSIONS) {
79445
+ if (bySession.size >= MAX_SESSIONS2) {
79264
79446
  const oldest = bySession.keys().next().value;
79265
79447
  if (oldest !== void 0) bySession.delete(oldest);
79266
79448
  }
@@ -79931,7 +80113,7 @@ function createWatchBreaker(deps) {
79931
80113
  }
79932
80114
 
79933
80115
  // src/engine.ts
79934
- var DAEMON_VERSION = true ? "2.0.8-dev.2839" : "2.0.0-alpha.0";
80116
+ var DAEMON_VERSION = true ? "2.0.8-dev.2870" : "2.0.0-alpha.0";
79935
80117
  var IGNORED_PATH = /[\\/](?:\.git|node_modules|\.errata|\.claude|\.codex|\.cursor|\.turbo|\.next|dist|coverage|test-results|playwright-report|__pycache__)(?:[\\/]|$)/;
79936
80118
  var IGNORED_NOISE = /castalia\.db|eventlog\.sqlite|turn-cursors/;
79937
80119
  var GIT_OP_MUTE_MS = 4e3;
@@ -80364,7 +80546,7 @@ function createWorkspaceEngine(opts) {
80364
80546
  }
80365
80547
  }
80366
80548
  };
80367
- const clip = (s, n) => s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
80549
+ const clip2 = (s, n) => s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
80368
80550
  const pickReviewItems = () => {
80369
80551
  const shared = opts.sharedStore;
80370
80552
  if (!shared) return [];
@@ -80375,12 +80557,12 @@ function createWorkspaceEngine(opts) {
80375
80557
  return [{
80376
80558
  kind: "abstraction",
80377
80559
  candidate: small.candidate,
80378
- members: small.members.map((m) => ({ id: m.id, description: clip(m.description, 110) }))
80560
+ members: small.members.map((m) => ({ id: m.id, description: clip2(m.description, 110) }))
80379
80561
  }];
80380
80562
  }
80381
80563
  }
80382
80564
  const [d] = pendingDiscriminators(shared, { limit: 1 });
80383
- return d ? [{ kind: "discriminator", route: d.route, presenting: clip(d.presenting, 140), cause: clip(d.cause, 140) }] : [];
80565
+ return d ? [{ kind: "discriminator", route: d.route, presenting: clip2(d.presenting, 140), cause: clip2(d.cause, 140) }] : [];
80384
80566
  } catch {
80385
80567
  return [];
80386
80568
  }
@@ -83358,6 +83540,7 @@ function buildDrainInstances(store, sharedStore, input) {
83358
83540
 
83359
83541
  // src/multi.ts
83360
83542
  init_webui();
83543
+ init_recall_card();
83361
83544
 
83362
83545
  // src/consolidate-worker-client.ts
83363
83546
  import { Worker as Worker2 } from "node:worker_threads";
@@ -83728,6 +83911,22 @@ function isClaimedElsewhere(err2) {
83728
83911
  const e = err2;
83729
83912
  return e.status === 409 && e.code === "claimed_elsewhere";
83730
83913
  }
83914
+ function claimHoldStands(onDisk) {
83915
+ return Boolean(onDisk && !onDisk.projectId && onDisk.projectLinkHold);
83916
+ }
83917
+ var LINK_FIELDS = [
83918
+ "projectId",
83919
+ "projectLocator",
83920
+ "projectLinkHold",
83921
+ "projectLinkDisabled",
83922
+ "rootCommitReportedFor"
83923
+ ];
83924
+ function adoptLinkFields(into, from) {
83925
+ for (const k of LINK_FIELDS) {
83926
+ if (from[k] === void 0) delete into[k];
83927
+ else into[k] = from[k];
83928
+ }
83929
+ }
83731
83930
  async function readRootCommit(root) {
83732
83931
  try {
83733
83932
  const { stdout } = await execFileAsync("git", ["rev-list", "--max-parents=0", "HEAD"], {
@@ -85397,6 +85596,7 @@ function applyLinkDecision(root, profile, config2, decision, now = Date.now) {
85397
85596
  projectLocator: decision.locator
85398
85597
  };
85399
85598
  delete nextProfile.projectLinkDisabled;
85599
+ delete nextProfile.projectLinkHold;
85400
85600
  let wroteIgnore = false;
85401
85601
  if (decision.writeIgnoreStarter !== false) {
85402
85602
  const target = join37(root, ".errataignore");
@@ -85793,9 +85993,9 @@ async function startMultiDaemon(opts = {}) {
85793
85993
  if (identityBlocks.delete(wsId)) console.log(`[identity] workspace unblocked \u2014 spooled capture drains on this pass`);
85794
85994
  };
85795
85995
  const projectHolds = /* @__PURE__ */ new Map();
85796
- const noteProjectHold = (wsId, wsName, reason) => {
85996
+ const noteProjectHold = (wsId, wsName, reason, claimRoot) => {
85797
85997
  if (!projectHolds.has(wsId)) {
85798
- projectHolds.set(wsId, { workspace: wsName, reason, since: Date.now() });
85998
+ projectHolds.set(wsId, { workspace: wsName, reason, since: Date.now(), ...claimRoot ? { claimRoot } : {} });
85799
85999
  console.error(
85800
86000
  `[project] HOLDING cloud push for workspace "${wsName}": cannot prove its project \u2014 ${reason}. Shipping without the assertion would publish project-scoped knowledge to the whole org; the batch stays pending instead.`
85801
86001
  );
@@ -85804,6 +86004,14 @@ async function startMultiDaemon(opts = {}) {
85804
86004
  const clearProjectHold = (wsId) => {
85805
86005
  if (projectHolds.delete(wsId)) console.log(`[project] project assertion restored \u2014 held contributions drain on this pass`);
85806
86006
  };
86007
+ const releaseSettledClaimHolds = () => {
86008
+ for (const [wsId, hold] of projectHolds) {
86009
+ if (hold.claimRoot && !claimHoldStands(loadProfile(hold.claimRoot))) {
86010
+ projectHolds.delete(wsId);
86011
+ console.log(`[project] claim hold on "${hold.workspace}" settled by \`errata link\` \u2014 ships on the next pass`);
86012
+ }
86013
+ }
86014
+ };
85807
86015
  const cloudForIdentity = (identity) => {
85808
86016
  if (identity.invalid) return blockedCloudClient(identity.reason);
85809
86017
  if (identity.rule === "workspace-binding" && identity.profileName) {
@@ -85898,35 +86106,38 @@ async function startMultiDaemon(opts = {}) {
85898
86106
  if (detail) boundaryFlushDetail = detail;
85899
86107
  if (units > 0 || items > 0) boundaryFlushLastAdvanceAt = Date.now();
85900
86108
  };
85901
- const flushSnapshot = () => ({
85902
- running: boundaryFlushing,
85903
- stage: boundaryFlushStage,
85904
- detail: boundaryFlushDetail,
85905
- ageMs: boundaryFlushStartedAt ? Date.now() - boundaryFlushStartedAt : 0,
85906
- units: boundaryFlushUnits,
85907
- items: boundaryFlushItems,
85908
- chunks: boundaryFlushBatches,
85909
- stalledMs: boundaryFlushLastAdvanceAt ? Date.now() - boundaryFlushLastAdvanceAt : 0,
85910
- // Held workspaces, so a hold is REPORTABLE and not merely logged once at the
85911
- // transition. `errata status` reads this rather than the on-disk profile:
85912
- // the profile records what the workspace INTENDS to assert, which is why
85913
- // status could print "→ project 019f462d" for hours while every batch was
85914
- // being held or shipped unlinked. A hold is the daemon's actual behavior.
85915
- holds: [
85916
- ...[...projectHolds.values()].map((h) => ({
85917
- workspace: h.workspace,
85918
- kind: "project",
85919
- reason: h.reason,
85920
- sinceMs: Date.now() - h.since
85921
- })),
85922
- ...[...identityBlocks.values()].map((h) => ({
85923
- workspace: h.workspace,
85924
- kind: "identity",
85925
- reason: h.reason,
85926
- sinceMs: Date.now() - h.since
85927
- }))
85928
- ]
85929
- });
86109
+ const flushSnapshot = () => {
86110
+ releaseSettledClaimHolds();
86111
+ return {
86112
+ running: boundaryFlushing,
86113
+ stage: boundaryFlushStage,
86114
+ detail: boundaryFlushDetail,
86115
+ ageMs: boundaryFlushStartedAt ? Date.now() - boundaryFlushStartedAt : 0,
86116
+ units: boundaryFlushUnits,
86117
+ items: boundaryFlushItems,
86118
+ chunks: boundaryFlushBatches,
86119
+ stalledMs: boundaryFlushLastAdvanceAt ? Date.now() - boundaryFlushLastAdvanceAt : 0,
86120
+ // Held workspaces, so a hold is REPORTABLE and not merely logged once at the
86121
+ // transition. `errata status` reads this rather than the on-disk profile:
86122
+ // the profile records what the workspace INTENDS to assert, which is why
86123
+ // status could print "→ project 019f462d" for hours while every batch was
86124
+ // being held or shipped unlinked. A hold is the daemon's actual behavior.
86125
+ holds: [
86126
+ ...[...projectHolds.values()].map((h) => ({
86127
+ workspace: h.workspace,
86128
+ kind: "project",
86129
+ reason: h.reason,
86130
+ sinceMs: Date.now() - h.since
86131
+ })),
86132
+ ...[...identityBlocks.values()].map((h) => ({
86133
+ workspace: h.workspace,
86134
+ kind: "identity",
86135
+ reason: h.reason,
86136
+ sinceMs: Date.now() - h.since
86137
+ }))
86138
+ ]
86139
+ };
86140
+ };
85930
86141
  const endFlush = () => {
85931
86142
  boundaryFlushing = false;
85932
86143
  boundaryFlushStage = "";
@@ -86307,6 +86518,10 @@ async function startMultiDaemon(opts = {}) {
86307
86518
  app.post("/api/message", (c) => forward(c, cwdOf, { affinity: "follow" }));
86308
86519
  app.post("/api/session-end", (c) => forward(c, cwdOf, { affinity: "follow" }));
86309
86520
  app.post("/api/context-inject", (c) => forward(c, cwdOf, { affinity: "follow" }));
86521
+ app.post("/api/statusline", async (c) => {
86522
+ const body2 = await c.req.json().catch(() => ({}));
86523
+ return c.text(statuslineText(String(body2["session_id"] ?? "")));
86524
+ });
86310
86525
  for (const r of records) {
86311
86526
  mountedIds.add(r.id);
86312
86527
  app.route(`/ws/${r.id}`, r.webApp);
@@ -86380,9 +86595,9 @@ async function startMultiDaemon(opts = {}) {
86380
86595
  const reindexAll = async (rOpts) => {
86381
86596
  const out2 = /* @__PURE__ */ new Map();
86382
86597
  const toIndex = records.filter((r) => rOpts?.force || r.engine.store.nodeCount() === 0);
86383
- const plural = (n) => `repositor${n === 1 ? "y" : "ies"}`;
86598
+ const plural2 = (n) => `repositor${n === 1 ? "y" : "ies"}`;
86384
86599
  if (toIndex.length > 0) {
86385
- notifyEvent("index-started", `Indexing ${toIndex.length} ${plural(toIndex.length)}\u2026`);
86600
+ notifyEvent("index-started", `Indexing ${toIndex.length} ${plural2(toIndex.length)}\u2026`);
86386
86601
  }
86387
86602
  let indexed = 0;
86388
86603
  for (const r of records) {
@@ -86404,7 +86619,7 @@ async function startMultiDaemon(opts = {}) {
86404
86619
  }
86405
86620
  }
86406
86621
  if (toIndex.length > 0) {
86407
- notifyEvent("index-completed", `Indexed ${indexed} ${plural(indexed)}`);
86622
+ notifyEvent("index-completed", `Indexed ${indexed} ${plural2(indexed)}`);
86408
86623
  }
86409
86624
  return out2;
86410
86625
  };
@@ -86503,8 +86718,10 @@ async function startMultiDaemon(opts = {}) {
86503
86718
  if (!hasCloudCredential(c)) return;
86504
86719
  const client = cloudNow();
86505
86720
  for (const r of records) {
86506
- if (!effectiveConsent(c, r.engine.profile).sync) continue;
86507
- const out2 = await ensureProjectLink(r.root, r.engine.profile, client);
86721
+ const working = loadProfile(r.root) ?? r.engine.profile;
86722
+ if (!effectiveConsent(c, working).sync) continue;
86723
+ const out2 = await ensureProjectLink(r.root, working, client);
86724
+ if (working !== r.engine.profile) adoptLinkFields(r.engine.profile, working);
86508
86725
  if (out2.linked) {
86509
86726
  console.log(
86510
86727
  `[errata] linked ${r.engine.profile.name} \u2192 project ${out2.projectId}${out2.created ? " (created)" : ""}`
@@ -86512,7 +86729,7 @@ async function startMultiDaemon(opts = {}) {
86512
86729
  } else if (out2.reason === "error") {
86513
86730
  console.warn(`[errata] project link failed for ${r.engine.profile.name}: ${out2.detail}`);
86514
86731
  } else if (out2.reason === "held") {
86515
- noteProjectHold(r.id, r.engine.profile.name, out2.detail);
86732
+ noteProjectHold(r.id, r.engine.profile.name, out2.detail, r.root);
86516
86733
  }
86517
86734
  let primeSalt = null;
86518
86735
  if (r.engine.profile.projectId && !r.engine.profile.projectHydratedAt) {
@@ -86976,7 +87193,8 @@ async function startMultiDaemon(opts = {}) {
86976
87193
  noteProjectHold(
86977
87194
  r.id,
86978
87195
  r.entry.name,
86979
- "this repo is already on errata in another workspace \u2014 run `errata link` to join it or create your own project"
87196
+ "this repo is already on errata in another workspace \u2014 run `errata link` to join it or create your own project",
87197
+ r.root
86980
87198
  );
86981
87199
  continue;
86982
87200
  }
@@ -87935,6 +88153,7 @@ try {
87935
88153
  const event = payload.hook_event_name;
87936
88154
  if (!Number.isInteger(port) || port < 1 || port > 65535) process.exit(0);
87937
88155
  const contexts = [];
88156
+ const messages = [];
87938
88157
  const post = async (endpoint, relay = false) => {
87939
88158
  try {
87940
88159
  const response = await fetch('http://127.0.0.1:' + port + '/api/' + endpoint, {
@@ -87945,6 +88164,8 @@ try {
87945
88164
  const result = await response.json();
87946
88165
  const context = result.hookSpecificOutput?.additionalContext;
87947
88166
  if (typeof context === 'string' && context.trim()) contexts.push(context);
88167
+ // The user-visible recall card; Codex renders it inline as '↳ Hook · …'.
88168
+ if (typeof result.systemMessage === 'string' && result.systemMessage.trim()) messages.push(result.systemMessage);
87948
88169
  } catch { /* An unavailable daemon must never block the agent. */ }
87949
88170
  };
87950
88171
  if (event === 'PreToolUse' || event === 'PostToolUse') await post('hook', true);
@@ -87955,10 +88176,13 @@ try {
87955
88176
  // Background extraction finishing later arrives at the next tool boundary.
87956
88177
  await post('context-inject', true);
87957
88178
  }
87958
- if (contexts.length) {
87959
- process.stdout.write(JSON.stringify({ hookSpecificOutput: {
87960
- hookEventName: event, additionalContext: contexts.join('\n\n'),
87961
- } }));
88179
+ if (contexts.length || messages.length) {
88180
+ process.stdout.write(JSON.stringify({
88181
+ ...(messages.length ? { systemMessage: messages.join('\n\n') } : {}),
88182
+ ...(contexts.length ? { hookSpecificOutput: {
88183
+ hookEventName: event, additionalContext: contexts.join('\n\n'),
88184
+ } } : {}),
88185
+ }));
87962
88186
  }
87963
88187
  } catch { /* Invalid input is also fail-open. */ }
87964
88188
  `;
@@ -88152,6 +88376,45 @@ function consolidationGapMs(lastPassMs, policy) {
88152
88376
  return Math.max(policy.baseFloorMs, lastPassMs * policy.dutyFactor);
88153
88377
  }
88154
88378
 
88379
+ // src/claude-statusline.ts
88380
+ import { mkdirSync as mkdirSync10, writeFileSync as writeFileSync26 } from "node:fs";
88381
+ import { join as join41 } from "node:path";
88382
+ var STATUSLINE_RELAY_SOURCE = String.raw`// errata-managed: regenerated by errata install-hooks claude
88383
+ import { readFileSync } from 'node:fs';
88384
+
88385
+ try {
88386
+ const port = Number(process.argv[2]);
88387
+ if (Number.isInteger(port) && port > 0 && port < 65536) {
88388
+ const response = await fetch('http://127.0.0.1:' + port + '/api/statusline', {
88389
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
88390
+ body: readFileSync(0, 'utf8') || '{}', signal: AbortSignal.timeout(800),
88391
+ });
88392
+ if (response.ok) process.stdout.write(await response.text());
88393
+ }
88394
+ } catch { /* An unavailable daemon leaves the status line empty. */ }
88395
+ `;
88396
+ var STATUSLINE_RELAY_FILE = "errata-statusline.mjs";
88397
+ function buildStatusLine(port, nodePath, relayPath) {
88398
+ if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error("Invalid status line port");
88399
+ const q = (p) => {
88400
+ if (/["$`\r\n]/.test(p)) throw new Error("Unsupported character in status line path");
88401
+ return `"${p.replace(/\\/g, "/")}"`;
88402
+ };
88403
+ return { type: "command", command: `${q(nodePath)} ${q(relayPath)} ${port}`, padding: 0 };
88404
+ }
88405
+ function isErrataStatusLine(value) {
88406
+ return !!value && typeof value === "object" && typeof value["command"] === "string" && value["command"].includes(STATUSLINE_RELAY_FILE);
88407
+ }
88408
+ function installStatusLine(settings, root, port, nodePath = process.execPath) {
88409
+ if (settings["statusLine"] !== void 0 && !isErrataStatusLine(settings["statusLine"])) return false;
88410
+ const dir = join41(root, ".claude", "hooks");
88411
+ const relay = join41(dir, STATUSLINE_RELAY_FILE);
88412
+ mkdirSync10(dir, { recursive: true });
88413
+ writeFileSync26(relay, STATUSLINE_RELAY_SOURCE, "utf8");
88414
+ settings["statusLine"] = buildStatusLine(port, nodePath, relay);
88415
+ return true;
88416
+ }
88417
+
88155
88418
  // src/opencode-plugin.ts
88156
88419
  var OPENCODE_PLUGIN_MARKER = "// @inerrata opencode plugin \u2014 errata-managed (errata install-hooks opencode)";
88157
88420
  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.";
@@ -88558,13 +88821,13 @@ async function cmdInit() {
88558
88821
  console.log("");
88559
88822
  console.log("installing harness hooks...");
88560
88823
  const { existsSync: existsSync35 } = await import("node:fs");
88561
- const { join: join42 } = await import("node:path");
88824
+ const { join: join43 } = await import("node:path");
88562
88825
  try {
88563
88826
  await installClaudeHooks(port);
88564
88827
  } catch (err2) {
88565
88828
  console.warn(` \u26A0\uFE0F Claude hook install failed: ${err2 instanceof Error ? err2.message : err2}`);
88566
88829
  }
88567
- if (existsSync35(join42(ROOT, ".cursor"))) {
88830
+ if (existsSync35(join43(ROOT, ".cursor"))) {
88568
88831
  try {
88569
88832
  await installCursorMcpConfig();
88570
88833
  } catch (err2) {
@@ -89389,7 +89652,6 @@ async function cmdLink(args2) {
89389
89652
  return;
89390
89653
  }
89391
89654
  delete profile.projectLinkDisabled;
89392
- delete profile.projectLinkHold;
89393
89655
  const remote = flag("--remote");
89394
89656
  if (remote) {
89395
89657
  const loc = detectRepoLocator(ROOT, remote);
@@ -90439,7 +90701,7 @@ async function gatherReportData(generatedAt) {
90439
90701
  };
90440
90702
  }
90441
90703
  async function cmdReport(args2) {
90442
- const { mkdirSync: mkdirSync10, writeFileSync: writeFileSync26 } = await import("node:fs");
90704
+ const { mkdirSync: mkdirSync11, writeFileSync: writeFileSync27 } = await import("node:fs");
90443
90705
  const { renderReport: renderReport2 } = await Promise.resolve().then(() => (init_report_render(), report_render_exports));
90444
90706
  const includeFutureVerbs = args2.includes("--future-verbs");
90445
90707
  const now = /* @__PURE__ */ new Date();
@@ -90450,10 +90712,10 @@ async function cmdReport(args2) {
90450
90712
  process.exit(2);
90451
90713
  }
90452
90714
  const outDir = workspacePaths(ROOT).configDir;
90453
- mkdirSync10(outDir, { recursive: true });
90715
+ mkdirSync11(outDir, { recursive: true });
90454
90716
  const files = renderReport2(data, { includeFutureVerbs });
90455
- for (const f of files) writeFileSync26(join41(outDir, f.name), f.html, "utf8");
90456
- const indexPath = join41(outDir, "report.html");
90717
+ for (const f of files) writeFileSync27(join42(outDir, f.name), f.html, "utf8");
90718
+ const indexPath = join42(outDir, "report.html");
90457
90719
  console.log(`report \u2192 ${indexPath}`);
90458
90720
  console.log(
90459
90721
  ` ${data.repos.length} repo(s) \xB7 ${files.length} file(s) \xB7 ${data.rollup.nodes.toLocaleString("en-US")} nodes`
@@ -90573,11 +90835,11 @@ async function cmdInstallHooks(args2) {
90573
90835
  }
90574
90836
  }
90575
90837
  async function installOpencodePlugin(port) {
90576
- const { mkdirSync: mkdirSync10, existsSync: existsSync35, readFileSync: readFileSync36, writeFileSync: writeFileSync26 } = await import("node:fs");
90577
- const { join: join42 } = await import("node:path");
90578
- const dir = join42(ROOT, ".opencode", "plugin");
90579
- if (!existsSync35(dir)) mkdirSync10(dir, { recursive: true });
90580
- const file2 = join42(dir, "inerrata.js");
90838
+ const { mkdirSync: mkdirSync11, existsSync: existsSync35, readFileSync: readFileSync36, writeFileSync: writeFileSync27 } = await import("node:fs");
90839
+ const { join: join43 } = await import("node:path");
90840
+ const dir = join43(ROOT, ".opencode", "plugin");
90841
+ if (!existsSync35(dir)) mkdirSync11(dir, { recursive: true });
90842
+ const file2 = join43(dir, "inerrata.js");
90581
90843
  if (existsSync35(file2)) {
90582
90844
  const head2 = readFileSync36(file2, "utf8").split("\n")[0] ?? "";
90583
90845
  if (head2.trim() !== OPENCODE_PLUGIN_MARKER) {
@@ -90585,8 +90847,8 @@ async function installOpencodePlugin(port) {
90585
90847
  process.exit(2);
90586
90848
  }
90587
90849
  }
90588
- writeFileSync26(file2, buildOpencodePluginSource(port, { version: DAEMON_VERSION }), "utf8");
90589
- const cfgFile = join42(ROOT, "opencode.json");
90850
+ writeFileSync27(file2, buildOpencodePluginSource(port, { version: DAEMON_VERSION }), "utf8");
90851
+ const cfgFile = join43(ROOT, "opencode.json");
90590
90852
  let cfg = { $schema: "https://opencode.ai/config.json" };
90591
90853
  if (existsSync35(cfgFile)) {
90592
90854
  try {
@@ -90601,7 +90863,7 @@ async function installOpencodePlugin(port) {
90601
90863
  if (!hadErrata) {
90602
90864
  const inv = errataMcpInvocation();
90603
90865
  cfg.mcp["errata"] = { type: "local", command: [inv.command, ...inv.args] };
90604
- writeFileSync26(cfgFile, JSON.stringify(cfg, null, 2) + "\n", "utf8");
90866
+ writeFileSync27(cfgFile, JSON.stringify(cfg, null, 2) + "\n", "utf8");
90605
90867
  }
90606
90868
  console.log(`installed OpenCode plugin \u2192 ${file2}`);
90607
90869
  if (hadErrata) console.log(` mcp.errata already present in ${cfgFile} \u2014 left untouched`);
@@ -90621,11 +90883,11 @@ function errataMcpInvocation() {
90621
90883
  return { command: cmd2, args: args2 };
90622
90884
  }
90623
90885
  async function installClaudeHooks(port) {
90624
- const { mkdirSync: mkdirSync10, existsSync: existsSync35, readFileSync: readFileSync36, writeFileSync: writeFileSync26 } = await import("node:fs");
90625
- const { join: join42 } = await import("node:path");
90626
- const dir = join42(ROOT, ".claude");
90627
- if (!existsSync35(dir)) mkdirSync10(dir, { recursive: true });
90628
- const file2 = join42(dir, "settings.json");
90886
+ const { mkdirSync: mkdirSync11, existsSync: existsSync35, readFileSync: readFileSync36, writeFileSync: writeFileSync27 } = await import("node:fs");
90887
+ const { join: join43 } = await import("node:path");
90888
+ const dir = join43(ROOT, ".claude");
90889
+ if (!existsSync35(dir)) mkdirSync11(dir, { recursive: true });
90890
+ const file2 = join43(dir, "settings.json");
90629
90891
  let settings = {};
90630
90892
  if (existsSync35(file2)) {
90631
90893
  try {
@@ -90647,7 +90909,8 @@ async function installClaudeHooks(port) {
90647
90909
  };
90648
90910
  for (const [event, command, eventMatcher] of [
90649
90911
  ["PreToolUse", preCmd, matcher],
90650
- ["PostToolUse", postCmd, postMatcher]
90912
+ ["PostToolUse", postCmd, postMatcher],
90913
+ ["PostToolUseFailure", postCmd, postMatcher]
90651
90914
  ]) {
90652
90915
  const list = settings.hooks[event] ??= [];
90653
90916
  dropErrata(list);
@@ -90675,10 +90938,20 @@ async function installClaudeHooks(port) {
90675
90938
  dropErrata(list);
90676
90939
  list.push({ hooks: [{ type: "command", command: injectCmd }] });
90677
90940
  }
90678
- writeFileSync26(file2, JSON.stringify(settings, null, 2) + "\n", "utf8");
90941
+ const { homedir: homedir8 } = await import("node:os");
90942
+ let userStatusLine;
90943
+ try {
90944
+ userStatusLine = JSON.parse(readFileSync36(join43(homedir8(), ".claude", "settings.json"), "utf8"))["statusLine"];
90945
+ } catch {
90946
+ }
90947
+ const statusLineInstalled = (userStatusLine === void 0 || isErrataStatusLine(userStatusLine)) && installStatusLine(settings, ROOT, port);
90948
+ writeFileSync27(file2, JSON.stringify(settings, null, 2) + "\n", "utf8");
90679
90949
  console.log(`installed Claude Code hooks \u2192 ${file2}`);
90950
+ console.log(
90951
+ statusLineInstalled ? " status line: errata recall card in colour (daemon env ERRATA_RECALL_CARD=off hides the cards)" : " status line: kept your own \u2014 errata's recall card shows inline only"
90952
+ );
90680
90953
  await installClaudeMcpConfig();
90681
- const claudeMd = join42(ROOT, "CLAUDE.md");
90954
+ const claudeMd = join43(ROOT, "CLAUDE.md");
90682
90955
  const recall = writeManagedBlock(claudeMd, { body: RECALL_FIRST_BLOCK });
90683
90956
  if (recall.kind === "collision") {
90684
90957
  console.warn(
@@ -90690,11 +90963,11 @@ async function installClaudeHooks(port) {
90690
90963
  console.log(` endpoint: http://127.0.0.1:${port}/api/hook`);
90691
90964
  }
90692
90965
  async function installClaudeMcpConfig() {
90693
- const { mkdirSync: mkdirSync10, existsSync: existsSync35, readFileSync: readFileSync36, writeFileSync: writeFileSync26 } = await import("node:fs");
90694
- const { join: join42, dirname: dirname13 } = await import("node:path");
90695
- const file2 = join42(ROOT, ".mcp.json");
90966
+ const { mkdirSync: mkdirSync11, existsSync: existsSync35, readFileSync: readFileSync36, writeFileSync: writeFileSync27 } = await import("node:fs");
90967
+ const { join: join43, dirname: dirname13 } = await import("node:path");
90968
+ const file2 = join43(ROOT, ".mcp.json");
90696
90969
  const dir = dirname13(file2);
90697
- if (!existsSync35(dir)) mkdirSync10(dir, { recursive: true });
90970
+ if (!existsSync35(dir)) mkdirSync11(dir, { recursive: true });
90698
90971
  let cfg = {};
90699
90972
  if (existsSync35(file2)) {
90700
90973
  try {
@@ -90706,17 +90979,17 @@ async function installClaudeMcpConfig() {
90706
90979
  }
90707
90980
  cfg.mcpServers ??= {};
90708
90981
  cfg.mcpServers["errata"] = errataMcpInvocation();
90709
- writeFileSync26(file2, JSON.stringify(cfg, null, 2) + "\n", "utf8");
90982
+ writeFileSync27(file2, JSON.stringify(cfg, null, 2) + "\n", "utf8");
90710
90983
  console.log(`installed Claude Code MCP server config \u2192 ${file2}`);
90711
90984
  console.log(` Claude Code will spawn \`errata mcp\` on workspace open.`);
90712
90985
  console.log(` Tools: errata.search / locate / neighbors / callers_of / what_uses / show / similar`);
90713
90986
  }
90714
90987
  async function installCursorMcpConfig() {
90715
- const { mkdirSync: mkdirSync10, existsSync: existsSync35, readFileSync: readFileSync36, writeFileSync: writeFileSync26 } = await import("node:fs");
90716
- const { join: join42 } = await import("node:path");
90717
- const dir = join42(ROOT, ".cursor");
90718
- if (!existsSync35(dir)) mkdirSync10(dir, { recursive: true });
90719
- const file2 = join42(dir, "mcp.json");
90988
+ const { mkdirSync: mkdirSync11, existsSync: existsSync35, readFileSync: readFileSync36, writeFileSync: writeFileSync27 } = await import("node:fs");
90989
+ const { join: join43 } = await import("node:path");
90990
+ const dir = join43(ROOT, ".cursor");
90991
+ if (!existsSync35(dir)) mkdirSync11(dir, { recursive: true });
90992
+ const file2 = join43(dir, "mcp.json");
90720
90993
  let cfg = {};
90721
90994
  if (existsSync35(file2)) {
90722
90995
  try {
@@ -90728,7 +91001,7 @@ async function installCursorMcpConfig() {
90728
91001
  }
90729
91002
  cfg.mcpServers ??= {};
90730
91003
  cfg.mcpServers["errata"] = errataMcpInvocation();
90731
- writeFileSync26(file2, JSON.stringify(cfg, null, 2) + "\n", "utf8");
91004
+ writeFileSync27(file2, JSON.stringify(cfg, null, 2) + "\n", "utf8");
90732
91005
  console.log(`installed Cursor MCP server config \u2192 ${file2}`);
90733
91006
  console.log(` Cursor will spawn \`errata mcp\` on workspace open.`);
90734
91007
  console.log(` Tools: errata.search / locate / neighbors / callers_of / what_uses`);
@@ -90739,7 +91012,7 @@ async function installCodexHooks(port) {
90739
91012
  const gitRoot = nearestGitRoot(process.cwd());
90740
91013
  const hookRoot = gitRoot && mainCheckoutOfWorktree(gitRoot) ? gitRoot : ROOT;
90741
91014
  const file2 = writeCodexHooks(hookRoot, port);
90742
- const instructions = writeManagedBlock(join41(hookRoot, "AGENTS.md"), {
91015
+ const instructions = writeManagedBlock(join42(hookRoot, "AGENTS.md"), {
90743
91016
  body: AGENTS_POINTER_BODY,
90744
91017
  stable: true
90745
91018
  });
@@ -91143,7 +91416,7 @@ async function cmdDash(args2) {
91143
91416
  await yieldToLoop2();
91144
91417
  try {
91145
91418
  const items = selectDurableMemory(handle2.sharedStore, r.engine.profile);
91146
- const res = bleedRules(join41(r.root, ".claude", "rules"), items);
91419
+ const res = bleedRules(join42(r.root, ".claude", "rules"), items);
91147
91420
  if (res.written || res.pruned) {
91148
91421
  console.log(
91149
91422
  `[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.8-dev.2839",
3
+ "version": "2.0.8-dev.2870",
4
4
  "description": "errata - local-first observation engine for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {