@inerrata-corporation/errata 2.0.8-dev.2849 → 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 +325 -82
  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.2849" : "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";
@@ -86335,6 +86518,10 @@ async function startMultiDaemon(opts = {}) {
86335
86518
  app.post("/api/message", (c) => forward(c, cwdOf, { affinity: "follow" }));
86336
86519
  app.post("/api/session-end", (c) => forward(c, cwdOf, { affinity: "follow" }));
86337
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
+ });
86338
86525
  for (const r of records) {
86339
86526
  mountedIds.add(r.id);
86340
86527
  app.route(`/ws/${r.id}`, r.webApp);
@@ -86408,9 +86595,9 @@ async function startMultiDaemon(opts = {}) {
86408
86595
  const reindexAll = async (rOpts) => {
86409
86596
  const out2 = /* @__PURE__ */ new Map();
86410
86597
  const toIndex = records.filter((r) => rOpts?.force || r.engine.store.nodeCount() === 0);
86411
- const plural = (n) => `repositor${n === 1 ? "y" : "ies"}`;
86598
+ const plural2 = (n) => `repositor${n === 1 ? "y" : "ies"}`;
86412
86599
  if (toIndex.length > 0) {
86413
- notifyEvent("index-started", `Indexing ${toIndex.length} ${plural(toIndex.length)}\u2026`);
86600
+ notifyEvent("index-started", `Indexing ${toIndex.length} ${plural2(toIndex.length)}\u2026`);
86414
86601
  }
86415
86602
  let indexed = 0;
86416
86603
  for (const r of records) {
@@ -86432,7 +86619,7 @@ async function startMultiDaemon(opts = {}) {
86432
86619
  }
86433
86620
  }
86434
86621
  if (toIndex.length > 0) {
86435
- notifyEvent("index-completed", `Indexed ${indexed} ${plural(indexed)}`);
86622
+ notifyEvent("index-completed", `Indexed ${indexed} ${plural2(indexed)}`);
86436
86623
  }
86437
86624
  return out2;
86438
86625
  };
@@ -87966,6 +88153,7 @@ try {
87966
88153
  const event = payload.hook_event_name;
87967
88154
  if (!Number.isInteger(port) || port < 1 || port > 65535) process.exit(0);
87968
88155
  const contexts = [];
88156
+ const messages = [];
87969
88157
  const post = async (endpoint, relay = false) => {
87970
88158
  try {
87971
88159
  const response = await fetch('http://127.0.0.1:' + port + '/api/' + endpoint, {
@@ -87976,6 +88164,8 @@ try {
87976
88164
  const result = await response.json();
87977
88165
  const context = result.hookSpecificOutput?.additionalContext;
87978
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);
87979
88169
  } catch { /* An unavailable daemon must never block the agent. */ }
87980
88170
  };
87981
88171
  if (event === 'PreToolUse' || event === 'PostToolUse') await post('hook', true);
@@ -87986,10 +88176,13 @@ try {
87986
88176
  // Background extraction finishing later arrives at the next tool boundary.
87987
88177
  await post('context-inject', true);
87988
88178
  }
87989
- if (contexts.length) {
87990
- process.stdout.write(JSON.stringify({ hookSpecificOutput: {
87991
- hookEventName: event, additionalContext: contexts.join('\n\n'),
87992
- } }));
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
+ }));
87993
88186
  }
87994
88187
  } catch { /* Invalid input is also fail-open. */ }
87995
88188
  `;
@@ -88183,6 +88376,45 @@ function consolidationGapMs(lastPassMs, policy) {
88183
88376
  return Math.max(policy.baseFloorMs, lastPassMs * policy.dutyFactor);
88184
88377
  }
88185
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
+
88186
88418
  // src/opencode-plugin.ts
88187
88419
  var OPENCODE_PLUGIN_MARKER = "// @inerrata opencode plugin \u2014 errata-managed (errata install-hooks opencode)";
88188
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.";
@@ -88589,13 +88821,13 @@ async function cmdInit() {
88589
88821
  console.log("");
88590
88822
  console.log("installing harness hooks...");
88591
88823
  const { existsSync: existsSync35 } = await import("node:fs");
88592
- const { join: join42 } = await import("node:path");
88824
+ const { join: join43 } = await import("node:path");
88593
88825
  try {
88594
88826
  await installClaudeHooks(port);
88595
88827
  } catch (err2) {
88596
88828
  console.warn(` \u26A0\uFE0F Claude hook install failed: ${err2 instanceof Error ? err2.message : err2}`);
88597
88829
  }
88598
- if (existsSync35(join42(ROOT, ".cursor"))) {
88830
+ if (existsSync35(join43(ROOT, ".cursor"))) {
88599
88831
  try {
88600
88832
  await installCursorMcpConfig();
88601
88833
  } catch (err2) {
@@ -90469,7 +90701,7 @@ async function gatherReportData(generatedAt) {
90469
90701
  };
90470
90702
  }
90471
90703
  async function cmdReport(args2) {
90472
- const { mkdirSync: mkdirSync10, writeFileSync: writeFileSync26 } = await import("node:fs");
90704
+ const { mkdirSync: mkdirSync11, writeFileSync: writeFileSync27 } = await import("node:fs");
90473
90705
  const { renderReport: renderReport2 } = await Promise.resolve().then(() => (init_report_render(), report_render_exports));
90474
90706
  const includeFutureVerbs = args2.includes("--future-verbs");
90475
90707
  const now = /* @__PURE__ */ new Date();
@@ -90480,10 +90712,10 @@ async function cmdReport(args2) {
90480
90712
  process.exit(2);
90481
90713
  }
90482
90714
  const outDir = workspacePaths(ROOT).configDir;
90483
- mkdirSync10(outDir, { recursive: true });
90715
+ mkdirSync11(outDir, { recursive: true });
90484
90716
  const files = renderReport2(data, { includeFutureVerbs });
90485
- for (const f of files) writeFileSync26(join41(outDir, f.name), f.html, "utf8");
90486
- 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");
90487
90719
  console.log(`report \u2192 ${indexPath}`);
90488
90720
  console.log(
90489
90721
  ` ${data.repos.length} repo(s) \xB7 ${files.length} file(s) \xB7 ${data.rollup.nodes.toLocaleString("en-US")} nodes`
@@ -90603,11 +90835,11 @@ async function cmdInstallHooks(args2) {
90603
90835
  }
90604
90836
  }
90605
90837
  async function installOpencodePlugin(port) {
90606
- const { mkdirSync: mkdirSync10, existsSync: existsSync35, readFileSync: readFileSync36, writeFileSync: writeFileSync26 } = await import("node:fs");
90607
- const { join: join42 } = await import("node:path");
90608
- const dir = join42(ROOT, ".opencode", "plugin");
90609
- if (!existsSync35(dir)) mkdirSync10(dir, { recursive: true });
90610
- 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");
90611
90843
  if (existsSync35(file2)) {
90612
90844
  const head2 = readFileSync36(file2, "utf8").split("\n")[0] ?? "";
90613
90845
  if (head2.trim() !== OPENCODE_PLUGIN_MARKER) {
@@ -90615,8 +90847,8 @@ async function installOpencodePlugin(port) {
90615
90847
  process.exit(2);
90616
90848
  }
90617
90849
  }
90618
- writeFileSync26(file2, buildOpencodePluginSource(port, { version: DAEMON_VERSION }), "utf8");
90619
- const cfgFile = join42(ROOT, "opencode.json");
90850
+ writeFileSync27(file2, buildOpencodePluginSource(port, { version: DAEMON_VERSION }), "utf8");
90851
+ const cfgFile = join43(ROOT, "opencode.json");
90620
90852
  let cfg = { $schema: "https://opencode.ai/config.json" };
90621
90853
  if (existsSync35(cfgFile)) {
90622
90854
  try {
@@ -90631,7 +90863,7 @@ async function installOpencodePlugin(port) {
90631
90863
  if (!hadErrata) {
90632
90864
  const inv = errataMcpInvocation();
90633
90865
  cfg.mcp["errata"] = { type: "local", command: [inv.command, ...inv.args] };
90634
- writeFileSync26(cfgFile, JSON.stringify(cfg, null, 2) + "\n", "utf8");
90866
+ writeFileSync27(cfgFile, JSON.stringify(cfg, null, 2) + "\n", "utf8");
90635
90867
  }
90636
90868
  console.log(`installed OpenCode plugin \u2192 ${file2}`);
90637
90869
  if (hadErrata) console.log(` mcp.errata already present in ${cfgFile} \u2014 left untouched`);
@@ -90651,11 +90883,11 @@ function errataMcpInvocation() {
90651
90883
  return { command: cmd2, args: args2 };
90652
90884
  }
90653
90885
  async function installClaudeHooks(port) {
90654
- const { mkdirSync: mkdirSync10, existsSync: existsSync35, readFileSync: readFileSync36, writeFileSync: writeFileSync26 } = await import("node:fs");
90655
- const { join: join42 } = await import("node:path");
90656
- const dir = join42(ROOT, ".claude");
90657
- if (!existsSync35(dir)) mkdirSync10(dir, { recursive: true });
90658
- 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");
90659
90891
  let settings = {};
90660
90892
  if (existsSync35(file2)) {
90661
90893
  try {
@@ -90677,7 +90909,8 @@ async function installClaudeHooks(port) {
90677
90909
  };
90678
90910
  for (const [event, command, eventMatcher] of [
90679
90911
  ["PreToolUse", preCmd, matcher],
90680
- ["PostToolUse", postCmd, postMatcher]
90912
+ ["PostToolUse", postCmd, postMatcher],
90913
+ ["PostToolUseFailure", postCmd, postMatcher]
90681
90914
  ]) {
90682
90915
  const list = settings.hooks[event] ??= [];
90683
90916
  dropErrata(list);
@@ -90705,10 +90938,20 @@ async function installClaudeHooks(port) {
90705
90938
  dropErrata(list);
90706
90939
  list.push({ hooks: [{ type: "command", command: injectCmd }] });
90707
90940
  }
90708
- 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");
90709
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
+ );
90710
90953
  await installClaudeMcpConfig();
90711
- const claudeMd = join42(ROOT, "CLAUDE.md");
90954
+ const claudeMd = join43(ROOT, "CLAUDE.md");
90712
90955
  const recall = writeManagedBlock(claudeMd, { body: RECALL_FIRST_BLOCK });
90713
90956
  if (recall.kind === "collision") {
90714
90957
  console.warn(
@@ -90720,11 +90963,11 @@ async function installClaudeHooks(port) {
90720
90963
  console.log(` endpoint: http://127.0.0.1:${port}/api/hook`);
90721
90964
  }
90722
90965
  async function installClaudeMcpConfig() {
90723
- const { mkdirSync: mkdirSync10, existsSync: existsSync35, readFileSync: readFileSync36, writeFileSync: writeFileSync26 } = await import("node:fs");
90724
- const { join: join42, dirname: dirname13 } = await import("node:path");
90725
- 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");
90726
90969
  const dir = dirname13(file2);
90727
- if (!existsSync35(dir)) mkdirSync10(dir, { recursive: true });
90970
+ if (!existsSync35(dir)) mkdirSync11(dir, { recursive: true });
90728
90971
  let cfg = {};
90729
90972
  if (existsSync35(file2)) {
90730
90973
  try {
@@ -90736,17 +90979,17 @@ async function installClaudeMcpConfig() {
90736
90979
  }
90737
90980
  cfg.mcpServers ??= {};
90738
90981
  cfg.mcpServers["errata"] = errataMcpInvocation();
90739
- writeFileSync26(file2, JSON.stringify(cfg, null, 2) + "\n", "utf8");
90982
+ writeFileSync27(file2, JSON.stringify(cfg, null, 2) + "\n", "utf8");
90740
90983
  console.log(`installed Claude Code MCP server config \u2192 ${file2}`);
90741
90984
  console.log(` Claude Code will spawn \`errata mcp\` on workspace open.`);
90742
90985
  console.log(` Tools: errata.search / locate / neighbors / callers_of / what_uses / show / similar`);
90743
90986
  }
90744
90987
  async function installCursorMcpConfig() {
90745
- const { mkdirSync: mkdirSync10, existsSync: existsSync35, readFileSync: readFileSync36, writeFileSync: writeFileSync26 } = await import("node:fs");
90746
- const { join: join42 } = await import("node:path");
90747
- const dir = join42(ROOT, ".cursor");
90748
- if (!existsSync35(dir)) mkdirSync10(dir, { recursive: true });
90749
- 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");
90750
90993
  let cfg = {};
90751
90994
  if (existsSync35(file2)) {
90752
90995
  try {
@@ -90758,7 +91001,7 @@ async function installCursorMcpConfig() {
90758
91001
  }
90759
91002
  cfg.mcpServers ??= {};
90760
91003
  cfg.mcpServers["errata"] = errataMcpInvocation();
90761
- writeFileSync26(file2, JSON.stringify(cfg, null, 2) + "\n", "utf8");
91004
+ writeFileSync27(file2, JSON.stringify(cfg, null, 2) + "\n", "utf8");
90762
91005
  console.log(`installed Cursor MCP server config \u2192 ${file2}`);
90763
91006
  console.log(` Cursor will spawn \`errata mcp\` on workspace open.`);
90764
91007
  console.log(` Tools: errata.search / locate / neighbors / callers_of / what_uses`);
@@ -90769,7 +91012,7 @@ async function installCodexHooks(port) {
90769
91012
  const gitRoot = nearestGitRoot(process.cwd());
90770
91013
  const hookRoot = gitRoot && mainCheckoutOfWorktree(gitRoot) ? gitRoot : ROOT;
90771
91014
  const file2 = writeCodexHooks(hookRoot, port);
90772
- const instructions = writeManagedBlock(join41(hookRoot, "AGENTS.md"), {
91015
+ const instructions = writeManagedBlock(join42(hookRoot, "AGENTS.md"), {
90773
91016
  body: AGENTS_POINTER_BODY,
90774
91017
  stable: true
90775
91018
  });
@@ -91173,7 +91416,7 @@ async function cmdDash(args2) {
91173
91416
  await yieldToLoop2();
91174
91417
  try {
91175
91418
  const items = selectDurableMemory(handle2.sharedStore, r.engine.profile);
91176
- const res = bleedRules(join41(r.root, ".claude", "rules"), items);
91419
+ const res = bleedRules(join42(r.root, ".claude", "rules"), items);
91177
91420
  if (res.written || res.pruned) {
91178
91421
  console.log(
91179
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.2849",
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": {