@inerrata-corporation/errata 2.0.8-dev.2849 → 2.0.8-dev.2881

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 +326 -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
  }
@@ -79527,6 +79709,7 @@ function osaQuote(s) {
79527
79709
  return `"${s.replace(/[\\"]/g, "\\$&")}"`;
79528
79710
  }
79529
79711
  function notify(title, body2) {
79712
+ if (process.env["VITEST"] === "true" || process.env["NODE_ENV"] === "test") return;
79530
79713
  switch (process.platform) {
79531
79714
  case "darwin":
79532
79715
  run2("osascript", [
@@ -79931,7 +80114,7 @@ function createWatchBreaker(deps) {
79931
80114
  }
79932
80115
 
79933
80116
  // src/engine.ts
79934
- var DAEMON_VERSION = true ? "2.0.8-dev.2849" : "2.0.0-alpha.0";
80117
+ var DAEMON_VERSION = true ? "2.0.8-dev.2881" : "2.0.0-alpha.0";
79935
80118
  var IGNORED_PATH = /[\\/](?:\.git|node_modules|\.errata|\.claude|\.codex|\.cursor|\.turbo|\.next|dist|coverage|test-results|playwright-report|__pycache__)(?:[\\/]|$)/;
79936
80119
  var IGNORED_NOISE = /castalia\.db|eventlog\.sqlite|turn-cursors/;
79937
80120
  var GIT_OP_MUTE_MS = 4e3;
@@ -80364,7 +80547,7 @@ function createWorkspaceEngine(opts) {
80364
80547
  }
80365
80548
  }
80366
80549
  };
80367
- const clip = (s, n) => s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
80550
+ const clip2 = (s, n) => s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
80368
80551
  const pickReviewItems = () => {
80369
80552
  const shared = opts.sharedStore;
80370
80553
  if (!shared) return [];
@@ -80375,12 +80558,12 @@ function createWorkspaceEngine(opts) {
80375
80558
  return [{
80376
80559
  kind: "abstraction",
80377
80560
  candidate: small.candidate,
80378
- members: small.members.map((m) => ({ id: m.id, description: clip(m.description, 110) }))
80561
+ members: small.members.map((m) => ({ id: m.id, description: clip2(m.description, 110) }))
80379
80562
  }];
80380
80563
  }
80381
80564
  }
80382
80565
  const [d] = pendingDiscriminators(shared, { limit: 1 });
80383
- return d ? [{ kind: "discriminator", route: d.route, presenting: clip(d.presenting, 140), cause: clip(d.cause, 140) }] : [];
80566
+ return d ? [{ kind: "discriminator", route: d.route, presenting: clip2(d.presenting, 140), cause: clip2(d.cause, 140) }] : [];
80384
80567
  } catch {
80385
80568
  return [];
80386
80569
  }
@@ -83358,6 +83541,7 @@ function buildDrainInstances(store, sharedStore, input) {
83358
83541
 
83359
83542
  // src/multi.ts
83360
83543
  init_webui();
83544
+ init_recall_card();
83361
83545
 
83362
83546
  // src/consolidate-worker-client.ts
83363
83547
  import { Worker as Worker2 } from "node:worker_threads";
@@ -86335,6 +86519,10 @@ async function startMultiDaemon(opts = {}) {
86335
86519
  app.post("/api/message", (c) => forward(c, cwdOf, { affinity: "follow" }));
86336
86520
  app.post("/api/session-end", (c) => forward(c, cwdOf, { affinity: "follow" }));
86337
86521
  app.post("/api/context-inject", (c) => forward(c, cwdOf, { affinity: "follow" }));
86522
+ app.post("/api/statusline", async (c) => {
86523
+ const body2 = await c.req.json().catch(() => ({}));
86524
+ return c.text(statuslineText(String(body2["session_id"] ?? "")));
86525
+ });
86338
86526
  for (const r of records) {
86339
86527
  mountedIds.add(r.id);
86340
86528
  app.route(`/ws/${r.id}`, r.webApp);
@@ -86408,9 +86596,9 @@ async function startMultiDaemon(opts = {}) {
86408
86596
  const reindexAll = async (rOpts) => {
86409
86597
  const out2 = /* @__PURE__ */ new Map();
86410
86598
  const toIndex = records.filter((r) => rOpts?.force || r.engine.store.nodeCount() === 0);
86411
- const plural = (n) => `repositor${n === 1 ? "y" : "ies"}`;
86599
+ const plural2 = (n) => `repositor${n === 1 ? "y" : "ies"}`;
86412
86600
  if (toIndex.length > 0) {
86413
- notifyEvent("index-started", `Indexing ${toIndex.length} ${plural(toIndex.length)}\u2026`);
86601
+ notifyEvent("index-started", `Indexing ${toIndex.length} ${plural2(toIndex.length)}\u2026`);
86414
86602
  }
86415
86603
  let indexed = 0;
86416
86604
  for (const r of records) {
@@ -86432,7 +86620,7 @@ async function startMultiDaemon(opts = {}) {
86432
86620
  }
86433
86621
  }
86434
86622
  if (toIndex.length > 0) {
86435
- notifyEvent("index-completed", `Indexed ${indexed} ${plural(indexed)}`);
86623
+ notifyEvent("index-completed", `Indexed ${indexed} ${plural2(indexed)}`);
86436
86624
  }
86437
86625
  return out2;
86438
86626
  };
@@ -87966,6 +88154,7 @@ try {
87966
88154
  const event = payload.hook_event_name;
87967
88155
  if (!Number.isInteger(port) || port < 1 || port > 65535) process.exit(0);
87968
88156
  const contexts = [];
88157
+ const messages = [];
87969
88158
  const post = async (endpoint, relay = false) => {
87970
88159
  try {
87971
88160
  const response = await fetch('http://127.0.0.1:' + port + '/api/' + endpoint, {
@@ -87976,6 +88165,8 @@ try {
87976
88165
  const result = await response.json();
87977
88166
  const context = result.hookSpecificOutput?.additionalContext;
87978
88167
  if (typeof context === 'string' && context.trim()) contexts.push(context);
88168
+ // The user-visible recall card; Codex renders it inline as '↳ Hook · …'.
88169
+ if (typeof result.systemMessage === 'string' && result.systemMessage.trim()) messages.push(result.systemMessage);
87979
88170
  } catch { /* An unavailable daemon must never block the agent. */ }
87980
88171
  };
87981
88172
  if (event === 'PreToolUse' || event === 'PostToolUse') await post('hook', true);
@@ -87986,10 +88177,13 @@ try {
87986
88177
  // Background extraction finishing later arrives at the next tool boundary.
87987
88178
  await post('context-inject', true);
87988
88179
  }
87989
- if (contexts.length) {
87990
- process.stdout.write(JSON.stringify({ hookSpecificOutput: {
87991
- hookEventName: event, additionalContext: contexts.join('\n\n'),
87992
- } }));
88180
+ if (contexts.length || messages.length) {
88181
+ process.stdout.write(JSON.stringify({
88182
+ ...(messages.length ? { systemMessage: messages.join('\n\n') } : {}),
88183
+ ...(contexts.length ? { hookSpecificOutput: {
88184
+ hookEventName: event, additionalContext: contexts.join('\n\n'),
88185
+ } } : {}),
88186
+ }));
87993
88187
  }
87994
88188
  } catch { /* Invalid input is also fail-open. */ }
87995
88189
  `;
@@ -88183,6 +88377,45 @@ function consolidationGapMs(lastPassMs, policy) {
88183
88377
  return Math.max(policy.baseFloorMs, lastPassMs * policy.dutyFactor);
88184
88378
  }
88185
88379
 
88380
+ // src/claude-statusline.ts
88381
+ import { mkdirSync as mkdirSync10, writeFileSync as writeFileSync26 } from "node:fs";
88382
+ import { join as join41 } from "node:path";
88383
+ var STATUSLINE_RELAY_SOURCE = String.raw`// errata-managed: regenerated by errata install-hooks claude
88384
+ import { readFileSync } from 'node:fs';
88385
+
88386
+ try {
88387
+ const port = Number(process.argv[2]);
88388
+ if (Number.isInteger(port) && port > 0 && port < 65536) {
88389
+ const response = await fetch('http://127.0.0.1:' + port + '/api/statusline', {
88390
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
88391
+ body: readFileSync(0, 'utf8') || '{}', signal: AbortSignal.timeout(800),
88392
+ });
88393
+ if (response.ok) process.stdout.write(await response.text());
88394
+ }
88395
+ } catch { /* An unavailable daemon leaves the status line empty. */ }
88396
+ `;
88397
+ var STATUSLINE_RELAY_FILE = "errata-statusline.mjs";
88398
+ function buildStatusLine(port, nodePath, relayPath) {
88399
+ if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error("Invalid status line port");
88400
+ const q = (p) => {
88401
+ if (/["$`\r\n]/.test(p)) throw new Error("Unsupported character in status line path");
88402
+ return `"${p.replace(/\\/g, "/")}"`;
88403
+ };
88404
+ return { type: "command", command: `${q(nodePath)} ${q(relayPath)} ${port}`, padding: 0 };
88405
+ }
88406
+ function isErrataStatusLine(value) {
88407
+ return !!value && typeof value === "object" && typeof value["command"] === "string" && value["command"].includes(STATUSLINE_RELAY_FILE);
88408
+ }
88409
+ function installStatusLine(settings, root, port, nodePath = process.execPath) {
88410
+ if (settings["statusLine"] !== void 0 && !isErrataStatusLine(settings["statusLine"])) return false;
88411
+ const dir = join41(root, ".claude", "hooks");
88412
+ const relay = join41(dir, STATUSLINE_RELAY_FILE);
88413
+ mkdirSync10(dir, { recursive: true });
88414
+ writeFileSync26(relay, STATUSLINE_RELAY_SOURCE, "utf8");
88415
+ settings["statusLine"] = buildStatusLine(port, nodePath, relay);
88416
+ return true;
88417
+ }
88418
+
88186
88419
  // src/opencode-plugin.ts
88187
88420
  var OPENCODE_PLUGIN_MARKER = "// @inerrata opencode plugin \u2014 errata-managed (errata install-hooks opencode)";
88188
88421
  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 +88822,13 @@ async function cmdInit() {
88589
88822
  console.log("");
88590
88823
  console.log("installing harness hooks...");
88591
88824
  const { existsSync: existsSync35 } = await import("node:fs");
88592
- const { join: join42 } = await import("node:path");
88825
+ const { join: join43 } = await import("node:path");
88593
88826
  try {
88594
88827
  await installClaudeHooks(port);
88595
88828
  } catch (err2) {
88596
88829
  console.warn(` \u26A0\uFE0F Claude hook install failed: ${err2 instanceof Error ? err2.message : err2}`);
88597
88830
  }
88598
- if (existsSync35(join42(ROOT, ".cursor"))) {
88831
+ if (existsSync35(join43(ROOT, ".cursor"))) {
88599
88832
  try {
88600
88833
  await installCursorMcpConfig();
88601
88834
  } catch (err2) {
@@ -90469,7 +90702,7 @@ async function gatherReportData(generatedAt) {
90469
90702
  };
90470
90703
  }
90471
90704
  async function cmdReport(args2) {
90472
- const { mkdirSync: mkdirSync10, writeFileSync: writeFileSync26 } = await import("node:fs");
90705
+ const { mkdirSync: mkdirSync11, writeFileSync: writeFileSync27 } = await import("node:fs");
90473
90706
  const { renderReport: renderReport2 } = await Promise.resolve().then(() => (init_report_render(), report_render_exports));
90474
90707
  const includeFutureVerbs = args2.includes("--future-verbs");
90475
90708
  const now = /* @__PURE__ */ new Date();
@@ -90480,10 +90713,10 @@ async function cmdReport(args2) {
90480
90713
  process.exit(2);
90481
90714
  }
90482
90715
  const outDir = workspacePaths(ROOT).configDir;
90483
- mkdirSync10(outDir, { recursive: true });
90716
+ mkdirSync11(outDir, { recursive: true });
90484
90717
  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");
90718
+ for (const f of files) writeFileSync27(join42(outDir, f.name), f.html, "utf8");
90719
+ const indexPath = join42(outDir, "report.html");
90487
90720
  console.log(`report \u2192 ${indexPath}`);
90488
90721
  console.log(
90489
90722
  ` ${data.repos.length} repo(s) \xB7 ${files.length} file(s) \xB7 ${data.rollup.nodes.toLocaleString("en-US")} nodes`
@@ -90603,11 +90836,11 @@ async function cmdInstallHooks(args2) {
90603
90836
  }
90604
90837
  }
90605
90838
  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");
90839
+ const { mkdirSync: mkdirSync11, existsSync: existsSync35, readFileSync: readFileSync36, writeFileSync: writeFileSync27 } = await import("node:fs");
90840
+ const { join: join43 } = await import("node:path");
90841
+ const dir = join43(ROOT, ".opencode", "plugin");
90842
+ if (!existsSync35(dir)) mkdirSync11(dir, { recursive: true });
90843
+ const file2 = join43(dir, "inerrata.js");
90611
90844
  if (existsSync35(file2)) {
90612
90845
  const head2 = readFileSync36(file2, "utf8").split("\n")[0] ?? "";
90613
90846
  if (head2.trim() !== OPENCODE_PLUGIN_MARKER) {
@@ -90615,8 +90848,8 @@ async function installOpencodePlugin(port) {
90615
90848
  process.exit(2);
90616
90849
  }
90617
90850
  }
90618
- writeFileSync26(file2, buildOpencodePluginSource(port, { version: DAEMON_VERSION }), "utf8");
90619
- const cfgFile = join42(ROOT, "opencode.json");
90851
+ writeFileSync27(file2, buildOpencodePluginSource(port, { version: DAEMON_VERSION }), "utf8");
90852
+ const cfgFile = join43(ROOT, "opencode.json");
90620
90853
  let cfg = { $schema: "https://opencode.ai/config.json" };
90621
90854
  if (existsSync35(cfgFile)) {
90622
90855
  try {
@@ -90631,7 +90864,7 @@ async function installOpencodePlugin(port) {
90631
90864
  if (!hadErrata) {
90632
90865
  const inv = errataMcpInvocation();
90633
90866
  cfg.mcp["errata"] = { type: "local", command: [inv.command, ...inv.args] };
90634
- writeFileSync26(cfgFile, JSON.stringify(cfg, null, 2) + "\n", "utf8");
90867
+ writeFileSync27(cfgFile, JSON.stringify(cfg, null, 2) + "\n", "utf8");
90635
90868
  }
90636
90869
  console.log(`installed OpenCode plugin \u2192 ${file2}`);
90637
90870
  if (hadErrata) console.log(` mcp.errata already present in ${cfgFile} \u2014 left untouched`);
@@ -90651,11 +90884,11 @@ function errataMcpInvocation() {
90651
90884
  return { command: cmd2, args: args2 };
90652
90885
  }
90653
90886
  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");
90887
+ const { mkdirSync: mkdirSync11, existsSync: existsSync35, readFileSync: readFileSync36, writeFileSync: writeFileSync27 } = await import("node:fs");
90888
+ const { join: join43 } = await import("node:path");
90889
+ const dir = join43(ROOT, ".claude");
90890
+ if (!existsSync35(dir)) mkdirSync11(dir, { recursive: true });
90891
+ const file2 = join43(dir, "settings.json");
90659
90892
  let settings = {};
90660
90893
  if (existsSync35(file2)) {
90661
90894
  try {
@@ -90677,7 +90910,8 @@ async function installClaudeHooks(port) {
90677
90910
  };
90678
90911
  for (const [event, command, eventMatcher] of [
90679
90912
  ["PreToolUse", preCmd, matcher],
90680
- ["PostToolUse", postCmd, postMatcher]
90913
+ ["PostToolUse", postCmd, postMatcher],
90914
+ ["PostToolUseFailure", postCmd, postMatcher]
90681
90915
  ]) {
90682
90916
  const list = settings.hooks[event] ??= [];
90683
90917
  dropErrata(list);
@@ -90705,10 +90939,20 @@ async function installClaudeHooks(port) {
90705
90939
  dropErrata(list);
90706
90940
  list.push({ hooks: [{ type: "command", command: injectCmd }] });
90707
90941
  }
90708
- writeFileSync26(file2, JSON.stringify(settings, null, 2) + "\n", "utf8");
90942
+ const { homedir: homedir8 } = await import("node:os");
90943
+ let userStatusLine;
90944
+ try {
90945
+ userStatusLine = JSON.parse(readFileSync36(join43(homedir8(), ".claude", "settings.json"), "utf8"))["statusLine"];
90946
+ } catch {
90947
+ }
90948
+ const statusLineInstalled = (userStatusLine === void 0 || isErrataStatusLine(userStatusLine)) && installStatusLine(settings, ROOT, port);
90949
+ writeFileSync27(file2, JSON.stringify(settings, null, 2) + "\n", "utf8");
90709
90950
  console.log(`installed Claude Code hooks \u2192 ${file2}`);
90951
+ console.log(
90952
+ 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"
90953
+ );
90710
90954
  await installClaudeMcpConfig();
90711
- const claudeMd = join42(ROOT, "CLAUDE.md");
90955
+ const claudeMd = join43(ROOT, "CLAUDE.md");
90712
90956
  const recall = writeManagedBlock(claudeMd, { body: RECALL_FIRST_BLOCK });
90713
90957
  if (recall.kind === "collision") {
90714
90958
  console.warn(
@@ -90720,11 +90964,11 @@ async function installClaudeHooks(port) {
90720
90964
  console.log(` endpoint: http://127.0.0.1:${port}/api/hook`);
90721
90965
  }
90722
90966
  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");
90967
+ const { mkdirSync: mkdirSync11, existsSync: existsSync35, readFileSync: readFileSync36, writeFileSync: writeFileSync27 } = await import("node:fs");
90968
+ const { join: join43, dirname: dirname13 } = await import("node:path");
90969
+ const file2 = join43(ROOT, ".mcp.json");
90726
90970
  const dir = dirname13(file2);
90727
- if (!existsSync35(dir)) mkdirSync10(dir, { recursive: true });
90971
+ if (!existsSync35(dir)) mkdirSync11(dir, { recursive: true });
90728
90972
  let cfg = {};
90729
90973
  if (existsSync35(file2)) {
90730
90974
  try {
@@ -90736,17 +90980,17 @@ async function installClaudeMcpConfig() {
90736
90980
  }
90737
90981
  cfg.mcpServers ??= {};
90738
90982
  cfg.mcpServers["errata"] = errataMcpInvocation();
90739
- writeFileSync26(file2, JSON.stringify(cfg, null, 2) + "\n", "utf8");
90983
+ writeFileSync27(file2, JSON.stringify(cfg, null, 2) + "\n", "utf8");
90740
90984
  console.log(`installed Claude Code MCP server config \u2192 ${file2}`);
90741
90985
  console.log(` Claude Code will spawn \`errata mcp\` on workspace open.`);
90742
90986
  console.log(` Tools: errata.search / locate / neighbors / callers_of / what_uses / show / similar`);
90743
90987
  }
90744
90988
  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");
90989
+ const { mkdirSync: mkdirSync11, existsSync: existsSync35, readFileSync: readFileSync36, writeFileSync: writeFileSync27 } = await import("node:fs");
90990
+ const { join: join43 } = await import("node:path");
90991
+ const dir = join43(ROOT, ".cursor");
90992
+ if (!existsSync35(dir)) mkdirSync11(dir, { recursive: true });
90993
+ const file2 = join43(dir, "mcp.json");
90750
90994
  let cfg = {};
90751
90995
  if (existsSync35(file2)) {
90752
90996
  try {
@@ -90758,7 +91002,7 @@ async function installCursorMcpConfig() {
90758
91002
  }
90759
91003
  cfg.mcpServers ??= {};
90760
91004
  cfg.mcpServers["errata"] = errataMcpInvocation();
90761
- writeFileSync26(file2, JSON.stringify(cfg, null, 2) + "\n", "utf8");
91005
+ writeFileSync27(file2, JSON.stringify(cfg, null, 2) + "\n", "utf8");
90762
91006
  console.log(`installed Cursor MCP server config \u2192 ${file2}`);
90763
91007
  console.log(` Cursor will spawn \`errata mcp\` on workspace open.`);
90764
91008
  console.log(` Tools: errata.search / locate / neighbors / callers_of / what_uses`);
@@ -90769,7 +91013,7 @@ async function installCodexHooks(port) {
90769
91013
  const gitRoot = nearestGitRoot(process.cwd());
90770
91014
  const hookRoot = gitRoot && mainCheckoutOfWorktree(gitRoot) ? gitRoot : ROOT;
90771
91015
  const file2 = writeCodexHooks(hookRoot, port);
90772
- const instructions = writeManagedBlock(join41(hookRoot, "AGENTS.md"), {
91016
+ const instructions = writeManagedBlock(join42(hookRoot, "AGENTS.md"), {
90773
91017
  body: AGENTS_POINTER_BODY,
90774
91018
  stable: true
90775
91019
  });
@@ -91173,7 +91417,7 @@ async function cmdDash(args2) {
91173
91417
  await yieldToLoop2();
91174
91418
  try {
91175
91419
  const items = selectDurableMemory(handle2.sharedStore, r.engine.profile);
91176
- const res = bleedRules(join41(r.root, ".claude", "rules"), items);
91420
+ const res = bleedRules(join42(r.root, ".claude", "rules"), items);
91177
91421
  if (res.written || res.pruned) {
91178
91422
  console.log(
91179
91423
  `[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.2881",
4
4
  "description": "errata - local-first observation engine for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {