@mrciphersmith/keryx 0.3.113 → 0.3.115

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -23150,7 +23150,7 @@ var package_default;
23150
23150
  var init_package = __esm(() => {
23151
23151
  package_default = {
23152
23152
  name: "@mrciphersmith/keryx",
23153
- version: "0.3.113",
23153
+ version: "0.3.115",
23154
23154
  description: "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
23155
23155
  private: false,
23156
23156
  publishConfig: {
@@ -52419,6 +52419,7 @@ var init_codex_catalog_version = __esm(() => {
52419
52419
  });
52420
52420
 
52421
52421
  // src/commands/subscription-models.ts
52422
+ import { resolve as resolve2 } from "path";
52422
52423
  function positiveInt(value) {
52423
52424
  return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : undefined;
52424
52425
  }
@@ -52426,7 +52427,22 @@ async function fetchOpenAiCodexModels(fetchFn, opts = {}) {
52426
52427
  const { contextWindows: _windows, ...result } = await fetchOpenAiCodexCatalog(fetchFn, opts);
52427
52428
  return result;
52428
52429
  }
52429
- async function fetchOpenAiCodexCatalog(fetchFn, opts = {}) {
52430
+ function fetchOpenAiCodexCatalog(fetchFn, opts = {}) {
52431
+ let requests = catalogInFlight.get(fetchFn);
52432
+ if (requests === undefined) {
52433
+ requests = new Map;
52434
+ catalogInFlight.set(fetchFn, requests);
52435
+ }
52436
+ const key = JSON.stringify([resolve2(keryxConfigDir(opts.configDir)), opts.timeoutMs ?? 8000, opts.refreshCatalogVersion ?? false]);
52437
+ const existing = requests.get(key);
52438
+ if (existing !== undefined)
52439
+ return existing;
52440
+ const scope = requests;
52441
+ const request = requestOpenAiCodexCatalog(fetchFn, opts).finally(() => scope.delete(key));
52442
+ scope.set(key, request);
52443
+ return request;
52444
+ }
52445
+ async function requestOpenAiCodexCatalog(fetchFn, opts = {}) {
52430
52446
  const controller = new AbortController;
52431
52447
  const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? 8000);
52432
52448
  let authenticated = false;
@@ -52476,8 +52492,9 @@ async function fetchOpenAiCodexCatalog(fetchFn, opts = {}) {
52476
52492
  clearTimeout(timer);
52477
52493
  }
52478
52494
  }
52479
- var OPENAI_CODEX_PICKER;
52495
+ var OPENAI_CODEX_PICKER, catalogInFlight;
52480
52496
  var init_subscription_models = __esm(() => {
52497
+ init_config_dir();
52481
52498
  init_openai_subscription();
52482
52499
  init_codex_catalog_version();
52483
52500
  OPENAI_CODEX_PICKER = {
@@ -52486,6 +52503,7 @@ var init_subscription_models = __esm(() => {
52486
52503
  baseUrl: "https://chatgpt.com/backend-api/codex",
52487
52504
  models: ["gpt-5.3-codex"]
52488
52505
  };
52506
+ catalogInFlight = new WeakMap;
52489
52507
  });
52490
52508
 
52491
52509
  // src/commands/live-codex-models.ts
@@ -60559,7 +60577,7 @@ var init_output_spill = __esm(() => {
60559
60577
 
60560
60578
  // src/harness/tool/builtin/interactive-tools.ts
60561
60579
  import { readdir as readdir24 } from "fs/promises";
60562
- import { basename, dirname, isAbsolute, join as join3, relative, resolve as resolve2, sep } from "path";
60580
+ import { basename, dirname, isAbsolute, join as join3, relative, resolve as resolve3, sep } from "path";
60563
60581
  import { lstatSync as lstatSync5, realpathSync as realpathSync4 } from "fs";
60564
60582
  async function readFromLine(file, startLine, cap) {
60565
60583
  const reader = file.stream().getReader();
@@ -60617,7 +60635,7 @@ async function readFromLine(file, startLine, cap) {
60617
60635
  }
60618
60636
  function confineToRoot(root, candidate) {
60619
60637
  const rootReal = realpathOr(root);
60620
- const target = resolve2(rootReal, candidate ?? ".");
60638
+ const target = resolve3(rootReal, candidate ?? ".");
60621
60639
  const effective = realpathThroughAncestors(target);
60622
60640
  if (effective === undefined) {
60623
60641
  return null;
@@ -60635,7 +60653,7 @@ function realpathOr(candidate) {
60635
60653
  try {
60636
60654
  return realpathSync4(candidate);
60637
60655
  } catch {
60638
- return resolve2(candidate);
60656
+ return resolve3(candidate);
60639
60657
  }
60640
60658
  }
60641
60659
  function realpathThroughAncestors(absolute) {
@@ -60652,7 +60670,7 @@ function realpathThroughAncestors(absolute) {
60652
60670
  } catch {}
60653
60671
  const parent = dirname(current);
60654
60672
  if (parent === current) {
60655
- return resolve2(absolute);
60673
+ return resolve3(absolute);
60656
60674
  }
60657
60675
  missing.push(basename(current));
60658
60676
  current = parent;
@@ -68818,10 +68836,12 @@ function researchErrors(value, findings) {
68818
68836
  ids.add(id);
68819
68837
  if (!nonempty(o.source) || !nonempty(o.question))
68820
68838
  errors.push(`${id}: source and question are required`);
68821
- if (!["finding", "refuted", "out-of-scope"].includes(String(o.status)))
68839
+ if (!["finding", "refuted", "out-of-scope", "narrowed"].includes(String(o.status)))
68822
68840
  errors.push(`${id}: unresolved research (${String(o.status)})`);
68823
68841
  if (!nonempty(o.evidence) || !nonempty(o.reason))
68824
68842
  errors.push(`${id}: closure needs evidence and reason`);
68843
+ if (o.status === "narrowed" && !nonempty(o.remains))
68844
+ errors.push(`${id}: narrowed needs a "what remains" text`);
68825
68845
  if (o.status === "finding") {
68826
68846
  const matches = findings.filter((f) => f && typeof f === "object" && (f.id === o.finding || f.global_id === o.finding));
68827
68847
  if (!nonempty(o.finding) || matches.length !== 1)
@@ -75018,7 +75038,7 @@ var init_github = __esm(() => {
75018
75038
 
75019
75039
  // src/harness/tool/metaproject-adapter.ts
75020
75040
  import { lstat as lstat10, readdir as readdir32, readFile as readFile92, realpath as realpath17, stat as stat25 } from "fs/promises";
75021
- import { isAbsolute as isAbsolute2, join as join4, relative as relative2, resolve as resolve3 } from "path";
75041
+ import { isAbsolute as isAbsolute2, join as join4, relative as relative2, resolve as resolve4 } from "path";
75022
75042
  function isMissingExecutable(cause) {
75023
75043
  const message = cause instanceof Error ? cause.message : String(cause);
75024
75044
  return /Executable not found|\bENOENT\b|not found in \$?PATH/i.test(message);
@@ -75060,7 +75080,7 @@ function portableMemoryPath(candidate) {
75060
75080
  }
75061
75081
  function confineToWiki(cwd, candidate) {
75062
75082
  const wikiRoot = join4(cwd, ".metaproject", "wiki");
75063
- const target = resolve3(wikiRoot, candidate);
75083
+ const target = resolve4(wikiRoot, candidate);
75064
75084
  const rel = relative2(wikiRoot, target);
75065
75085
  if (rel === "") {
75066
75086
  return null;
@@ -75117,7 +75137,7 @@ async function detectMemoryStoreUnreadable(cwd) {
75117
75137
  }
75118
75138
  function confineToSkills(cwd, candidate) {
75119
75139
  const skillsRoot = join4(cwd, ".metaproject", "skills", "gdskills");
75120
- const target = resolve3(cwd, candidate);
75140
+ const target = resolve4(cwd, candidate);
75121
75141
  const rel = relative2(skillsRoot, target);
75122
75142
  if (rel === "") {
75123
75143
  return null;
@@ -78919,6 +78939,127 @@ function arbiterSession() {
78919
78939
  }
78920
78940
  var session;
78921
78941
 
78942
+ // src/review/rebuttal-citation.ts
78943
+ function citedPaths(text) {
78944
+ const found = new Set;
78945
+ for (const match of text.replace(URL2, " ").matchAll(CITED)) {
78946
+ const cited = match[1];
78947
+ if (cited.includes("/") || ROOT_DOCUMENT.test(cited))
78948
+ found.add(cited);
78949
+ }
78950
+ return [...found];
78951
+ }
78952
+ async function flagMissingCitations(findings, exists) {
78953
+ let added = 0;
78954
+ for (const finding of findings) {
78955
+ const disposition = finding.disposition;
78956
+ if (disposition === undefined || typeof disposition.evidence !== "string")
78957
+ continue;
78958
+ const missing = [];
78959
+ for (const cited of citedPaths(disposition.evidence)) {
78960
+ if (disposition.evidence.includes(marker(cited)))
78961
+ continue;
78962
+ if (!await exists(cited))
78963
+ missing.push(cited);
78964
+ }
78965
+ if (missing.length === 0)
78966
+ continue;
78967
+ disposition.evidence = `${disposition.evidence}
78968
+ ${missing.map(marker).join(`
78969
+ `)}`;
78970
+ added += missing.length;
78971
+ }
78972
+ return added;
78973
+ }
78974
+ var URL2, CITED, ROOT_DOCUMENT, marker = (cited) => `citation not found: ${cited}`;
78975
+ var init_rebuttal_citation = __esm(() => {
78976
+ URL2 = /\bhttps?:\/\/\S+/gi;
78977
+ CITED = /(?<![\w./-])((?:\.\.?\/)*(?:[\w.-]+\/)*[\w.-]+\.(?:mdc|mdx|md|txt|json|ya?ml|toml))(?![\w-])/g;
78978
+ ROOT_DOCUMENT = /^[A-Z][A-Z0-9_-]*\.(?:md|mdx|txt)$/;
78979
+ });
78980
+
78981
+ // src/review/complete-head-gate.ts
78982
+ async function completeHeadGate(input) {
78983
+ const { target } = input.manifest;
78984
+ if (target.kind !== "pr")
78985
+ return { warnings: [] };
78986
+ let pr;
78987
+ try {
78988
+ pr = await input.lookup(target.ref);
78989
+ } catch (error) {
78990
+ return { warnings: [`could not read the PR state for ${target.ref} (${error instanceof Error ? error.message : String(error)}); the head and merge check was skipped`] };
78991
+ }
78992
+ if (pr === null || !pr.exists)
78993
+ return { warnings: [`could not read the PR state for ${target.ref}; the head and merge check was skipped`] };
78994
+ const recorded = target.head ?? null;
78995
+ const reviewed = recorded !== null && USABLE_SHA.test(recorded) ? recorded : null;
78996
+ const unusable = recorded !== null && reviewed === null ? [`the reviewed head recorded for ${target.ref} ("${recorded}") is not a commit SHA; the moved-head check was skipped`] : [];
78997
+ const merged = typeof pr.state === "string" && pr.state.toUpperCase() === "MERGED";
78998
+ const prHead = typeof pr.headSha === "string" && pr.headSha !== "" ? pr.headSha : null;
78999
+ const moved = reviewed !== null && prHead !== null && !sameCommit(reviewed, prHead);
79000
+ if (!merged && !moved)
79001
+ return { warnings: unusable };
79002
+ const what = [merged ? "the PR is merged" : undefined, moved ? `the PR head moved (reviewed ${reviewed.slice(0, 7)}, now ${prHead.slice(0, 7)})` : undefined].filter(Boolean).join(" and ");
79003
+ const first = input.report.split(/\r?\n/).find((line) => line.trim() !== "" && !/^\s*<!--.*-->\s*$/.test(line)) ?? "";
79004
+ const names = reviewed === null ? /\b[0-9a-f]{7,40}\b/i.test(first) : first.toLowerCase().includes(reviewed.slice(0, 7).toLowerCase());
79005
+ if (names && SAYS_SO.test(first))
79006
+ return { warnings: unusable };
79007
+ throw new Error(`Cannot complete managed review; ${what}. The report's first line must name the reviewed SHA${reviewed === null ? "" : ` (${reviewed.slice(0, 7)})`} and say the PR moved, for example: "Reviewed ${(reviewed ?? "<sha>").slice(0, 7)} \u2014 the PR has since ${merged ? "merged" : "moved on"}."`);
79008
+ }
79009
+ var MOVE_WORD = "(?:merged|moved|advanced|superseded|closed|force-pushed)", PR_WORD = "(?:pr|pull request|head)", SAYS_SO, USABLE_SHA, sameCommit = (a, b) => {
79010
+ const [x, y] = [a.toLowerCase(), b.toLowerCase()];
79011
+ return x.startsWith(y) || y.startsWith(x);
79012
+ };
79013
+ var init_complete_head_gate = __esm(() => {
79014
+ SAYS_SO = new RegExp(`\\b${PR_WORD}\\b.{0,40}\\b${MOVE_WORD}\\b|\\b${MOVE_WORD}\\b.{0,40}\\b${PR_WORD}\\b`, "i");
79015
+ USABLE_SHA = /^[0-9a-f]{7,40}$/i;
79016
+ });
79017
+
79018
+ // src/review/evidence-level.ts
79019
+ function itemsOf(text) {
79020
+ return text.split(/[,;]/).map((item) => item.trim()).filter((item) => item.length > 0 && !PLACEHOLDER.test(item));
79021
+ }
79022
+ function withoutFences(report) {
79023
+ let fence = null;
79024
+ const kept = [];
79025
+ for (const line of report.split(/\r?\n/)) {
79026
+ const marker = /^\s*(```+|~~~+)/.exec(line)?.[1];
79027
+ if (marker !== undefined) {
79028
+ if (fence === null)
79029
+ fence = marker[0];
79030
+ else if (marker[0] === fence)
79031
+ fence = null;
79032
+ continue;
79033
+ }
79034
+ if (fence === null)
79035
+ kept.push(line);
79036
+ }
79037
+ return kept;
79038
+ }
79039
+ function parseEvidenceLevel(report) {
79040
+ const line = withoutFences(report).map((l) => LINE.exec(l)).find((m) => m !== null);
79041
+ if (!line)
79042
+ return { ok: false, reason: "the report has no `Evidence level:` line" };
79043
+ const value = line[1].replace(/(?:\*\*|__)\s*$/, "").trim();
79044
+ const staticOnly = /^static[ -]only\b/i.exec(value);
79045
+ if (staticOnly) {
79046
+ const items = itemsOf(value.slice(staticOnly[0].length).replace(SEPARATORS, "").replace(/^(?:did not run|not run|did not)\s*:?\s*/i, ""));
79047
+ return items.length > 0 ? { ok: true, level: "static-only", items } : { ok: false, reason: "`static only` must list what did not run" };
79048
+ }
79049
+ const ran = /^ran\b/i.exec(value);
79050
+ if (ran) {
79051
+ const items = itemsOf(value.slice(ran[0].length).replace(SEPARATORS, ""));
79052
+ return items.length > 0 ? { ok: true, level: "ran", items } : { ok: false, reason: "`ran` must list what ran" };
79053
+ }
79054
+ return { ok: false, reason: "the evidence level must start with `static only` or `ran`" };
79055
+ }
79056
+ var REPORT_FIELD_EVIDENCE_LEVEL = "evidence-level", LINE, SEPARATORS, PLACEHOLDER;
79057
+ var init_evidence_level = __esm(() => {
79058
+ LINE = /^\s*(?:[-*+]\s+)?(?:\*\*|__)?evidence level\s*:?\s*(?:\*\*|__)?\s*:?\s*(.*)$/i;
79059
+ SEPARATORS = /^[\s:\u2014\u2013-]+/;
79060
+ PLACEHOLDER = /^(?:none|nothing|n\/a|na|nil|-|\u2014|\u2013)\.?$/i;
79061
+ });
79062
+
78922
79063
  // src/review/verification.ts
78923
79064
  function verificationClaims(source) {
78924
79065
  if (source === undefined) {
@@ -79868,7 +80009,7 @@ var init_repair = __esm(() => {
79868
80009
  });
79869
80010
 
79870
80011
  // src/review/managed.ts
79871
- import { mkdir as mkdir52, readFile as readFile98, readdir as readdir33, realpath as realpath18, rm as rm13, stat as stat27 } from "fs/promises";
80012
+ import { lstat as lstat12, mkdir as mkdir52, readFile as readFile98, readdir as readdir33, realpath as realpath18, rm as rm13, stat as stat27 } from "fs/promises";
79872
80013
  import path188 from "path";
79873
80014
  function reviewsRoot(cwd) {
79874
80015
  return path188.join(cwd, ".metaproject", "reviews");
@@ -79937,6 +80078,7 @@ async function createManagedReviewPackage(input) {
79937
80078
  const repairs = repairMechanicalOmissions(reported);
79938
80079
  const head = await resolveTargetHead(input);
79939
80080
  await anchorFindings(reported, input, head);
80081
+ refuseUnresolvedLocators(reported, input, head);
79940
80082
  assignGlobalIds(reported, reviewId);
79941
80083
  const merged = mergeVerifications(reported, input.verifications ?? [], {
79942
80084
  mode: input.verificationMode ?? DEFAULT_VERIFICATION_MODE
@@ -79955,6 +80097,7 @@ async function createManagedReviewPackage(input) {
79955
80097
  ...roundDismissed
79956
80098
  ];
79957
80099
  assignGlobalIds(findings, reviewId);
80100
+ await flagMissingCitations(findings, (cited) => citationExists(input.cwd, head, cited));
79958
80101
  const carried = input.scope === undefined ? await readPreFilterScopeBlock(packageDir) : undefined;
79959
80102
  const concurrency = concurrencyPlan(input, coverage);
79960
80103
  const caps = {
@@ -80150,6 +80293,18 @@ async function completeManagedReview(cwd, ref, options = {}) {
80150
80293
  if (missing.length > 0) {
80151
80294
  throw new Error(`Cannot complete managed review; missing artifacts: ${missing.join(", ")}`);
80152
80295
  }
80296
+ const warnings = [];
80297
+ const reportText = await readFile98(path188.join(packageDir, "report.md"), "utf8");
80298
+ if (options.prLookup !== undefined)
80299
+ warnings.push(...(await completeHeadGate({ manifest, report: reportText, lookup: options.prLookup })).warnings);
80300
+ const evidence = parseEvidenceLevel(reportText);
80301
+ if (!evidence.ok) {
80302
+ const problem = `the report's evidence level is missing or invalid: ${evidence.reason}`;
80303
+ if (manifest.report_fields?.includes(REPORT_FIELD_EVIDENCE_LEVEL)) {
80304
+ throw new Error(`Cannot complete managed review; ${problem}. Add a line \`Evidence level: static only \u2014 did not run: <what>\` or \`Evidence level: ran \u2014 <what>\` to report.md`);
80305
+ }
80306
+ warnings.push(`${problem} (this package predates the field, so it is a warning)`);
80307
+ }
80153
80308
  const researchFindings = JSON.parse(await readFile98(path188.join(packageDir, "findings.json"), "utf8"));
80154
80309
  const researchPath = path188.join(packageDir, "research.json");
80155
80310
  const researchBefore = options.waiveCoverage === undefined ? undefined : await readFile98(researchPath, "utf8").catch(() => {
@@ -80193,7 +80348,7 @@ async function completeManagedReview(cwd, ref, options = {}) {
80193
80348
  packagePath: path188.relative(cwd, packageDir),
80194
80349
  ...options.now === undefined ? {} : { now: options.now }
80195
80350
  });
80196
- return { manifest: updated, reviewNotes, dispositions: tallyDispositions(recorded), ...coverageWaiver === undefined ? {} : { coverageWaiver } };
80351
+ return { manifest: updated, reviewNotes, dispositions: tallyDispositions(recorded), warnings, ...coverageWaiver === undefined ? {} : { coverageWaiver } };
80197
80352
  }
80198
80353
  async function validateManagedReviewManifest(cwd, manifest) {
80199
80354
  const errors = [];
@@ -80283,6 +80438,7 @@ function buildManifest(args) {
80283
80438
  coverage: args.coverage,
80284
80439
  filter_stats: args.filterStats,
80285
80440
  ...args.repairs.length === 0 ? {} : { repairs: args.repairs },
80441
+ report_fields: [REPORT_FIELD_EVIDENCE_LEVEL],
80286
80442
  ...args.input.cost === undefined ? {} : { cost: args.input.cost },
80287
80443
  ...crossFamily === undefined ? {} : { cross_family_review: crossFamily },
80288
80444
  ...args.input.automatic === true ? { automatic: true } : {},
@@ -80358,7 +80514,7 @@ function triage(findings, args) {
80358
80514
  }
80359
80515
  async function anchorFindings(findings, input, head) {
80360
80516
  const read = input.readTreeFile ?? (head === null ? readFromWorkingTree(input) : async (relative) => {
80361
- const contained = await containedRelativePath(input.cwd, relative);
80517
+ const contained = lexicalRelativePath(relative);
80362
80518
  return contained === null ? null : readFileAtCommit(input.cwd, head, contained);
80363
80519
  });
80364
80520
  for (const finding of findings) {
@@ -80384,17 +80540,42 @@ async function anchorFindings(findings, input, head) {
80384
80540
  };
80385
80541
  }
80386
80542
  }
80387
- async function containedRelativePath(cwd, relative) {
80388
- const root = await realpath18(cwd).catch(() => path188.resolve(cwd));
80389
- const resolved = path188.resolve(root, relative);
80390
- const real = await realpath18(resolved).catch(() => null);
80391
- if (real === null || real !== root && !real.startsWith(`${root}${path188.sep}`)) {
80543
+ function refuseUnresolvedLocators(findings, input, head) {
80544
+ if (head === null || input.readTreeFile !== undefined) {
80545
+ return;
80546
+ }
80547
+ const unresolved = findings.filter((finding) => finding.source !== "external" && finding.disposition === undefined && finding.locator?.state === "unlocatable");
80548
+ if (unresolved.length === 0) {
80549
+ return;
80550
+ }
80551
+ const lines = unresolved.map((finding) => {
80552
+ const locator = finding.locator?.state === "unlocatable" ? finding.locator : undefined;
80553
+ const line = locator?.reported_line ?? null;
80554
+ return ` - ${finding.id ?? finding.summary} (${finding.file}${line === null ? "" : `:${line}`}): ${locator?.reason ?? ""}`;
80555
+ });
80556
+ throw new Error(`Cannot ingest review; ${unresolved.length} finding(s) cite a quote that does not resolve at the reviewed SHA ${head.slice(0, 12)}:
80557
+ ${lines.join(`
80558
+ `)}
80559
+ Causes: the quote is not in the file at that commit, appears in it more than once, or the file or the commit is not in this checkout (check \`git cat-file -e ${head.slice(0, 12)}\`). Re-quote one distinct line from \`git show ${head.slice(0, 7)}:<path>\` or dismiss the finding with evidence.`);
80560
+ }
80561
+ async function citationExists(cwd, head, cited) {
80562
+ const relative = lexicalRelativePath(cited);
80563
+ if (relative === null) {
80564
+ return false;
80565
+ }
80566
+ if (head !== null && await readFileAtCommit(cwd, head, relative) !== null) {
80567
+ return true;
80568
+ }
80569
+ return lstat12(path188.resolve(cwd, relative)).then(() => true, () => false);
80570
+ }
80571
+ function lexicalRelativePath(relative) {
80572
+ const normalised = path188.posix.normalize(relative.replace(/\\/g, "/"));
80573
+ if (normalised === "." || normalised === ".." || normalised.startsWith("../") || path188.posix.isAbsolute(normalised)) {
80392
80574
  return null;
80393
80575
  }
80394
- return real;
80576
+ return normalised;
80395
80577
  }
80396
- async function readFileAtCommit(cwd, commit, absolute) {
80397
- const relative = path188.relative(await realpath18(cwd).catch(() => path188.resolve(cwd)), absolute);
80578
+ async function readFileAtCommit(cwd, commit, relative) {
80398
80579
  try {
80399
80580
  const proc = Bun.spawn(["git", "show", `${commit}:${relative}`], {
80400
80581
  cwd,
@@ -81091,6 +81272,9 @@ async function loadDocpackSchema(cwd) {
81091
81272
  var REQUIRED_ARTIFACTS, BLAST_RADIUS_ARTIFACT = "blast-radius.json", EMBEDDED_FINDINGS_FENCE, SEVERITY_PREFIX, NOT_RECORDED = (field) => `not recorded: derived from a markdown review report, which carried no ${field} field`, LABEL_LINE, HEADING_MARKER, FINDING_IDENTIFIER, TITLE_SEPARATOR, REFERENCE_PUNCTUATION, PRE_FILTER_SCOPE_BLOCK, BLAST_RADIUS_RECORD_SOURCE, MAX_SAME_DAY_ROUNDS = 99;
81092
81273
  var init_managed = __esm(() => {
81093
81274
  init_coverage_complete();
81275
+ init_rebuttal_citation();
81276
+ init_complete_head_gate();
81277
+ init_evidence_level();
81094
81278
  init_research();
81095
81279
  init_validator();
81096
81280
  init_contracts();
@@ -88980,7 +89164,7 @@ function sha256Hex5(bytes) {
88980
89164
  var init_checksum = () => {};
88981
89165
 
88982
89166
  // src/bundle/external.ts
88983
- import { mkdir as mkdir54, lstat as lstat12, readFile as readFile108, readdir as readdir40, rename as rename11, stat as stat29, unlink, writeFile as writeFile33 } from "fs/promises";
89167
+ import { mkdir as mkdir54, lstat as lstat13, readFile as readFile108, readdir as readdir40, rename as rename11, stat as stat29, unlink, writeFile as writeFile33 } from "fs/promises";
88984
89168
  import { createHmac as createHmac4, randomBytes as randomBytes10, timingSafeEqual as timingSafeEqual2 } from "crypto";
88985
89169
  import path215 from "path";
88986
89170
  function emptyRegistry() {
@@ -89024,7 +89208,7 @@ async function loadIntegrityKey(env, homeDir) {
89024
89208
  const keyPath = integrityKeyPathFor(env, homeDir);
89025
89209
  let st;
89026
89210
  try {
89027
- st = await lstat12(keyPath);
89211
+ st = await lstat13(keyPath);
89028
89212
  } catch (err) {
89029
89213
  if (err?.code === "ENOENT")
89030
89214
  return { ok: false, reason: "missing" };
@@ -89464,7 +89648,7 @@ var init_external = __esm(() => {
89464
89648
 
89465
89649
  // src/gdskills/governance/scout.ts
89466
89650
  import { existsSync as existsSync32, readFileSync as readFileSync19, statSync as statSync8 } from "fs";
89467
- import { lstat as lstat13, mkdtemp as mkdtemp2, readdir as readdir41, readFile as readFile109 } from "fs/promises";
89651
+ import { lstat as lstat14, mkdtemp as mkdtemp2, readdir as readdir41, readFile as readFile109 } from "fs/promises";
89468
89652
  import { tmpdir as tmpdir3 } from "os";
89469
89653
  import path216 from "path";
89470
89654
  function stemLite(token) {
@@ -89690,7 +89874,7 @@ async function scoutImports(query, opts = {}) {
89690
89874
  async function collectSkillDirectorySnapshot(dir, limits = DEFAULT_SNAPSHOT_VETTING_LIMITS) {
89691
89875
  let rootStat;
89692
89876
  try {
89693
- rootStat = await lstat13(dir);
89877
+ rootStat = await lstat14(dir);
89694
89878
  } catch (error) {
89695
89879
  return { ok: false, reason: "unreadable-dir", message: `cannot read ${dir}: ${error instanceof Error ? error.message : String(error)}` };
89696
89880
  }
@@ -89722,7 +89906,7 @@ async function collectSkillDirectorySnapshot(dir, limits = DEFAULT_SNAPSHOT_VETT
89722
89906
  const rel = relPrefix.length > 0 ? `${relPrefix}/${entry.name}` : entry.name;
89723
89907
  let entryStat;
89724
89908
  try {
89725
- entryStat = await lstat13(abs);
89909
+ entryStat = await lstat14(abs);
89726
89910
  } catch (error) {
89727
89911
  return { ok: false, reason: "unreadable-file", message: `cannot stat ${rel}: ${error instanceof Error ? error.message : String(error)}` };
89728
89912
  }
@@ -94092,7 +94276,7 @@ var init_slate_lifecycle = __esm(() => {
94092
94276
 
94093
94277
  // src/harness/tool/builtin/slate-memory-tools.ts
94094
94278
  import { createReadStream } from "fs";
94095
- import { lstat as lstat14, realpath as realpath19 } from "fs/promises";
94279
+ import { lstat as lstat15, realpath as realpath19 } from "fs/promises";
94096
94280
  import path223 from "path";
94097
94281
  import { createInterface } from "readline";
94098
94282
  function failure3(message) {
@@ -94110,7 +94294,7 @@ async function confinedFile(sessionDir, candidate, within) {
94110
94294
  try {
94111
94295
  const root = await realpath19(within === undefined ? sessionDir : path223.join(sessionDir, within));
94112
94296
  const sessionReal = await realpath19(sessionDir);
94113
- const info = await lstat14(path223.resolve(candidate));
94297
+ const info = await lstat15(path223.resolve(candidate));
94114
94298
  if (info.isSymbolicLink() || !info.isFile())
94115
94299
  return null;
94116
94300
  const real = await realpath19(path223.resolve(candidate));
@@ -95022,6 +95206,9 @@ ${block}
95022
95206
  function asksForAutomaticReview(deps, options, userLine) {
95023
95207
  return options.automaticReview ?? ((options.origin ?? "operator") === "operator" && deps.subagentBudget === undefined && isAutomaticReviewRequest(userLine));
95024
95208
  }
95209
+ function agentContextWindow(deps) {
95210
+ return deps.getContextWindow !== undefined ? deps.getContextWindow() : deps.contextWindow;
95211
+ }
95025
95212
  async function runAgentTurn(io, deps, history, userLine, options = {}) {
95026
95213
  const controlNonce = deps.controlNonce ?? generateControlNonce();
95027
95214
  recordBunfigBaseline();
@@ -95254,7 +95441,7 @@ async function pruneHistory(io, deps, history, sessionDir, minSavingTokens, shou
95254
95441
  return { pruned: 0, collapsed: 0, reasoningStripped: 0, savedTokens: 0 };
95255
95442
  }
95256
95443
  const lengthBefore = history.length;
95257
- const thresholds = pruneThresholdsForWindow(deps.contextWindow);
95444
+ const thresholds = pruneThresholdsForWindow(agentContextWindow(deps));
95258
95445
  const result = await pruneToolOutputs(history, {
95259
95446
  sessionDir,
95260
95447
  protectTokens: thresholds.protectTokens,
@@ -95292,7 +95479,7 @@ async function pruneOrRewriteHistory(input) {
95292
95479
  round: input.round,
95293
95480
  maxRounds: input.maxRounds
95294
95481
  });
95295
- const forced = needsCompaction(estimateRequestTokens(history, input.systemInstruction, input.toolDefs), deps.contextWindow);
95482
+ const forced = needsCompaction(estimateRequestTokens(history, input.systemInstruction, input.toolDefs), agentContextWindow(deps));
95296
95483
  const logDecision = (d) => {
95297
95484
  const key = `${d.kind}:${d.reason}`;
95298
95485
  if (d.apply) {
@@ -95337,7 +95524,7 @@ ${describeRewriteDecision(d)}
95337
95524
  sessionDir: wmDir,
95338
95525
  slate,
95339
95526
  frame: { nonce: input.controlNonce, scrub, ts: input.now() },
95340
- ...deps.contextWindow !== undefined ? { contextWindow: deps.contextWindow } : {},
95527
+ ...agentContextWindow(deps) !== undefined ? { contextWindow: agentContextWindow(deps) } : {},
95341
95528
  providerId: deps.providerId,
95342
95529
  remainingRounds,
95343
95530
  atPlanBoundary: boundary,
@@ -95411,13 +95598,13 @@ async function pruneThenCompact(io, deps, history, systemInstruction, toolDefs,
95411
95598
  usageAnchors.delete(history);
95412
95599
  }
95413
95600
  const estimate = estimateRequestTokens(history, systemInstruction, toolDefs);
95414
- if (!needsCompaction(estimate, deps.contextWindow)) {
95601
+ if (!needsCompaction(estimate, agentContextWindow(deps))) {
95415
95602
  return;
95416
95603
  }
95417
95604
  await firePreCompactBestEffort(deps, estimate);
95418
95605
  const compacted = compactWithFallback(history, {
95419
95606
  keepLastUserTurns: 3,
95420
- fits: (ctx) => !needsCompaction(estimateRequestTokens(ctx, systemInstruction, toolDefs), deps.contextWindow)
95607
+ fits: (ctx) => !needsCompaction(estimateRequestTokens(ctx, systemInstruction, toolDefs), agentContextWindow(deps))
95421
95608
  });
95422
95609
  if (!compacted.noop) {
95423
95610
  history.splice(0, history.length, ...compacted.context);
@@ -95803,11 +95990,11 @@ ${planSnapshot}`;
95803
95990
  `);
95804
95991
  }
95805
95992
  const preRequestEstimate = pruneResult.pruned + pruneResult.reasoningStripped > 0 ? estimateRequestTokens(history, roundSystemInstruction, toolDefs) : estimateWithUsageAnchor(history, roundSystemInstruction, toolDefs, liveAnchor);
95806
- if (needsCompaction(preRequestEstimate, deps.contextWindow)) {
95993
+ if (needsCompaction(preRequestEstimate, agentContextWindow(deps))) {
95807
95994
  await firePreCompactBestEffort(deps, preRequestEstimate);
95808
95995
  const compacted = compactWithFallback(history, {
95809
95996
  keepLastUserTurns: 3,
95810
- fits: (ctx) => !needsCompaction(estimateRequestTokens(ctx, roundSystemInstruction, toolDefs), deps.contextWindow)
95997
+ fits: (ctx) => !needsCompaction(estimateRequestTokens(ctx, roundSystemInstruction, toolDefs), agentContextWindow(deps))
95811
95998
  });
95812
95999
  if (!compacted.noop) {
95813
96000
  history.splice(0, history.length, ...compacted.context);
@@ -96121,7 +96308,7 @@ ${planSnapshot}`;
96121
96308
  usageAnchors.delete(history);
96122
96309
  forgetHiddenHashes();
96123
96310
  }
96124
- const overflowTarget = overflowTargetTokens(parseOverflowLimits(pendingOverflowError.message), deps.contextWindow, overflowEstimate);
96311
+ const overflowTarget = overflowTargetTokens(parseOverflowLimits(pendingOverflowError.message), agentContextWindow(deps), overflowEstimate);
96125
96312
  const compacted = compactWithFallback(history, {
96126
96313
  keepLastUserTurns: 3,
96127
96314
  fits: (ctx) => overflowTarget !== undefined && estimateRequestTokens(ctx, roundSystemInstruction, toolDefs) <= overflowTarget
@@ -99580,7 +99767,7 @@ async function resolveGitHooksRoot(projectRoot) {
99580
99767
  var init_git_hooks = () => {};
99581
99768
 
99582
99769
  // src/lib/managed-git-hook.ts
99583
- import { chmod as chmod9, lstat as lstat15, mkdir as mkdir57, readFile as readFile112, realpath as realpath20, writeFile as writeFile35 } from "fs/promises";
99770
+ import { chmod as chmod9, lstat as lstat16, mkdir as mkdir57, readFile as readFile112, realpath as realpath20, writeFile as writeFile35 } from "fs/promises";
99584
99771
  import path230 from "path";
99585
99772
  function escapeRegExp6(value) {
99586
99773
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -99611,13 +99798,13 @@ async function resolveContainedHookPath(projectRoot, hookName) {
99611
99798
  const commonDirReal = await realpath20(path230.dirname(hooksRoot)).catch(() => path230.dirname(hooksRoot));
99612
99799
  const projectRootReal = await realpath20(projectRoot).catch(() => projectRoot);
99613
99800
  const acceptedContainers = [commonDirReal, projectRootReal];
99614
- const hooksRootStat = await lstat15(hooksRoot).catch(() => null);
99801
+ const hooksRootStat = await lstat16(hooksRoot).catch(() => null);
99615
99802
  const hooksRootReal = await realpath20(hooksRoot).catch(() => hooksRoot);
99616
99803
  if (hooksRootStat?.isSymbolicLink()) {
99617
99804
  await assertResolvesInside(hooksRoot, acceptedContainers, "the git hooks directory");
99618
99805
  }
99619
99806
  const hookPath = path230.join(hooksRoot, hookName);
99620
- const hookStat = await lstat15(hookPath).catch(() => null);
99807
+ const hookStat = await lstat16(hookPath).catch(() => null);
99621
99808
  if (hookStat?.isSymbolicLink()) {
99622
99809
  await assertResolvesInside(hookPath, [hooksRootReal, projectRootReal], "the git hook file");
99623
99810
  }
@@ -105195,7 +105382,7 @@ var init_metaproject_operations = __esm(() => {
105195
105382
 
105196
105383
  // src/harness/tool/builtin/metaproject-tools.ts
105197
105384
  import { existsSync as existsSync43 } from "fs";
105198
- import { resolve as resolve4 } from "path";
105385
+ import { resolve as resolve5 } from "path";
105199
105386
  function normalizeSearchResult(result) {
105200
105387
  if (result.isError && RG_UNAVAILABLE_SIGNATURE.test(result.output)) {
105201
105388
  return { output: SEARCH_CODE_RG_UNAVAILABLE_MESSAGE, isError: true };
@@ -105203,7 +105390,7 @@ function normalizeSearchResult(result) {
105203
105390
  return result;
105204
105391
  }
105205
105392
  function keryxSelfCommand(main = Bun.main, execPath = process.execPath, exists = existsSync43, entries = KERYX_ENTRIES) {
105206
- if (entries.includes(resolve4(main)) && exists(main)) {
105393
+ if (entries.includes(resolve5(main)) && exists(main)) {
105207
105394
  return [execPath, ...SAFE_BUN_SPAWN_ARGS, main];
105208
105395
  }
105209
105396
  if (isCompiledBinaryEntry(main)) {
@@ -105359,8 +105546,8 @@ var init_metaproject_tools = __esm(() => {
105359
105546
  RG_UNAVAILABLE_SIGNATURE = /ripgrep \(rg\) is not installed|Executable not found[^\n]*\brg\b|\brg\b[^\n]*\bENOENT\b|\bENOENT\b[^\n]*\brg\b/i;
105360
105547
  SEARCH_CODE_RG_UNAVAILABLE_MESSAGE = "ripgrep (rg) is not installed or not on PATH, and search_code needs it. Install it " + "(`brew install ripgrep` / `apt install ripgrep`), or use read_file and list_dir to " + "inspect files directly instead of retrying search_code.";
105361
105548
  KERYX_ENTRIES = [
105362
- resolve4(import.meta.dir, "..", "..", "..", "cli.ts"),
105363
- resolve4(import.meta.dir, "cli.js")
105549
+ resolve5(import.meta.dir, "..", "..", "..", "cli.ts"),
105550
+ resolve5(import.meta.dir, "cli.js")
105364
105551
  ];
105365
105552
  });
105366
105553
 
@@ -107924,7 +108111,7 @@ var init_registry3 = __esm(() => {
107924
108111
  // src/harness/tool/builtin/spawn-subagent-tool.ts
107925
108112
  import { createHash as createHash74, randomUUID as randomUUID28 } from "crypto";
107926
108113
  import { execFile as execFile7 } from "child_process";
107927
- import { lstat as lstat16, mkdir as mkdir60, mkdtemp as mkdtemp3, readFile as readFile114, realpath as realpath21, rm as rm16, stat as stat31, writeFile as writeFile38 } from "fs/promises";
108114
+ import { lstat as lstat17, mkdir as mkdir60, mkdtemp as mkdtemp3, readFile as readFile114, realpath as realpath21, rm as rm16, stat as stat31, writeFile as writeFile38 } from "fs/promises";
107928
108115
  import { tmpdir as tmpdir7 } from "os";
107929
108116
  import path243 from "path";
107930
108117
  import { promisify as promisify5 } from "util";
@@ -108232,7 +108419,7 @@ async function verifyWorktreeCandidate(candidate, commonDir, worktreesDir) {
108232
108419
  try {
108233
108420
  const realCandidate = await realpath21(candidate);
108234
108421
  const dotGit = path243.join(realCandidate, ".git");
108235
- const dotGitStat = await lstat16(dotGit);
108422
+ const dotGitStat = await lstat17(dotGit);
108236
108423
  if (dotGitStat.isDirectory()) {
108237
108424
  return await realpath21(dotGit) === commonDir ? realCandidate : undefined;
108238
108425
  }
@@ -126701,6 +126888,12 @@ function layersLockPath(packageDir) {
126701
126888
  function packageMemoryDir(packageDir) {
126702
126889
  return path275.join(packageDir, LAYERS_DIR, MEMORY_DIR);
126703
126890
  }
126891
+ function failedRecently(existing, now) {
126892
+ if (existing.graph.status !== "failed")
126893
+ return false;
126894
+ const age = now.getTime() - Date.parse(existing.graph.failedAt ?? existing.refreshedAt);
126895
+ return age >= 0 && age < FAILED_RETRY_WINDOW_MS;
126896
+ }
126704
126897
  function layersRefreshInFlight(packageDir) {
126705
126898
  return isLockHeld(layersLockPath(packageDir), LOCK_STALE_MS2);
126706
126899
  }
@@ -126842,20 +127035,35 @@ async function sweepStaleWorktrees(deps, projectRoot) {
126842
127035
  await rm19(worktree, { recursive: true, force: true }).catch(() => {});
126843
127036
  }
126844
127037
  }
126845
- async function withDetachedWorktree(deps, input, rev, cleanupExtra, use) {
126846
- const worktree = await mkdtemp6(path275.join(tmpdir12(), WORKTREE_PREFIX));
126847
- const cleanup = async () => {
126848
- await cleanupExtra().catch(() => {});
126849
- await deps.git(input.projectRoot, ["worktree", "remove", "--force", "--end-of-options", worktree]);
126850
- await rm19(worktree, { recursive: true, force: true }).catch(() => {});
126851
- };
127038
+ async function withSignalCleanup(packageDir, body) {
127039
+ const cleanups = new Set;
126852
127040
  const onSignal = (signal) => {
126853
127041
  for (const name of SIGNALS)
126854
127042
  process.off(name, onSignal);
126855
- cleanup().then(() => rm19(layersLockPath(input.packageDir), { recursive: true, force: true })).catch(() => {}).finally(() => process.kill(process.pid, signal));
127043
+ const settled = Promise.all([...cleanups].map((cleanup) => cleanup().catch(() => {})));
127044
+ const bound = new Promise((resolve) => setTimeout(resolve, SIGNAL_CLEANUP_MAX_MS).unref());
127045
+ Promise.race([settled, bound]).then(() => rm19(layersLockPath(packageDir), { recursive: true, force: true })).catch(() => {}).finally(() => process.kill(process.pid, signal));
126856
127046
  };
126857
127047
  for (const name of SIGNALS)
126858
127048
  process.on(name, onSignal);
127049
+ try {
127050
+ return await body((cleanup) => {
127051
+ cleanups.add(cleanup);
127052
+ return () => void cleanups.delete(cleanup);
127053
+ });
127054
+ } finally {
127055
+ for (const name of SIGNALS)
127056
+ process.off(name, onSignal);
127057
+ }
127058
+ }
127059
+ async function withDetachedWorktree(deps, input, rev, cleanupExtra, register, use) {
127060
+ const worktree = await mkdtemp6(path275.join(tmpdir12(), WORKTREE_PREFIX));
127061
+ const cleanup = async () => {
127062
+ await cleanupExtra().catch(() => {});
127063
+ await deps.git(input.projectRoot, ["worktree", "remove", "--force", "--end-of-options", worktree]);
127064
+ await rm19(worktree, { recursive: true, force: true }).catch(() => {});
127065
+ };
127066
+ const unregister = register(cleanup);
126859
127067
  try {
126860
127068
  const add = ["-c", "core.hooksPath=/dev/null", "-c", "core.fsmonitor=false", "worktree", "add", "--detach", "--force", "--end-of-options", worktree, rev];
126861
127069
  let added = await deps.git(input.projectRoot, add);
@@ -126867,19 +127075,22 @@ async function withDetachedWorktree(deps, input, rev, cleanupExtra, use) {
126867
127075
  throw new Error(`could not check out ${rev} to build its graph`);
126868
127076
  return await use(worktree, await deps.git(worktree, ["rev-parse", "HEAD"]));
126869
127077
  } finally {
126870
- for (const name of SIGNALS)
126871
- process.off(name, onSignal);
126872
- await cleanup();
127078
+ try {
127079
+ await cleanup();
127080
+ } finally {
127081
+ unregister();
127082
+ }
126873
127083
  }
126874
127084
  }
126875
- async function buildPackageGraph(deps, input, prHead) {
127085
+ async function buildPackageGraph(deps, input, prHead, register) {
126876
127086
  const failed = (error) => ({
126877
127087
  status: "failed",
126878
127088
  commit: null,
126879
127089
  builtAt: null,
126880
127090
  nodes: 0,
126881
127091
  edges: 0,
126882
- error
127092
+ error,
127093
+ failedAt: deps.now().toISOString()
126883
127094
  });
126884
127095
  if (prHead === null)
126885
127096
  return failed("no commit to build the graph at");
@@ -126887,7 +127098,7 @@ async function buildPackageGraph(deps, input, prHead) {
126887
127098
  const staging = `${dataDir}.${randomUUID35()}`;
126888
127099
  const dropStaging = () => rm19(staging, { recursive: true, force: true }).catch(() => {});
126889
127100
  try {
126890
- return await withDetachedWorktree(deps, input, prHead, dropStaging, async (worktree, builtFrom) => {
127101
+ return await withDetachedWorktree(deps, input, prHead, dropStaging, register, async (worktree, builtFrom) => {
126891
127102
  if (builtFrom !== prHead) {
126892
127103
  return failed(`graph would be built at ${builtFrom ?? "an unknown commit"}, not the PR head ${prHead}`);
126893
127104
  }
@@ -127005,7 +127216,7 @@ async function refreshLayers(input) {
127005
127216
  base = fromTarget ?? `${prHead}~1`;
127006
127217
  }
127007
127218
  await writeSelfIgnoringDir(path275.join(input.packageDir, LAYERS_DIR));
127008
- return withFileLock(layersLockPath(input.packageDir), async () => {
127219
+ return withFileLock(layersLockPath(input.packageDir), () => withSignalCleanup(input.packageDir, async (register) => {
127009
127220
  await recoverGraphDirectory(input.packageDir);
127010
127221
  await sweepStaleWorktrees(deps, input.projectRoot);
127011
127222
  const existing = await readLayers(input.packageDir);
@@ -127013,8 +127224,10 @@ async function refreshLayers(input) {
127013
127224
  let graph;
127014
127225
  if (input.force !== true && sameHead && await layersCurrent(input.packageDir, existing, prHead)) {
127015
127226
  graph = existing.graph;
127227
+ } else if (input.force !== true && sameHead && failedRecently(existing, deps.now())) {
127228
+ graph = existing.graph;
127016
127229
  } else {
127017
- graph = await buildPackageGraph(deps, input, prHead);
127230
+ graph = await buildPackageGraph(deps, input, prHead, register);
127018
127231
  if (graph.status === "failed" && sameHead && await layersCurrent(input.packageDir, existing, prHead)) {
127019
127232
  graph = existing.graph;
127020
127233
  }
@@ -127032,10 +127245,10 @@ async function refreshLayers(input) {
127032
127245
  wiki,
127033
127246
  memory
127034
127247
  };
127035
- await writeFileAtomic(layersFilePath(input.packageDir), `${JSON.stringify(layers, null, 2)}
127248
+ await deps.writeLayers(layersFilePath(input.packageDir), `${JSON.stringify(layers, null, 2)}
127036
127249
  `);
127037
127250
  return layers;
127038
- }, { timeoutMs: LOCK_WAIT_MS, staleMs: LOCK_STALE_MS2 });
127251
+ }), { timeoutMs: LOCK_WAIT_MS, staleMs: LOCK_STALE_MS2 });
127039
127252
  }
127040
127253
  async function loadPackageGraph(packageDir, prHead) {
127041
127254
  if (await layersRefreshInFlight(packageDir)) {
@@ -127045,7 +127258,8 @@ async function loadPackageGraph(packageDir, prHead) {
127045
127258
  if (layers === null)
127046
127259
  throw new PackageGraphError(`no ${LAYERS_FILE} in ${packageDir}; run \`keryx review layers refresh --package ${shellWord(packageDir)}\``);
127047
127260
  if (layers.graph.status !== "ok") {
127048
- throw new PackageGraphError(`the package graph is ${layers.graph.status}${layers.graph.error ? `: ${layers.graph.error}` : ""}`);
127261
+ const retry = layers.graph.status === "failed" ? `; a failed build is not retried for ${FAILED_RETRY_WINDOW_MS / 60000} minutes at the same head, run \`keryx review layers refresh --package ${shellWord(packageDir)} --force\` to retry now` : "";
127262
+ throw new PackageGraphError(`the package graph is ${layers.graph.status}${layers.graph.error ? `: ${layers.graph.error}` : ""}${retry}`);
127049
127263
  }
127050
127264
  if (!await graphIntact(packageDir, layers)) {
127051
127265
  throw new PackageGraphError(`the package graph directory is missing or does not match ${LAYERS_FILE}; run \`keryx review layers refresh --package ${shellWord(packageDir)}\``);
@@ -127057,7 +127271,7 @@ async function loadPackageGraph(packageDir, prHead) {
127057
127271
  const dataDir = packageGraphDir(packageDir);
127058
127272
  return { graph: await loadGraph(packageDir, dataDir), dataDir, layers };
127059
127273
  }
127060
- var execFileAsync9, LAYERS_FILE = "layers.json", LAYERS_VERSION = 1, LAYERS_DIR = "layers", GRAPH_DIR = "graph", GRAPH_BUILD_FILE = "build.json", MEMORY_DIR = "memory", LOCK_NAME = "refresh.lock", MANIFEST_FILE2 = "manifest.json", LOCK_STALE_MS2 = 60000, LOCK_WAIT_MS, WORKTREE_PREFIX = "keryx-layers-", WORKTREE_SWEEP_AGE_MS, SIGNALS, WIKI_PATH_PREFIX = ".metaproject/wiki", MEMORY_PATH_PREFIX = ".metaproject/memory", defaultLayerDeps, COMMIT_ID, PackageGraphError;
127274
+ var execFileAsync9, LAYERS_FILE = "layers.json", LAYERS_VERSION = 1, LAYERS_DIR = "layers", GRAPH_DIR = "graph", GRAPH_BUILD_FILE = "build.json", MEMORY_DIR = "memory", LOCK_NAME = "refresh.lock", MANIFEST_FILE2 = "manifest.json", LOCK_STALE_MS2 = 60000, LOCK_WAIT_MS, WORKTREE_PREFIX = "keryx-layers-", WORKTREE_SWEEP_AGE_MS, FAILED_RETRY_WINDOW_MS, SIGNALS, SIGNAL_CLEANUP_MAX_MS = 5000, WIKI_PATH_PREFIX = ".metaproject/wiki", MEMORY_PATH_PREFIX = ".metaproject/memory", defaultLayerDeps, COMMIT_ID, PackageGraphError;
127061
127275
  var init_layers = __esm(() => {
127062
127276
  init_build();
127063
127277
  init_fs();
@@ -127071,6 +127285,7 @@ var init_layers = __esm(() => {
127071
127285
  execFileAsync9 = promisify9(execFile11);
127072
127286
  LOCK_WAIT_MS = 16 * 60000;
127073
127287
  WORKTREE_SWEEP_AGE_MS = 30 * 60000;
127288
+ FAILED_RETRY_WINDOW_MS = 5 * 60000;
127074
127289
  SIGNALS = ["SIGINT", "SIGTERM"];
127075
127290
  defaultLayerDeps = {
127076
127291
  async git(cwd, args) {
@@ -127090,6 +127305,7 @@ var init_layers = __esm(() => {
127090
127305
  async indexMemory(cwd, indexDir) {
127091
127306
  return createMemoryService().index({ cwd, embeddings: false, indexDir });
127092
127307
  },
127308
+ writeLayers: writeFileAtomic,
127093
127309
  now: () => new Date
127094
127310
  };
127095
127311
  COMMIT_ID = /^[0-9a-f]{7,64}$/i;
@@ -127119,7 +127335,7 @@ function layersSummaryLine(layers) {
127119
127335
  function layersDetailLines(layers) {
127120
127336
  const lines = [];
127121
127337
  const graph = layers.graph;
127122
- lines.push(graph.status === "ok" ? `graph: ok @${shortSha(graph.commit)} = ${graphHeadState(layers)} (PR head ${shortSha(layers.prHead)}), ${graph.nodes} nodes, ${graph.edges} edges, built ${graph.builtAt ?? "?"}` : `graph: ${graph.status}${graph.error ? ` \u2014 ${graph.error}` : ""}`);
127338
+ lines.push(graph.status === "ok" ? `graph: ok @${shortSha(graph.commit)} = ${graphHeadState(layers)} (PR head ${shortSha(layers.prHead)}), ${graph.nodes} nodes, ${graph.edges} edges, built ${graph.builtAt ?? "?"}` : `graph: ${graph.status}${graph.error ? ` \u2014 ${graph.error}` : ""}${graph.status === "failed" ? " (not retried automatically for a few minutes at the same head; refresh --force retries now)" : ""}`);
127123
127339
  const wiki = layers.wiki;
127124
127340
  lines.push(`wiki: ${wiki.status} @${shortSha(wiki.commit)}, divergence ${divergenceText(wiki.divergence)}, ${wiki.pagesRefreshed} page(s) ${wiki.written ? "refreshed" : "would refresh (dry run)"}, ` + `${wiki.pagesStaleProse === null ? "stale prose unknown" : `${wiki.pagesStaleProse} page(s) with stale prose`}${wiki.error ? ` \u2014 ${wiki.error}` : ""}`);
127125
127341
  const memory = layers.memory;
@@ -127458,7 +127674,7 @@ var init_review_panel = __esm(() => {
127458
127674
  // src/tui/review-layers-actions.ts
127459
127675
  import { execFile as execFile12 } from "child_process";
127460
127676
  function layersRefreshArgv(entry, packageDir) {
127461
- return [...SAFE_BUN_SPAWN_ARGS, entry, "review", "layers", "refresh", "--package", packageDir, "--json"];
127677
+ return [...SAFE_BUN_SPAWN_ARGS, entry, "review", "layers", "refresh", "--package", packageDir, "--force", "--json"];
127462
127678
  }
127463
127679
  function killLiveChildren() {
127464
127680
  for (const child of liveChildren)
@@ -127474,8 +127690,14 @@ function makeLayersRefreshRunner(entry, timeoutMs = REFRESH_TIMEOUT_MS) {
127474
127690
  exitHookInstalled = true;
127475
127691
  process.once("exit", killLiveChildren);
127476
127692
  }
127693
+ const startedAt = Date.now();
127477
127694
  const child = execFile12(process.execPath, layersRefreshArgv(entry, packageDir), { cwd: projectRoot, timeout: timeoutMs, maxBuffer: 16 * 1024 * 1024 }, (error, stdout, stderr) => {
127478
127695
  liveChildren.delete(child);
127696
+ if (error?.killed === true || /maxBuffer/i.test(error?.message ?? "")) {
127697
+ const timedOut = error?.killed === true && Date.now() - startedAt >= timeoutMs - 100;
127698
+ reject(new Error(timedOut ? `the layers refresh did not finish within ${Math.round(timeoutMs / 1000)}s and was stopped` : "the layers refresh printed more output than the limit allows and was stopped"));
127699
+ return;
127700
+ }
127479
127701
  try {
127480
127702
  resolve(JSON.parse(stdout));
127481
127703
  } catch {
@@ -137684,7 +137906,7 @@ function protectJsonIds(text) {
137684
137906
  const head = text.trim();
137685
137907
  if (head.length === 0 || head[0] !== "[" && head[0] !== "{")
137686
137908
  return;
137687
- if (text.includes(PLACEHOLDER))
137909
+ if (text.includes(PLACEHOLDER2))
137688
137910
  return;
137689
137911
  try {
137690
137912
  JSON.parse(head);
@@ -137694,7 +137916,7 @@ function protectJsonIds(text) {
137694
137916
  const hidden = [];
137695
137917
  const hide = (value) => {
137696
137918
  hidden.push(value);
137697
- return `${PLACEHOLDER}${letters(hidden.length - 1)}x`;
137919
+ return `${PLACEHOLDER2}${letters(hidden.length - 1)}x`;
137698
137920
  };
137699
137921
  let out = "";
137700
137922
  let i = 0;
@@ -137724,13 +137946,13 @@ function protectJsonIds(text) {
137724
137946
  }
137725
137947
  return {
137726
137948
  text: out,
137727
- restore: (scrubbed) => scrubbed.replace(new RegExp(`${PLACEHOLDER}([a-j]+)x`, "g"), (whole, index) => {
137949
+ restore: (scrubbed) => scrubbed.replace(new RegExp(`${PLACEHOLDER2}([a-j]+)x`, "g"), (whole, index) => {
137728
137950
  const at = Number(index.replace(/[a-j]/g, (c) => String(c.charCodeAt(0) - 97)));
137729
137951
  return hidden[at] ?? whole;
137730
137952
  })
137731
137953
  };
137732
137954
  }
137733
- var PLACEHOLDER = "kxjsonid", GITHUB_URL_PREFIX, GITHUB_URL_REST, SENSITIVE_KEY, NUMBER2;
137955
+ var PLACEHOLDER2 = "kxjsonid", GITHUB_URL_PREFIX, GITHUB_URL_REST, SENSITIVE_KEY, NUMBER2;
137734
137956
  var init_granted_json_ids = __esm(() => {
137735
137957
  GITHUB_URL_PREFIX = /^https:\/\/(?:api\.)?github\.com\/(?:repos\/)?[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\//;
137736
137958
  GITHUB_URL_REST = /^(?:actions\/runs|pull|issues)\/\d+(?:\/[A-Za-z0-9_-]+)*(?:#[A-Za-z_-]+-?\d+)?$/;
@@ -146462,6 +146684,7 @@ function mountStartupIndicator(otui, r, initialLabel, opts = {}) {
146462
146684
  return;
146463
146685
  }
146464
146686
  text.content = otui.t`${dimChunk(otui, `${SPINNER_FRAMES[frame % SPINNER_FRAMES.length]} ${label}`)}`;
146687
+ r.requestRender();
146465
146688
  };
146466
146689
  paint();
146467
146690
  const schedule = opts.setInterval ?? ((fn, ms) => setInterval(fn, ms));
@@ -146483,6 +146706,7 @@ function mountStartupIndicator(otui, r, initialLabel, opts = {}) {
146483
146706
  removed = true;
146484
146707
  unschedule(timer);
146485
146708
  r.root.remove(box);
146709
+ r.requestRender();
146486
146710
  }
146487
146711
  };
146488
146712
  }
@@ -158195,6 +158419,15 @@ async function fetchCodexLimits(fetchFn, model, configDir, timeoutMs) {
158195
158419
  return {};
158196
158420
  }
158197
158421
  }
158422
+ function startSessionLimits(input, load = loadSessionLimits) {
158423
+ let contextWindow;
158424
+ const ready = new Promise((resolve) => setTimeout(resolve, 0)).then(async () => {
158425
+ try {
158426
+ contextWindow = (await load(input)).contextWindow;
158427
+ } catch {}
158428
+ });
158429
+ return { getContextWindow: () => contextWindow, ready };
158430
+ }
158198
158431
  async function loadSessionLimits(input) {
158199
158432
  const fetchFn = input.fetch ?? globalThis.fetch;
158200
158433
  const env = envWithSavedApiKeys(input.env ?? process.env);
@@ -180210,15 +180443,12 @@ Example: keryx shell --provider ollama --model llama3.1:latest`);
180210
180443
  steerable: true
180211
180444
  })
180212
180445
  });
180213
- let contextWindow;
180214
- try {
180215
- const limits = await loadSessionLimits({
180216
- provider: sel.provider,
180217
- model: sel.model,
180218
- ...sel.baseUrl !== undefined ? { baseUrl: sel.baseUrl } : {}
180219
- });
180220
- contextWindow = limits.contextWindow;
180221
- } catch {}
180446
+ const limits = startSessionLimits({
180447
+ provider: sel.provider,
180448
+ model: sel.model,
180449
+ ...sel.baseUrl !== undefined ? { baseUrl: sel.baseUrl } : {},
180450
+ ...runtime.cacheDir !== undefined ? { configDir: runtime.cacheDir } : {}
180451
+ });
180222
180452
  const deps = {
180223
180453
  provider: agentProvider,
180224
180454
  providerId: sel.provider,
@@ -180250,7 +180480,7 @@ Example: keryx shell --provider ollama --model llama3.1:latest`);
180250
180480
  sweepBackgroundJobs: () => jobRegistry.sweepAll(),
180251
180481
  jobRegistry,
180252
180482
  ...resetSubagentBudget !== undefined ? { resetSubagentBudget } : {},
180253
- ...contextWindow !== undefined ? { contextWindow } : {},
180483
+ getContextWindow: limits.getContextWindow,
180254
180484
  ...Object.keys(resolvedModelParams).length > 0 ? { modelParams: resolvedModelParams } : {},
180255
180485
  ...(() => {
180256
180486
  const h = getShellHooks();
@@ -181120,7 +181350,7 @@ var init_retired_rules = __esm(() => {
181120
181350
  });
181121
181351
 
181122
181352
  // src/gdskills/guarded-fs-ops.ts
181123
- import { cp as cp2, lstat as lstat17, readFile as readFile171, readdir as readdir59, realpath as realpath25, unlink as unlink3 } from "fs/promises";
181353
+ import { cp as cp2, lstat as lstat18, readFile as readFile171, readdir as readdir59, realpath as realpath25, unlink as unlink3 } from "fs/promises";
181124
181354
  import { existsSync as existsSync63 } from "fs";
181125
181355
  import { createHash as createHash84 } from "crypto";
181126
181356
  import path340 from "path";
@@ -181211,7 +181441,7 @@ async function removeStaleRuntimeBuilds(skillsRoot, fsOps = { unlink: unlink3 })
181211
181441
  continue;
181212
181442
  const installedPath = path340.join(installedDir, entryName);
181213
181443
  try {
181214
- const stats = await lstat17(installedPath);
181444
+ const stats = await lstat18(installedPath);
181215
181445
  if (!stats.isFile()) {
181216
181446
  outcomes.push({ path: installedPath, action: "kept-not-regular-file" });
181217
181447
  continue;
@@ -181230,7 +181460,7 @@ async function removeStaleRuntimeBuilds(skillsRoot, fsOps = { unlink: unlink3 })
181230
181460
  }
181231
181461
  async function resolveSweepableDir(dir) {
181232
181462
  try {
181233
- const stats = await lstat17(dir);
181463
+ const stats = await lstat18(dir);
181234
181464
  if (stats.isSymbolicLink() || !stats.isDirectory()) {
181235
181465
  return {
181236
181466
  kind: "blocked",
@@ -181305,7 +181535,7 @@ async function removeUnmodifiedRetiredRules(rulesTarget, fsOps = { unlink: unlin
181305
181535
  const installedPath = path340.join(rulesTarget, retired.fileName);
181306
181536
  let stage = "lstat";
181307
181537
  try {
181308
- const stats = await lstat17(installedPath);
181538
+ const stats = await lstat18(installedPath);
181309
181539
  if (!stats.isFile()) {
181310
181540
  outcomes.push({ fileName: retired.fileName, action: "kept-not-regular-file" });
181311
181541
  continue;
@@ -181376,7 +181606,7 @@ var init_guarded_fs_ops = __esm(() => {
181376
181606
  });
181377
181607
 
181378
181608
  // src/gdskills/install.ts
181379
- import { lstat as lstat18, readFile as readFile172, readlink as readlink2 } from "fs/promises";
181609
+ import { lstat as lstat19, readFile as readFile172, readlink as readlink2 } from "fs/promises";
181380
181610
  import { existsSync as existsSync64 } from "fs";
181381
181611
  import path341 from "path";
181382
181612
  import { fileURLToPath as fileURLToPath6 } from "url";
@@ -181489,7 +181719,7 @@ async function installBundledSharedSkills(skillsRoot, metaprojectRoot) {
181489
181719
  async function checkInstallDestination(destination, displayPath) {
181490
181720
  let stats;
181491
181721
  try {
181492
- stats = await lstat18(destination);
181722
+ stats = await lstat19(destination);
181493
181723
  } catch (error) {
181494
181724
  if (isErrnoException(error) && error.code === "ENOENT") {
181495
181725
  return { usable: true };
@@ -181572,7 +181802,7 @@ async function installContracts(contractsRoot, metaprojectRoot) {
181572
181802
  async function checkInstallFile(destination, displayPath) {
181573
181803
  let stats;
181574
181804
  try {
181575
- stats = await lstat18(destination);
181805
+ stats = await lstat19(destination);
181576
181806
  } catch (error) {
181577
181807
  if (isMissingPathError(error)) {
181578
181808
  return { usable: true };
@@ -189768,7 +189998,7 @@ var init_plan = __esm(() => {
189768
189998
  });
189769
189999
 
189770
190000
  // src/gdskills/manifest/state.ts
189771
- import { lstat as lstat19, readFile as readFile182 } from "fs/promises";
190001
+ import { lstat as lstat20, readFile as readFile182 } from "fs/promises";
189772
190002
  import path363 from "path";
189773
190003
  function isValidInstallState(value) {
189774
190004
  return validateAgainstSchemaObject(INSTALL_STATE_SCHEMA, value).valid;
@@ -189811,7 +190041,7 @@ async function hasSymlinkSegment(repoRoot, posixRel) {
189811
190041
  current = path363.join(current, segment);
189812
190042
  let stat;
189813
190043
  try {
189814
- stat = await lstat19(current);
190044
+ stat = await lstat20(current);
189815
190045
  } catch {
189816
190046
  return false;
189817
190047
  }
@@ -190390,7 +190620,7 @@ var init_sync2 = __esm(() => {
190390
190620
  });
190391
190621
 
190392
190622
  // src/gdskills/import-skills.ts
190393
- import { lstat as lstat20, readdir as readdir64, readFile as readFile185, stat as stat41 } from "fs/promises";
190623
+ import { lstat as lstat21, readdir as readdir64, readFile as readFile185, stat as stat41 } from "fs/promises";
190394
190624
  import { homedir as homedir15 } from "os";
190395
190625
  import path368 from "path";
190396
190626
  function emptyOnlyError(label) {
@@ -191401,7 +191631,7 @@ async function decideReferencedRules(options, sources, refusedPackages) {
191401
191631
  async function listTemplateFiles(sourcePath) {
191402
191632
  const root = path368.join(path368.dirname(sourcePath), TEMPLATES_DIR);
191403
191633
  try {
191404
- if (!(await lstat20(root)).isDirectory())
191634
+ if (!(await lstat21(root)).isDirectory())
191405
191635
  return [];
191406
191636
  } catch {
191407
191637
  return [];
@@ -191823,7 +192053,7 @@ var init_import_skills = __esm(() => {
191823
192053
  });
191824
192054
 
191825
192055
  // src/gdskills/remove-skill.ts
191826
- import { lstat as lstat21, readdir as readdir65, readFile as readFile186, realpath as realpath26 } from "fs/promises";
192056
+ import { lstat as lstat22, readdir as readdir65, readFile as readFile186, realpath as realpath26 } from "fs/promises";
191827
192057
  import path369 from "path";
191828
192058
  async function removeProjectSkill(projectRoot, options) {
191829
192059
  if (!await pathExists(path369.join(projectRoot, ".metaproject"))) {
@@ -192018,7 +192248,7 @@ function normalizeSkillKey(input) {
192018
192248
  }
192019
192249
  async function lstatOrMissing(target) {
192020
192250
  try {
192021
- return await lstat21(target);
192251
+ return await lstat22(target);
192022
192252
  } catch (error) {
192023
192253
  if (isMissingPathError(error))
192024
192254
  return;
@@ -192083,7 +192313,7 @@ async function locateRewritable(root, relativePath) {
192083
192313
  if (absolute === root || !isPathInside(root, absolute)) {
192084
192314
  throw new Error(`${LABEL}: ${relativePath} resolves to ${absolute}, outside the project. Refusing to rewrite a file the project does not own. ${UNCHANGED}`);
192085
192315
  }
192086
- if (!(await lstat21(absolute)).isFile()) {
192316
+ if (!(await lstat22(absolute)).isFile()) {
192087
192317
  throw new Error(`${LABEL}: ${relativePath} is not a regular file. ${UNCHANGED}`);
192088
192318
  }
192089
192319
  const resolvedPath = toPosix(path369.relative(root, absolute));
@@ -197025,7 +197255,7 @@ function buildLedger(input) {
197025
197255
  continue;
197026
197256
  }
197027
197257
  obligations.push({ ...base, status: "unresolved" });
197028
- gaps.push({ detail: `${id}: raw finding is not in the canonical findings and has no closure`, next: `add a disposition {"id":"${id}","status":"finding|refuted|out-of-scope","evidence":\u2026,"reason":\u2026} to --dispositions` });
197258
+ gaps.push({ detail: `${id}: raw finding is not in the canonical findings and has no closure`, next: `add a disposition {"id":"${id}","status":"finding|refuted|out-of-scope|narrowed","evidence":\u2026,"reason":\u2026,"remains":\u2026 (narrowed only)} to --dispositions` });
197029
197259
  }
197030
197260
  }
197031
197261
  for (const given of input.dispositions) {
@@ -197053,7 +197283,9 @@ function closeWith(given, canonical) {
197053
197283
  return;
197054
197284
  if (given.status === "finding" && !(text4(given.finding) && canonical.filter((f) => f.id === given.finding || f.global_id === given.finding).length === 1))
197055
197285
  return;
197056
- return { status: given.status, evidence: given.evidence, reason: given.reason, ...given.status === "finding" ? { finding: given.finding } : {} };
197286
+ if (given.status === "narrowed" && !text4(given.remains))
197287
+ return;
197288
+ return { status: given.status, evidence: given.evidence, reason: given.reason, ...given.status === "finding" ? { finding: given.finding } : {}, ...given.status === "narrowed" ? { remains: given.remains } : {} };
197057
197289
  }
197058
197290
  async function ledgerBlockers(ledger, canonical, options) {
197059
197291
  const dispatch = dispatchErrors(ledger.dispatch);
@@ -197069,7 +197301,7 @@ var init_ledger2 = __esm(() => {
197069
197301
  init_slice();
197070
197302
  init_waves();
197071
197303
  FINISHED = new Set(["DONE", "DONE_WITH_CONCERNS"]);
197072
- CLOSED = new Set(["finding", "refuted", "out-of-scope"]);
197304
+ CLOSED = new Set(["finding", "refuted", "out-of-scope", "narrowed"]);
197073
197305
  OBJECT_START = /\{\s*["}]/y;
197074
197306
  RESULT_KEYS = new Set(["status", "reviewer", "findings"]);
197075
197307
  KEY_COLON = /\s*:/y;
@@ -197101,8 +197333,8 @@ needs_context and every slice its path gate requires covered by its dispatched p
197101
197333
  complete run (source-only, executionRequired false); anything else \u2014 NEEDS_CONTEXT, BLOCKED,
197102
197334
  INCOMPLETE, missing slices, no result \u2014 stays incomplete or not-run, never declared complete.
197103
197335
  Each raw finding becomes an obligation, closed only by a matching canonical finding or a
197104
- valid --dispositions entry ({id, status finding|refuted|out-of-scope, evidence, reason,
197105
- finding?}; default: the obligations already in research.json). scopeReviewed and rawReconciled
197336
+ valid --dispositions entry ({id, status finding|refuted|out-of-scope|narrowed, evidence, reason,
197337
+ finding?, remains? \u2014 required for narrowed: what of the finding still stands}; default: the obligations already in research.json). scopeReviewed and rawReconciled
197106
197338
  are true only when established.
197107
197339
 
197108
197340
  Slices are dispatched in waves bounded by the manifest's wave budget; a reviewer that covered
@@ -197348,24 +197580,36 @@ import path380 from "path";
197348
197580
  function sizeOf(block) {
197349
197581
  return Buffer.byteLength(JSON.stringify(block), "utf8");
197350
197582
  }
197583
+ function reasonOf(error) {
197584
+ const first = (error instanceof Error ? error.message : String(error)).split(`
197585
+ `)[0] ?? "unknown error";
197586
+ return first.length > MAX_REASON_CHARS ? `${first.slice(0, MAX_REASON_CHARS)}\u2026` : first;
197587
+ }
197351
197588
  function unique2(values) {
197352
197589
  return [...new Set(values)].sort();
197353
197590
  }
197354
197591
  function affectedFor(graph, files, depth) {
197355
197592
  const own = new Set(files);
197356
197593
  const found = new Set;
197594
+ const failures = [];
197357
197595
  for (const file of files) {
197358
197596
  let result;
197359
197597
  try {
197360
197598
  result = computeAffected(graph, file, { depth });
197361
- } catch {
197599
+ } catch (error) {
197600
+ failures.push(`${file}: ${reasonOf(error)}`);
197362
197601
  continue;
197363
197602
  }
197364
197603
  for (const dependent of result.dependents)
197365
197604
  if (!own.has(dependent))
197366
197605
  found.add(dependent);
197367
197606
  }
197368
- return [...found].sort();
197607
+ const named = failures.slice(0, MAX_NAMED_FAILURES).join("; ");
197608
+ const more = failures.length > MAX_NAMED_FAILURES ? ` (and ${failures.length - MAX_NAMED_FAILURES} more)` : "";
197609
+ return {
197610
+ files: [...found].sort(),
197611
+ unavailable: failures.length === 0 ? null : `not computed for ${failures.length} of ${files.length} file(s) \u2014 ${named}${more}`
197612
+ };
197369
197613
  }
197370
197614
  function symbolsFor(graph, files) {
197371
197615
  const symbols = graph.symbols ?? [];
@@ -197415,29 +197659,34 @@ function relatedTestsFor(graph, files, affected) {
197415
197659
  found.add(dependent);
197416
197660
  return [...found].sort();
197417
197661
  }
197418
- async function wikiFor(projectRoot, graph, files) {
197662
+ async function wikiFor(projectRoot, graph, files, readPage = (absolutePath) => readFile196(absolutePath, "utf8")) {
197419
197663
  let pages;
197420
197664
  try {
197421
197665
  pages = await collectPages(projectRoot);
197422
- } catch {
197423
- return [];
197666
+ } catch (error) {
197667
+ return { items: [], unavailable: `the wiki could not be read \u2014 ${reasonOf(error)}` };
197424
197668
  }
197425
197669
  const knownPaths = new Set(graph.nodes.map((node) => node.path));
197426
197670
  const keyFilesIndex = computeModuleKeyFiles(graph);
197427
197671
  const wanted = new Set(files);
197428
197672
  const out = [];
197673
+ const unreadable = [];
197429
197674
  for (const page of pages) {
197430
197675
  let content;
197431
197676
  try {
197432
- content = await readFile196(page.absolutePath, "utf8");
197677
+ content = await readPage(page.absolutePath);
197433
197678
  } catch {
197679
+ unreadable.push(page.relativePath);
197434
197680
  continue;
197435
197681
  }
197436
197682
  const set = resolveDescribeSet({ page, content, knownPaths, keyFilesIndex });
197437
197683
  if (set.paths.some((candidate) => wanted.has(candidate)))
197438
197684
  out.push({ page: page.relativePath, title: page.title });
197439
197685
  }
197440
- return out.sort((a, b) => a.page.localeCompare(b.page));
197686
+ return {
197687
+ items: out.sort((a, b) => a.page.localeCompare(b.page)),
197688
+ unavailable: unreadable.length === 0 ? null : `${unreadable.length} of ${pages.length} wiki page(s) could not be read \u2014 ${unreadable.slice(0, MAX_NAMED_FAILURES).join(", ")}`
197689
+ };
197441
197690
  }
197442
197691
  function modulesOf(file) {
197443
197692
  const parts = file.split("/").filter(Boolean);
@@ -197451,8 +197700,8 @@ async function memoryFor(projectRoot, files) {
197451
197700
  let entries;
197452
197701
  try {
197453
197702
  entries = await collectEntries(projectRoot);
197454
- } catch {
197455
- return [];
197703
+ } catch (error) {
197704
+ return { items: [], unavailable: `the memory could not be read \u2014 ${reasonOf(error)}` };
197456
197705
  }
197457
197706
  const modules = new Set(files.flatMap(modulesOf));
197458
197707
  const wanted = new Set(files);
@@ -197466,7 +197715,7 @@ async function memoryFor(projectRoot, files) {
197466
197715
  if (byModule || byFile)
197467
197716
  out.push({ path: entry.relativePath, title: entry.title, status: entry.status, module: module ?? "" });
197468
197717
  }
197469
- return out.sort((a, b) => a.path.localeCompare(b.path));
197718
+ return { items: out.sort((a, b) => a.path.localeCompare(b.path)), unavailable: null };
197470
197719
  }
197471
197720
  function fetchCommand(section, files, packageDir, projectRoot) {
197472
197721
  const first = files[0] ?? "<file>";
@@ -197496,18 +197745,28 @@ async function buildLayersBlock(input) {
197496
197745
  const { graph, layers } = await loadPackageGraph(input.packageDir);
197497
197746
  const base = async (depth) => {
197498
197747
  const affected = affectedFor(graph, files, depth);
197748
+ const wiki = await wikiFor(input.projectRoot, graph, files, input.readPage);
197749
+ const memory = await memoryFor(input.projectRoot, files);
197750
+ const unavailable = [];
197751
+ if (affected.unavailable !== null)
197752
+ unavailable.push({ section: "affected", reason: affected.unavailable });
197753
+ if (wiki.unavailable !== null)
197754
+ unavailable.push({ section: "wiki", reason: wiki.unavailable });
197755
+ if (memory.unavailable !== null)
197756
+ unavailable.push({ section: "memory", reason: memory.unavailable });
197499
197757
  return {
197500
197758
  version: LAYERS_BLOCK_VERSION,
197501
197759
  files,
197502
197760
  prHead: layers.prHead,
197503
197761
  graphCommit: layers.graphCommit,
197504
197762
  maxBytes,
197505
- affected: { depth, files: affected },
197763
+ affected: { depth, files: affected.files },
197506
197764
  symbols: symbolsFor(graph, files),
197507
- relatedTests: relatedTestsFor(graph, files, affected),
197508
- wiki: await wikiFor(input.projectRoot, graph, files),
197509
- memory: await memoryFor(input.projectRoot, files),
197510
- truncated: []
197765
+ relatedTests: relatedTestsFor(graph, files, affected.files),
197766
+ wiki: wiki.items,
197767
+ memory: memory.items,
197768
+ truncated: [],
197769
+ unavailable
197511
197770
  };
197512
197771
  };
197513
197772
  let block = await base(1);
@@ -197577,11 +197836,13 @@ function layersBlockLines(block) {
197577
197836
  ` wiki pages: ${block.wiki.length}`,
197578
197837
  ` memory entries: ${block.memory.length}`
197579
197838
  ];
197839
+ for (const gap of block.unavailable)
197840
+ lines.push(` ${gap.section}: unavailable (${gap.reason})`);
197580
197841
  for (const cut of block.truncated)
197581
197842
  lines.push(` truncated ${cut.section}: ${cut.shown}/${cut.total} \u2014 ${cut.command}`);
197582
197843
  return lines;
197583
197844
  }
197584
- var LAYERS_BLOCK_VERSION = 1, DEFAULT_LAYERS_BLOCK_MAX_BYTES = 8192, MAX_SYMBOLS_PER_FILE = 6, MAX_NEIGHBOURS_PER_SYMBOL = 4, MAX_BLOCK_FILES = 100, LAYERS_BLOCK_SECTIONS, CONTROL_CHARACTER;
197845
+ var LAYERS_BLOCK_VERSION = 1, DEFAULT_LAYERS_BLOCK_MAX_BYTES = 8192, MAX_SYMBOLS_PER_FILE = 6, MAX_NEIGHBOURS_PER_SYMBOL = 4, MAX_REASON_CHARS = 160, MAX_BLOCK_FILES = 100, LAYERS_BLOCK_SECTIONS, MAX_NAMED_FAILURES = 3, CONTROL_CHARACTER;
197585
197846
  var init_layers_payload = __esm(() => {
197586
197847
  init_affected();
197587
197848
  init_shell_quote();
@@ -197602,7 +197863,7 @@ function printLayersHelp() {
197602
197863
 
197603
197864
  keryx review layers refresh --package <dir> [--head <sha>] [--base <ref>] [--write-wiki] [--force] [--json]
197604
197865
  keryx review layers status --package <dir> [--json]
197605
- keryx review layers block --package <dir> --files <a,b,...> [--max-bytes <n>] [--json]
197866
+ keryx review layers block --package <dir> --files <a,b,...> [--max-bytes <n>] [--force] [--json]
197606
197867
 
197607
197868
  \`block\` prints the bounded \`layers\` block a reviewer payload carries for those files: files
197608
197869
  the graph says are affected (depth 1, then 2 while under the cap), symbol neighbours, related
@@ -197618,7 +197879,8 @@ The graph is built at the PR head into <dir>/layers/graph; the tracked graph art
197618
197879
  the working tree are not touched. The wiki and the memory are read from the project's
197619
197880
  working copy, and layers.json marks how far that is from the PR head. The wiki refresh is a
197620
197881
  dry run unless --write-wiki is given. A graph that is already built at this head and intact is
197621
- kept (the wiki and the memory are still refreshed); --force rebuilds it.`);
197882
+ kept (the wiki and the memory are still refreshed); --force rebuilds it. A graph that failed at
197883
+ this head is not rebuilt for 5 minutes after the failure; --force or a new head rebuilds it.`);
197622
197884
  }
197623
197885
  function wantsHelp2(args) {
197624
197886
  return args.includes("--help") || args.includes("-h");
@@ -197649,7 +197911,8 @@ async function ensurePackageLayers(packageDir, options = {}) {
197649
197911
  projectRoot,
197650
197912
  packageDir,
197651
197913
  ...head === undefined ? {} : { prHead: head },
197652
- ...options.base === undefined ? {} : { base: options.base }
197914
+ ...options.base === undefined ? {} : { base: options.base },
197915
+ ...options.force === true ? { force: true } : {}
197653
197916
  });
197654
197917
  }
197655
197918
  function print(layers, json) {
@@ -197708,7 +197971,7 @@ async function runLayers(args, deps) {
197708
197971
  if (maxBytes !== undefined && (!Number.isInteger(maxBytes) || maxBytes < 256))
197709
197972
  throw new Error("--max-bytes must be an integer of at least 256; a block with every list empty needs more than that, and the refusal names how much.");
197710
197973
  const projectRoot = resolveProjectRoot2(process.cwd());
197711
- await ensurePackageLayers(packageDir, { projectRoot });
197974
+ await ensurePackageLayers(packageDir, { projectRoot, force: rest.includes("--force") });
197712
197975
  const block = await buildLayersBlock({ projectRoot, packageDir, files, ...maxBytes === undefined ? {} : { maxBytes } });
197713
197976
  console.log(rest.includes("--json") ? JSON.stringify(block, null, 2) : layersBlockLines(block).join(`
197714
197977
  `));
@@ -197727,7 +197990,7 @@ var init_review_layers = __esm(() => {
197727
197990
  init_layers_payload();
197728
197991
  LAYERS_REFRESH_FLAGS = ["--package", "--head", "--base", "--write-wiki", "--force", "--json", "--help", "-h"];
197729
197992
  LAYERS_STATUS_FLAGS = ["--package", "--json", "--help", "-h"];
197730
- LAYERS_BLOCK_FLAGS = ["--package", "--files", "--max-bytes", "--json", "--help", "-h"];
197993
+ LAYERS_BLOCK_FLAGS = ["--package", "--files", "--max-bytes", "--force", "--json", "--help", "-h"];
197731
197994
  });
197732
197995
 
197733
197996
  // src/commands/review-chunks.ts
@@ -199687,6 +199950,11 @@ function rejectUnknownFlags(args, allowed, usage) {
199687
199950
  throw new Error(`Unknown option${unknown.length > 1 ? "s" : ""} for \`keryx review ${usage}\`: ${[...new Set(unknown)].join(", ")}. Accepted: ${allowed.join(", ")}. Refused rather than ignored \u2014 a flag that is silently dropped writes nothing and reports success.`);
199688
199951
  }
199689
199952
  }
199953
+ async function trackerPrLookup(ref) {
199954
+ if (!await githubAdapter.detect())
199955
+ return null;
199956
+ return githubAdapter.prStatus(ref);
199957
+ }
199690
199958
  async function reviewCommand(args, seams) {
199691
199959
  const command = args[0];
199692
199960
  if (!command || command === "--help" || command === "-h") {
@@ -201667,8 +201935,11 @@ async function runComplete3(args, seams) {
201667
201935
  manifest,
201668
201936
  reviewNotes,
201669
201937
  dispositions: tally,
201670
- coverageWaiver
201671
- } = await completeManagedReview(process.cwd(), ref, { dispositions, ...waiver?.value === undefined ? {} : { waiveCoverage: waiver.value } });
201938
+ coverageWaiver,
201939
+ warnings
201940
+ } = await completeManagedReview(process.cwd(), ref, { dispositions, prLookup: seams?.prLookup ?? trackerPrLookup, ...waiver?.value === undefined ? {} : { waiveCoverage: waiver.value } });
201941
+ for (const warning of warnings)
201942
+ console.error(`warning: ${warning}`);
201672
201943
  if (coverageWaiver !== undefined) {
201673
201944
  const shown = coverageWaiver.files.slice(0, FIRST_FILES).join(", ");
201674
201945
  console.log(`coverage waived by the operator (${coverageWaiver.time}): "${coverageWaiver.operatorQuote}"; ${coverageWaiver.files.length} file(s) not reviewed: ${shown}${coverageWaiver.files.length > FIRST_FILES ? ", \u2026" : ""}`);
@@ -201927,7 +202198,7 @@ Usage:
201927
202198
  [--package <review-dir>] [--json | --brief] [--out <file>]
201928
202199
  keryx review layers refresh --package <review-dir> [--head <sha>] [--base <ref>] [--write-wiki] [--force] [--json]
201929
202200
  keryx review layers status --package <review-dir> [--json]
201930
- keryx review layers block --package <review-dir> --files a,b [--max-bytes <n>] [--json]
202201
+ keryx review layers block --package <review-dir> --files a,b [--max-bytes <n>] [--force] [--json]
201931
202202
  Graph, wiki and memory for one review; no model is called. An intact graph at
201932
202203
  the same head is reused (--force rebuilds); a second refresh waits up to 16 min.
201933
202204
  keryx review chunks plan|estimate|next|record|merge --package <review-dir>
@@ -208130,7 +208401,7 @@ var init_tools3 = __esm(() => {
208130
208401
 
208131
208402
  // src/mcp/resources.ts
208132
208403
  import path396 from "path";
208133
- import { lstat as lstat22, readdir as readdir76, realpath as realpath28, stat as stat44 } from "fs/promises";
208404
+ import { lstat as lstat23, readdir as readdir76, realpath as realpath28, stat as stat44 } from "fs/promises";
208134
208405
  function mimeForPath(filePath) {
208135
208406
  if (filePath.endsWith(".json") || filePath.endsWith(".jsonl")) {
208136
208407
  return "application/json";
@@ -208153,7 +208424,7 @@ async function walkFiles(root, ownerRoot = root, ancestors = new Set) {
208153
208424
  if (!await pathExists(root)) {
208154
208425
  return [];
208155
208426
  }
208156
- const ownerInfo = await lstat22(ownerRoot).catch(() => null);
208427
+ const ownerInfo = await lstat23(ownerRoot).catch(() => null);
208157
208428
  if (!ownerInfo || !ownerInfo.isDirectory() || ownerInfo.isSymbolicLink())
208158
208429
  return [];
208159
208430
  const ownerReal = await realpath28(ownerRoot).catch(() => null);
@@ -224400,7 +224671,7 @@ function describeAvailability(entry, availability) {
224400
224671
  const range = verdict.state === "in-range" ? "within the recorded range" : verdict.state === "below-min" ? `below the recorded minimum ${verdict.min} \u2014 parsing may drift` : verdict.state === "above-max" ? `above the recorded maximum ${verdict.max} \u2014 parsing may drift` : "version banner not recognised";
224401
224672
  return `installed, ${version} (${range}); login not verified \u2014 keryx cannot know`;
224402
224673
  }
224403
- function marker(availability) {
224674
+ function marker2(availability) {
224404
224675
  if (availability.state === "available")
224405
224676
  return style.cyan("\u25CF");
224406
224677
  if (availability.state === "binary-missing")
@@ -224440,7 +224711,7 @@ function renderExternalAgents(rows, capability) {
224440
224711
  lines.push(capability.available ? "capability: enabled" : `capability: unavailable \u2014 ${capability.reason ?? "no reason given"}`);
224441
224712
  lines.push("");
224442
224713
  for (const row of rows) {
224443
- lines.push(` ${marker(row.availability)} ${row.entry.id} ${row.entry.label}`);
224714
+ lines.push(` ${marker2(row.availability)} ${row.entry.id} ${row.entry.label}`);
224444
224715
  lines.push(` ${describeAvailability(row.entry, row.availability)}`);
224445
224716
  lines.push(` transport: ${transportOf(row.entry)} sandbox: ${row.entry.sandboxModes.join(", ")} streaming: ${row.entry.streamingInput} ` + `resumable: ${row.entry.resumable} reports cost: ${row.entry.reportsCost}`);
224446
224717
  }
@@ -230794,7 +231065,7 @@ var init_types13 = __esm(() => {
230794
231065
  });
230795
231066
 
230796
231067
  // src/bundle/paths.ts
230797
- import { lstat as lstat23, readFile as readFile214 } from "fs/promises";
231068
+ import { lstat as lstat24, readFile as readFile214 } from "fs/promises";
230798
231069
  import path423 from "path";
230799
231070
  function scopeRoot(scope, ctx) {
230800
231071
  if (scope === "user") {
@@ -230988,7 +231259,7 @@ async function resolveContainedPathForWrite(root, relPath) {
230988
231259
  async function refuseSymlinkChain(root, relPath) {
230989
231260
  const rootResolved = path423.resolve(root);
230990
231261
  try {
230991
- const rootStat = await lstat23(rootResolved);
231262
+ const rootStat = await lstat24(rootResolved);
230992
231263
  if (rootStat.isSymbolicLink()) {
230993
231264
  return {
230994
231265
  ok: false,
@@ -231001,7 +231272,7 @@ async function refuseSymlinkChain(root, relPath) {
231001
231272
  for (const segment of segments) {
231002
231273
  current = path423.join(current, segment);
231003
231274
  try {
231004
- const stat = await lstat23(current);
231275
+ const stat = await lstat24(current);
231005
231276
  if (stat.isSymbolicLink()) {
231006
231277
  return {
231007
231278
  ok: false,
@@ -231051,7 +231322,7 @@ var init_paths5 = __esm(() => {
231051
231322
  });
231052
231323
 
231053
231324
  // src/bundle/archive.ts
231054
- import { readdir as readdir79, readFile as readFile215, lstat as lstat24 } from "fs/promises";
231325
+ import { readdir as readdir79, readFile as readFile215, lstat as lstat25 } from "fs/promises";
231055
231326
  import path424 from "path";
231056
231327
  import { gzipSync, gunzipSync } from "zlib";
231057
231328
  function resolveLimits(limits) {
@@ -231259,7 +231530,7 @@ async function openBundle(bundlePath, limits) {
231259
231530
  const resolved = resolveLimits(limits);
231260
231531
  let stat;
231261
231532
  try {
231262
- stat = await lstat24(bundlePath);
231533
+ stat = await lstat25(bundlePath);
231263
231534
  } catch {
231264
231535
  return { ok: false, refusal: { reason: BUNDLE_REFUSAL.notABundle, message: `no such path: ${bundlePath}` } };
231265
231536
  }
@@ -231273,7 +231544,7 @@ async function openBundle(bundlePath, limits) {
231273
231544
  for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
231274
231545
  const abs = path424.join(dir, entry.name);
231275
231546
  const rel = relPrefix.length > 0 ? `${relPrefix}/${entry.name}` : entry.name;
231276
- const entryStat = await lstat24(abs);
231547
+ const entryStat = await lstat25(abs);
231277
231548
  if (entryStat.isSymbolicLink()) {
231278
231549
  return { reason: BUNDLE_REFUSAL.symlinkRefused, path: rel, message: `${rel} is a symlink; bundles may not contain symlinks` };
231279
231550
  }
@@ -232148,7 +232419,7 @@ var init_learned_pattern_schema = __esm(() => {
232148
232419
 
232149
232420
  // src/bundle/export.ts
232150
232421
  import { execFileSync as execFileSync3 } from "child_process";
232151
- import { mkdir as mkdir89, readdir as readdir80, readFile as readFile216, writeFile as writeFile63, lstat as lstat25 } from "fs/promises";
232422
+ import { mkdir as mkdir89, readdir as readdir80, readFile as readFile216, writeFile as writeFile63, lstat as lstat26 } from "fs/promises";
232152
232423
  import { existsSync as existsSync78 } from "fs";
232153
232424
  import path425 from "path";
232154
232425
  function globToRegExp6(glob) {
@@ -232235,7 +232506,7 @@ async function walkFiles2(dir, relPrefix, out, skipped) {
232235
232506
  for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
232236
232507
  const abs = path425.join(dir, entry.name);
232237
232508
  const rel = relPrefix.length > 0 ? `${relPrefix}/${entry.name}` : entry.name;
232238
- const st = await lstat25(abs);
232509
+ const st = await lstat26(abs);
232239
232510
  if (st.isSymbolicLink()) {
232240
232511
  skipped.push({ path: rel, reason: "symlink" });
232241
232512
  continue;
@@ -232283,7 +232554,7 @@ async function collectAgents(root, skipped) {
232283
232554
  const candidates = [];
232284
232555
  for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
232285
232556
  const abs = path425.join(base, entry.name);
232286
- const st = await lstat25(abs);
232557
+ const st = await lstat26(abs);
232287
232558
  if (st.isSymbolicLink()) {
232288
232559
  skipped.push({ path: `agents/${entry.name}`, reason: "symlink" });
232289
232560
  continue;
@@ -232308,7 +232579,7 @@ async function collectMemory(root, skipped) {
232308
232579
  }
232309
232580
  for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
232310
232581
  const abs = path425.join(base, entry.name);
232311
- const st = await lstat25(abs);
232582
+ const st = await lstat26(abs);
232312
232583
  if (st.isSymbolicLink()) {
232313
232584
  skipped.push({ path: `memory/${folder}/${entry.name}`, reason: "symlink" });
232314
232585
  continue;
@@ -232324,7 +232595,7 @@ async function collectHookConfig(root) {
232324
232595
  const abs = path425.join(root, "hooks.json");
232325
232596
  if (!existsSync78(abs))
232326
232597
  return [];
232327
- const st = await lstat25(abs);
232598
+ const st = await lstat26(abs);
232328
232599
  if (st.isSymbolicLink() || !st.isFile())
232329
232600
  return [];
232330
232601
  return [{ bundlePath: "hooks.json", kind: "hook-config", absolutePath: abs }];
@@ -232705,7 +232976,7 @@ var init_applied_state = __esm(() => {
232705
232976
  });
232706
232977
 
232707
232978
  // src/lib/private-dir.ts
232708
- import { lstat as lstat26, mkdir as mkdir90, open as open12, readFile as readFile218 } from "fs/promises";
232979
+ import { lstat as lstat27, mkdir as mkdir90, open as open12, readFile as readFile218 } from "fs/promises";
232709
232980
  import path427 from "path";
232710
232981
  async function checkPrivateDirGitignore(dir, root) {
232711
232982
  if (root !== undefined) {
@@ -232724,7 +232995,7 @@ async function checkPrivateDirGitignore(dir, root) {
232724
232995
  }
232725
232996
  let dirStats;
232726
232997
  try {
232727
- dirStats = await lstat26(dir);
232998
+ dirStats = await lstat27(dir);
232728
232999
  } catch (error) {
232729
233000
  if (isNotFound(error)) {
232730
233001
  return { ok: true, action: "create" };
@@ -232745,7 +233016,7 @@ async function checkPrivateDirGitignore(dir, root) {
232745
233016
  const gitignorePath = path427.join(dir, ".gitignore");
232746
233017
  let stats;
232747
233018
  try {
232748
- stats = await lstat26(gitignorePath);
233019
+ stats = await lstat27(gitignorePath);
232749
233020
  } catch (error) {
232750
233021
  if (isNotFound(error)) {
232751
233022
  return { ok: true, action: "create" };