squadrant 0.27.1 → 0.28.0

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.
@@ -305,7 +305,8 @@ function validateRuleFrontmatter(fm) {
305
305
  if (fm[k] !== void 0 && !isStrArr(fm[k]))
306
306
  p.push(`${k} must be a string array`);
307
307
  }
308
- if (!Array.isArray(fm.sources) || fm.sources.length === 0) {
308
+ const mayBeSourceless = fm.status === "stale" || fm.status === "retired";
309
+ if (!Array.isArray(fm.sources) || fm.sources.length === 0 && !mayBeSourceless) {
309
310
  p.push("sources must be a non-empty array");
310
311
  } else {
311
312
  fm.sources.forEach((s, i) => {
@@ -339,6 +340,26 @@ function validateSourceEntry(e) {
339
340
  p.push("domain must be a string");
340
341
  if (e.sensitivity !== void 0 && e.sensitivity !== "local-only")
341
342
  p.push("sensitivity must be 'local-only' when set");
343
+ if (e.review !== void 0 && e.review !== "required")
344
+ p.push("review must be 'required' when set");
345
+ return p;
346
+ }
347
+ function validateErratum(e) {
348
+ if (!isObj(e))
349
+ return ["erratum must be an object"];
350
+ const p = [];
351
+ if (typeof e.id !== "string" || !RULE_ID_RE.test(e.id))
352
+ p.push(`id must match ${RULE_ID_RE}`);
353
+ for (const k of ["ref", "quote", "reason"]) {
354
+ if (typeof e[k] !== "string" || e[k] === "")
355
+ p.push(`${k} must be a non-empty string`);
356
+ }
357
+ if (e.evidence !== void 0 && !isStrArr(e.evidence))
358
+ p.push("evidence must be a string array");
359
+ for (const k of ["checked", "by", "at", "decision"]) {
360
+ if (e[k] !== void 0 && typeof e[k] !== "string")
361
+ p.push(`${k} must be a string`);
362
+ }
342
363
  return p;
343
364
  }
344
365
  function kbConfigs(cfg) {
@@ -463,22 +484,22 @@ function defaultCmuxConfigPath() {
463
484
  return join(homedir(), ".config", "cmux", "cmux.json");
464
485
  }
465
486
  function ensureSocketAutomation(opts = {}) {
466
- const path42 = opts.path ?? defaultCmuxConfigPath();
467
- if (!existsSync(path42)) {
468
- mkdirSync(dirname(path42), { recursive: true });
469
- writeFileSync(path42, MINIMAL_TEMPLATE);
470
- return { path: path42, changed: true, alreadySet: false };
487
+ const path43 = opts.path ?? defaultCmuxConfigPath();
488
+ if (!existsSync(path43)) {
489
+ mkdirSync(dirname(path43), { recursive: true });
490
+ writeFileSync(path43, MINIMAL_TEMPLATE);
491
+ return { path: path43, changed: true, alreadySet: false };
471
492
  }
472
- const text = readFileSync(path42, "utf-8");
493
+ const text = readFileSync(path43, "utf-8");
473
494
  const current = parse(text)?.automation?.socketControlMode;
474
495
  if (current === AUTOMATION_MODE) {
475
- return { path: path42, changed: false, alreadySet: true };
496
+ return { path: path43, changed: false, alreadySet: true };
476
497
  }
477
498
  const edits = modify(text, [...SOCKET_CONTROL_MODE_PATH], AUTOMATION_MODE, {
478
499
  formattingOptions: { insertSpaces: true, tabSize: 2 }
479
500
  });
480
- writeFileSync(path42, applyEdits(text, edits));
481
- return { path: path42, changed: true, alreadySet: false };
501
+ writeFileSync(path43, applyEdits(text, edits));
502
+ return { path: path43, changed: true, alreadySet: false };
482
503
  }
483
504
  var SOCKET_CONTROL_MODE_PATH, AUTOMATION_MODE, MINIMAL_TEMPLATE;
484
505
  var init_cmux_config = __esm({
@@ -628,9 +649,9 @@ import { join as join4 } from "path";
628
649
  function defaultStatePath() {
629
650
  return join4(homedir3(), ".config", "squadrant", "state", "cmux-autoconfig.json");
630
651
  }
631
- function readState(path42) {
652
+ function readState(path43) {
632
653
  try {
633
- return JSON.parse(readConfigFileSync(path42));
654
+ return JSON.parse(readConfigFileSync(path43));
634
655
  } catch {
635
656
  return {};
636
657
  }
@@ -2577,10 +2598,10 @@ function detectForeignInstall(parsed, thisEntry, registeredEntryExists) {
2577
2598
  isUpgrade: isVersionUpgrade(parsed.daemonEntry, thisEntry)
2578
2599
  };
2579
2600
  }
2580
- function sanitizePathForPlist(path42) {
2601
+ function sanitizePathForPlist(path43) {
2581
2602
  const seen = /* @__PURE__ */ new Set();
2582
2603
  const stable = [];
2583
- for (const p of path42.split(":")) {
2604
+ for (const p of path43.split(":")) {
2584
2605
  if (!p)
2585
2606
  continue;
2586
2607
  if (p.includes("/.claude/plugins/"))
@@ -5120,11 +5141,11 @@ var init_captain_record = __esm({
5120
5141
  // packages/core/dist/opencode-session.js
5121
5142
  import { execFileSync as execFileSync4 } from "child_process";
5122
5143
  async function listSessions(port, fetchImpl = fetch, timeoutMs = 5e3) {
5123
- for (const path42 of ["/session", "/api/session"]) {
5144
+ for (const path43 of ["/session", "/api/session"]) {
5124
5145
  const ac = new AbortController();
5125
5146
  const t = setTimeout(() => ac.abort(), timeoutMs);
5126
5147
  try {
5127
- const res = await fetchImpl(`http://127.0.0.1:${port}${path42}`, { signal: ac.signal });
5148
+ const res = await fetchImpl(`http://127.0.0.1:${port}${path43}`, { signal: ac.signal });
5128
5149
  if (!res.ok)
5129
5150
  continue;
5130
5151
  const rows = await res.json();
@@ -5817,10 +5838,10 @@ function distBuiltAt() {
5817
5838
  return 0;
5818
5839
  }
5819
5840
  }
5820
- function gatherLogStats(path42, now, windowMs) {
5841
+ function gatherLogStats(path43, now, windowMs) {
5821
5842
  let sizeBytes = 0;
5822
5843
  try {
5823
- sizeBytes = statSync3(path42).size;
5844
+ sizeBytes = statSync3(path43).size;
5824
5845
  } catch {
5825
5846
  return { errorCount: 0, sizeBytes: 0, windowMs };
5826
5847
  }
@@ -5831,7 +5852,7 @@ function gatherLogStats(path42, now, windowMs) {
5831
5852
  const len = sizeBytes - start;
5832
5853
  let text = "";
5833
5854
  try {
5834
- const fd = openSync2(path42, "r");
5855
+ const fd = openSync2(path43, "r");
5835
5856
  try {
5836
5857
  const buf = Buffer.alloc(len);
5837
5858
  readSync(fd, buf, 0, len, start);
@@ -8077,8 +8098,8 @@ var init_stream = __esm({
8077
8098
  // packages/core/dist/router/shim.js
8078
8099
  import { createServer as createServer3 } from "http";
8079
8100
  import { Readable } from "stream";
8080
- function joinUrl(base, path42) {
8081
- return base.replace(/\/+$/, "") + path42;
8101
+ function joinUrl(base, path43) {
8102
+ return base.replace(/\/+$/, "") + path43;
8082
8103
  }
8083
8104
  async function readBody(req) {
8084
8105
  const chunks = [];
@@ -8254,8 +8275,8 @@ function createRouterShim(opts) {
8254
8275
  writeJson(res, 200, { input_tokens: Math.max(1, Math.ceil(text.length / 4)) });
8255
8276
  }
8256
8277
  function handler(req, res) {
8257
- const path42 = pathnameOf(req.url);
8258
- if (req.method === "POST" && path42 === "/v1/messages") {
8278
+ const path43 = pathnameOf(req.url);
8279
+ if (req.method === "POST" && path43 === "/v1/messages") {
8259
8280
  void handleMessages(req, res).catch((err) => {
8260
8281
  log(`router internal error: ${err instanceof Error ? err.message : String(err)}`);
8261
8282
  if (!res.headersSent) {
@@ -8267,18 +8288,18 @@ function createRouterShim(opts) {
8267
8288
  });
8268
8289
  return;
8269
8290
  }
8270
- if (req.method === "POST" && path42 === "/v1/messages/count_tokens") {
8291
+ if (req.method === "POST" && path43 === "/v1/messages/count_tokens") {
8271
8292
  void handleCountTokens(req, res).catch(() => {
8272
8293
  if (!res.headersSent)
8273
8294
  writeJson(res, 502, anthropicError(502, "api_error", "router internal error").body);
8274
8295
  });
8275
8296
  return;
8276
8297
  }
8277
- if (req.method === "GET" && path42 === "/v1/models") {
8298
+ if (req.method === "GET" && path43 === "/v1/models") {
8278
8299
  writeJson(res, 200, modelsList([...advertisedModels]));
8279
8300
  return;
8280
8301
  }
8281
- if (req.method === "GET" && path42 === "/healthz") {
8302
+ if (req.method === "GET" && path43 === "/healthz") {
8282
8303
  writeJson(res, 200, { ready: server?.listening === true, upstreamReachable, lastError });
8283
8304
  return;
8284
8305
  }
@@ -8547,11 +8568,11 @@ async function probeCaptainChannel(project, opts = {}) {
8547
8568
  }
8548
8569
  }
8549
8570
  async function httpReachable(port, fetchImpl, timeoutMs) {
8550
- for (const path42 of ["/session", "/api/session"]) {
8571
+ for (const path43 of ["/session", "/api/session"]) {
8551
8572
  const ac = new AbortController();
8552
8573
  const t = setTimeout(() => ac.abort(), timeoutMs);
8553
8574
  try {
8554
- const res = await fetchImpl(`http://127.0.0.1:${port}${path42}`, { signal: ac.signal });
8575
+ const res = await fetchImpl(`http://127.0.0.1:${port}${path43}`, { signal: ac.signal });
8555
8576
  if (res.ok)
8556
8577
  return true;
8557
8578
  } catch {
@@ -9473,6 +9494,11 @@ function expandSource(kbRoot, entry) {
9473
9494
  const re = globToRegExp(abs.split(path16.sep).join("/"));
9474
9495
  return walkFiles(root).filter((f) => re.test(f.split(path16.sep).join("/"))).sort();
9475
9496
  }
9497
+ function sourceCovers(kbRoot, entry, ref) {
9498
+ const abs = path16.resolve(kbRoot, expandHome(entry.path));
9499
+ const file = fileForRef(kbRoot, ref);
9500
+ return /[*?]/.test(entry.path) ? globToRegExp(abs.split(path16.sep).join("/")).test(file.split(path16.sep).join("/")) : abs === file;
9501
+ }
9476
9502
  function detectChange(kbRoot, file) {
9477
9503
  const ref = refFor(kbRoot, file);
9478
9504
  const st = fs15.statSync(file);
@@ -9569,7 +9595,7 @@ async function planIngest(kbRoot, opts) {
9569
9595
  }
9570
9596
  return plan;
9571
9597
  }
9572
- var execFileAsync, CONVERTED_DIR, DEFAULT_MAX_SECTIONS_PER_PASS, CHARS_PER_TOKEN, PASS_THROUGH, SECTION_TARGET_CHARS, markitdownConverter, expandHome, fileForRef, cacheKey, convertedFile, stampFile, sha256;
9598
+ var execFileAsync, CONVERTED_DIR, DEFAULT_MAX_SECTIONS_PER_PASS, CHARS_PER_TOKEN, PASS_THROUGH, SECTION_TARGET_CHARS, markitdownConverter, expandHome, fileForRef, cacheKey, convertedFile, stampFile, isGatedRef, sha256;
9573
9599
  var init_ingest = __esm({
9574
9600
  "packages/core/dist/knowledge/ingest.js"() {
9575
9601
  execFileAsync = promisify2(execFile2);
@@ -9587,6 +9613,7 @@ var init_ingest = __esm({
9587
9613
  cacheKey = (ref) => `${ref.replace(/[^A-Za-z0-9._-]+/g, "_")}.${crypto3.createHash("sha1").update(ref).digest("hex").slice(0, 6)}`;
9588
9614
  convertedFile = (kbRoot, ref) => path16.join(kbRoot, CONVERTED_DIR, `${cacheKey(ref)}.md`);
9589
9615
  stampFile = (kbRoot, ref) => path16.join(kbRoot, CONVERTED_DIR, `${cacheKey(ref)}.sha`);
9616
+ isGatedRef = (kbRoot, sources, ref) => sources.some((e) => e.review === "required" && sourceCovers(kbRoot, e, ref));
9590
9617
  sha256 = (file) => crypto3.createHash("sha256").update(fs15.readFileSync(file)).digest("hex");
9591
9618
  }
9592
9619
  });
@@ -9779,9 +9806,14 @@ function validateKb(opts) {
9779
9806
  });
9780
9807
  }
9781
9808
  const { hay, convertible } = cache.get(key);
9782
- if (findQuote(hay, src.quote))
9809
+ const span = findQuote(hay, src.quote);
9810
+ if (span) {
9783
9811
  verified++;
9784
- else if (convertible)
9812
+ const active = r.status === "active" && !path17.relative(opts.rulesDir, r.file).split(path17.sep).includes(PROPOSED_DIR);
9813
+ const err = active ? (opts.errata ?? []).find((e) => e.ref === src.ref && ((o) => !!o && spansOverlap(o, span))(findQuote(hay, e.quote))) : void 0;
9814
+ if (err)
9815
+ checks.push({ level: "warn", message: `${r.id}: grounded in erratum ${err.id} (${err.reason}); re-check or retire it (${r.file})` });
9816
+ } else if (convertible)
9785
9817
  notIngested++;
9786
9818
  else
9787
9819
  checks.push({ level: "fail", message: `quote not found in ${file}: ${r.id} sources[${i}] (${r.file})` });
@@ -12986,6 +13018,7 @@ var init_js_yaml = __esm({
12986
13018
  // packages/core/dist/knowledge/sources.js
12987
13019
  import fs19 from "fs";
12988
13020
  import path19 from "path";
13021
+ import matter3 from "gray-matter";
12989
13022
  function loadSources(kbRoot) {
12990
13023
  const file = path19.join(kbRoot, "sources.yaml");
12991
13024
  if (!fs19.existsSync(file))
@@ -13011,20 +13044,77 @@ function loadSources(kbRoot) {
13011
13044
  });
13012
13045
  return { sources, errors };
13013
13046
  }
13047
+ function dropEntryBlock(text, entryPath) {
13048
+ const lines = text.split("\n");
13049
+ const q = entryPath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
13050
+ const head = new RegExp(`^-\\s+path:\\s*(['"]?)${q}\\1\\s*(#.*)?$`);
13051
+ const i = lines.findIndex((l) => head.test(l));
13052
+ if (i < 0)
13053
+ return null;
13054
+ let j = i + 1;
13055
+ while (j < lines.length && /^\s+\S/.test(lines[j]))
13056
+ j++;
13057
+ return [...lines.slice(0, i), ...lines.slice(j)].join("\n");
13058
+ }
13059
+ function removeSource(kbRoot, rulesDir, entryPath, opts = {}) {
13060
+ const file = path19.join(kbRoot, "sources.yaml");
13061
+ const { sources } = loadSources(kbRoot);
13062
+ const entry = sources.find((s) => s.path === entryPath);
13063
+ if (!entry)
13064
+ throw new Error(`no sources.yaml entry with path '${entryPath}' (give it exactly as listed by \`knowledge sources\`)`);
13065
+ const edited = dropEntryBlock(fs19.readFileSync(file, "utf8"), entryPath);
13066
+ if (edited === null)
13067
+ throw new Error(`could not edit sources.yaml for '${entryPath}' (not a plain block entry); remove it by hand, then rerun to strip the refs`);
13068
+ const res = { dryRun: !!opts.dryRun, entryRemoved: true, stripped: [], stale: [] };
13069
+ const writes = [];
13070
+ for (const rule of loadRulesDir(rulesDir, "kb").rules) {
13071
+ const kept = rule.sources.filter((s) => !sourceCovers(kbRoot, entry, s.ref));
13072
+ if (kept.length === rule.sources.length)
13073
+ continue;
13074
+ res.stripped.push(rule.id);
13075
+ const parsed = matter3(fs19.readFileSync(rule.file, "utf8"), { engines: { javascript: () => {
13076
+ throw new Error("only YAML front matter is allowed");
13077
+ } } });
13078
+ const fm = { ...parsed.data, sources: kept };
13079
+ if (!kept.length) {
13080
+ res.stale.push(rule.id);
13081
+ if (fm.status === "active")
13082
+ fm.status = "stale";
13083
+ }
13084
+ writes.push({ file: rule.file, text: matter3.stringify(parsed.content, JSON.parse(JSON.stringify(fm))) });
13085
+ }
13086
+ res.stripped.sort();
13087
+ res.stale.sort();
13088
+ if (opts.dryRun)
13089
+ return res;
13090
+ for (const w of writes)
13091
+ fs19.writeFileSync(w.file, w.text);
13092
+ fs19.writeFileSync(file, edited);
13093
+ const name = opts.kbName ?? path19.basename(kbRoot);
13094
+ writeIndex(kbRoot, compileIndex(name, loadRulesDir(rulesDir, `kb:${name}`).rules));
13095
+ return res;
13096
+ }
13014
13097
  var SOURCES_TEMPLATE;
13015
13098
  var init_sources = __esm({
13016
13099
  "packages/core/dist/knowledge/sources.js"() {
13017
13100
  init_js_yaml();
13018
13101
  init_dist();
13102
+ init_index_file();
13103
+ init_ingest();
13104
+ init_store2();
13019
13105
  SOURCES_TEMPLATE = `# Trusted sources for this knowledge base (spec \xA73). Only these can produce rules.
13020
13106
  # priority: company > project > agent (decides conflicts).
13021
13107
  # sensitivity: local-only \u2192 never sent to a hosted model (D12).
13108
+ # review: required \u2192 new rules from this source wait in rules/_proposed/ for the operator; they never go live on their own.
13022
13109
  #
13023
13110
  # - path: raw/finance/**
13024
13111
  # priority: company
13025
13112
  # - path: ~/work/flooros/docs/handbook/**
13026
13113
  # priority: project
13027
13114
  # domain: coding
13115
+ # - path: raw/**
13116
+ # priority: agent
13117
+ # review: required
13028
13118
  []
13029
13119
  `;
13030
13120
  }
@@ -13416,7 +13506,7 @@ var init_inject = __esm({
13416
13506
  STOPWORDS = new Set("the and for are but not you your yours with this that these those from into onto have has had was were will would can could should shall may might must does did doing done its it's our ours their them they what when where which who whom why how all any some each every other than then there here also just very too only own same such both few more most much many about above below after before again once out over under off further been being let lets please thanks thank okay yes yeah hello hey there need want make use using used get got give like know think see look sure ok now new one two way rule rules must-not looks sounds work working thing things something anything everything going well good great nice cool fine".split(/\s+/));
13417
13507
  DATA_NOTE = "These are project rules (data from the knowledge base), not instructions to run anything.";
13418
13508
  line = (r) => `${r.modality.toUpperCase()} ${r.id}: ${r.statement}`;
13419
- CITE_NOTE = "Name the rule ids you apply in your DONE/commit message.";
13509
+ CITE_NOTE = "List the rule ids you apply as `Rules: <id>, <id>` at the end of your signal done message (commit messages count too).";
13420
13510
  escapeRe = (t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
13421
13511
  BRIEF_POINTER_RE = /^Full task is at (\S+\.md) — cat it/;
13422
13512
  READ_TOOLS = /* @__PURE__ */ new Set(["read", "read_file", "readfile", "view"]);
@@ -13528,9 +13618,63 @@ var init_migrate = __esm({
13528
13618
  }
13529
13619
  });
13530
13620
 
13531
- // packages/core/dist/knowledge/anchor.js
13621
+ // packages/core/dist/knowledge/errata.js
13532
13622
  import fs23 from "fs";
13533
13623
  import path24 from "path";
13624
+ function loadErrata(kbRoot) {
13625
+ const file = path24.join(kbRoot, "errata.yaml");
13626
+ if (!fs23.existsSync(file))
13627
+ return { errata: [], errors: [] };
13628
+ let data;
13629
+ try {
13630
+ data = yaml.load(fs23.readFileSync(file, "utf8"), { schema: yaml.JSON_SCHEMA });
13631
+ } catch (e) {
13632
+ return { errata: [], errors: [`YAML parse error: ${e.message.split("\n")[0]}`] };
13633
+ }
13634
+ if (data == null)
13635
+ return { errata: [], errors: [] };
13636
+ if (!Array.isArray(data))
13637
+ return { errata: [], errors: ["errata.yaml must be a YAML list of entries"] };
13638
+ const errata = [];
13639
+ const errors = [];
13640
+ data.forEach((entry, i) => {
13641
+ const problems = validateErratum(entry);
13642
+ if (problems.length)
13643
+ errors.push(...problems.map((p) => `entry ${i}: ${p}`));
13644
+ else
13645
+ errata.push(entry);
13646
+ });
13647
+ return { errata, errors };
13648
+ }
13649
+ function orphanedErrata(kbRoot, errata, refs) {
13650
+ const only = refs ? new Set(refs) : void 0;
13651
+ const hays = /* @__PURE__ */ new Map();
13652
+ const out = [];
13653
+ for (const e of errata) {
13654
+ if (only && !only.has(e.ref))
13655
+ continue;
13656
+ if (!hays.has(e.ref)) {
13657
+ const conv = readState2(kbRoot, e.ref) ? convertedFile(kbRoot, e.ref) : null;
13658
+ hays.set(e.ref, conv && fs23.existsSync(conv) ? normalizedViews(fs23.readFileSync(conv, "utf8")) : null);
13659
+ }
13660
+ const hay = hays.get(e.ref);
13661
+ if (hay && !findQuote(hay, e.quote))
13662
+ out.push({ id: e.id, ref: e.ref });
13663
+ }
13664
+ return out;
13665
+ }
13666
+ var init_errata = __esm({
13667
+ "packages/core/dist/knowledge/errata.js"() {
13668
+ init_js_yaml();
13669
+ init_dist();
13670
+ init_ingest();
13671
+ init_verify();
13672
+ }
13673
+ });
13674
+
13675
+ // packages/core/dist/knowledge/anchor.js
13676
+ import fs24 from "fs";
13677
+ import path25 from "path";
13534
13678
  function* repoFiles(root) {
13535
13679
  let count = 0;
13536
13680
  const stack = [root];
@@ -13538,43 +13682,43 @@ function* repoFiles(root) {
13538
13682
  const dir = stack.pop();
13539
13683
  let ents;
13540
13684
  try {
13541
- ents = fs23.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name < b.name ? 1 : -1);
13685
+ ents = fs24.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name < b.name ? 1 : -1);
13542
13686
  } catch {
13543
13687
  continue;
13544
13688
  }
13545
13689
  for (const ent of ents) {
13546
13690
  if (ent.isDirectory()) {
13547
13691
  if (!SKIP_DIRS.has(ent.name))
13548
- stack.push(path24.join(dir, ent.name));
13549
- } else if (ent.isFile() && CODE_EXT.has(path24.extname(ent.name))) {
13692
+ stack.push(path25.join(dir, ent.name));
13693
+ } else if (ent.isFile() && CODE_EXT.has(path25.extname(ent.name))) {
13550
13694
  if (++count > MAX_FILES)
13551
13695
  return;
13552
- yield path24.join(dir, ent.name);
13696
+ yield path25.join(dir, ent.name);
13553
13697
  }
13554
13698
  }
13555
13699
  }
13556
13700
  }
13557
13701
  function computeAnchor(repoRoot, triggers) {
13558
13702
  const terms2 = anchorTerms(triggers);
13559
- if (!terms2.length || !fs23.existsSync(repoRoot))
13703
+ if (!terms2.length || !fs24.existsSync(repoRoot))
13560
13704
  return null;
13561
13705
  const hits = [];
13562
13706
  for (const file of repoFiles(repoRoot)) {
13563
13707
  let text;
13564
13708
  try {
13565
- if (fs23.statSync(file).size > MAX_FILE_BYTES)
13709
+ if (fs24.statSync(file).size > MAX_FILE_BYTES)
13566
13710
  continue;
13567
- text = fs23.readFileSync(file, "utf8");
13711
+ text = fs24.readFileSync(file, "utf8");
13568
13712
  } catch {
13569
13713
  continue;
13570
13714
  }
13571
- const hay = `${path24.relative(repoRoot, file)}
13715
+ const hay = `${path25.relative(repoRoot, file)}
13572
13716
  ${text}`.toLowerCase();
13573
13717
  const score = terms2.filter((t) => hay.includes(t)).length;
13574
13718
  if (!score)
13575
13719
  continue;
13576
13720
  const symbols2 = [...text.matchAll(SYMBOL_RE)].map((m) => m[1]).filter((s) => terms2.some((t) => s.toLowerCase().includes(t)));
13577
- hits.push({ rel: path24.relative(repoRoot, file).split(path24.sep).join("/"), score, symbols: symbols2 });
13721
+ hits.push({ rel: path25.relative(repoRoot, file).split(path25.sep).join("/"), score, symbols: symbols2 });
13578
13722
  }
13579
13723
  if (!hits.length)
13580
13724
  return null;
@@ -13598,11 +13742,11 @@ var init_anchor = __esm({
13598
13742
  });
13599
13743
 
13600
13744
  // packages/core/dist/knowledge/extract.js
13601
- import fs24 from "fs";
13602
- import path25 from "path";
13603
- import matter3 from "gray-matter";
13745
+ import fs25 from "fs";
13746
+ import path26 from "path";
13747
+ import matter4 from "gray-matter";
13604
13748
  function serialize(fm, body) {
13605
- return matter3.stringify(body.trim() + "\n", clean(fm));
13749
+ return matter4.stringify(body.trim() + "\n", clean(fm));
13606
13750
  }
13607
13751
  function validateCandidate(c) {
13608
13752
  const p = [];
@@ -13645,9 +13789,9 @@ function upsertSource(fm, src) {
13645
13789
  }
13646
13790
  function applyCandidates(input, opts) {
13647
13791
  const { kb, kbRoot, rulesDir } = opts;
13648
- const res = { created: [], merged: [], proposed: [], superseded: [], stale: [], rejected: [], doclingFlags: [], idOverrides: [] };
13792
+ const res = { created: [], merged: [], proposed: [], superseded: [], stale: [], rejected: [], doclingFlags: [], idOverrides: [], orphanedErrata: [] };
13649
13793
  const events = [];
13650
- const ev2 = (event, itemId, domain, extra = {}) => events.push({ kb, level: "group", project: "", domain, itemId, event, ...extra });
13794
+ const ev2 = (event, itemId, extra = {}) => events.push({ kb, level: "group", project: "", domain: "rules", itemId, event, ...extra });
13651
13795
  const loaded = loadRulesDir(rulesDir, `kb:${kb}`);
13652
13796
  const work = /* @__PURE__ */ new Map();
13653
13797
  for (const r of loaded.rules) {
@@ -13666,13 +13810,13 @@ function applyCandidates(input, opts) {
13666
13810
  }
13667
13811
  const st = readState2(kbRoot, cand.source);
13668
13812
  const convFile = st ? convertedFile(kbRoot, cand.source) : null;
13669
- if (!st || !fs24.existsSync(convFile)) {
13813
+ if (!st || !fs25.existsSync(convFile)) {
13670
13814
  res.rejected.push({ id: cand.id, source: cand.source, reason: "unknown source (not ingested)" });
13671
13815
  continue;
13672
13816
  }
13673
13817
  let hay = norm.get(cand.source);
13674
13818
  if (!hay) {
13675
- hay = normalizedViews(fs24.readFileSync(convFile, "utf8"));
13819
+ hay = normalizedViews(fs25.readFileSync(convFile, "utf8"));
13676
13820
  norm.set(cand.source, hay);
13677
13821
  }
13678
13822
  const tot = totals.get(cand.source) ?? { total: 0, dropped: 0 };
@@ -13682,7 +13826,14 @@ function applyCandidates(input, opts) {
13682
13826
  if (!offset) {
13683
13827
  tot.dropped++;
13684
13828
  res.rejected.push({ id: cand.id, source: cand.source, reason: "ungrounded" });
13685
- ev2("item.rejected", cand.id, cand.domain, { reason: "ungrounded" });
13829
+ ev2("item.rejected", cand.id, { reason: "ungrounded" });
13830
+ continue;
13831
+ }
13832
+ const hit = (opts.errata ?? []).find((e) => e.ref === cand.source && ((o) => !!o && spansOverlap(o, offset))(findQuote(hay, e.quote)));
13833
+ if (hit) {
13834
+ tot.total--;
13835
+ res.rejected.push({ id: cand.id, source: cand.source, reason: `errata:${hit.id}` });
13836
+ ev2("item.rejected", cand.id, { reason: `errata:${hit.id}` });
13686
13837
  continue;
13687
13838
  }
13688
13839
  let id = cand.id;
@@ -13714,7 +13865,7 @@ ${cand.rationale.trim()}` : "");
13714
13865
  }
13715
13866
  continue;
13716
13867
  }
13717
- if (!target && contradicted.length === 0) {
13868
+ if (!target && contradicted.length === 0 && !isGatedRef(kbRoot, opts.sources ?? [], cand.source)) {
13718
13869
  const fm2 = {
13719
13870
  id,
13720
13871
  domain: cand.domain,
@@ -13727,9 +13878,9 @@ ${cand.rationale.trim()}` : "");
13727
13878
  supersedes: [],
13728
13879
  conflictsWith: []
13729
13880
  };
13730
- work.set(id, { fm: fm2, body: bodyOf(), file: path25.join(rulesDir, cand.domain, `${id}.md`), dirty: true, isNew: true });
13881
+ work.set(id, { fm: fm2, body: bodyOf(), file: path26.join(rulesDir, cand.domain, `${id}.md`), dirty: true, isNew: true });
13731
13882
  res.created.push(id);
13732
- ev2("item.applied", id, cand.domain);
13883
+ ev2("item.applied", id);
13733
13884
  continue;
13734
13885
  }
13735
13886
  const fm = {
@@ -13744,26 +13895,26 @@ ${cand.rationale.trim()}` : "");
13744
13895
  supersedes: [],
13745
13896
  conflictsWith: contradicted
13746
13897
  };
13747
- const dir = path25.join(rulesDir, PROPOSED_DIR);
13748
- const file = path25.join(dir, `${id}@${st.sha.slice(0, SHA_LEN)}.md`);
13898
+ const dir = path26.join(rulesDir, PROPOSED_DIR);
13899
+ const file = path26.join(dir, `${id}@${st.sha.slice(0, SHA_LEN)}.md`);
13749
13900
  const text = serialize(fm, bodyOf());
13750
- if (fs24.existsSync(file) && fs24.readFileSync(file, "utf8") === text)
13901
+ if (fs25.existsSync(file) && fs25.readFileSync(file, "utf8") === text)
13751
13902
  continue;
13752
- fs24.mkdirSync(dir, { recursive: true });
13753
- for (const other of fs24.readdirSync(dir)) {
13754
- if (!other.startsWith(`${id}@`) || !other.endsWith(".md") || path25.join(dir, other) === file)
13903
+ fs25.mkdirSync(dir, { recursive: true });
13904
+ for (const other of fs25.readdirSync(dir)) {
13905
+ if (!other.startsWith(`${id}@`) || !other.endsWith(".md") || path26.join(dir, other) === file)
13755
13906
  continue;
13756
- const old = matter3(fs24.readFileSync(path25.join(dir, other), "utf8"));
13907
+ const old = matter4(fs25.readFileSync(path26.join(dir, other), "utf8"));
13757
13908
  if (!old.data.sources?.some((s) => s.ref === cand.source))
13758
13909
  continue;
13759
- fs24.writeFileSync(path25.join(dir, other.replace(/\.md$/, ".superseded")), matter3.stringify(old.content, { ...old.data, supersededBy: path25.basename(file) }));
13760
- fs24.rmSync(path25.join(dir, other));
13910
+ fs25.writeFileSync(path26.join(dir, other.replace(/\.md$/, ".superseded")), matter4.stringify(old.content, { ...old.data, supersededBy: path26.basename(file) }));
13911
+ fs25.rmSync(path26.join(dir, other));
13761
13912
  res.superseded.push(other.replace(/\.md$/, ""));
13762
- ev2("item.superseded", id, fm.domain);
13913
+ ev2("item.superseded", id);
13763
13914
  }
13764
- fs24.writeFileSync(file, text);
13915
+ fs25.writeFileSync(file, text);
13765
13916
  res.proposed.push(`${id}@${st.sha.slice(0, SHA_LEN)}`);
13766
- ev2("item.proposed", id, fm.domain);
13917
+ ev2("item.proposed", id);
13767
13918
  }
13768
13919
  for (const [id, w] of work) {
13769
13920
  if (touched.has(id) || w.fm.status !== "active" || w.isNew)
@@ -13789,11 +13940,12 @@ ${cand.rationale.trim()}` : "");
13789
13940
  for (const w of work.values()) {
13790
13941
  if (!w.dirty)
13791
13942
  continue;
13792
- fs24.mkdirSync(path25.dirname(w.file), { recursive: true });
13943
+ fs25.mkdirSync(path26.dirname(w.file), { recursive: true });
13793
13944
  const text = serialize(w.fm, w.body);
13794
- if (!fs24.existsSync(w.file) || fs24.readFileSync(w.file, "utf8") !== text)
13795
- fs24.writeFileSync(w.file, text);
13945
+ if (!fs25.existsSync(w.file) || fs25.readFileSync(w.file, "utf8") !== text)
13946
+ fs25.writeFileSync(w.file, text);
13796
13947
  }
13948
+ res.orphanedErrata = orphanedErrata(kbRoot, opts.errata ?? [], totals.keys());
13797
13949
  for (const [ref, t] of totals)
13798
13950
  if (needsDocling(t.dropped, t.total))
13799
13951
  res.doclingFlags.push({ ref, ...t });
@@ -13810,6 +13962,7 @@ var init_extract = __esm({
13810
13962
  init_anchor();
13811
13963
  init_index_file();
13812
13964
  init_ingest();
13965
+ init_errata();
13813
13966
  init_paths();
13814
13967
  init_store2();
13815
13968
  init_verify();
@@ -13826,20 +13979,20 @@ var init_extract = __esm({
13826
13979
  });
13827
13980
 
13828
13981
  // packages/core/dist/knowledge/schedule.js
13829
- import fs25 from "fs";
13830
- import path26 from "path";
13982
+ import fs26 from "fs";
13983
+ import path27 from "path";
13831
13984
  function readSchedule(stateRoot) {
13832
13985
  try {
13833
- return JSON.parse(fs25.readFileSync(scheduleFile(stateRoot), "utf8"));
13986
+ return JSON.parse(fs26.readFileSync(scheduleFile(stateRoot), "utf8"));
13834
13987
  } catch {
13835
13988
  return {};
13836
13989
  }
13837
13990
  }
13838
13991
  function writeSchedule(stateRoot, s) {
13839
- fs25.mkdirSync(stateRoot, { recursive: true });
13992
+ fs26.mkdirSync(stateRoot, { recursive: true });
13840
13993
  const tmp = `${scheduleFile(stateRoot)}.${process.pid}.tmp`;
13841
- fs25.writeFileSync(tmp, JSON.stringify(s, null, 2) + "\n");
13842
- fs25.renameSync(tmp, scheduleFile(stateRoot));
13994
+ fs26.writeFileSync(tmp, JSON.stringify(s, null, 2) + "\n");
13995
+ fs26.renameSync(tmp, scheduleFile(stateRoot));
13843
13996
  }
13844
13997
  function snapshotSources(kbRoot, sources) {
13845
13998
  const out = {};
@@ -13919,7 +14072,7 @@ async function tickKnowledgeSchedule(d) {
13919
14072
  continue;
13920
14073
  try {
13921
14074
  const root = kbDir(d.cfg, kb);
13922
- if (!fs25.existsSync(root))
14075
+ if (!fs26.existsSync(root))
13923
14076
  continue;
13924
14077
  const { sources } = loadSources(root);
13925
14078
  if (!sources.length)
@@ -13984,7 +14137,7 @@ var init_schedule = __esm({
13984
14137
  init_sources();
13985
14138
  RUNNING_STALE_MS = 6 * 60 * 60 * 1e3;
13986
14139
  DAY_MS = 24 * 60 * 60 * 1e3;
13987
- scheduleFile = (stateRoot) => path26.join(stateRoot, "knowledge-schedule.json");
14140
+ scheduleFile = (stateRoot) => path27.join(stateRoot, "knowledge-schedule.json");
13988
14141
  reconcileConfig = (cfg, kb) => ({ ...DEFAULT_RECONCILE, ...kbConfigs(cfg)[kb]?.reconcile });
13989
14142
  }
13990
14143
  });
@@ -13992,19 +14145,19 @@ var init_schedule = __esm({
13992
14145
  // packages/core/dist/knowledge/review.js
13993
14146
  import crypto4 from "crypto";
13994
14147
  import { execFileSync as execFileSync7 } from "child_process";
13995
- import fs26 from "fs";
13996
- import path27 from "path";
13997
- import matter4 from "gray-matter";
14148
+ import fs27 from "fs";
14149
+ import path28 from "path";
14150
+ import matter5 from "gray-matter";
13998
14151
  function readEscalations(kbRoot) {
13999
14152
  try {
14000
- return JSON.parse(fs26.readFileSync(escalationsFile(kbRoot), "utf8")).items;
14153
+ return JSON.parse(fs27.readFileSync(escalationsFile(kbRoot), "utf8")).items;
14001
14154
  } catch {
14002
14155
  return [];
14003
14156
  }
14004
14157
  }
14005
14158
  function writeEscalations(kbRoot, items) {
14006
- fs26.mkdirSync(kbRoot, { recursive: true });
14007
- fs26.writeFileSync(escalationsFile(kbRoot), JSON.stringify({ items }, null, 2) + "\n");
14159
+ fs27.mkdirSync(kbRoot, { recursive: true });
14160
+ fs27.writeFileSync(escalationsFile(kbRoot), JSON.stringify({ items }, null, 2) + "\n");
14008
14161
  }
14009
14162
  function addEscalation(kbRoot, e) {
14010
14163
  const items = readEscalations(kbRoot);
@@ -14014,8 +14167,8 @@ function addEscalation(kbRoot, e) {
14014
14167
  return true;
14015
14168
  }
14016
14169
  function appendVerdict(kbRoot, v2, now) {
14017
- fs26.mkdirSync(kbRoot, { recursive: true });
14018
- fs26.appendFileSync(verdictsFile(kbRoot), JSON.stringify({ ts: now.toISOString(), ...v2 }) + "\n");
14170
+ fs27.mkdirSync(kbRoot, { recursive: true });
14171
+ fs27.appendFileSync(verdictsFile(kbRoot), JSON.stringify({ ts: now.toISOString(), ...v2 }) + "\n");
14019
14172
  }
14020
14173
  function priorityOfRef(c, ref) {
14021
14174
  if (ref.startsWith("agent:"))
@@ -14036,20 +14189,20 @@ function loadProposals(c) {
14036
14189
  const byId = new Map(live.map((r) => [r.id, r]));
14037
14190
  const proposals = [];
14038
14191
  const pdirs = /* @__PURE__ */ new Set();
14039
- for (const f of fs26.existsSync(dir) ? walkDirs(dir) : [])
14040
- if (path27.basename(f) === PROPOSED_DIR)
14192
+ for (const f of fs27.existsSync(dir) ? walkDirs(dir) : [])
14193
+ if (path28.basename(f) === PROPOSED_DIR)
14041
14194
  pdirs.add(f);
14042
14195
  for (const pd of pdirs) {
14043
14196
  for (const rule of loadRulesDir(pd, `kb:${c.kb}`, { includeProposed: true }).rules) {
14044
- proposals.push({ key: path27.basename(rule.file).replace(/\.md$/, ""), rule, live: byId.get(rule.id) });
14197
+ proposals.push({ key: path28.basename(rule.file).replace(/\.md$/, ""), rule, live: byId.get(rule.id) });
14045
14198
  }
14046
14199
  }
14047
14200
  return { proposals, live };
14048
14201
  }
14049
14202
  function walkDirs(dir, out = []) {
14050
- for (const e of fs26.readdirSync(dir, { withFileTypes: true }))
14203
+ for (const e of fs27.readdirSync(dir, { withFileTypes: true }))
14051
14204
  if (e.isDirectory()) {
14052
- const f = path27.join(dir, e.name);
14205
+ const f = path28.join(dir, e.name);
14053
14206
  out.push(f);
14054
14207
  walkDirs(f, out);
14055
14208
  }
@@ -14078,9 +14231,9 @@ function promoteProposal(c, p, by, justification, events) {
14078
14231
  continue;
14079
14232
  const o = readFm(other.file);
14080
14233
  o.fm.status = "retired";
14081
- fs26.writeFileSync(other.file, fileText(o.fm, o.body));
14234
+ fs27.writeFileSync(other.file, fileText(o.fm, o.body));
14082
14235
  retired.push(id);
14083
- events.push(ev(c, "item.superseded", id, o.fm.domain));
14236
+ events.push(ev(c, "item.superseded", id));
14084
14237
  }
14085
14238
  const next = { ...fm, status: "active", approvedBy: by, justification, supersedes: [.../* @__PURE__ */ new Set([...fm.supersedes ?? [], ...retired])], conflictsWith: [] };
14086
14239
  if (p.live?.decision)
@@ -14088,17 +14241,17 @@ function promoteProposal(c, p, by, justification, events) {
14088
14241
  const anchors = anchorsFor(c, { ...next, anchors: p.live?.anchors ?? fm.anchors });
14089
14242
  if (anchors)
14090
14243
  next.anchors = anchors;
14091
- const target = p.live?.file ?? path27.join(dir, next.domain, `${next.id}.md`);
14092
- fs26.mkdirSync(path27.dirname(target), { recursive: true });
14093
- fs26.writeFileSync(target, fileText(next, body));
14094
- fs26.rmSync(p.rule.file);
14095
- events.push(ev(c, "item.approved", next.id, next.domain), ev(c, "item.applied", next.id, next.domain));
14244
+ const target = p.live?.file ?? path28.join(dir, next.domain, `${next.id}.md`);
14245
+ fs27.mkdirSync(path28.dirname(target), { recursive: true });
14246
+ fs27.writeFileSync(target, fileText(next, body));
14247
+ fs27.rmSync(p.rule.file);
14248
+ events.push(ev(c, "item.approved", next.id), ev(c, "item.applied", next.id));
14096
14249
  if (p.live)
14097
- events.push(ev(c, "item.superseded", next.id, next.domain));
14250
+ events.push(ev(c, "item.superseded", next.id));
14098
14251
  }
14099
14252
  function dropProposal(c, p, events) {
14100
- fs26.rmSync(p.rule.file);
14101
- events.push(ev(c, "item.rejected", p.rule.id, p.rule.domain, { reason: "reviewer" }));
14253
+ fs27.rmSync(p.rule.file);
14254
+ events.push(ev(c, "item.rejected", p.rule.id, { reason: "reviewer" }));
14102
14255
  }
14103
14256
  function mergeInto(c, p, targetId, live, events) {
14104
14257
  const t = live.find((r) => r.id === targetId);
@@ -14108,9 +14261,9 @@ function mergeInto(c, p, targetId, live, events) {
14108
14261
  for (const s of p.rule.sources)
14109
14262
  if (!tf.fm.sources.some((x) => x.ref === s.ref))
14110
14263
  tf.fm.sources.push(s);
14111
- fs26.writeFileSync(t.file, fileText(tf.fm, tf.body));
14112
- fs26.rmSync(p.rule.file);
14113
- events.push(ev(c, "item.superseded", p.rule.id, p.rule.domain), ev(c, "item.applied", targetId, t.domain));
14264
+ fs27.writeFileSync(t.file, fileText(tf.fm, tf.body));
14265
+ fs27.rmSync(p.rule.file);
14266
+ events.push(ev(c, "item.superseded", p.rule.id), ev(c, "item.applied", targetId));
14114
14267
  return true;
14115
14268
  }
14116
14269
  function evaluateCodeConflict(c, rule, cc) {
@@ -14184,14 +14337,15 @@ function applyDecisions(file, c) {
14184
14337
  f.fm.status = "active";
14185
14338
  if (d.loc && f.fm.sources[0])
14186
14339
  f.fm.sources[0].loc = d.loc;
14187
- if (conf >= threshold && !d.flag && !reasons.length) {
14188
- fs26.writeFileSync(rule.file, fileText(f.fm, f.body));
14340
+ const noSource = !rule.sources.length;
14341
+ if (conf >= threshold && !d.flag && !reasons.length && !noSource) {
14342
+ fs27.writeFileSync(rule.file, fileText(f.fm, f.body));
14189
14343
  res.applied.push(d.item);
14190
- events.push(ev(c, "item.applied", rule.id, rule.domain));
14344
+ events.push(ev(c, "item.applied", rule.id));
14191
14345
  verdict(d, "applied", { confidence: conf });
14192
14346
  continue;
14193
14347
  }
14194
- reasons.push(conf < threshold ? "low-confidence" : "reviewer-flagged");
14348
+ reasons.push(noSource ? "no-source" : conf < threshold ? "low-confidence" : "reviewer-flagged");
14195
14349
  } else if (d.decision === "approve") {
14196
14350
  if (strong(rule.modality))
14197
14351
  reasons.push("retiring must/must-not");
@@ -14202,9 +14356,9 @@ function applyDecisions(file, c) {
14202
14356
  if (!reasons.length) {
14203
14357
  const f = readFm(rule.file);
14204
14358
  f.fm.status = "retired";
14205
- fs26.writeFileSync(rule.file, fileText(f.fm, f.body));
14359
+ fs27.writeFileSync(rule.file, fileText(f.fm, f.body));
14206
14360
  res.applied.push(d.item);
14207
- events.push(ev(c, "item.archived", rule.id, rule.domain));
14361
+ events.push(ev(c, "item.archived", rule.id));
14208
14362
  verdict(d, "applied", { confidence: conf });
14209
14363
  continue;
14210
14364
  }
@@ -14219,6 +14373,8 @@ function applyDecisions(file, c) {
14219
14373
  reasons.push("reviewer-flagged");
14220
14374
  if (conf < threshold)
14221
14375
  reasons.push("low-confidence");
14376
+ if (p.rule.sources.some((s) => isGatedRef(kbRoot, c.sources, s.ref)))
14377
+ reasons.push("review-required");
14222
14378
  if (d.decision === "approve") {
14223
14379
  const myPri = priorityOfRule(c, p.rule);
14224
14380
  const conflicted = (p.rule.conflictsWith ?? []).map((id) => live.find((r) => r.id === id)).filter((r) => !!r);
@@ -14280,7 +14436,7 @@ function applyPair(pd, c, res, threshold, events, kbRoot, now) {
14280
14436
  for (const [x, y] of [[a, b], [b, a]]) {
14281
14437
  const f = readFm(x.file);
14282
14438
  f.fm.conflictsWith = [.../* @__PURE__ */ new Set([...f.fm.conflictsWith ?? [], y.id])];
14283
- fs26.writeFileSync(x.file, fileText(f.fm, f.body));
14439
+ fs27.writeFileSync(x.file, fileText(f.fm, f.body));
14284
14440
  }
14285
14441
  return;
14286
14442
  }
@@ -14295,11 +14451,11 @@ function applyPair(pd, c, res, threshold, events, kbRoot, now) {
14295
14451
  if (!kf.fm.sources.some((x) => x.ref === s.ref))
14296
14452
  kf.fm.sources.push(s);
14297
14453
  kf.fm.supersedes = [.../* @__PURE__ */ new Set([...kf.fm.supersedes ?? [], drop.id])];
14298
- fs26.writeFileSync(keep.file, fileText(kf.fm, kf.body));
14454
+ fs27.writeFileSync(keep.file, fileText(kf.fm, kf.body));
14299
14455
  const df = readFm(drop.file);
14300
14456
  df.fm.status = "retired";
14301
- fs26.writeFileSync(drop.file, fileText(df.fm, df.body));
14302
- events.push(ev(c, "item.superseded", drop.id, drop.domain), ev(c, "item.applied", keep.id, keep.domain));
14457
+ fs27.writeFileSync(drop.file, fileText(df.fm, df.body));
14458
+ events.push(ev(c, "item.superseded", drop.id), ev(c, "item.applied", keep.id));
14303
14459
  res.applied.push(key);
14304
14460
  }
14305
14461
  function resolveEscalation(c, key, r, by, reason) {
@@ -14329,9 +14485,9 @@ function resolveEscalation(c, key, r, by, reason) {
14329
14485
  f.fm.decision = decision;
14330
14486
  if (r.verdict === "supersede-with-code") {
14331
14487
  f.fm.status = "retired";
14332
- events.push(ev(c, "item.superseded", rule.id, rule.domain));
14488
+ events.push(ev(c, "item.superseded", rule.id));
14333
14489
  }
14334
- fs26.writeFileSync(rule.file, fileText(f.fm, f.body));
14490
+ fs27.writeFileSync(rule.file, fileText(f.fm, f.body));
14335
14491
  return done(`${rule.id}: ${r.verdict}`);
14336
14492
  }
14337
14493
  if (esc.kind === "proposal") {
@@ -14348,23 +14504,26 @@ function resolveEscalation(c, key, r, by, reason) {
14348
14504
  }
14349
14505
  if (esc.kind === "stale-retire") {
14350
14506
  const rule = live.find((x) => x.id === esc.ruleId);
14507
+ if (rule && r.kind !== "approve" && !rule.sources.length) {
14508
+ throw new Error(`Rule ${rule.id} has no source, so it cannot be kept active. Add a source to the rule (or re-extract it), or retire it with --approve.`);
14509
+ }
14351
14510
  if (rule) {
14352
14511
  const f = readFm(rule.file);
14353
14512
  f.fm.status = r.kind === "approve" ? "retired" : "active";
14354
- fs26.writeFileSync(rule.file, fileText(f.fm, f.body));
14355
- events.push(ev(c, r.kind === "approve" ? "item.archived" : "item.applied", rule.id, rule.domain));
14513
+ fs27.writeFileSync(rule.file, fileText(f.fm, f.body));
14514
+ events.push(ev(c, r.kind === "approve" ? "item.archived" : "item.applied", rule.id));
14356
14515
  }
14357
14516
  return done(`${esc.ruleId}: ${r.kind === "approve" ? "retired" : "kept"}`);
14358
14517
  }
14359
14518
  if (esc.kind === "override-base-changed") {
14360
- const ov = esc.item && fs26.existsSync(esc.item) ? esc.item : void 0;
14519
+ const ov = esc.item && fs27.existsSync(esc.item) ? esc.item : void 0;
14361
14520
  if (ov) {
14362
14521
  if (r.kind === "reject")
14363
- fs26.rmSync(ov);
14522
+ fs27.rmSync(ov);
14364
14523
  else {
14365
14524
  const f = readFm(ov);
14366
14525
  f.fm.overridesBase = esc.evidence?.docRev;
14367
- fs26.writeFileSync(ov, fileText(f.fm, f.body));
14526
+ fs27.writeFileSync(ov, fileText(f.fm, f.body));
14368
14527
  }
14369
14528
  }
14370
14529
  return done(`${esc.ruleId}: override ${r.kind === "reject" ? "dropped" : "kept"}`);
@@ -14388,11 +14547,11 @@ ${o.evidence}`).digest("hex");
14388
14547
  supersedes: [],
14389
14548
  conflictsWith: []
14390
14549
  };
14391
- const dir = path27.join(kbRulesDir(c.cfg, c.kb), PROPOSED_DIR);
14392
- fs26.mkdirSync(dir, { recursive: true });
14393
- const file = path27.join(dir, `${id}@${sha.slice(0, SHA_LEN2)}.md`);
14394
- fs26.writeFileSync(file, fileText(fm, o.statement));
14395
- appendAudit([ev(c, "item.proposed", id, domain)], c.audit);
14550
+ const dir = path28.join(kbRulesDir(c.cfg, c.kb), PROPOSED_DIR);
14551
+ fs27.mkdirSync(dir, { recursive: true });
14552
+ const file = path28.join(dir, `${id}@${sha.slice(0, SHA_LEN2)}.md`);
14553
+ fs27.writeFileSync(file, fileText(fm, o.statement));
14554
+ appendAudit([ev(c, "item.proposed", id)], c.audit);
14396
14555
  return file;
14397
14556
  }
14398
14557
  function buildReviewPacket(c) {
@@ -14432,8 +14591,8 @@ var init_review = __esm({
14432
14591
  PRIORITY_RANK = { company: 3, project: 2, agent: 1 };
14433
14592
  SHA_LEN2 = 12;
14434
14593
  strong = (m) => m === "must" || m === "must-not";
14435
- escalationsFile = (kbRoot) => path27.join(kbRoot, "escalations.json");
14436
- verdictsFile = (kbRoot) => path27.join(kbRoot, "verdicts.jsonl");
14594
+ escalationsFile = (kbRoot) => path28.join(kbRoot, "escalations.json");
14595
+ verdictsFile = (kbRoot) => path28.join(kbRoot, "verdicts.jsonl");
14437
14596
  needsYouCount = (kbRoot) => readEscalations(kbRoot).filter((e) => e.needsYou).length;
14438
14597
  git = (root, args) => execFileSync7("git", ["-C", root, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
14439
14598
  realGit = {
@@ -14454,34 +14613,34 @@ var init_review = __esm({
14454
14613
  },
14455
14614
  docDate(file) {
14456
14615
  try {
14457
- const d = git(path27.dirname(file), ["log", "-1", "--format=%cI", "--", path27.basename(file)]);
14616
+ const d = git(path28.dirname(file), ["log", "-1", "--format=%cI", "--", path28.basename(file)]);
14458
14617
  if (d)
14459
14618
  return d;
14460
14619
  } catch {
14461
14620
  }
14462
14621
  try {
14463
- return fs26.statSync(file).mtime.toISOString();
14622
+ return fs27.statSync(file).mtime.toISOString();
14464
14623
  } catch {
14465
14624
  return null;
14466
14625
  }
14467
14626
  }
14468
14627
  };
14469
14628
  nowOf = (c) => c.now?.() ?? /* @__PURE__ */ new Date();
14470
- fileText = (fm, body) => matter4.stringify(body.trim() + "\n", JSON.parse(JSON.stringify(fm)));
14629
+ fileText = (fm, body) => matter5.stringify(body.trim() + "\n", JSON.parse(JSON.stringify(fm)));
14471
14630
  readFm = (file) => {
14472
- const m = matter4(fs26.readFileSync(file, "utf8"));
14631
+ const m = matter5(fs27.readFileSync(file, "utf8"));
14473
14632
  return { fm: m.data, body: m.content };
14474
14633
  };
14475
- ev = (c, event, itemId, domain, extra = {}) => ({ kb: c.kb, level: "group", project: "", domain, itemId, event, ...extra });
14634
+ ev = (c, event, itemId, extra = {}) => ({ kb: c.kb, level: "group", project: "", domain: "rules", itemId, event, ...extra });
14476
14635
  docRevOf = (r) => r.sources.map((s) => `${s.ref}@${s.sha.slice(0, SHA_LEN2)}`).join(",");
14477
14636
  }
14478
14637
  });
14479
14638
 
14480
14639
  // packages/core/dist/knowledge/fullpass.js
14481
14640
  import crypto5 from "crypto";
14482
- import fs27 from "fs";
14483
- import path28 from "path";
14484
- import matter5 from "gray-matter";
14641
+ import fs28 from "fs";
14642
+ import path29 from "path";
14643
+ import matter6 from "gray-matter";
14485
14644
  function dedupPairs(rules) {
14486
14645
  const active = rules.filter((r) => r.status === "active").sort((x, y) => x.id.localeCompare(y.id));
14487
14646
  const out = [];
@@ -14497,12 +14656,12 @@ function dedupPairs(rules) {
14497
14656
  return out.slice(0, MAX_DEDUP_PAIRS);
14498
14657
  }
14499
14658
  function readAuditRecords(kb, since, dir = defaultAuditDir()) {
14500
- if (!fs27.existsSync(dir))
14659
+ if (!fs28.existsSync(dir))
14501
14660
  return [];
14502
14661
  const sinceMonth = since.toISOString().slice(0, 7);
14503
14662
  const out = [];
14504
- for (const f of fs27.readdirSync(dir).filter((n) => n.endsWith(".jsonl") && n.slice(0, 7) >= sinceMonth).sort()) {
14505
- for (const line2 of fs27.readFileSync(path28.join(dir, f), "utf8").split("\n")) {
14663
+ for (const f of fs28.readdirSync(dir).filter((n) => n.endsWith(".jsonl") && n.slice(0, 7) >= sinceMonth).sort()) {
14664
+ for (const line2 of fs28.readFileSync(path29.join(dir, f), "utf8").split("\n")) {
14506
14665
  if (!line2.trim())
14507
14666
  continue;
14508
14667
  try {
@@ -14565,8 +14724,8 @@ function checkOverrides(cfg, kb) {
14565
14724
  continue;
14566
14725
  const h = baseHash(b);
14567
14726
  if (!o.overridesBase) {
14568
- const m = matter5(fs27.readFileSync(o.file, "utf8"));
14569
- fs27.writeFileSync(o.file, matter5.stringify(m.content, { ...m.data, overridesBase: h }));
14727
+ const m = matter6(fs28.readFileSync(o.file, "utf8"));
14728
+ fs28.writeFileSync(o.file, matter6.stringify(m.content, { ...m.data, overridesBase: h }));
14570
14729
  continue;
14571
14730
  }
14572
14731
  out.push({ project, ruleId: o.id, file: o.file, baseHash: h, changed: o.overridesBase !== h });
@@ -14609,8 +14768,8 @@ var init_fullpass = __esm({
14609
14768
  });
14610
14769
 
14611
14770
  // packages/core/dist/knowledge/scores.js
14612
- import fs28 from "fs";
14613
- import path29 from "path";
14771
+ import fs29 from "fs";
14772
+ import path30 from "path";
14614
14773
  function tally(records, into = emptyTally()) {
14615
14774
  for (const e of records) {
14616
14775
  const domain = typeof e.domain === "string" && e.domain ? e.domain : "rules";
@@ -14619,7 +14778,7 @@ function tally(records, into = emptyTally()) {
14619
14778
  d.writes++;
14620
14779
  if (READ_EVENTS.has(e.event ?? ""))
14621
14780
  d.reads++;
14622
- if (!e.itemId)
14781
+ if (!e.itemId || WRITE_EVENTS.has(e.event ?? ""))
14623
14782
  continue;
14624
14783
  const c = into.items[`${domain}|${e.itemId}`] ??= { ...zero(), domain, itemId: e.itemId };
14625
14784
  switch (e.event) {
@@ -14656,7 +14815,7 @@ function tally(records, into = emptyTally()) {
14656
14815
  }
14657
14816
  function parseLines(file) {
14658
14817
  const out = [];
14659
- for (const line2 of fs28.readFileSync(file, "utf8").split("\n")) {
14818
+ for (const line2 of fs29.readFileSync(file, "utf8").split("\n")) {
14660
14819
  if (!line2.trim())
14661
14820
  continue;
14662
14821
  try {
@@ -14668,37 +14827,37 @@ function parseLines(file) {
14668
14827
  }
14669
14828
  function compactAudit(o) {
14670
14829
  const dir = o.dir ?? defaultAuditDir();
14671
- if (!fs28.existsSync(dir))
14830
+ if (!fs29.existsSync(dir))
14672
14831
  return [];
14673
14832
  const cutoffMonth = new Date(o.now.getTime() - SCORE_WINDOW_DAYS * DAY_MS3).toISOString().slice(0, 7);
14674
14833
  const done = [];
14675
- for (const f of fs28.readdirSync(dir).sort()) {
14834
+ for (const f of fs29.readdirSync(dir).sort()) {
14676
14835
  const m = MONTH_FILE_RE.exec(f);
14677
14836
  if (!m || m[1] >= cutoffMonth)
14678
14837
  continue;
14679
14838
  const [, month, machine] = m;
14680
14839
  const byKb = /* @__PURE__ */ new Map();
14681
- for (const r of parseLines(path29.join(dir, f)))
14840
+ for (const r of parseLines(path30.join(dir, f)))
14682
14841
  byKb.set(r.kb ?? "", [...byKb.get(r.kb ?? "") ?? [], r]);
14683
- const statsPath = path29.join(dir, statsFileName(month, machine));
14684
- const prior = fs28.existsSync(statsPath) ? JSON.parse(fs28.readFileSync(statsPath, "utf8")) : null;
14842
+ const statsPath = path30.join(dir, statsFileName(month, machine));
14843
+ const prior = fs29.existsSync(statsPath) ? JSON.parse(fs29.readFileSync(statsPath, "utf8")) : null;
14685
14844
  const kbs = prior?.kbs ?? {};
14686
14845
  for (const [kb, recs] of byKb)
14687
14846
  kbs[kb] = tally(recs, kbs[kb]);
14688
- fs28.writeFileSync(`${statsPath}.tmp`, JSON.stringify({ month, machine, kbs }));
14689
- fs28.renameSync(`${statsPath}.tmp`, statsPath);
14690
- fs28.unlinkSync(path29.join(dir, f));
14847
+ fs29.writeFileSync(`${statsPath}.tmp`, JSON.stringify({ month, machine, kbs }));
14848
+ fs29.renameSync(`${statsPath}.tmp`, statsPath);
14849
+ fs29.unlinkSync(path30.join(dir, f));
14691
14850
  done.push(month);
14692
14851
  }
14693
14852
  return done;
14694
14853
  }
14695
14854
  function readCompacted(kb, dir) {
14696
14855
  const t = emptyTally();
14697
- if (!fs28.existsSync(dir))
14856
+ if (!fs29.existsSync(dir))
14698
14857
  return t;
14699
- for (const f of fs28.readdirSync(dir).filter((n) => n.endsWith(".stats.json"))) {
14858
+ for (const f of fs29.readdirSync(dir).filter((n) => n.endsWith(".stats.json"))) {
14700
14859
  try {
14701
- const k = JSON.parse(fs28.readFileSync(path29.join(dir, f), "utf8")).kbs[kb];
14860
+ const k = JSON.parse(fs29.readFileSync(path30.join(dir, f), "utf8")).kbs[kb];
14702
14861
  if (k)
14703
14862
  mergeTally(t, k);
14704
14863
  } catch {
@@ -14748,7 +14907,7 @@ function computeScores(i) {
14748
14907
  noise: w.noise,
14749
14908
  wrong: w.wrong,
14750
14909
  fireRate: w.surfaced,
14751
- pullRate: w.shown,
14910
+ pullRate: w.shown + w.searched,
14752
14911
  useRate: ratio(w.cited, w.surfaced),
14753
14912
  noiseRate: ratio(w.noise, w.surfaced),
14754
14913
  violationRate: ratio(w.violated, w.surfaced),
@@ -14790,21 +14949,21 @@ function computeScores(i) {
14790
14949
  function scoreKb(o) {
14791
14950
  const dir = o.auditDir ?? defaultAuditDir();
14792
14951
  const records = [];
14793
- if (fs28.existsSync(dir)) {
14794
- for (const f of fs28.readdirSync(dir).filter((n) => MONTH_FILE_RE.test(n)).sort()) {
14795
- records.push(...parseLines(path29.join(dir, f)).filter((r) => r.kb === o.kb));
14952
+ if (fs29.existsSync(dir)) {
14953
+ for (const f of fs29.readdirSync(dir).filter((n) => MONTH_FILE_RE.test(n)).sort()) {
14954
+ records.push(...parseLines(path30.join(dir, f)).filter((r) => r.kb === o.kb));
14796
14955
  }
14797
14956
  }
14798
14957
  let verdicts = [];
14799
14958
  try {
14800
- verdicts = fs28.readFileSync(path29.join(o.kbRoot, "verdicts.jsonl"), "utf8").split("\n").filter(Boolean).map((l) => JSON.parse(l));
14959
+ verdicts = fs29.readFileSync(path30.join(o.kbRoot, "verdicts.jsonl"), "utf8").split("\n").filter(Boolean).map((l) => JSON.parse(l));
14801
14960
  } catch {
14802
14961
  }
14803
14962
  return computeScores({ kb: o.kb, records, now: o.now, known: o.known, compacted: readCompacted(o.kb, dir), verdicts });
14804
14963
  }
14805
14964
  function writeScores(kbRoot, s) {
14806
- fs28.mkdirSync(kbRoot, { recursive: true });
14807
- fs28.writeFileSync(scoresFile(kbRoot), JSON.stringify(s, null, 2) + "\n");
14965
+ fs29.mkdirSync(kbRoot, { recursive: true });
14966
+ fs29.writeFileSync(scoresFile(kbRoot), JSON.stringify(s, null, 2) + "\n");
14808
14967
  }
14809
14968
  function renderScoresSection(s) {
14810
14969
  const pct = (x) => x === null ? "n/a" : `${Math.round(x * 100)}%`;
@@ -14817,7 +14976,7 @@ function renderScoresSection(s) {
14817
14976
  ...used.length ? [
14818
14977
  "| Item | Surfaced | Pulled | Cited | Use | Noise | Violated |",
14819
14978
  "|---|---|---|---|---|---|---|",
14820
- ...used.map((x) => `| ${x.itemId} | ${x.surfaced} | ${x.shown} | ${x.cited} | ${pct(x.useRate)} | ${pct(x.noiseRate)} | ${pct(x.violationRate)} |`)
14979
+ ...used.map((x) => `| ${x.itemId} | ${x.surfaced} | ${x.pullRate} | ${x.cited} | ${pct(x.useRate)} | ${pct(x.noiseRate)} | ${pct(x.violationRate)} |`)
14821
14980
  ] : ["No items surfaced."],
14822
14981
  "",
14823
14982
  `- Never fired (${never.length}): ${never.slice(0, 20).join(", ") || "none"}${never.length > 20 ? ", \u2026" : ""}`,
@@ -14845,13 +15004,13 @@ var init_scores = __esm({
14845
15004
  MONTH_FILE_RE = /^(\d{4}-\d{2})\.(.+)\.jsonl$/;
14846
15005
  statsFileName = (month, machine) => `${month}.${machine}.stats.json`;
14847
15006
  ratio = (n, d) => d ? Math.round(n / d * 1e3) / 1e3 : null;
14848
- scoresFile = (kbRoot) => path29.join(kbRoot, "scores.json");
15007
+ scoresFile = (kbRoot) => path30.join(kbRoot, "scores.json");
14849
15008
  }
14850
15009
  });
14851
15010
 
14852
15011
  // packages/core/dist/knowledge/report.js
14853
- import fs29 from "fs";
14854
- import path30 from "path";
15012
+ import fs30 from "fs";
15013
+ import path31 from "path";
14855
15014
  function renderReport(cfg, kb, o) {
14856
15015
  const { rules } = loadKbRules(cfg, kb, { includeProposed: true });
14857
15016
  const count = (s) => rules.filter((r) => r.status === s).length;
@@ -14873,6 +15032,11 @@ function renderReport(cfg, kb, o) {
14873
15032
  const info = esc.filter((e) => !e.needsYou);
14874
15033
  if (info.length)
14875
15034
  L.push("", `## For information (${info.length})`, "", ...info.map((e) => `- **${e.key}**: ${e.explanation}`));
15035
+ const { errata, errors: errataErrors } = loadErrata(kbDir(cfg, kb));
15036
+ const orphans = orphanedErrata(kbDir(cfg, kb), errata);
15037
+ if (orphans.length || errataErrors.length) {
15038
+ L.push("", `## Errata to re-check (${orphans.length + errataErrors.length})`, "", ...orphans.map((e) => `- **${e.id}** (${e.ref}): quote no longer found in the source; the raw file changed. Re-check it in errata.yaml.`), ...errataErrors.map((e) => `- errata.yaml: ${e}`));
15039
+ }
14876
15040
  if (o.full) {
14877
15041
  const { usage: usage2, caps } = o.full;
14878
15042
  L.push("", "## Usage (30 days)", "", `- Surfaced: ${Object.values(usage2.surfaced).reduce((a, b) => a + b, 0)} times across ${Object.keys(usage2.surfaced).length} rules.`, `- Never surfaced (re-check triggers): ${usage2.neverSurfaced.join(", ") || "none"}`, `- Noisy (narrow triggers): ${usage2.noisy.join(", ") || "none"}`, `- Violation feedback (reword): ${usage2.violated.join(", ") || "none"}`, `- Domains over cap: ${caps.map((c) => `${c.domain} (${c.count}/${c.cap})`).join(", ") || "none"}`);
@@ -14882,12 +15046,13 @@ function renderReport(cfg, kb, o) {
14882
15046
  return L.join("\n") + "\n";
14883
15047
  }
14884
15048
  function writeReport(cfg, kb, o) {
14885
- const file = path30.join(kbDir(cfg, kb), "REPORT.md");
14886
- fs29.writeFileSync(file, renderReport(cfg, kb, o));
15049
+ const file = path31.join(kbDir(cfg, kb), "REPORT.md");
15050
+ fs30.writeFileSync(file, renderReport(cfg, kb, o));
14887
15051
  return file;
14888
15052
  }
14889
15053
  var init_report = __esm({
14890
15054
  "packages/core/dist/knowledge/report.js"() {
15055
+ init_errata();
14891
15056
  init_layers();
14892
15057
  init_paths();
14893
15058
  init_review();
@@ -14931,44 +15096,63 @@ var init_reconcile = __esm({
14931
15096
 
14932
15097
  // packages/core/dist/knowledge/cite.js
14933
15098
  function scanAndLogCitations(i) {
14934
- const at = i.at ?? i.sink?.now?.() ?? /* @__PURE__ */ new Date();
14935
- const since = new Date(at.getTime() - (i.lookbackDays ?? CITE_LOOKBACK_DAYS) * DAY_MS4);
14936
- const dir = i.sink?.dir ?? defaultAuditDir();
14937
- const delivered = /* @__PURE__ */ new Map();
14938
- const already = /* @__PURE__ */ new Set();
14939
- for (const r of readAuditRecords(void 0, since, dir)) {
14940
- if (!r.itemId || r.project !== i.project)
14941
- continue;
14942
- if (r.event === "item.cited" && r.ref === i.ref)
14943
- already.add(r.itemId);
14944
- if (!(r.event === "item.surfaced" || r.event === "item.shown") || Date.parse(r.ts) > at.getTime())
14945
- continue;
14946
- if (i.session && r.session && r.session !== i.session)
14947
- continue;
14948
- delivered.set(r.itemId, r);
14949
- }
14950
- const hits = [...delivered].filter(([id]) => !already.has(id) && mentionsId(i.text, id));
14951
- appendAudit(hits.map(([itemId, r]) => ({
14952
- kb: String(r.kb ?? ""),
14953
- level: String(r.level ?? "project"),
14954
- project: i.project,
14955
- domain: String(r.domain ?? "rules"),
14956
- itemId,
14957
- event: "item.cited",
14958
- ref: i.ref,
14959
- ...i.agent ? { agent: i.agent } : {},
14960
- ...i.session ? { session: i.session } : {}
14961
- })), i.sink);
14962
- return hits.map(([id]) => id);
15099
+ return scanCitations([i], i);
15100
+ }
15101
+ function scanCitations(texts, o) {
15102
+ if (!texts.length)
15103
+ return [];
15104
+ const now = o.sink?.now?.() ?? /* @__PURE__ */ new Date();
15105
+ const lookbackMs = (o.lookbackDays ?? CITE_LOOKBACK_DAYS) * DAY_MS4;
15106
+ const ats = texts.map((t) => (t.at ?? now).getTime());
15107
+ const records = readAuditRecords(void 0, new Date(Math.min(...ats) - lookbackMs), o.sink?.dir ?? defaultAuditDir()).filter((r) => r.itemId && r.project === o.project);
15108
+ const already = new Set(records.filter((r) => r.event === "item.cited").map((r) => `${r.ref}|${r.itemId}`));
15109
+ const deliveries = records.filter((r) => (r.event === "item.surfaced" || r.event === "item.shown" || r.event === "item.searched") && !(o.session && r.session && r.session !== o.session));
15110
+ const events = [];
15111
+ texts.forEach((t, n) => {
15112
+ const at = ats[n];
15113
+ const delivered = /* @__PURE__ */ new Map();
15114
+ for (const r of deliveries) {
15115
+ const ts = Date.parse(r.ts);
15116
+ if (ts <= at && ts >= at - lookbackMs)
15117
+ delivered.set(r.itemId, r);
15118
+ }
15119
+ for (const [itemId, r] of delivered) {
15120
+ if (already.has(`${t.ref}|${itemId}`) || !mentionsId(t.text, itemId))
15121
+ continue;
15122
+ already.add(`${t.ref}|${itemId}`);
15123
+ events.push({
15124
+ kb: String(r.kb ?? ""),
15125
+ level: String(r.level ?? "project"),
15126
+ project: o.project,
15127
+ domain: String(r.domain ?? "rules"),
15128
+ itemId,
15129
+ event: "item.cited",
15130
+ ref: t.ref,
15131
+ ...o.agent ? { agent: o.agent } : {},
15132
+ ...o.session ? { session: o.session } : {}
15133
+ });
15134
+ }
15135
+ });
15136
+ appendAudit(events, o.sink);
15137
+ return events.map((e) => e.itemId);
14963
15138
  }
14964
15139
  function readGitMessages(repo, since, run) {
14965
- const out = run(["-C", repo, "log", `--since=${since.toISOString()}`, "--format=%x1e%H%x1f%cI%x1f%B"]);
15140
+ return parseLog(run(["-C", repo, "log", "--all", `--since=${since.toISOString()}`, LOG_FORMAT]));
15141
+ }
15142
+ function readBranchOnlyMessages(repo, run, max = 200) {
15143
+ const branch = run(["-C", repo, "rev-parse", "--abbrev-ref", "HEAD"]).trim();
15144
+ if (!branch)
15145
+ return [];
15146
+ const own = branch === "HEAD" ? [] : [`--exclude=${branch}`];
15147
+ return parseLog(run(["-C", repo, "log", `--max-count=${max}`, "HEAD", "--not", ...own, "--branches", ...own.length ? [`--exclude=*/${branch}`] : [], "--remotes", LOG_FORMAT]));
15148
+ }
15149
+ function parseLog(out) {
14966
15150
  return out.split("").filter((c) => c.trim()).map((c) => {
14967
15151
  const [sha, at, ...rest] = c.split("");
14968
15152
  return { sha: sha.trim(), at: new Date(at), text: rest.join("") };
14969
15153
  });
14970
15154
  }
14971
- var DAY_MS4, CITE_LOOKBACK_DAYS, escapeRe2, mentionsId;
15155
+ var DAY_MS4, CITE_LOOKBACK_DAYS, escapeRe2, mentionsId, LOG_FORMAT;
14972
15156
  var init_cite = __esm({
14973
15157
  "packages/core/dist/knowledge/cite.js"() {
14974
15158
  init_audit();
@@ -14977,6 +15161,7 @@ var init_cite = __esm({
14977
15161
  CITE_LOOKBACK_DAYS = 7;
14978
15162
  escapeRe2 = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
14979
15163
  mentionsId = (text, id) => new RegExp(`(?<![\\w.-])${escapeRe2(id)}(?![\\w-])`).test(text);
15164
+ LOG_FORMAT = "--format=%x1e%H%x1f%cI%x1f%B";
14980
15165
  }
14981
15166
  });
14982
15167
 
@@ -14994,6 +15179,7 @@ var init_knowledge2 = __esm({
14994
15179
  init_migrate();
14995
15180
  init_verify();
14996
15181
  init_ingest();
15182
+ init_errata();
14997
15183
  init_anchor();
14998
15184
  init_extract();
14999
15185
  init_match();
@@ -15244,6 +15430,7 @@ __export(dist_exports, {
15244
15430
  isDaemonSocketLive: () => isDaemonSocketLive,
15245
15431
  isGateCacheEnabled: () => isGateCacheEnabled,
15246
15432
  isGateSession: () => isGateSession,
15433
+ isGatedRef: () => isGatedRef,
15247
15434
  isNotifyActive: () => isNotifyActive,
15248
15435
  isOperatorInitiatedCommand: () => isOperatorInitiatedCommand,
15249
15436
  isPendingProposalDup: () => isPendingProposalDup,
@@ -15261,6 +15448,7 @@ __export(dist_exports, {
15261
15448
  launchOneWorkspace: () => launchOneWorkspace,
15262
15449
  legacyKbDir: () => legacyKbDir,
15263
15450
  listSessions: () => listSessions,
15451
+ loadErrata: () => loadErrata,
15264
15452
  loadKbRules: () => loadKbRules,
15265
15453
  loadPending: () => loadPending,
15266
15454
  loadProposals: () => loadProposals,
@@ -15295,6 +15483,7 @@ __export(dist_exports, {
15295
15483
  normalizedViews: () => normalizedViews,
15296
15484
  notePaneScreen: () => notePaneScreen,
15297
15485
  notifyToggle: () => notifyToggle,
15486
+ orphanedErrata: () => orphanedErrata,
15298
15487
  parseBearer: () => parseBearer,
15299
15488
  parseClassifierVerdict: () => parseClassifierVerdict,
15300
15489
  parseCommand: () => parseCommand,
@@ -15319,6 +15508,7 @@ __export(dist_exports, {
15319
15508
  pruneTopics: () => pruneTopics,
15320
15509
  purgeExpiredWorkItems: () => purgeExpiredWorkItems,
15321
15510
  readAuditRecords: () => readAuditRecords,
15511
+ readBranchOnlyMessages: () => readBranchOnlyMessages,
15322
15512
  readCaptainAddress: () => readCaptainAddress,
15323
15513
  readCursor: () => readCursor,
15324
15514
  readEscalations: () => readEscalations,
@@ -15345,6 +15535,7 @@ __export(dist_exports, {
15345
15535
  refFor: () => refFor,
15346
15536
  releaseDaemonLock: () => releaseDaemonLock,
15347
15537
  removeRunningMarker: () => removeRunningMarker,
15538
+ removeSource: () => removeSource,
15348
15539
  renderEnvAssignments: () => renderEnvAssignments,
15349
15540
  renderPlist: () => renderPlist,
15350
15541
  renderReport: () => renderReport,
@@ -15407,6 +15598,7 @@ __export(dist_exports, {
15407
15598
  saveSessions: () => saveSessions,
15408
15599
  saveState: () => saveState,
15409
15600
  scanAndLogCitations: () => scanAndLogCitations,
15601
+ scanCitations: () => scanCitations,
15410
15602
  scheduleFile: () => scheduleFile,
15411
15603
  scoreKb: () => scoreKb,
15412
15604
  scoresFile: () => scoresFile,
@@ -15427,6 +15619,7 @@ __export(dist_exports, {
15427
15619
  sideNextAutoName: () => sideNextAutoName,
15428
15620
  sideTitleFor: () => sideTitleFor,
15429
15621
  snapshotSources: () => snapshotSources,
15622
+ sourceCovers: () => sourceCovers,
15430
15623
  spansOverlap: () => spansOverlap,
15431
15624
  splitSections: () => splitSections,
15432
15625
  splitStatement: () => splitStatement,
@@ -15561,7 +15754,7 @@ import { readdirSync as readdirSync4, readFileSync as readFileSync12 } from "fs"
15561
15754
  import { join as join18 } from "path";
15562
15755
 
15563
15756
  // packages/agents/dist/claude/registry.js
15564
- import fs30 from "fs";
15757
+ import fs31 from "fs";
15565
15758
  import { join as join17 } from "path";
15566
15759
  import { homedir as homedir7 } from "os";
15567
15760
  var CLAUDE_SESSIONS_DIR = join17(homedir7(), ".claude", "sessions");
@@ -15606,11 +15799,11 @@ function toLifecycleSnapshot(entry, taskId, alive, now) {
15606
15799
  function readClaudeStatusBySocketPath(socketPath) {
15607
15800
  let files;
15608
15801
  try {
15609
- files = fs30.readdirSync(CLAUDE_SESSIONS_DIR);
15802
+ files = fs31.readdirSync(CLAUDE_SESSIONS_DIR);
15610
15803
  } catch {
15611
15804
  return void 0;
15612
15805
  }
15613
- const entries = parseRegistryDir(files, (name) => fs30.readFileSync(join17(CLAUDE_SESSIONS_DIR, name), "utf8"));
15806
+ const entries = parseRegistryDir(files, (name) => fs31.readFileSync(join17(CLAUDE_SESSIONS_DIR, name), "utf8"));
15614
15807
  const entry = entries.find((e) => e.messagingSocketPath === socketPath);
15615
15808
  if (!entry)
15616
15809
  return void 0;
@@ -15633,38 +15826,38 @@ import { homedir as homedir8 } from "os";
15633
15826
  var SQUADRANT_STATE_DIR = join19(homedir8(), ".config", "squadrant", "state");
15634
15827
 
15635
15828
  // packages/agents/dist/drivers/launch-cmd.js
15636
- import fs31 from "fs";
15637
- import path31 from "path";
15829
+ import fs32 from "fs";
15830
+ import path32 from "path";
15638
15831
 
15639
15832
  // packages/agents/dist/projection/cursor.js
15640
15833
  import { mkdir, readFile, writeFile } from "fs/promises";
15641
- import path32 from "path";
15834
+ import path33 from "path";
15642
15835
  import os10 from "os";
15643
15836
 
15644
15837
  // packages/agents/dist/projection/codex.js
15645
15838
  import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
15646
- import path33 from "path";
15839
+ import path34 from "path";
15647
15840
  import os11 from "os";
15648
15841
 
15649
15842
  // packages/agents/dist/projection/gemini.js
15650
15843
  import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
15651
- import path34 from "path";
15844
+ import path35 from "path";
15652
15845
  import os12 from "os";
15653
15846
 
15654
15847
  // packages/agents/dist/projection/opencode.js
15655
15848
  import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
15656
- import path35 from "path";
15849
+ import path36 from "path";
15657
15850
  import os13 from "os";
15658
15851
 
15659
15852
  // packages/agents/dist/projection/opencode-skills.js
15660
- import fs32 from "fs";
15853
+ import fs33 from "fs";
15661
15854
  import os14 from "os";
15662
- import path36 from "path";
15855
+ import path37 from "path";
15663
15856
  var OPENCODE_SKILL_MARKER = ".squadrant-managed";
15664
15857
  var MARKER_BODY = "squadrant\n";
15665
15858
  function defaultOpencodeSkillsRoot(env = process.env) {
15666
- const configHome = env.XDG_CONFIG_HOME || path36.join(os14.homedir(), ".config");
15667
- return path36.join(configHome, "opencode", "skills");
15859
+ const configHome = env.XDG_CONFIG_HOME || path37.join(os14.homedir(), ".config");
15860
+ return path37.join(configHome, "opencode", "skills");
15668
15861
  }
15669
15862
  var SAFE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
15670
15863
  function parseSkillName(raw) {
@@ -15679,62 +15872,62 @@ function parseSkillName(raw) {
15679
15872
  }
15680
15873
  function listSourceSkills(sourceSkillsDir) {
15681
15874
  const out = /* @__PURE__ */ new Map();
15682
- if (!fs32.existsSync(sourceSkillsDir))
15875
+ if (!fs33.existsSync(sourceSkillsDir))
15683
15876
  return out;
15684
- for (const entry of fs32.readdirSync(sourceSkillsDir, { withFileTypes: true })) {
15877
+ for (const entry of fs33.readdirSync(sourceSkillsDir, { withFileTypes: true })) {
15685
15878
  if (!entry.isDirectory())
15686
15879
  continue;
15687
- const skillPath = path36.join(sourceSkillsDir, entry.name, "SKILL.md");
15688
- if (!fs32.existsSync(skillPath))
15880
+ const skillPath = path37.join(sourceSkillsDir, entry.name, "SKILL.md");
15881
+ if (!fs33.existsSync(skillPath))
15689
15882
  continue;
15690
- const name = parseSkillName(fs32.readFileSync(skillPath, "utf-8"));
15883
+ const name = parseSkillName(fs33.readFileSync(skillPath, "utf-8"));
15691
15884
  if (name && SAFE_NAME.test(name))
15692
15885
  out.set(name, skillPath);
15693
15886
  }
15694
15887
  return out;
15695
15888
  }
15696
15889
  function hasMarker(dir) {
15697
- return fs32.existsSync(path36.join(dir, OPENCODE_SKILL_MARKER));
15890
+ return fs33.existsSync(path37.join(dir, OPENCODE_SKILL_MARKER));
15698
15891
  }
15699
15892
  function syncOpencodeSkills(opts) {
15700
15893
  const result = { written: [], unchanged: [], removed: [], skipped: [] };
15701
15894
  const wanted = listSourceSkills(opts.sourceSkillsDir);
15702
- if (wanted.size === 0 && !fs32.existsSync(opts.sourceSkillsDir))
15895
+ if (wanted.size === 0 && !fs33.existsSync(opts.sourceSkillsDir))
15703
15896
  return result;
15704
15897
  if (!opts.dryRun)
15705
- fs32.mkdirSync(opts.skillsRoot, { recursive: true });
15898
+ fs33.mkdirSync(opts.skillsRoot, { recursive: true });
15706
15899
  for (const [name, srcPath] of wanted) {
15707
- const targetDir = path36.join(opts.skillsRoot, name);
15708
- const targetSkill = path36.join(targetDir, "SKILL.md");
15900
+ const targetDir = path37.join(opts.skillsRoot, name);
15901
+ const targetSkill = path37.join(targetDir, "SKILL.md");
15709
15902
  const managed = hasMarker(targetDir);
15710
- if (fs32.existsSync(targetSkill) && !managed) {
15903
+ if (fs33.existsSync(targetSkill) && !managed) {
15711
15904
  result.skipped.push({ name, reason: "foreign-collision" });
15712
15905
  continue;
15713
15906
  }
15714
- const current = fs32.existsSync(targetSkill) ? fs32.readFileSync(targetSkill) : null;
15715
- if (current && current.equals(fs32.readFileSync(srcPath))) {
15907
+ const current = fs33.existsSync(targetSkill) ? fs33.readFileSync(targetSkill) : null;
15908
+ if (current && current.equals(fs33.readFileSync(srcPath))) {
15716
15909
  if (!managed && !opts.dryRun) {
15717
- fs32.writeFileSync(path36.join(targetDir, OPENCODE_SKILL_MARKER), MARKER_BODY);
15910
+ fs33.writeFileSync(path37.join(targetDir, OPENCODE_SKILL_MARKER), MARKER_BODY);
15718
15911
  }
15719
15912
  result.unchanged.push(name);
15720
15913
  continue;
15721
15914
  }
15722
15915
  if (!opts.dryRun) {
15723
- fs32.mkdirSync(targetDir, { recursive: true });
15724
- fs32.copyFileSync(srcPath, targetSkill);
15725
- fs32.writeFileSync(path36.join(targetDir, OPENCODE_SKILL_MARKER), MARKER_BODY);
15916
+ fs33.mkdirSync(targetDir, { recursive: true });
15917
+ fs33.copyFileSync(srcPath, targetSkill);
15918
+ fs33.writeFileSync(path37.join(targetDir, OPENCODE_SKILL_MARKER), MARKER_BODY);
15726
15919
  }
15727
15920
  result.written.push(name);
15728
15921
  }
15729
- if (fs32.existsSync(opts.skillsRoot)) {
15730
- for (const entry of fs32.readdirSync(opts.skillsRoot, { withFileTypes: true })) {
15922
+ if (fs33.existsSync(opts.skillsRoot)) {
15923
+ for (const entry of fs33.readdirSync(opts.skillsRoot, { withFileTypes: true })) {
15731
15924
  if (!entry.isDirectory() || wanted.has(entry.name))
15732
15925
  continue;
15733
- const dir = path36.join(opts.skillsRoot, entry.name);
15926
+ const dir = path37.join(opts.skillsRoot, entry.name);
15734
15927
  if (!hasMarker(dir))
15735
15928
  continue;
15736
15929
  if (!opts.dryRun)
15737
- fs32.rmSync(dir, { recursive: true, force: true });
15930
+ fs33.rmSync(dir, { recursive: true, force: true });
15738
15931
  result.removed.push(entry.name);
15739
15932
  }
15740
15933
  }
@@ -15742,7 +15935,7 @@ function syncOpencodeSkills(opts) {
15742
15935
  }
15743
15936
  function syncShippedOpencodeSkills(opts) {
15744
15937
  return syncOpencodeSkills({
15745
- sourceSkillsDir: path36.join(opts.pkgRoot, "plugin", "skills"),
15938
+ sourceSkillsDir: path37.join(opts.pkgRoot, "plugin", "skills"),
15746
15939
  skillsRoot: opts.skillsRoot ?? defaultOpencodeSkillsRoot(),
15747
15940
  dryRun: opts.dryRun
15748
15941
  });
@@ -15750,7 +15943,7 @@ function syncShippedOpencodeSkills(opts) {
15750
15943
 
15751
15944
  // packages/agents/dist/projection/claude.js
15752
15945
  import { mkdir as mkdir5, readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
15753
- import path37 from "path";
15946
+ import path38 from "path";
15754
15947
  import os15 from "os";
15755
15948
 
15756
15949
  // packages/agents/dist/codex/app-server-client.js
@@ -17095,9 +17288,9 @@ var ClaudeReceiptListener = class {
17095
17288
  };
17096
17289
 
17097
17290
  // packages/agents/dist/claude/api-key-approval.js
17098
- import fs33 from "fs";
17291
+ import fs34 from "fs";
17099
17292
  import os16 from "os";
17100
- import path38 from "path";
17293
+ import path39 from "path";
17101
17294
 
17102
17295
  // packages/agents/dist/opencode/http-channel.js
17103
17296
  init_dist2();
@@ -17189,10 +17382,10 @@ var OpencodeHttpChannel = class {
17189
17382
  return cached;
17190
17383
  const directory = this.directoryFor?.(taskId);
17191
17384
  const createdAfterMs = this.createdAfterFor?.(taskId);
17192
- for (const path42 of ["/session?", "/api/session?"]) {
17385
+ for (const path43 of ["/session?", "/api/session?"]) {
17193
17386
  let res;
17194
17387
  try {
17195
- res = await this.request(`http://127.0.0.1:${port}${path42}`, { method: "GET" });
17388
+ res = await this.request(`http://127.0.0.1:${port}${path43}`, { method: "GET" });
17196
17389
  } catch {
17197
17390
  return void 0;
17198
17391
  }
@@ -17266,9 +17459,9 @@ function createOpencodeFactAdapter(deps) {
17266
17459
  }
17267
17460
 
17268
17461
  // packages/agents/dist/rules-hooks/install.js
17269
- import fs34 from "fs";
17462
+ import fs35 from "fs";
17270
17463
  import os17 from "os";
17271
- import path39 from "path";
17464
+ import path40 from "path";
17272
17465
 
17273
17466
  // packages/workspaces/dist/runtimes/cmux.js
17274
17467
  init_dist();
@@ -18006,9 +18199,9 @@ var NotifierRegistry = class {
18006
18199
  };
18007
18200
 
18008
18201
  // packages/workspaces/dist/workspaces/obsidian.js
18009
- import fs35 from "fs/promises";
18202
+ import fs36 from "fs/promises";
18010
18203
  import { existsSync as existsSync11 } from "fs";
18011
- import path40 from "path";
18204
+ import path41 from "path";
18012
18205
 
18013
18206
  // packages/workspaces/dist/workspaces/registry.js
18014
18207
  init_dist();
@@ -18558,9 +18751,9 @@ function defaultListFiles(dir) {
18558
18751
  return [];
18559
18752
  }
18560
18753
  }
18561
- function defaultReadFile(path42) {
18754
+ function defaultReadFile(path43) {
18562
18755
  try {
18563
- return readFileSync17(path42, "utf-8");
18756
+ return readFileSync17(path43, "utf-8");
18564
18757
  } catch {
18565
18758
  return void 0;
18566
18759
  }
@@ -18940,16 +19133,16 @@ function extractDetail(sub, payload) {
18940
19133
  }
18941
19134
  return void 0;
18942
19135
  }
18943
- function defaultReadFile2(path42) {
19136
+ function defaultReadFile2(path43) {
18944
19137
  try {
18945
- return readFileSync18(path42, "utf-8");
19138
+ return readFileSync18(path43, "utf-8");
18946
19139
  } catch {
18947
19140
  return void 0;
18948
19141
  }
18949
19142
  }
18950
- function defaultWriteFile(path42, content) {
18951
- mkdirSync8(path42.replace(/\/[^/]+$/, ""), { recursive: true });
18952
- writeFileSync11(path42, content, "utf-8");
19143
+ function defaultWriteFile(path43, content) {
19144
+ mkdirSync8(path43.replace(/\/[^/]+$/, ""), { recursive: true });
19145
+ writeFileSync11(path43, content, "utf-8");
18953
19146
  }
18954
19147
 
18955
19148
  // packages/workspaces/dist/crew-pane.js
@@ -19034,9 +19227,9 @@ init_dist();
19034
19227
 
19035
19228
  // packages/cli/src/lib/daemon-restart-broadcast.ts
19036
19229
  init_dist();
19037
- import path41 from "path";
19230
+ import path42 from "path";
19038
19231
  function statePath2(stateRoot) {
19039
- return path41.join(stateRoot, "daemon-restart-state.json");
19232
+ return path42.join(stateRoot, "daemon-restart-state.json");
19040
19233
  }
19041
19234
  function computeRestartSignature(version, buildMtimeMs) {
19042
19235
  return `${version}::${buildMtimeMs}`;
@@ -19085,7 +19278,7 @@ async function maybeBroadcastDaemonRestart(opts) {
19085
19278
 
19086
19279
  // packages/cli/src/lib/captain-channel-factory.ts
19087
19280
  import { createServer as createServer4, connect as netConnect } from "net";
19088
- import fs36 from "fs";
19281
+ import fs37 from "fs";
19089
19282
  import { join as join26 } from "path";
19090
19283
  import { randomUUID as randomUUID5 } from "crypto";
19091
19284
  import chalk2 from "chalk";
@@ -19094,15 +19287,15 @@ var shared;
19094
19287
  var registryEntryPath = () => join26(CLAUDE_SESSIONS_DIR, `${process.pid}.json`);
19095
19288
  function unregisterSenderIdentity() {
19096
19289
  try {
19097
- fs36.unlinkSync(registryEntryPath());
19290
+ fs37.unlinkSync(registryEntryPath());
19098
19291
  } catch {
19099
19292
  }
19100
19293
  }
19101
19294
  function registerSenderIdentity(socketPath) {
19102
19295
  try {
19103
19296
  unregisterSenderIdentity();
19104
- fs36.mkdirSync(CLAUDE_SESSIONS_DIR, { recursive: true });
19105
- fs36.writeFileSync(
19297
+ fs37.mkdirSync(CLAUDE_SESSIONS_DIR, { recursive: true });
19298
+ fs37.writeFileSync(
19106
19299
  registryEntryPath(),
19107
19300
  JSON.stringify({
19108
19301
  pid: process.pid,
@@ -19128,7 +19321,7 @@ async function sharedReceiptListener() {
19128
19321
  // must never be the reason we refuse to start.
19129
19322
  unlinkStale: (p) => {
19130
19323
  try {
19131
- fs36.unlinkSync(p);
19324
+ fs37.unlinkSync(p);
19132
19325
  } catch {
19133
19326
  }
19134
19327
  },
@@ -19153,7 +19346,7 @@ async function buildCaptainChannel() {
19153
19346
  // in /tmp/x registers cwd "/private/tmp/x" on macOS, so cwd matching silently
19154
19347
  // missed and every captain ping reported accepted-unconfirmed.
19155
19348
  statusFor: (taskId) => readClaudeStatusBySocketPath(captainSocketPath(taskId)),
19156
- wire: (p, e) => writeLine(p, e, { connect: (path42) => netConnect(path42) }),
19349
+ wire: (p, e) => writeLine(p, e, { connect: (path43) => netConnect(path43) }),
19157
19350
  receipts,
19158
19351
  newMsgId: () => randomUUID5(),
19159
19352
  sleep: (ms) => new Promise((r) => setTimeout(r, ms)),