@mrciphersmith/keryx 0.3.124 → 0.3.126

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
@@ -19499,7 +19499,7 @@ var init_skill_length_ceilings = __esm(() => {
19499
19499
  "review/review-jev-scenarios": 182,
19500
19500
  "review/review-layout": 237,
19501
19501
  "review/review-logic": 365,
19502
- "review/review-orchestrator": 1572,
19502
+ "review/review-orchestrator": 1571,
19503
19503
  "review/review-performance": 354,
19504
19504
  "review/review-pr-contract": 191,
19505
19505
  "review/review-pr-feedback": 893,
@@ -23312,7 +23312,7 @@ var package_default;
23312
23312
  var init_package = __esm(() => {
23313
23313
  package_default = {
23314
23314
  name: "@mrciphersmith/keryx",
23315
- version: "0.3.124",
23315
+ version: "0.3.126",
23316
23316
  description: "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
23317
23317
  private: false,
23318
23318
  publishConfig: {
@@ -36527,6 +36527,19 @@ function leadingYamlBlock(markdown) {
36527
36527
  `, close + 4);
36528
36528
  return markdown.slice(0, lineEnd < 0 ? markdown.length : lineEnd + 1);
36529
36529
  }
36530
+ function unwrapFencedPage(markdown) {
36531
+ const source = toLf(markdown);
36532
+ const meta = leadingYamlBlock(source);
36533
+ if (meta === null)
36534
+ return markdown;
36535
+ const body = source.slice(meta.length);
36536
+ const unwrapped = unwrapFencedReply(body);
36537
+ if (unwrapped === body)
36538
+ return markdown;
36539
+ const innerMeta = leadingYamlBlock(unwrapped);
36540
+ const inner = innerMeta === null ? unwrapped : unwrapped.slice(innerMeta.length);
36541
+ return inEolOf(markdown, `${meta}${inner.replace(/^\n+/, "")}`);
36542
+ }
36530
36543
  function preserveEnrichInvariants(original, enriched, entryNote) {
36531
36544
  const source = toLf(original);
36532
36545
  let next = toLf(enriched);
@@ -36569,7 +36582,8 @@ function isDeclaredGenerated(heading) {
36569
36582
  return isReferenceHeading(heading) || /^related wiki$/i.test(heading);
36570
36583
  }
36571
36584
  function mergeGeneratedSections(existing, generated, entryNote) {
36572
- const source = toLf(existing);
36585
+ const original = toLf(existing);
36586
+ const source = toLf(unwrapFencedPage(original));
36573
36587
  const generatedLf = toLf(generated);
36574
36588
  const fresh = new Map(sectionsOf(generatedLf).map((section) => [section.heading.toLowerCase(), section]));
36575
36589
  const enriched = sectionsOf(source).some((section) => !fresh.has(section.heading.toLowerCase()) && !/^changelog$/i.test(section.heading));
@@ -36620,8 +36634,8 @@ ${replacement.lines.join(`
36620
36634
  }
36621
36635
  }
36622
36636
  const repaired = repairVersionAndOrder(next);
36623
- if (next === source)
36624
- return repaired === source ? existing : inEolOf(existing, repaired);
36637
+ if (next === original)
36638
+ return repaired === original ? existing : inEolOf(existing, repaired);
36625
36639
  const version = nextPageVersion(repaired);
36626
36640
  return inEolOf(existing, prependChangelogEntry(setPageVersion(repaired, version), `- ${version} - ${entryNote}`));
36627
36641
  }
@@ -43752,12 +43766,23 @@ function cardPressFields(source) {
43752
43766
  ...source?.pressedAt === undefined ? {} : { pressedAt: source.pressedAt }
43753
43767
  };
43754
43768
  }
43755
- var MANAGED_REVIEW_MODES, REVIEW_TARGET_KINDS, REVIEW_PACKAGE_STATUSES, REVIEW_COVERAGE_STATUSES, REVIEW_FINDING_SEVERITIES, REVIEW_FINDING_CONFIDENCES, FINDING_DISPOSITION_STATES, FINDING_DISMISSAL_STATES, EXTERNAL_TERMINAL_DISPOSITIONS, VERIFICATION_VERDICTS, VERIFICATION_METHODS, REASONING_CAPPED_VERDICT = "unverifiable", VERIFICATION_MODES, DEFAULT_VERIFICATION_MODE = "annotate", PREMISE_VERDICTS, HELD_PREMISE_VERDICTS;
43769
+ function isHostQuestion(value) {
43770
+ if (value === null || typeof value !== "object")
43771
+ return false;
43772
+ const hq = value;
43773
+ const filled = (field) => typeof field === "string" && field.trim() !== "";
43774
+ return HOST_QUESTION_SOURCES.includes(hq["source"]) && filled(hq["ref"]) && filled(hq["questionText"]) && filled(hq["answerText"]) && filled(hq["time"]);
43775
+ }
43776
+ function hostQuestionFields(source) {
43777
+ return source?.hostQuestion === undefined ? {} : { hostQuestion: source.hostQuestion };
43778
+ }
43779
+ var MANAGED_REVIEW_MODES, REVIEW_TARGET_KINDS, REVIEW_PACKAGE_STATUSES, REVIEW_COVERAGE_STATUSES, HOST_QUESTION_SOURCES, REVIEW_FINDING_SEVERITIES, REVIEW_FINDING_CONFIDENCES, FINDING_DISPOSITION_STATES, FINDING_DISMISSAL_STATES, EXTERNAL_TERMINAL_DISPOSITIONS, VERIFICATION_VERDICTS, VERIFICATION_METHODS, REASONING_CAPPED_VERDICT = "unverifiable", VERIFICATION_MODES, DEFAULT_VERIFICATION_MODE = "annotate", PREMISE_VERDICTS, HELD_PREMISE_VERDICTS;
43756
43780
  var init_types4 = __esm(() => {
43757
43781
  MANAGED_REVIEW_MODES = ["attach-review", "review-flow", "ingest"];
43758
43782
  REVIEW_TARGET_KINDS = ["pr", "issue", "branch", "path", "report"];
43759
43783
  REVIEW_PACKAGE_STATUSES = ["draft", "reviewed", "decided", "learned", "closed"];
43760
43784
  REVIEW_COVERAGE_STATUSES = ["run", "skipped", "failed", "needs_context"];
43785
+ HOST_QUESTION_SOURCES = ["claude-code", "codex", "keryx-shell", "helyx-poll", "other"];
43761
43786
  REVIEW_FINDING_SEVERITIES = ["blocker", "major", "minor", "info"];
43762
43787
  REVIEW_FINDING_CONFIDENCES = ["high", "medium", "low"];
43763
43788
  FINDING_DISPOSITION_STATES = [
@@ -46688,7 +46713,7 @@ function resultOf(record) {
46688
46713
  flow: record.flow
46689
46714
  };
46690
46715
  }
46691
- function findOpenTwin(records, hash, session, now) {
46716
+ function findOpenTwin(records, hash, session, now, host) {
46692
46717
  const answered = new Set;
46693
46718
  for (const r of records)
46694
46719
  if (r.kind === "answer")
@@ -46696,6 +46721,8 @@ function findOpenTwin(records, hash, session, now) {
46696
46721
  for (const r of records) {
46697
46722
  if (r.kind !== "open" || r.backfilled === true || answered.has(r.id))
46698
46723
  continue;
46724
+ if (r.hostQuestion?.source !== host?.source || r.hostQuestion?.ref !== host?.ref || r.hostQuestion?.verb !== host?.verb)
46725
+ continue;
46699
46726
  if ((r.session ?? undefined) !== (session !== undefined && session.length > 0 ? session : undefined))
46700
46727
  continue;
46701
46728
  const age = now.getTime() - Date.parse(r.at);
@@ -46730,14 +46757,14 @@ async function openDecision(input) {
46730
46757
  }
46731
46758
  async function openLocked(input, now, random, question, ids, recommendation) {
46732
46759
  const records = await readRecords(input.cwd);
46733
- const twin = findOpenTwin(records, questionHash(question, [...ids]), input.session, now);
46760
+ const twin = findOpenTwin(records, questionHash(question, [...ids]), input.session, now, input.hostQuestion);
46734
46761
  if (twin !== undefined)
46735
46762
  return resultOf(twin);
46736
46763
  const config = await loadDecisionsConfig(input.cwd);
46737
46764
  const tagged = input.action !== undefined && input.action.trim().length > 0;
46738
46765
  const irreversible = input.irreversible === true || tagged || isIrreversible(config.irreversible, question, input.action, input.options);
46739
46766
  const salt = input.salt ?? await loadRepoSalt(input.cwd);
46740
- const seq = input.seq ?? records.filter((r) => r.kind === "open").length + 1;
46767
+ const seq = input.seq ?? records.filter((r) => r.kind === "open" && r.hostQuestion === undefined).length + 1;
46741
46768
  const choice = chooseArm({
46742
46769
  salt,
46743
46770
  seq,
@@ -46786,6 +46813,7 @@ async function openLocked(input, now, random, question, ids, recommendation) {
46786
46813
  ...tagged ? { action: input.action?.trim() ?? "" } : {},
46787
46814
  ...blindRefused ? { blindRefused: true } : {},
46788
46815
  ...input.source !== undefined && input.source.trim().length > 0 ? { source: oneLine(input.source) } : {},
46816
+ ...input.hostQuestion === undefined ? {} : { hostQuestion: { source: oneLine(input.hostQuestion.source), ref: oneLine(input.hostQuestion.ref), verb: oneLine(input.hostQuestion.verb), answerText: storedOperatorText(input.hostQuestion.answerText) } },
46789
46817
  ...input.session !== undefined && input.session.length > 0 ? { session: input.session } : {}
46790
46818
  };
46791
46819
  await appendRecord(input.cwd, record);
@@ -46910,7 +46938,8 @@ var init_sources = __esm(() => {
46910
46938
  roundLimit: "round-limit",
46911
46939
  wikiEnrich: "tui-wiki-enrich",
46912
46940
  queueRoute: "tui-queue-route",
46913
- sessionLease: "tui-session-lease"
46941
+ sessionLease: "tui-session-lease",
46942
+ hostQuestion: "host-question"
46914
46943
  };
46915
46944
  WORK_DECISION_SOURCES = Object.values(DECISION_SOURCES);
46916
46945
  });
@@ -47393,6 +47422,14 @@ var init_quality = __esm(() => {
47393
47422
  CHILD_ENV_KEYS = ["PATH", "HOME", "LANG", "LC_ALL", "TMPDIR"];
47394
47423
  });
47395
47424
 
47425
+ // src/decisions/types.ts
47426
+ function isHostQuestionEntry(record) {
47427
+ return record !== null && record !== undefined && record.hostQuestion !== undefined && record.hostQuestion !== null;
47428
+ }
47429
+ function hostQuestionLabel(host) {
47430
+ return `answered via host question (${host.source}:${host.ref})`;
47431
+ }
47432
+
47396
47433
  // src/decisions/report.ts
47397
47434
  function emptyTally() {
47398
47435
  return { answered: 0, matched: 0 };
@@ -47567,6 +47604,22 @@ function buildLegacy(opens, firstOf) {
47567
47604
  out.matchShare = out.matchTally.answered === 0 ? null : out.matchTally.matched / out.matchTally.answered;
47568
47605
  return out;
47569
47606
  }
47607
+ function hostQuestionRow(open, answered) {
47608
+ const latest = [...answered].sort((a, b) => a.seq - b.seq).pop();
47609
+ const host = open.hostQuestion;
47610
+ return {
47611
+ id: open.id,
47612
+ label: hostQuestionLabel({ source: host?.source ?? "", ref: host?.ref ?? "" }),
47613
+ verb: host?.verb ?? "",
47614
+ question: open.question,
47615
+ options: open.options.map((option) => ({ id: option.id, label: option.label })),
47616
+ choice: latest?.choice ?? "",
47617
+ answerText: host?.answerText ?? "",
47618
+ source: host?.source ?? "",
47619
+ ref: host?.ref ?? "",
47620
+ at: open.at
47621
+ };
47622
+ }
47570
47623
  function buildReport2(records, skipped = 0, options = {}) {
47571
47624
  const opens = [];
47572
47625
  const answers = new Map;
@@ -47588,7 +47641,9 @@ function buildReport2(records, skipped = 0, options = {}) {
47588
47641
  }
47589
47642
  const excludeLegacy = options.excludeLegacy === true;
47590
47643
  const settings = options.settings ?? defaultSettings();
47591
- const kept = excludeLegacy ? opens.filter((open) => open.backfilled !== true && !isLegacy(open)) : opens;
47644
+ const hostOpens = opens.filter((open) => isHostQuestionEntry(open));
47645
+ const drawn = opens.filter((open) => !isHostQuestionEntry(open));
47646
+ const kept = excludeLegacy ? drawn.filter((open) => open.backfilled !== true && !isLegacy(open)) : drawn;
47592
47647
  const live = kept.filter((open) => open.backfilled !== true);
47593
47648
  const history = kept.filter((open) => open.backfilled === true);
47594
47649
  const firstAnswer = (id) => [...answers.get(id) ?? []].sort((a, b) => a.seq - b.seq)[0];
@@ -47627,6 +47682,7 @@ function buildReport2(records, skipped = 0, options = {}) {
47627
47682
  excludeLegacy,
47628
47683
  quality: buildQualityMatrix(options.quality ?? [], known),
47629
47684
  reasons: buildReasons(compared, headline, firstAnswer, reasons),
47685
+ hostQuestions: hostOpens.map((open) => hostQuestionRow(open, answers.get(open.id) ?? [])),
47630
47686
  ineligible: {
47631
47687
  decisions: randomized.filter((open) => !recordEligible(open)).length,
47632
47688
  answered: randomized.filter((open) => !recordEligible(open) && firstAnswer(open.id) !== undefined).length
@@ -47807,15 +47863,40 @@ function renderLegacy(report) {
47807
47863
  function seconds(ms) {
47808
47864
  return `${(ms / 1000).toFixed(1)}s`;
47809
47865
  }
47866
+ function renderHostQuestions(report) {
47867
+ const rows = report.hostQuestions ?? [];
47868
+ if (rows.length === 0)
47869
+ return [];
47870
+ const lines = [`Host questions (asked through the host's own tool, recorded by a keryx review verb; counted apart from the decisions above, in no arm or match number): ${rows.length}`];
47871
+ for (const row of rows) {
47872
+ lines.push(` ${oneLine(row.id)} [host-question, ${oneLine(row.label)}, ${oneLine(row.verb)}, ${oneLine(row.at)}]`);
47873
+ lines.push(` ${oneLine(row.question, FULL_TEXT)}`);
47874
+ lines.push(` options: ${row.options.map((option) => oneLine(option.id)).join(", ")}; chose ${row.choice === "" ? "(not answered)" : oneLine(row.choice)}`);
47875
+ lines.push(` answer: ${oneLine(row.answerText, FULL_TEXT)}`);
47876
+ }
47877
+ return lines;
47878
+ }
47810
47879
  function renderReport(report) {
47811
47880
  const skippedLine = report.skipped > 0 ? `Skipped ${report.skipped} unreadable journal record${report.skipped === 1 ? "" : "s"}.` : undefined;
47812
47881
  const configLines = report.settings.invalid.length > 0 ? renderSettings(report.settings) : [];
47882
+ const hostLines = renderHostQuestions(report);
47813
47883
  if (report.total === 0 && report.backfilled.total === 0) {
47884
+ if (hostLines.length > 0) {
47885
+ const lines = ["No live decisions recorded yet.", "", ...hostLines];
47886
+ if (configLines.length > 0)
47887
+ lines.push("", ...configLines);
47888
+ if (skippedLine !== undefined)
47889
+ lines.push("", skippedLine);
47890
+ return lines.join(`
47891
+ `);
47892
+ }
47814
47893
  return ["No decisions recorded yet.", ...configLines, ...skippedLine === undefined ? [] : [skippedLine]].join(`
47815
47894
  `);
47816
47895
  }
47817
47896
  if (report.total === 0) {
47818
47897
  const lines = ["No live decisions recorded yet.", "", ...renderBackfilled(report.backfilled)];
47898
+ if (hostLines.length > 0)
47899
+ lines.push("", ...hostLines);
47819
47900
  if (configLines.length > 0)
47820
47901
  lines.push("", ...configLines);
47821
47902
  if (skippedLine !== undefined)
@@ -47880,6 +47961,8 @@ function renderReport(report) {
47880
47961
  if (report.inferredFlow > 0) {
47881
47962
  lines.push("", `Flow attribution inferred (the one flow in progress, not named by KERYX_FLOW or the branch): ${report.inferredFlow} decision${report.inferredFlow === 1 ? "" : "s"}.`);
47882
47963
  }
47964
+ if (hostLines.length > 0)
47965
+ lines.push("", ...hostLines);
47883
47966
  if (report.backfilled.total > 0)
47884
47967
  lines.push("", ...renderBackfilled(report.backfilled));
47885
47968
  if (skippedLine !== undefined)
@@ -47918,7 +48001,8 @@ function toUtcIso(value) {
47918
48001
  return ISO_OFFSET.test(value) ? new Date(at).toISOString() : null;
47919
48002
  }
47920
48003
  function exportSummaryLine(summary) {
47921
- return `Export: ${summary.rows} decision${summary.rows === 1 ? "" : "s"}, ${summary.skipped} skipped, ${summary.blanked} invalid field${summary.blanked === 1 ? "" : "s"} blanked.`;
48004
+ const line = `Export: ${summary.rows} decision${summary.rows === 1 ? "" : "s"}, ${summary.skipped} skipped, ${summary.blanked} invalid field${summary.blanked === 1 ? "" : "s"} blanked.`;
48005
+ return summary.hostQuestions > 0 ? `${line} ${summary.hostQuestions} answered via host question, not exported as rows.` : line;
47922
48006
  }
47923
48007
 
47924
48008
  class Validator {
@@ -48002,7 +48086,14 @@ function buildExportWithSummary(records, quality = [], options = {}) {
48002
48086
  }
48003
48087
  }
48004
48088
  const rows = [];
48089
+ let hostQuestions = 0;
48005
48090
  for (const open of opens) {
48091
+ if (isHostQuestionEntry(open)) {
48092
+ const at = toUtcIso(open.at);
48093
+ if (options.since === undefined || at !== null && Date.parse(at) >= options.since.getTime())
48094
+ hostQuestions += 1;
48095
+ continue;
48096
+ }
48006
48097
  const arm = exportArm(open);
48007
48098
  if (arm === null || !Array.isArray(open.options) || !Array.isArray(open.order)) {
48008
48099
  check.skipped += 1;
@@ -48047,7 +48138,7 @@ function buildExportWithSummary(records, quality = [], options = {}) {
48047
48138
  ratings: ratingsOf(open.id, quality, check)
48048
48139
  });
48049
48140
  }
48050
- return { rows, summary: { rows: rows.length, skipped: check.skipped, blanked: check.blanked } };
48141
+ return { rows, summary: { rows: rows.length, skipped: check.skipped, blanked: check.blanked, hostQuestions } };
48051
48142
  }
48052
48143
  function ratingsOf(id, quality, check) {
48053
48144
  const out = [];
@@ -48473,7 +48564,7 @@ async function latestAnsweredDecision(cwd, flow) {
48473
48564
  const answered = new Set(records.filter((r) => r.kind === "answer").map((r) => r.id));
48474
48565
  for (let i = records.length - 1;i >= 0; i -= 1) {
48475
48566
  const record = records[i];
48476
- if (record !== undefined && record.kind === "open" && record.backfilled !== true && answered.has(record.id) && (flow === undefined || record.flow === flow))
48567
+ if (record !== undefined && record.kind === "open" && record.backfilled !== true && !isHostQuestionEntry(record) && answered.has(record.id) && (flow === undefined || record.flow === flow))
48477
48568
  return record.id;
48478
48569
  }
48479
48570
  return;
@@ -48559,6 +48650,9 @@ async function reportLine(cwd) {
48559
48650
  async function decisionCount(cwd) {
48560
48651
  return (await loadReport(cwd)).total;
48561
48652
  }
48653
+ async function hostQuestionCount(cwd) {
48654
+ return (await loadReport(cwd)).hostQuestions?.length ?? 0;
48655
+ }
48562
48656
  async function annotatedCount(cwd) {
48563
48657
  return (await loadReport(cwd)).annotated.length;
48564
48658
  }
@@ -48932,17 +49026,20 @@ function validPremiseSubject(value) {
48932
49026
  const subject = value;
48933
49027
  return text(subject.severity) && text(subject.verdict) && text(subject.problemSha256);
48934
49028
  }
49029
+ function viaConsistent(d) {
49030
+ return d.confirmedVia === "host-question" ? isHostQuestion(d.hostQuestion) : true;
49031
+ }
48935
49032
  function validPremise(entry) {
48936
49033
  if (entry === null || typeof entry !== "object")
48937
49034
  return false;
48938
49035
  const d = entry;
48939
- return d.kind === PREMISE_DECISION_KIND && text(d.finding) && (d.answer === "uphold" || d.answer === "dismiss") && text(d.reason) && text(d.quote) && text(d.at) && text(d.actor) && VIAS.includes(d.confirmedVia) && validPremiseSubject(d.subject);
49036
+ return d.kind === PREMISE_DECISION_KIND && text(d.finding) && (d.answer === "uphold" || d.answer === "dismiss") && text(d.reason) && text(d.quote) && text(d.at) && text(d.actor) && VIAS.includes(d.confirmedVia) && viaConsistent(d) && validPremiseSubject(d.subject);
48940
49037
  }
48941
49038
  function validGateDecision(entry) {
48942
49039
  if (entry === null || typeof entry !== "object")
48943
49040
  return false;
48944
49041
  const d = entry;
48945
- return OPERATOR_DECISION_KINDS.includes(d.kind) && text(d.quote) && text(d.reason) && VIAS.includes(d.confirmedVia) && text(d.at) && (d.subject === undefined || validSubject(d.subject));
49042
+ return OPERATOR_DECISION_KINDS.includes(d.kind) && text(d.quote) && text(d.reason) && VIAS.includes(d.confirmedVia) && viaConsistent(d) && text(d.at) && (d.subject === undefined || validSubject(d.subject));
48946
49043
  }
48947
49044
  function validDecision(entry) {
48948
49045
  return validGateDecision(entry) || validPremise(entry);
@@ -49001,7 +49098,8 @@ async function recordPremiseDecisionAfter(packageDir, input, applyFirst) {
49001
49098
  confirmedVia: input.confirmedVia,
49002
49099
  ...cardPressFields(input),
49003
49100
  subject: input.subject,
49004
- ...input.carriedFrom === undefined ? {} : { carriedFrom: input.carriedFrom }
49101
+ ...input.carriedFrom === undefined ? {} : { carriedFrom: input.carriedFrom },
49102
+ ...hostQuestionFields(input)
49005
49103
  };
49006
49104
  if (!validPremise(kept)) {
49007
49105
  throw new Error("a premise decision needs the finding, an answer of uphold or dismiss, a reason, the operator's words, the time, how it was confirmed and the finding it was shown");
@@ -49045,9 +49143,9 @@ async function recordOperatorDecision(packageDir, decision) {
49045
49143
  }
49046
49144
  function decisionWriters(kind, packageArg = "<review-dir>") {
49047
49145
  if (kind === PREMISE_DECISION_KIND) {
49048
- return `in keryx shell, call the review_decide_premise tool (the operator answers a card); in a terminal, run \`keryx review decide-premise --package ${packageArg} --finding <id> --answer uphold|dismiss --reason "<why>"\``;
49146
+ return `in keryx shell, call the review_decide_premise tool (the operator answers a card); elsewhere ask the operator with the host's question tool, or in a terminal, run \`keryx review decide-premise --package ${packageArg} --finding <id> --answer uphold|dismiss --reason "<why>"\` (from a host question add --answered-by <source>:<ref> --answer-text "<their words>" --question-text "<the question>")`;
49049
49147
  }
49050
- return kind === "mutation-waiver" ? `in keryx shell, call the review_waive_mutation tool (the operator answers a card); in a terminal, run \`keryx review waive-mutation --package ${packageArg} --reason "<why>"\`` : `in keryx shell, call the review_override_reviewers tool (the operator answers a card); in a terminal, run \`keryx review override-reviewers --package ${packageArg} --reason "<why>"\``;
49148
+ return kind === "mutation-waiver" ? `in keryx shell, call the review_waive_mutation tool (the operator answers a card); elsewhere ask the operator with the host's question tool, or in a terminal, run \`keryx review waive-mutation --package ${packageArg} --reason "<why>"\` (from a host question add --answered-by <source>:<ref> --answer-text "<their words>" --question-text "<the question>")` : `in keryx shell, call the review_override_reviewers tool (the operator answers a card); elsewhere ask the operator with the host's question tool, or in a terminal, run \`keryx review override-reviewers --package ${packageArg} --reason "<why>"\` (from a host question add --answered-by <source>:<ref> --answer-text "<their words>" --question-text "<the question>")`;
49051
49149
  }
49052
49150
  var OPERATOR_DECISIONS_FILE = "operator-decisions.json", OPERATOR_DECISIONS_LOCK = ".operator-decisions.lock", OPERATOR_DECISION_KINDS, PREMISE_DECISION_KIND = "premise-question", VIAS, PREMISE_DECISION_ACTOR = "operator", text = (value) => typeof value === "string" && value.trim().length > 0;
49053
49151
  var init_operator_decisions = __esm(() => {
@@ -49055,7 +49153,7 @@ var init_operator_decisions = __esm(() => {
49055
49153
  init_json();
49056
49154
  init_types4();
49057
49155
  OPERATOR_DECISION_KINDS = ["mutation-waiver", "reviewer-override"];
49058
- VIAS = ["tui-card", "remote-card", "terminal"];
49156
+ VIAS = ["tui-card", "remote-card", "terminal", "host-question"];
49059
49157
  });
49060
49158
 
49061
49159
  // src/review/mutate.ts
@@ -74403,7 +74501,8 @@ function applyPremiseDecision(finding, decision) {
74403
74501
  quote: decision.quote,
74404
74502
  at: decision.at,
74405
74503
  actor: decision.actor,
74406
- confirmedVia: decision.confirmedVia
74504
+ confirmedVia: decision.confirmedVia,
74505
+ ...hostQuestionFields(decision)
74407
74506
  };
74408
74507
  const { disposition: existing, ...rest } = record;
74409
74508
  const current = isRecord9(existing) ? existing : undefined;
@@ -74412,7 +74511,7 @@ function applyPremiseDecision(finding, decision) {
74412
74511
  return {
74413
74512
  ...ownDismissal ? rest : record,
74414
74513
  premise: { ...premise, operator },
74415
- ...decision.answer === "dismiss" && open ? { disposition: { state: "dismissed-incorrect", evidence: `operator (${decision.confirmedVia}, ${decision.at}) on the premise question: ${decision.reason}` } } : {}
74514
+ ...decision.answer === "dismiss" && open ? { disposition: { state: "dismissed-incorrect", evidence: `operator (${decision.confirmedVia}${decision.hostQuestion === undefined ? "" : ` ${decision.hostQuestion.source}:${decision.hostQuestion.ref}`}, ${decision.at}) on the premise question: ${decision.reason}` } } : {}
74416
74515
  };
74417
74516
  }
74418
74517
  function applyPremiseDecisions(findings, decisions) {
@@ -82100,10 +82199,43 @@ var init_patch_risk = __esm(() => {
82100
82199
  NEW_FILE_HEADER = /^\+\+\+ (.+)$/;
82101
82200
  });
82102
82201
 
82202
+ // src/review/host-question-verbs.ts
82203
+ var HOST_QUESTION_VERBS;
82204
+ var init_host_question_verbs = __esm(() => {
82205
+ HOST_QUESTION_VERBS = ["waive-mutation", "override-reviewers", "confirm-plan", "decide-premise"];
82206
+ });
82207
+
82103
82208
  // src/trigger/unattended.ts
82104
82209
  function isUnattendedExcludedTool(toolName) {
82105
82210
  return UNATTENDED_EXCLUDED_TOOLS.includes(toolName) || OPERATOR_CONFIRM_TOOL_NAMES.includes(toolName);
82106
82211
  }
82212
+ function recordsHostQuestion(text, depth = 0) {
82213
+ const joined = text.replace(/\\\r?\n/g, "").replace(/\$(?=["'])/g, "");
82214
+ const segments = splitSegments(joined);
82215
+ const feedsShell = segments.some((s) => s.precededBy === "|" && SHELL_WRAPPERS.has(commandWord(s.words)));
82216
+ const inner = (words) => depth < 3 && words.some((w) => /\s/.test(w) && recordsHostQuestion(w, depth + 1));
82217
+ for (const segment of segments) {
82218
+ const words = stripAssignments(segment.words).map((w) => w.replace(/\\/g, "").replace(/^[$(`{]+/, ""));
82219
+ const cmd = commandWord(words);
82220
+ if (PRINTING_COMMANDS.has(cmd)) {
82221
+ if (feedsShell && inner(words))
82222
+ return true;
82223
+ continue;
82224
+ }
82225
+ if (SHELL_WRAPPERS.has(cmd) && inner(words))
82226
+ return true;
82227
+ const reviewAt = words.findIndex((w, i) => w === "review" && words.slice(0, i).some((p) => KERYX_ENTRY.test(p)));
82228
+ if (reviewAt < 0)
82229
+ continue;
82230
+ const args = words.slice(reviewAt + 1);
82231
+ const verbAt = args.findIndex((w) => !w.startsWith("-"));
82232
+ if (verbAt < 0 || !HOST_QUESTION_VERB_SET.has(args[verbAt]))
82233
+ continue;
82234
+ if (args.slice(verbAt + 1).some((w) => w === "--answered-by" || w.startsWith("--answered-by=")))
82235
+ return true;
82236
+ }
82237
+ return false;
82238
+ }
82107
82239
  function isUnattendedProtectedPath(p) {
82108
82240
  const normalized = p.replace(/\\/g, "/").trim();
82109
82241
  return /(^|\/)flow\.json$/.test(normalized) || /(^|\/)acceptance-criteria\.md$/.test(normalized) || /(^|\/)\.metaproject\/triggers\.json$/.test(normalized) || /(^|\/)\.metaproject\/data\/trigger(\/|$)/.test(normalized) || /confirm-token\.json$/.test(normalized);
@@ -82112,6 +82244,9 @@ function unattendedShellRefusal(command) {
82112
82244
  const text = command.trim();
82113
82245
  if (text.length === 0)
82114
82246
  return;
82247
+ if (recordsHostQuestion(text)) {
82248
+ return "`--answered-by` records the operator's answer to a question the host asked \u2014 an unattended run has no operator to ask, so it never records one";
82249
+ }
82115
82250
  for (const pattern of FORBIDDEN_FLOW_VERBS) {
82116
82251
  const match = pattern.exec(text);
82117
82252
  if (match !== null) {
@@ -82186,9 +82321,10 @@ function unattendedRefusal(toolName, input) {
82186
82321
  }
82187
82322
  return;
82188
82323
  }
82189
- var UNATTENDED_EXCLUDED_TOOLS, OPERATOR_CONFIRM_TOOL_NAMES, FORBIDDEN_FLOW_VERBS, FORBIDDEN_GIT_VERBS, FORBIDDEN_GIT_BRANCH_FLAGS, GH_API_BODY_FLAGS, RAW_GIT, RAW_PUBLISH, PROTECTED_TEXT_MARKERS;
82324
+ var UNATTENDED_EXCLUDED_TOOLS, OPERATOR_CONFIRM_TOOL_NAMES, FORBIDDEN_FLOW_VERBS, HOST_QUESTION_VERB_SET, KERYX_ENTRY, SHELL_WRAPPERS, PRINTING_COMMANDS, FORBIDDEN_GIT_VERBS, FORBIDDEN_GIT_BRANCH_FLAGS, GH_API_BODY_FLAGS, RAW_GIT, RAW_PUBLISH, PROTECTED_TEXT_MARKERS;
82190
82325
  var init_unattended = __esm(() => {
82191
82326
  init_patch_risk();
82327
+ init_host_question_verbs();
82192
82328
  init_shell_syntax();
82193
82329
  init_command_risk();
82194
82330
  UNATTENDED_EXCLUDED_TOOLS = [
@@ -82240,6 +82376,10 @@ var init_unattended = __esm(() => {
82240
82376
  /\blaunchctl\b/,
82241
82377
  /\bloginctl\b/
82242
82378
  ];
82379
+ HOST_QUESTION_VERB_SET = new Set(HOST_QUESTION_VERBS);
82380
+ KERYX_ENTRY = /(?:^|\/)(?:keryx|cli\.[cm]?[jt]s)$/;
82381
+ SHELL_WRAPPERS = new Set(["sh", "bash", "zsh", "dash", "ksh", "eval"]);
82382
+ PRINTING_COMMANDS = new Set(["echo", "printf", "grep", "egrep", "fgrep", "rg", "ag", "cat"]);
82243
82383
  FORBIDDEN_GIT_VERBS = new Set(["push", "merge", "tag", "update-ref"]);
82244
82384
  FORBIDDEN_GIT_BRANCH_FLAGS = new Set(["-f", "--force", "-D", "-M", "--delete", "-d", "-m", "--move"]);
82245
82385
  GH_API_BODY_FLAGS = new Set(["-f", "-F", "--field", "--raw-field", "--input"]);
@@ -87745,13 +87885,19 @@ function premiseDecisionsCheck(ctx) {
87745
87885
  if (held.length === 0)
87746
87886
  return ok("no finding is held by the premise judge");
87747
87887
  const awaiting = awaitingOperator(records, decisions);
87748
- if (awaiting.length === 0)
87749
- return ok(`${held.length} held finding(s), every one with an operator decision`);
87888
+ if (awaiting.length === 0) {
87889
+ const hostQuestions = held.flatMap((finding) => {
87890
+ const decision = latestPremiseDecision(finding, decisions);
87891
+ return decision?.confirmedVia === "host-question" && decision.hostQuestion !== undefined ? [`${decision.hostQuestion.source}:${decision.hostQuestion.ref}`] : [];
87892
+ });
87893
+ const viaHost = hostQuestions.length === 0 ? "" : `; ${hostQuestions.length} confirmed via host-question (${hostQuestions.join(", ")}), not by a card`;
87894
+ return ok(`${held.length} held finding(s), every one with an operator decision${viaHost}`);
87895
+ }
87750
87896
  const evidence = `${awaiting.length} of ${held.length} held finding(s) have no operator decision: ${some(awaiting)}`;
87751
87897
  if (!ctx.premise.ingested) {
87752
87898
  return bad(`${evidence} (not ingested yet)`, `ingest the round with its premise results first (\`keryx review ingest ... --premises ${ctx.arg}/research-evidence/premises.json\`), then ask the operator to answer each held finding`);
87753
87899
  }
87754
- return bad(evidence, `ask the operator: in keryx shell call the review_decide_premise tool (a card); in a terminal run keryx review decide-premise --package ${ctx.arg} --finding <id> --answer uphold|dismiss --reason "<why>"`);
87900
+ return bad(evidence, `ask the operator: in keryx shell call the review_decide_premise tool (a card); elsewhere ask with the host's question tool, or in a terminal run keryx review decide-premise --package ${ctx.arg} --finding <id> --answer uphold|dismiss --reason "<why>" (from a host question add --answered-by <source>:<ref> --answer-text "<their words>" --question-text "<the question>")`);
87755
87901
  }
87756
87902
  function quotesLocatedCheck(ctx) {
87757
87903
  const flagged = ctx.findings.filter((finding) => asRecord2(finding.quote_unlocated) !== undefined);
@@ -125655,7 +125801,7 @@ async function carryPremiseDecisions(packageDir, previousDir, carriedIds, carrie
125655
125801
  return carried;
125656
125802
  }
125657
125803
  async function answerPremiseQuestion(packageDir, input, registry = {}) {
125658
- const { decision, applied: vouched } = await recordPremiseDecisionAfter(packageDir, { finding: input.finding.key, answer: input.answer, reason: input.reason, quote: input.quote, at: input.at, confirmedVia: input.confirmedVia, ...cardPressFields(input), subject: input.finding.subject }, (kept) => applyAnswerToFindings(packageDir, kept, registry));
125804
+ const { decision, applied: vouched } = await recordPremiseDecisionAfter(packageDir, { finding: input.finding.key, answer: input.answer, reason: input.reason, quote: input.quote, at: input.at, confirmedVia: input.confirmedVia, ...cardPressFields(input), subject: input.finding.subject, ...input.hostQuestion === undefined ? {} : { hostQuestion: input.hostQuestion } }, (kept) => applyAnswerToFindings(packageDir, kept, registry));
125659
125805
  if (vouched === true)
125660
125806
  reRecordRegistry(packageDir, registry);
125661
125807
  return decision;
@@ -125814,7 +125960,7 @@ function isPlanConfirmation(value) {
125814
125960
  if (value === null || typeof value !== "object" || Array.isArray(value))
125815
125961
  return false;
125816
125962
  const v = value;
125817
- return typeof v.planKey === "string" && v.planKey !== "" && typeof v.dispatches === "number" && Number.isFinite(v.dispatches) && Array.isArray(v.minutes) && v.minutes.length === 2 && v.minutes.every((m) => typeof m === "number" && Number.isFinite(m)) && typeof v.quote === "string" && v.quote.trim() !== "" && typeof v.confirmedVia === "string" && CONFIRMED_VIA.includes(v.confirmedVia) && typeof v.at === "string" && v.at !== "";
125963
+ return typeof v.planKey === "string" && v.planKey !== "" && typeof v.dispatches === "number" && Number.isFinite(v.dispatches) && Array.isArray(v.minutes) && v.minutes.length === 2 && v.minutes.every((m) => typeof m === "number" && Number.isFinite(m)) && typeof v.quote === "string" && v.quote.trim() !== "" && typeof v.confirmedVia === "string" && CONFIRMED_VIA.includes(v.confirmedVia) && (v.confirmedVia !== "host-question" || isHostQuestion(v.hostQuestion)) && typeof v.at === "string" && v.at !== "";
125818
125964
  }
125819
125965
  function readPlanConfirmation(packageDir) {
125820
125966
  try {
@@ -125923,7 +126069,8 @@ var init_estimate = __esm(() => {
125923
126069
  init_slice_routing();
125924
126070
  init_waves();
125925
126071
  init_round_split();
125926
- CONFIRMED_VIA = ["tui-card", "remote-card", "terminal"];
126072
+ init_types4();
126073
+ CONFIRMED_VIA = ["tui-card", "remote-card", "terminal", "host-question"];
125927
126074
  });
125928
126075
 
125929
126076
  // src/review/plan-confirmation.ts
@@ -125937,9 +126084,14 @@ function planKeyOf2(estimate) {
125937
126084
  function estimatePlan(packageDir) {
125938
126085
  return estimatePackage(packageDir);
125939
126086
  }
126087
+ async function readPlanConfirmation2(packageDir) {
126088
+ return readPlanConfirmation(packageDir);
126089
+ }
125940
126090
  async function recordPlanConfirmation(packageDir, estimate, confirmation) {
125941
126091
  if (confirmation.quote.trim() === "")
125942
126092
  throw new Error("a plan confirmation needs the operator's own words");
126093
+ if (confirmation.confirmedVia === "host-question" && confirmation.hostQuestion === undefined)
126094
+ throw new Error("a host-question plan confirmation needs the question and the answer");
125943
126095
  const record = {
125944
126096
  planKey: planKeyOf2(estimate),
125945
126097
  dispatches: estimate.dispatches,
@@ -125947,7 +126099,8 @@ async function recordPlanConfirmation(packageDir, estimate, confirmation) {
125947
126099
  quote: confirmation.quote.trim(),
125948
126100
  confirmedVia: confirmation.confirmedVia,
125949
126101
  ...cardPressFields(confirmation),
125950
- at: confirmation.at
126102
+ at: confirmation.at,
126103
+ ...confirmation.hostQuestion === undefined ? {} : { hostQuestion: confirmation.hostQuestion }
125951
126104
  };
125952
126105
  await writeFileAtomic(path290.join(packageDir, PLAN_CONFIRMATION_FILE), `${JSON.stringify(record, null, 2)}
125953
126106
  `);
@@ -134590,17 +134743,34 @@ function printOperatorCommandHelp(command) {
134590
134743
 
134591
134744
  Records the operator's waiver of the mutation pass in <review-dir>/operator-decisions.json: the review may
134592
134745
  record \`Not run: mutation pass\` with unrun mutants. Asks at the terminal; refused without a TTY. In keryx
134593
- shell the review_waive_mutation tool asks the same as a card.`,
134746
+ shell the review_waive_mutation tool asks the same as a card.
134747
+
134748
+ Without a TTY, record the operator's answer to a question you asked through the host's own question tool:
134749
+ keryx review waive-mutation --package <review-dir> --reason "<why>" --answered-by <source>:<ref> \\
134750
+ --answer-text "<the operator's words>" --question-text "<the question as asked>"
134751
+ ${hostHelpTail("an --answer-text that is not a plain yes (a decline, a negation, the word keep, no yes at all), a blank or oversize text, a missing --reason")}`,
134594
134752
  "override-reviewers": `keryx review override-reviewers --package <review-dir> --reason "<why>" [--selected a,b]
134595
134753
 
134596
134754
  Records the operator's decision to run a smaller reviewer set than \`keryx review required\` names (an
134597
134755
  override or mode diff) in <review-dir>/operator-decisions.json. Asks at the terminal; refused without a TTY.
134598
- In keryx shell the review_override_reviewers tool asks the same as a card.`,
134756
+ In keryx shell the review_override_reviewers tool asks the same as a card.
134757
+
134758
+ Without a TTY, record the operator's answer to a question you asked through the host's own question tool:
134759
+ keryx review override-reviewers --package <review-dir> --reason "<why>" [--selected a,b] --answered-by <source>:<ref> \\
134760
+ --answer-text "<the operator's words>" --question-text "<the question as asked>"
134761
+ ${hostHelpTail("an --answer-text that is not a plain yes (a decline, a negation, the word keep, no yes at all), a blank or oversize text, a missing --reason")}`,
134599
134762
  "confirm-plan": `keryx review confirm-plan --package <review-dir>
134600
134763
 
134601
134764
  Shows the plan's dispatches and minutes (\`keryx review estimate\`) and records the operator's yes in
134602
134765
  <review-dir>/plan-confirmation.json for exactly this plan. Asks at the terminal; refused without a TTY. In
134603
- keryx shell the review_confirm_plan tool asks the same as a card.`,
134766
+ keryx shell the review_confirm_plan tool asks the same as a card.
134767
+
134768
+ Without a TTY, record the operator's answer to a question you asked through the host's own question tool:
134769
+ keryx review confirm-plan --package <review-dir> --answered-by <source>:<ref> \\
134770
+ --answer-text "<the operator's words>" --question-text "<the question as asked>"
134771
+ The question must show this plan's dispatches and minutes (as \`keryx review estimate\` prints them): a question that
134772
+ does not is refused as a stale plan, so ask again with the current numbers.
134773
+ ${hostHelpTail("an --answer-text that is not a plain yes (a decline, a negation, no yes at all), a blank or oversize text")}`,
134604
134774
  "decide-premise": `keryx review decide-premise --package <review-dir> --finding <id> --answer uphold|dismiss --reason "<why>"
134605
134775
 
134606
134776
  Answers the premise question on a finding the premise judge held (verdict challenged or owner-decision) and
@@ -134608,7 +134778,14 @@ records it in <review-dir>/operator-decisions.json, with your typed words, and o
134608
134778
  (so the next round and the published round block read the answer too). uphold returns the finding to kept at its original
134609
134779
  severity; dismiss records it dismissed-incorrect with your reason as the evidence. Refused for an unknown
134610
134780
  finding, a finding that is not held, or a missing reason. Asks at the terminal; refused without a TTY. In
134611
- keryx shell the review_decide_premise tool asks the same as a card.`,
134781
+ keryx shell the review_decide_premise tool asks the same as a card.
134782
+
134783
+ Without a TTY, record the operator's answer to a question you asked through the host's own question tool:
134784
+ keryx review decide-premise --package <review-dir> --finding <id> --answer uphold|dismiss --reason "<why>" \\
134785
+ --answered-by <source>:<ref> --answer-text "<the operator's words>" --question-text "<the question as asked>"
134786
+ The operator's words must name the same choice as --answer (uphold or dismiss, or the same in Russian) and no other;
134787
+ the finding changes as on the terminal path.
134788
+ ${hostHelpTail("words that name the other choice or none, a blank or oversize text, a missing --reason, an unknown or not-held finding")}`,
134612
134789
  "verify-run": `keryx review verify-run --package <review-dir> --finding <id> [--timeout <seconds>] -- <command\u2026>
134613
134790
 
134614
134791
  Runs a verification command in this (the parent's) session and saves its stdout, stderr and exit status to
@@ -134654,6 +134831,9 @@ async function runOperatorCommand(command, args, seams = {}) {
134654
134831
  log("Hand that file to the verifier (context_files) and have it cite the file in the claim's evidence.");
134655
134832
  return 0;
134656
134833
  }
134834
+ if (own.some((arg) => arg === "--answered-by" || arg.startsWith("--answered-by="))) {
134835
+ return recordHostQuestion(command, own, packageDir, { cwd, log, env: seams.env ?? process.env });
134836
+ }
134657
134837
  const isTerminal = seams.isTerminal ?? (() => process.stdin.isTTY === true && process.stdout.isTTY === true);
134658
134838
  requireTerminal(command, isTerminal);
134659
134839
  const ask = seams.ask ?? terminalAsk;
@@ -134710,13 +134890,210 @@ async function runOperatorCommand(command, args, seams = {}) {
134710
134890
  log(`${kind} recorded (terminal, ${at}) in operator-decisions.json`);
134711
134891
  return 0;
134712
134892
  }
134713
- var OPERATOR_COMMANDS, CARD;
134893
+ function parseAnsweredBy(raw, command) {
134894
+ const refuse = (why) => {
134895
+ throw new Error(`--answered-by must be <source>:<ref>: <source> one of ${HOST_QUESTION_SOURCES.join(", ")} and <ref> the host's own id for the question, not empty, at most ${REF_MAX_LENGTH} characters, no control characters` + `${raw === undefined ? " (the flag has no value)" : ` (got ${shortened(raw)}${why})`}. See \`keryx review ${command} --help\`.`);
134896
+ };
134897
+ if (raw === undefined)
134898
+ return refuse("");
134899
+ const colon = raw.indexOf(":");
134900
+ const source = colon <= 0 ? "" : raw.slice(0, colon);
134901
+ const ref = colon <= 0 ? "" : raw.slice(colon + 1).trim();
134902
+ if (!HOST_QUESTION_SOURCES.includes(source) || ref === "")
134903
+ return refuse("");
134904
+ if (ref.length > REF_MAX_LENGTH)
134905
+ return refuse(`: the ref is ${ref.length} characters`);
134906
+ if ([...ref].some((char) => /\p{Cc}/u.test(char)))
134907
+ return refuse(": the ref holds a control character");
134908
+ return { source, ref };
134909
+ }
134910
+ function refuseNonAffirmative(answerText, verb) {
134911
+ const words = wordsOf(answerText);
134912
+ const choices = HOST_OPTIONS[verb];
134913
+ const fix = `Nothing was recorded. Ask the operator again and pass their own words, which must say yes (for example "yes, ${choices.confirm} it") and nothing against it. See \`keryx review ${verb} --help\`.`;
134914
+ if (words.length === 0 || LEAD_DECLINE.has(words[0])) {
134915
+ throw new Error(`--answer-text reads as a decline (${shortened(answerText)}): a declined question records nothing. ${fix}`);
134916
+ }
134917
+ if (words.some((word) => NEGATIONS.has(word)))
134918
+ throw new Error(`--answer-text reads as a decline (${shortened(answerText)}): it holds a negation, so it is not a yes. ${fix}`);
134919
+ if (words.includes(choices.decline))
134920
+ throw new Error(`--answer-text reads as a decline (${shortened(answerText)}): it names the other option, "${choices.decline}". ${fix}`);
134921
+ if (!words.some((word) => AFFIRMATIVE.has(word) || word === choices.confirm || choices.alsoAffirms?.includes(word) === true)) {
134922
+ throw new Error(`--answer-text is not a yes (${shortened(answerText)}): free text that does not say yes is not a decision. ${fix}`);
134923
+ }
134924
+ }
134925
+ function refuseOtherChoice(answerText, answer) {
134926
+ const clauses = answerText.toLowerCase().split(/[,.;:!?()\n]+/u).map(wordsOf).filter((clause) => clause.length > 0);
134927
+ const other = answer === "uphold" ? "dismiss" : "uphold";
134928
+ const chosen = CHOICE_WORDS[answer];
134929
+ const words = clauses.flat();
134930
+ const named = words.some((word) => chosen.has(word));
134931
+ const namesOther = words.some((word) => CHOICE_WORDS[other].has(word));
134932
+ const negated = clauses.some((clause) => clause.some((word, index) => (chosen.has(word) || CHOICE_WORDS[other].has(word)) && NEGATIONS.has(clause[index - 1] ?? "")));
134933
+ if (!named || namesOther || negated) {
134934
+ throw new Error(`--answer-text does not name the same choice as --answer ${answer}: the operator's words must say ${answer} (and not ${other}, and not behind a "not"; a bare yes or no is not a choice here). ` + `Nothing was recorded; ask the operator again, or pass the --answer they gave.`);
134935
+ }
134936
+ }
134937
+ function requireText(flag, value, command) {
134938
+ if (value === undefined || value.trim() === "")
134939
+ throw new Error(`${flag} "<text>" is required and must not be blank: it holds ${flag === "--answer-text" ? "the operator's own words" : "the question as it was asked"}. See \`keryx review ${command} --help\`.`);
134940
+ if (value.length > MAX_OPERATOR_TEXT_LENGTH)
134941
+ throw new Error(`${flag} is ${value.length} characters; at most ${MAX_OPERATOR_TEXT_LENGTH} are recorded. Pass the operator's words or the question as asked, shortened. See \`keryx review ${command} --help\`.`);
134942
+ return value;
134943
+ }
134944
+ function requireReason(own, command) {
134945
+ const reason = optionValue(own, "--reason");
134946
+ if (reason === undefined || reason.trim() === "")
134947
+ throw new Error(`Missing --reason "<why>". See \`keryx review ${command} --help\`.`);
134948
+ return reason.trim();
134949
+ }
134950
+ function refuseHostQuestionHere(command, packageDir, env) {
134951
+ if (env["KERYX_TOOL_CALL"] === "1") {
134952
+ throw new Error(`keryx review ${command} --answered-by is refused: this keryx was started from an agent's shell (KERYX_TOOL_CALL=1), and an agent can type into it. ` + `Nothing was recorded. The operator answers the ${CARD[command]} card instead.`);
134953
+ }
134954
+ if (isAutonomousEnv(env) || readRunProfile(packageDir)?.mode === "unattended") {
134955
+ throw new Error(`keryx review ${command} --answered-by is refused in an unattended run: nobody is there to be asked, so nothing was recorded.`);
134956
+ }
134957
+ }
134958
+ function refuseRepeat(command, host, held) {
134959
+ if (held === undefined || held.source !== host.hostQuestion.source || held.ref !== host.hostQuestion.ref)
134960
+ return;
134961
+ throw new Error(`keryx review ${command}: the host question ${held.source}:${held.ref} is already recorded here, so this call recorded nothing again. ` + `If this is a new question, give it its own <ref> in --answered-by.`);
134962
+ }
134963
+ function refuseUnboundQuestion(command, questionText, subject, tokens, countAlso) {
134964
+ if (tokens.length === 0)
134965
+ return;
134966
+ if (tokens.every((token) => namesToken(questionText, token)))
134967
+ return;
134968
+ if (countAlso && namesToken(questionText, String(tokens.length)))
134969
+ return;
134970
+ throw new Error(`--question-text does not name the ${subject}: ${tokens.length === 1 ? tokens[0] : `${tokens.slice(0, 6).join(", ")}${tokens.length > 6 ? ", ..." : ""}`}${countAlso ? ` (or their count, ${tokens.length})` : ""}. ` + `An answer to a question that does not say what it is about is not recorded. Ask the operator again with the ${subject} in the question. Nothing was recorded. See \`keryx review ${command} --help\`.`);
134971
+ }
134972
+ async function prepareConfirmPlan(packageDir, host) {
134973
+ const estimate = await estimatePlan(packageDir);
134974
+ const shown = new Set(host.questionText.match(/\d+/g) ?? []);
134975
+ if (![estimate.dispatches, estimate.minutes.min, estimate.minutes.max].every((value) => shown.has(String(value)))) {
134976
+ throw new Error(`The plan changed, or the question does not show it: the plan now is ${estimate.dispatches} reviewer dispatches (${estimate.minutes.min}-${estimate.minutes.max} min) ` + `and --question-text names other numbers. Ask the operator again with the current numbers in the question. Nothing was recorded.`);
134977
+ }
134978
+ refuseNonAffirmative(host.answerText, "confirm-plan");
134979
+ refuseRepeat("confirm-plan", host, (await readPlanConfirmation2(packageDir))?.hostQuestion);
134980
+ const commit = async () => {
134981
+ const record = await recordPlanConfirmation(packageDir, estimate, { quote: `${host.questionText}: ${host.answerText}`, confirmedVia: "host-question", at: host.at, hostQuestion: host.hostQuestion });
134982
+ return `plan confirmed (${host.where}): ${record.dispatches} dispatch(es), ${record.minutes[0]}-${record.minutes[1]} min`;
134983
+ };
134984
+ return { commit, choice: HOST_OPTIONS["confirm-plan"].confirm };
134985
+ }
134986
+ async function preparePremise(packageDir, host, input, deps) {
134987
+ const held = await findHeldFinding(packageDir, input.ref);
134988
+ if (!held.ok)
134989
+ throw new Error(held.refusal);
134990
+ const finding = held.finding;
134991
+ const shortId = finding.key.split("#").pop();
134992
+ if (!namesToken(host.questionText, finding.key))
134993
+ refuseUnboundQuestion("decide-premise", host.questionText, "finding id", [shortId], false);
134994
+ refuseOtherChoice(host.answerText, input.answer);
134995
+ const prior = (await readPremiseDecisions(packageDir)).find((decision) => decision.finding === finding.key && decision.hostQuestion?.source === host.hostQuestion.source && decision.hostQuestion.ref === host.hostQuestion.ref);
134996
+ refuseRepeat("decide-premise", host, prior?.hostQuestion);
134997
+ const commit = async () => {
134998
+ await answerPremiseQuestion(packageDir, { finding, answer: input.answer, reason: input.reason, quote: host.answerText, at: host.at, confirmedVia: "host-question", hostQuestion: host.hostQuestion }, { cwd: deps.cwd, warn: (line) => deps.log(`warning: ${line}`) });
134999
+ return `premise-question ${input.answer} recorded for ${finding.key} (${host.where}, ${host.at}) in operator-decisions.json and findings.json`;
135000
+ };
135001
+ return { commit, choice: input.answer };
135002
+ }
135003
+ async function prepareGate(packageDir, host, input) {
135004
+ const kind = input.command === "waive-mutation" ? "mutation-waiver" : "reviewer-override";
135005
+ const subject = await operatorDecisionSubject(packageDir, kind, kind === "mutation-waiver" ? undefined : input.selected);
135006
+ refuseUnboundQuestion(input.command, host.questionText, kind === "mutation-waiver" ? "unrun mutants" : "reviewers left out", subject.items, true);
135007
+ refuseNonAffirmative(host.answerText, input.command);
135008
+ const prior = (await readOperatorDecisions(packageDir)).find((decision) => decision.kind === kind && decision.hostQuestion?.source === host.hostQuestion.source && decision.hostQuestion.ref === host.hostQuestion.ref);
135009
+ refuseRepeat(input.command, host, prior?.hostQuestion);
135010
+ const commit = async () => {
135011
+ await recordOperatorDecision(packageDir, { kind, quote: host.answerText, reason: input.reason, confirmedVia: "host-question", at: host.at, subject, hostQuestion: host.hostQuestion });
135012
+ return `${kind} recorded (${host.where}, ${host.at}) in operator-decisions.json`;
135013
+ };
135014
+ return { commit, choice: HOST_OPTIONS[input.command].confirm };
135015
+ }
135016
+ function refuseOtherAnswer(own, command) {
135017
+ const choices = HOST_OPTIONS[command];
135018
+ const answerArg = optionValue(own, "--answer");
135019
+ if (answerArg === undefined || answerArg === choices.confirm)
135020
+ return;
135021
+ throw new Error(answerArg === choices.decline ? `--answer ${answerArg} is a decline: a declined question records nothing. See \`keryx review ${command} --help\`.` : `--answer must be ${choices.confirm} (the option this verb records) or left out; got ${JSON.stringify(answerArg.slice(0, 40))}. See \`keryx review ${command} --help\`.`);
135022
+ }
135023
+ async function prepareVerb(command, own, packageDir, host, deps) {
135024
+ switch (command) {
135025
+ case "decide-premise": {
135026
+ const answer = optionValue(own, "--answer");
135027
+ if (answer !== "uphold" && answer !== "dismiss")
135028
+ throw new Error("--answer must be uphold or dismiss. See `keryx review decide-premise --help`.");
135029
+ const reason = requireReason(own, command);
135030
+ const ref = optionValue(own, "--finding");
135031
+ if (ref === undefined || ref.trim() === "")
135032
+ throw new Error("Missing --finding <id>. See `keryx review decide-premise --help`.");
135033
+ return preparePremise(packageDir, host, { ref, answer, reason }, deps);
135034
+ }
135035
+ case "confirm-plan":
135036
+ refuseOtherAnswer(own, command);
135037
+ return prepareConfirmPlan(packageDir, host);
135038
+ case "waive-mutation":
135039
+ case "override-reviewers": {
135040
+ refuseOtherAnswer(own, command);
135041
+ const reason = requireReason(own, command);
135042
+ const selectedArg = optionValue(own, "--selected");
135043
+ const selected = selectedArg === undefined ? undefined : selectedArg.split(",").map((item) => item.trim()).filter(Boolean);
135044
+ return prepareGate(packageDir, host, { command, reason, selected });
135045
+ }
135046
+ }
135047
+ }
135048
+ async function recordHostQuestion(command, own, packageDir, context) {
135049
+ const { cwd, log, env } = context;
135050
+ refuseHostQuestionHere(command, packageDir, env);
135051
+ const by = parseAnsweredBy(optionValue(own, "--answered-by"), command);
135052
+ const answerText = requireText("--answer-text", optionValue(own, "--answer-text"), command);
135053
+ const questionText = requireText("--question-text", optionValue(own, "--question-text"), command);
135054
+ const at = new Date().toISOString();
135055
+ const host = { hostQuestion: { source: by.source, ref: by.ref, questionText, answerText, time: at }, answerText, questionText, at, where: `host-question, ${by.source}:${by.ref}` };
135056
+ const { commit, choice } = await prepareVerb(command, own, packageDir, host, { cwd, log });
135057
+ log(await commit());
135058
+ await journalHostQuestion(command, host, choice, { cwd, log, env });
135059
+ return 0;
135060
+ }
135061
+ async function journalHostQuestion(command, host, choice, context) {
135062
+ const { cwd, log, env } = context;
135063
+ const { source, ref } = host.hostQuestion;
135064
+ const choices = HOST_OPTIONS[command];
135065
+ try {
135066
+ const flow = await resolveFlowContext(cwd, env);
135067
+ const opened = await openDecision({
135068
+ cwd,
135069
+ question: host.questionText,
135070
+ options: choices.options.map((option) => ({ ...option, description: SYNTHESIZED_OPTION })),
135071
+ stage: `review ${command}`,
135072
+ flow: flow.flow,
135073
+ flowSource: flow.flowSource,
135074
+ channel: source,
135075
+ source: DECISION_SOURCES.hostQuestion,
135076
+ hostQuestion: { source, ref, verb: command, answerText: host.answerText },
135077
+ salt: "host-question"
135078
+ });
135079
+ await answerDecision({ cwd, id: opened.id, choice });
135080
+ } catch (error) {
135081
+ log(`warning: the answer is recorded, but the keryx decisions journal was not updated (${error instanceof Error ? error.message : String(error)})`);
135082
+ }
135083
+ }
135084
+ var OPERATOR_COMMANDS, CARD, hostHelpTail = (refusedFor) => `<source> is ${HOST_QUESTION_SOURCES.join(", ")}; <ref> is the host's own id for the question (a poll or message id, at most ${REF_MAX_LENGTH} characters).
135085
+ The record says confirmedVia host-question with both texts (each at most ${MAX_OPERATOR_TEXT_LENGTH} characters), and \`keryx decisions\` keeps the question.
135086
+ keryx cannot see the question: it records what you report. Refused, with nothing recorded, for ${refusedFor}, the same host question recorded twice,
135087
+ in an unattended run and under KERYX_TOOL_CALL=1.`, REF_MAX_LENGTH = 200, SYNTHESIZED_OPTION = "Synthesized by keryx from the verb: not an option the host offered.", HOST_OPTIONS, LEAD_DECLINE, CHOICE_WORDS, NEGATIONS, AFFIRMATIVE, wordsOf = (text) => text.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter((word) => word !== ""), shortened = (text) => JSON.stringify(text.trim().slice(0, 80)), escapeRegExp11 = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), namesToken = (text, token) => new RegExp(`(?<![\\p{L}\\p{N}_-])${escapeRegExp11(token)}(?![\\p{L}\\p{N}_-])`, "iu").test(text);
134714
135088
  var init_review_operator = __esm(() => {
135089
+ init_service6();
134715
135090
  init_args();
134716
135091
  init_coverage();
134717
135092
  init_operator_decisions();
134718
135093
  init_plan_confirmation();
134719
135094
  init_premise_question();
135095
+ init_run_profile();
135096
+ init_types4();
134720
135097
  init_verify_run();
134721
135098
  init_review_layers();
134722
135099
  OPERATOR_COMMANDS = ["waive-mutation", "override-reviewers", "confirm-plan", "decide-premise", "verify-run"];
@@ -134726,6 +135103,70 @@ var init_review_operator = __esm(() => {
134726
135103
  "confirm-plan": "review_confirm_plan",
134727
135104
  "decide-premise": "review_decide_premise"
134728
135105
  };
135106
+ HOST_OPTIONS = {
135107
+ "decide-premise": {
135108
+ confirm: "uphold",
135109
+ decline: "dismiss",
135110
+ options: [
135111
+ { id: "uphold", label: "Uphold: the finding stands at its original severity" },
135112
+ { id: "dismiss", label: "Dismiss: the finding is incorrect" }
135113
+ ]
135114
+ },
135115
+ "confirm-plan": {
135116
+ confirm: "yes",
135117
+ decline: "no",
135118
+ alsoAffirms: ["run"],
135119
+ options: [
135120
+ { id: "yes", label: "Yes: run the plan as estimated" },
135121
+ { id: "no", label: "No: do not run it" }
135122
+ ]
135123
+ },
135124
+ "waive-mutation": {
135125
+ confirm: "waive",
135126
+ decline: "keep",
135127
+ options: [
135128
+ { id: "waive", label: "Waive: skip the mutation pass" },
135129
+ { id: "keep", label: "Keep: run the mutation pass" }
135130
+ ]
135131
+ },
135132
+ "override-reviewers": {
135133
+ confirm: "override",
135134
+ decline: "keep",
135135
+ options: [
135136
+ { id: "override", label: "Override: run the smaller reviewer set" },
135137
+ { id: "keep", label: "Keep: run the required reviewers" }
135138
+ ]
135139
+ }
135140
+ };
135141
+ LEAD_DECLINE = new Set(["n", "no", "nope", "nah", "not", "never", "cancel", "stop", "decline", "declined", "reject", "\u043D\u0435\u0442", "\u043D\u0435", "\u043E\u0442\u043C\u0435\u043D\u0430"]);
135142
+ CHOICE_WORDS = {
135143
+ uphold: new Set(["uphold", "upheld", "upholds", "keep", "kept", "\u043E\u0441\u0442\u0430\u0432\u0438\u0442\u044C", "\u043E\u0441\u0442\u0430\u0432\u043B\u044F\u0435\u043C", "\u043E\u0441\u0442\u0430\u0432\u043B\u044F\u044E", "\u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044C", "\u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0430\u044E", "\u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0430\u0442\u044C"]),
135144
+ dismiss: new Set(["dismiss", "dismissed", "dismisses", "drop", "dropped", "\u043E\u0442\u043A\u043B\u043E\u043D\u0438\u0442\u044C", "\u043E\u0442\u043A\u043B\u043E\u043D\u044F\u044E", "\u043E\u0442\u043A\u043B\u043E\u043D\u044F\u0435\u043C", "\u0441\u043D\u044F\u0442\u044C", "\u0437\u0430\u043A\u0440\u044B\u0442\u044C"])
135145
+ };
135146
+ NEGATIONS = new Set(["not", "never", "no", "nope", "nah", "dont", "don", "t", "nt", "cannot", "cant", "\u043D\u0435\u0442", "\u043D\u0435", "\u043D\u0438", "\u0431\u0435\u0437"]);
135147
+ AFFIRMATIVE = new Set([
135148
+ "yes",
135149
+ "y",
135150
+ "yep",
135151
+ "yeah",
135152
+ "ok",
135153
+ "okay",
135154
+ "sure",
135155
+ "confirm",
135156
+ "confirmed",
135157
+ "approve",
135158
+ "approved",
135159
+ "agree",
135160
+ "agreed",
135161
+ "go",
135162
+ "\u0434\u0430",
135163
+ "\u043E\u043A",
135164
+ "\u043E\u043A\u0435\u0439",
135165
+ "\u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0430\u044E",
135166
+ "\u0441\u043E\u0433\u043B\u0430\u0441\u0435\u043D",
135167
+ "\u0434\u0430\u0432\u0430\u0439",
135168
+ "\u0437\u0430\u043F\u0443\u0441\u043A\u0430\u0439"
135169
+ ]);
134729
135170
  });
134730
135171
 
134731
135172
  // src/commands/review-chunks.ts
@@ -148354,7 +148795,7 @@ async function completionFacts(dir) {
148354
148795
  for (const decision of await readOperatorDecisions(dir)) {
148355
148796
  const items = decision.subject?.items ?? [];
148356
148797
  const what = decision.kind === "mutation-waiver" ? "mutation pass not run" : "reviewer override, left out";
148357
- waivers.push(`waiver: ${what}${items.length === 0 ? "" : ` ${cardSafe(items.join(", "))}`}: "${quoteWhole(decision.quote)}" (${decision.confirmedVia})`);
148798
+ waivers.push(`waiver: ${what}${items.length === 0 ? "" : ` ${cardSafe(items.join(", "))}`}: "${quoteWhole(decision.quote)}" (${decision.confirmedVia}${decision.hostQuestion === undefined ? "" : ` ${cardSafe(decision.hostQuestion.source)}:${cardSafe(decision.hostQuestion.ref)}`})`);
148358
148799
  }
148359
148800
  return {
148360
148801
  reviewId: manifest.reviewId ?? path337.basename(dir),
@@ -176138,7 +176579,7 @@ function summarizeExtras(report) {
176138
176579
  counts: ARMS.map((arm) => [arm, whole(counts[arm])])
176139
176580
  };
176140
176581
  }
176141
- return { reasons, ineligible, progress };
176582
+ return { reasons, ineligible, progress, hostQuestions: Array.isArray(report.hostQuestions) ? report.hostQuestions.length : 0 };
176142
176583
  }
176143
176584
  function nonNegative(value) {
176144
176585
  return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
@@ -176218,6 +176659,8 @@ function extrasLines(extras) {
176218
176659
  }
176219
176660
  if (extras.ineligible !== null)
176220
176661
  lines.push(`Not in the comparison (ineligible): ${extras.ineligible.decisions}, ${extras.ineligible.answered} answered`);
176662
+ if (extras.hostQuestions > 0)
176663
+ lines.push(`Answered via host question (a separate kind, not in the comparison): ${extras.hostQuestions}`);
176221
176664
  if (extras.progress !== null) {
176222
176665
  const p = extras.progress;
176223
176666
  lines.push(`Progress, flow 392 AC11: ${p.decisions}/${p.decisionsTarget} decisions, ${p.blind}/${p.blindTarget} blind`, `Progress per arm (threshold ${p.threshold}): ${p.counts.map(([arm, n]) => `${arm} ${n}`).join(", ")}`);
@@ -176331,12 +176774,13 @@ async function openDecisionsArms(otui, chrome, options) {
176331
176774
  const text = await armsText(cwd);
176332
176775
  return presentDecisions((hostOtui, hostChrome, input) => openModal(hostOtui, hostChrome, input), otui, chrome, { ...rest, text, tabLabel: "Arms" });
176333
176776
  }
176334
- function projectDecisionsPanel(count, width, backfilled = 0, annotated = 0) {
176335
- if (count <= 0 && backfilled <= 0)
176777
+ function projectDecisionsPanel(count, width, backfilled = 0, annotated = 0, hostQuestions = 0) {
176778
+ if (count <= 0 && backfilled <= 0 && hostQuestions <= 0)
176336
176779
  return { visible: false, text: "" };
176337
176780
  const live = count > 0 ? `${count} decision${count === 1 ? "" : "s"}` : "";
176338
176781
  const before = backfilled > 0 ? `${count > 0 ? " + " : ""}${backfilled} before` : "";
176339
- const label = `${live}${before}`;
176782
+ const via = hostQuestions > 0 ? `${count > 0 || backfilled > 0 ? " + " : ""}${hostQuestions} via host` : "";
176783
+ const label = `${live}${before}${via}`;
176340
176784
  const marked = annotated > 0 ? `${label} ${OWN_MARK}${annotated}` : label;
176341
176785
  const candidates = [`${marked} \xB7 /decisions`, marked, `${label} \xB7 /decisions`, label];
176342
176786
  const text = candidates.find((candidate) => candidate.length <= width) ?? label.slice(0, Math.max(1, width));
@@ -176362,6 +176806,7 @@ function mountDecisionsSidebar(options) {
176362
176806
  const count = options.count ?? (() => decisionCount(options.cwd));
176363
176807
  const backfilledTotal = options.backfilled ?? (() => backfilledCount(options.cwd));
176364
176808
  const annotatedTotal = options.annotated ?? (() => annotatedCount(options.cwd));
176809
+ const hostTotal = options.hostQuestions ?? (() => hostQuestionCount(options.cwd));
176365
176810
  const box = new core.BoxRenderable(r, { id: "sb-decisions", flexDirection: "column", flexShrink: 0 });
176366
176811
  options.parent.add(box);
176367
176812
  const armsBox = new core.BoxRenderable(r, { id: "sb-decisions-arms", flexDirection: "column", flexShrink: 0 });
@@ -176449,9 +176894,15 @@ function mountDecisionsSidebar(options) {
176449
176894
  } catch {
176450
176895
  marked = 0;
176451
176896
  }
176897
+ let viaHost;
176898
+ try {
176899
+ viaHost = await hostTotal();
176900
+ } catch {
176901
+ viaHost = 0;
176902
+ }
176452
176903
  if (disposed || generation !== refreshGeneration)
176453
176904
  return;
176454
- projected = projectDecisionsPanel(n, options.width, before, marked);
176905
+ projected = projectDecisionsPanel(n, options.width, before, marked, viaHost);
176455
176906
  paint();
176456
176907
  let nextArms;
176457
176908
  try {
@@ -216271,7 +216722,7 @@ function parseSkillMetadata(skillMd, registryEntry) {
216271
216722
  };
216272
216723
  }
216273
216724
  function readMetadataLine(content, label) {
216274
- const match = content.match(new RegExp(`^${escapeRegExp11(label)}:\\s*(.+)$`, "m"));
216725
+ const match = content.match(new RegExp(`^${escapeRegExp12(label)}:\\s*(.+)$`, "m"));
216275
216726
  return match?.[1]?.trim();
216276
216727
  }
216277
216728
  function updateLastVerified(skillMd, verifiedAt) {
@@ -216306,7 +216757,7 @@ function inferModuleFromPath(packageRoot) {
216306
216757
  const next = parts[index + 1];
216307
216758
  return index >= 0 && next ? next : "general";
216308
216759
  }
216309
- function escapeRegExp11(value) {
216760
+ function escapeRegExp12(value) {
216310
216761
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
216311
216762
  }
216312
216763
  var init_verify2 = __esm(() => {