@mrciphersmith/keryx 0.3.133 → 0.3.134

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
@@ -15025,8 +15025,12 @@ function renderSecurityPrePushHook() {
15025
15025
  # HEAD-only heuristic under-scanned a first push). Fall back to the tracked
15026
15026
  # push/upstream range only when stdin yields nothing usable.
15027
15027
  changed_files=""
15028
+ saw_ref=0
15029
+ destination_remote="$1"
15030
+ remote_tips="$(git for-each-ref --format='%(objectname)' "refs/remotes/$destination_remote/" 2>/dev/null)"
15028
15031
  while read -r local_ref local_sha remote_ref remote_sha; do
15029
15032
  [ -z "$local_ref" ] && continue
15033
+ saw_ref=1
15030
15034
  # Skip deleted refs (local sha all-zero): there is nothing to scan.
15031
15035
  case "$local_sha" in
15032
15036
  *[!0]*) : ;;
@@ -15041,7 +15045,10 @@ function renderSecurityPrePushHook() {
15041
15045
  # New ref: scan all commits unique to this push. Use the merge-base with
15042
15046
  # any existing remote as the base, falling back to the empty tree when
15043
15047
  # the branch shares no history with a remote (truly first push).
15044
- remotes="$(git for-each-ref --format='%(objectname)' refs/remotes 2>/dev/null | tr '\\n' ' ')"
15048
+ remotes=""
15049
+ if [ -n "$destination_remote" ]; then
15050
+ remotes="$(printf '%s\\n' "$remote_tips" | tr '\\n' ' ')"
15051
+ fi
15045
15052
  base=""
15046
15053
  if [ -n "$remotes" ]; then
15047
15054
  base="$(git merge-base "$local_sha" $remotes 2>/dev/null || true)"
@@ -15052,6 +15059,23 @@ function renderSecurityPrePushHook() {
15052
15059
  range_files="$(git diff --name-only --diff-filter=ACMR "$base".."$local_sha" 2>/dev/null || true)"
15053
15060
  ;;
15054
15061
  esac
15062
+ # Avoid rescanning content identical to a known tip on the destination.
15063
+ # Keep locally modified inherited files and never trust unrelated remotes.
15064
+ # Failed comparisons retain the candidates (conservative fallback).
15065
+ if [ -n "$destination_remote" ] && [ -n "$range_files" ]; then
15066
+ range_files="$(printf '%s\\n' "$range_files" | LC_ALL=C sort -u)"
15067
+ for published_tip in $remote_tips; do
15068
+ if unpublished="$(git diff --name-only --no-renames "$published_tip" "$local_sha" 2>/dev/null)" &&
15069
+ worktree_changes="$(git diff --name-only --no-renames "$local_sha" -- 2>/dev/null)"; then
15070
+ differences="$(printf '%s\\n%s\\n' "$unpublished" "$worktree_changes" | LC_ALL=C sort -u)"
15071
+ range_files="$(printf '%s\\n' "$range_files" | while IFS= read -r candidate; do
15072
+ if printf '%s\\n' "$differences" | grep -Fqx -- "$candidate"; then
15073
+ printf '%s\\n' "$candidate"
15074
+ fi
15075
+ done)"
15076
+ fi
15077
+ done
15078
+ fi
15055
15079
  if [ -n "$range_files" ]; then
15056
15080
  changed_files="$changed_files
15057
15081
  $range_files"
@@ -15060,7 +15084,7 @@ $range_files"
15060
15084
 
15061
15085
  # Fall back to the tracked push/upstream range (then HEAD) when stdin was empty
15062
15086
  # (e.g. the hook was invoked manually without ref lines).
15063
- if [ -z "$(printf '%s' "$changed_files" | tr -d '[:space:]')" ]; then
15087
+ if [ "$saw_ref" -eq 0 ]; then
15064
15088
  range="HEAD"
15065
15089
  push_ref="$(git rev-parse --abbrev-ref --symbolic-full-name '@{push}' 2>/dev/null || true)"
15066
15090
  if [ -n "$push_ref" ]; then
@@ -15116,7 +15140,7 @@ $range_files"
15116
15140
  return 0
15117
15141
  }
15118
15142
 
15119
- keryx_security_pre_push || exit $?
15143
+ keryx_security_pre_push "$@" || exit $?
15120
15144
  `;
15121
15145
  }
15122
15146
  function renderGdwikiPostCommitHook() {
@@ -18853,6 +18877,12 @@ var init_catalog = __esm(() => {
18853
18877
  "Anchor class_scope on the callers that break, never on the changed line.",
18854
18878
  "Say when the radius could not answer \u2014 a non-code change has an empty one."
18855
18879
  ]),
18880
+ skill("review-degradation", "review", ["full"], "Ask whether the code is worse after a change than on the base branch (behaviour, defaults, speed, structure) and whether that can be shown, over the diff plus every consumer of each changed shared symbol.", [
18881
+ "Read the diff against the description before reading any consumer.",
18882
+ "Trace the consumers the code graph lists for each changed shared export; the set is the graph's, not yours.",
18883
+ "Name one degradation class and one evidence kind per finding; a major or blocker without evidence is lowered to info at ingest.",
18884
+ "Say what was traced when nothing was found, and say when the consumer cap dropped any."
18885
+ ]),
18856
18886
  skill("review-pr-contract", "review", ["recommended", "full"], "Compare what the PR body claims (checklist boxes, stated changes) with the diff, and look for documentation outside the diff that the change made stale.", [
18857
18887
  "Work every checklist box in pr-context.md as a row: quoted box, state, quoted diff line, expectation.",
18858
18888
  "File a finding only where a quoted box or claim disagrees with a quoted diff or document line.",
@@ -19546,6 +19576,7 @@ var init_skill_length_ceilings = __esm(() => {
19546
19576
  "review/review-backend": 361,
19547
19577
  "review/review-clean-code": 538,
19548
19578
  "review/review-core-boundaries": 158,
19579
+ "review/review-degradation": 193,
19549
19580
  "review/review-flow-graph": 183,
19550
19581
  "review/review-frontend": 607,
19551
19582
  "review/review-frontend-conventions": 206,
@@ -23371,7 +23402,7 @@ var package_default;
23371
23402
  var init_package = __esm(() => {
23372
23403
  package_default = {
23373
23404
  name: "@mrciphersmith/keryx",
23374
- version: "0.3.133",
23405
+ version: "0.3.134",
23375
23406
  description: "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
23376
23407
  private: false,
23377
23408
  publishConfig: {
@@ -23444,7 +23475,6 @@ var init_package = __esm(() => {
23444
23475
  "README.md",
23445
23476
  "package.json"
23446
23477
  ],
23447
- dependencies: {},
23448
23478
  overrides: {
23449
23479
  "fast-uri": "^3.1.9",
23450
23480
  "body-parser": "^2.4.0",
@@ -23456,6 +23486,7 @@ var init_package = __esm(() => {
23456
23486
  protobufjs: "7.6.6",
23457
23487
  sharp: "0.35.5"
23458
23488
  },
23489
+ dependencies: {},
23459
23490
  optionalDependencies: {
23460
23491
  "@modelcontextprotocol/sdk": "^1.32.1",
23461
23492
  "@opentui/core": "^0.4.5",
@@ -43837,7 +43868,7 @@ function isHostQuestion(value) {
43837
43868
  function hostQuestionFields(source) {
43838
43869
  return source?.hostQuestion === undefined ? {} : { hostQuestion: source.hostQuestion };
43839
43870
  }
43840
- 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;
43871
+ 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, REVIEW_DEGRADATION_CLASSES, REVIEW_EVIDENCE_KINDS;
43841
43872
  var init_types4 = __esm(() => {
43842
43873
  MANAGED_REVIEW_MODES = ["attach-review", "review-flow", "ingest"];
43843
43874
  REVIEW_TARGET_KINDS = ["pr", "issue", "branch", "path", "report"];
@@ -43869,6 +43900,16 @@ var init_types4 = __esm(() => {
43869
43900
  VERIFICATION_MODES = ["off", "annotate", "filter"];
43870
43901
  PREMISE_VERDICTS = ["upheld", "downgraded", "challenged", "owner-decision"];
43871
43902
  HELD_PREMISE_VERDICTS = ["challenged", "owner-decision"];
43903
+ REVIEW_DEGRADATION_CLASSES = [
43904
+ "behaviour-delta",
43905
+ "default-change",
43906
+ "diff-vs-description",
43907
+ "debt",
43908
+ "performance",
43909
+ "cross-pr",
43910
+ "blind-test"
43911
+ ];
43912
+ REVIEW_EVIDENCE_KINDS = ["probe", "consumer-trace", "merge-trace", "red-test", "base-diff"];
43872
43913
  });
43873
43914
 
43874
43915
  // src/review/blast-radius.ts
@@ -50117,6 +50158,10 @@ function lightConventionsReviewer(roster, inventory) {
50117
50158
  const group = Object.values(overlapGroupsOf(inventory)).find((members) => members.includes(pick));
50118
50159
  return group?.find((member) => roster.includes(member)) ?? pick;
50119
50160
  }
50161
+ async function installedReviewerNames(root) {
50162
+ const inventory = await collectReviewers(root);
50163
+ return new Set([...inventory.bundled, ...inventory.project].map((reviewer) => reviewer.name));
50164
+ }
50120
50165
  async function requiredReviewers(root, mode, files, options = {}) {
50121
50166
  const inventory = await collectReviewers(root);
50122
50167
  const stack = await detectProjectStack(root);
@@ -51099,6 +51144,9 @@ function collapseOverlapGroups(roles, groups = BUNDLED_OVERLAP_GROUPS) {
51099
51144
  function isRoutedManifest(manifest) {
51100
51145
  return manifest.slices.some((slice) => slice.retryOf === undefined && Array.isArray(slice.roles));
51101
51146
  }
51147
+ function changesSharedCodeCandidate(region) {
51148
+ return deepReasonOfFile(region.path) === "source code";
51149
+ }
51102
51150
  function contentRolesOf(region) {
51103
51151
  const changed = changedLinesOf(region.text);
51104
51152
  const code = CODE_FILE2.test(region.path) && !isTestFilePath(region.path) && !DOC_FILE.test(region.path);
@@ -51223,7 +51271,7 @@ function renderRoutingSummary(summary) {
51223
51271
  const roles = Object.entries(summary.byRole).sort(([a], [b]) => a.localeCompare(b)).map(([role, count]) => `${role} ${count}`).join(", ");
51224
51272
  return `routing: ${summary.deep} deep (${summary.audit} audit), ${summary.light} light of ${summary.slices} slice(s); slices per role: ${roles || "none"}`;
51225
51273
  }
51226
- var ALWAYS_ROLE = "review-logic", REGRESSION_ROLE = "review-regression", DEEP_ROLES, DEEP_RISK_CLASSES, DEFAULT_REACH_THRESHOLD = 25, JEV_DEEP_THRESHOLD = 0.7, AUDIT_RATE = 0.125, CODE_FILE2, DOC_FILE, STYLE_FILE, FRONTEND_FILE, CONFIG_FILE, BACKEND_DIR2, DATA_FILE2, AGENT_INSTRUCTION_FILE, CONTRACT_FILE, SECURITY_PRINCIPLE, SOURCE_CODE_FILE, FIXTURE_FILE, GENERATED_FILE, ENTRY_FILE, ROLE_RULES, ROUTABLE_ROLES, BUNDLED_OVERLAP_GROUPS, ROLES_RUN_WITHOUT_SLICES, changedLinesOf = (text) => text.split(`
51274
+ var ALWAYS_ROLE = "review-logic", REGRESSION_ROLE = "review-regression", DEGRADATION_ROLE = "review-degradation", DEEP_ROLES, DEEP_RISK_CLASSES, DEFAULT_REACH_THRESHOLD = 25, JEV_DEEP_THRESHOLD = 0.7, AUDIT_RATE = 0.125, CODE_FILE2, DOC_FILE, STYLE_FILE, FRONTEND_FILE, CONFIG_FILE, BACKEND_DIR2, DATA_FILE2, AGENT_INSTRUCTION_FILE, CONTRACT_FILE, SECURITY_PRINCIPLE, SOURCE_CODE_FILE, FIXTURE_FILE, GENERATED_FILE, SHARED_SYMBOL_CHANGE, ENTRY_FILE, ROLE_RULES, ROUTABLE_ROLES, BUNDLED_OVERLAP_GROUPS, ROLES_RUN_WITHOUT_SLICES, changedLinesOf = (text) => text.split(`
51227
51275
  `).filter((line) => line.startsWith("+") || line.startsWith("-")).join(`
51228
51276
  `), auditKey = (head, id) => createHash43("sha256").update(`${head}\x00${id}`).digest("hex"), AUDIT_MIN_LIGHT = 4;
51229
51277
  var init_slice_routing = __esm(() => {
@@ -51255,6 +51303,7 @@ var init_slice_routing = __esm(() => {
51255
51303
  SOURCE_CODE_FILE = /\.(ts|tsx|js|jsx|mjs|cjs|mts|cts|py|go|rs|java|kt|kts|swift|rb|php|cs|c|cc|cpp|cxx|h|hh|hpp|m|mm|scala|lua|dart|ex|exs|sh|bash|zsh|sql|vue|svelte)$/i;
51256
51304
  FIXTURE_FILE = /((^|\/)(__tests__|__snapshots__|__fixtures__|fixtures?|testdata|test-data)\/|\.snap$)/i;
51257
51305
  GENERATED_FILE = /((^|\/)(dist|build|out|vendor|node_modules|generated|__generated__)\/|\.(min\.(js|css)|generated\.\w+|lock|lockb)$|(^|\/)(package-lock\.json|bun\.lockb?|yarn\.lock|pnpm-lock\.yaml)$)/i;
51306
+ SHARED_SYMBOL_CHANGE = /^[+-]\s*export\s+(\*|\{|default\b|declare\b|abstract\b|(async\s+)?(function|class|interface|type|const|let|var|enum)\b)/m;
51258
51307
  ENTRY_FILE = /(^|\/)(index|mod|main)\.[cm]?[jt]sx?$/;
51259
51308
  ROLE_RULES = [
51260
51309
  { role: "review-testing-practices", why: "test file", path: (f) => isTestFilePath(f) || BUNDLED_PATH_GATES["review-testing-practices"](f) || /\.stories\.[jt]sx?$/.test(f) },
@@ -51271,6 +51320,7 @@ var init_slice_routing = __esm(() => {
51271
51320
  { role: "review-architecture", why: "agent instructions (skill, rule, AGENTS/CLAUDE file, prompt)", path: (f) => AGENT_INSTRUCTION_FILE.test(f) },
51272
51321
  { role: "review-architecture", why: "contract or schema", path: (f) => CONTRACT_FILE.test(f) },
51273
51322
  { role: "review-architecture", why: "exported signature or contract type changed", content: /^[+-]\s*export\s+(declare\s+)?(type|interface|abstract\s+class|enum)\b|^[+-]\s*export\s+(async\s+)?function\s+\w+\s*[<(]/m },
51323
+ { role: DEGRADATION_ROLE, why: "shared (exported) symbol changed", content: SHARED_SYMBOL_CHANGE, codeOnly: true },
51274
51324
  { role: "review-security-code", why: "process, argv, git, input-derived path, env or network", content: SECURITY_PRINCIPLE, codeOnly: true },
51275
51325
  { role: "review-security-code", why: "process, filesystem write or trust-sensitive code", content: /\b(exec|execSync|execFile|spawn|spawnSync|Bun\.spawn|child_process|eval|new Function|innerHTML|dangerouslySetInnerHTML|password|passwd|secret|token|credential|api[_-]?key|sanitiz\w*|escape\w*|redact\w*|untrusted|trust\w*|permission\w*|allow-?list|deny-?list|sandbox\w*|cookie|cors|csrf|jwt|writeFile\w*|rm|unlink|rmdir|chmod|symlink\w*|realpath)\b/i, codeOnly: true },
51276
51326
  { role: "review-security-code", why: "auth, crypto or secret path", path: (f) => /\b(auth|login|session|permission|acl|rbac|credential|crypto|cipher|secret|security|trust|sandbox)\w*/i.test(f) && !isTestFilePath(f) && !DOC_FILE.test(f) },
@@ -85865,9 +85915,12 @@ function fixRosterRoles(findingIds, prior) {
85865
85915
  }
85866
85916
  function fixContentRoles(text) {
85867
85917
  const roles = new Set;
85868
- for (const region of parseScopedDiff(text))
85918
+ for (const region of parseScopedDiff(text)) {
85869
85919
  for (const hit of contentRolesOf(region))
85870
85920
  roles.add(hit.role);
85921
+ if (changesSharedCodeCandidate(region))
85922
+ roles.add(DEGRADATION_ROLE);
85923
+ }
85871
85924
  return collapseOverlapGroups(roles).sort();
85872
85925
  }
85873
85926
  function fixSliceRoster(findingIds, prior, text) {
@@ -86072,12 +86125,15 @@ function secondLookClaimBacked(entry, context, text) {
86072
86125
  return false;
86073
86126
  return everyChangedLine(text, (file, walked) => inSecondLookHunk(hunks, file, walked));
86074
86127
  }
86075
- function fixSliceRoles(manifest) {
86076
- return [...new Set(manifest.slices.filter((slice) => slice.retryOf === undefined && (slice.kind === "fix" || slice.kind === "second-look")).flatMap((slice) => slice.roles ?? []))].sort();
86128
+ function effectiveSliceRoles(slice, installed) {
86129
+ return (slice.roles ?? []).filter((role) => installed.has(role));
86077
86130
  }
86078
- function manifestRosterRoles(manifest) {
86079
- const routed = manifest.slices.filter((slice) => slice.retryOf === undefined).flatMap((slice) => (slice.roles ?? []).filter((role) => ALWAYS_DISPATCHED_ROUTED_ROLES.has(role)));
86080
- return [...new Set([...fixSliceRoles(manifest), ...routed])].sort();
86131
+ function fixSliceRoles(manifest, installed) {
86132
+ return [...new Set(manifest.slices.filter((slice) => slice.retryOf === undefined && (slice.kind === "fix" || slice.kind === "second-look")).flatMap((slice) => effectiveSliceRoles(slice, installed)))].sort();
86133
+ }
86134
+ function manifestRosterRoles(manifest, installed) {
86135
+ const routed = manifest.slices.filter((slice) => slice.retryOf === undefined).flatMap((slice) => effectiveSliceRoles(slice, installed).filter((role) => ALWAYS_DISPATCHED_ROUTED_ROLES.has(role)));
86136
+ return [...new Set([...fixSliceRoles(manifest, installed), ...routed])].sort();
86081
86137
  }
86082
86138
  function splitCounts(manifest) {
86083
86139
  const originals = manifest.slices.filter((slice) => slice.retryOf === undefined);
@@ -86106,13 +86162,13 @@ function priorDispositionsOf(result) {
86106
86162
  }
86107
86163
  return out;
86108
86164
  }
86109
- function fixSliceCoverage(manifest, readBy, dispositionsBy, skipped = new Set) {
86165
+ function fixSliceCoverage(manifest, readBy, dispositionsBy, skipped, installed) {
86110
86166
  const covered = [];
86111
86167
  const gaps = [];
86112
86168
  for (const slice of manifest.slices) {
86113
86169
  if (slice.retryOf !== undefined || slice.kind !== "fix")
86114
86170
  continue;
86115
- const roster = (slice.roles ?? []).filter((role) => !skipped.has(role));
86171
+ const roster = effectiveSliceRoles(slice, installed).filter((role) => !skipped.has(role));
86116
86172
  const ids = slice.findingIds ?? [];
86117
86173
  const unread = roster.filter((role) => readBy.get(role)?.has(slice.id) !== true);
86118
86174
  const undisposed = ids.filter((id) => !roster.some((role) => dispositionsBy.get(role)?.has(id) === true));
@@ -86177,6 +86233,15 @@ var init_round_split = __esm(() => {
86177
86233
  ALWAYS_DISPATCHED_ROUTED_ROLES = new Set(["review-security-code"]);
86178
86234
  });
86179
86235
 
86236
+ // src/review/roster.ts
86237
+ async function installedRosterRoles(root, manifest) {
86238
+ return manifestRosterRoles(manifest, await installedReviewerNames(root));
86239
+ }
86240
+ var init_roster = __esm(() => {
86241
+ init_coverage();
86242
+ init_round_split();
86243
+ });
86244
+
86180
86245
  // src/review/estimate.ts
86181
86246
  import { createHash as createHash57 } from "crypto";
86182
86247
  import { existsSync as existsSync34, readFileSync as readFileSync20 } from "fs";
@@ -86357,7 +86422,7 @@ async function estimatePackage(packageDir, options = {}) {
86357
86422
  if (problem !== undefined)
86358
86423
  throw new Error(`${path204.relative(root, found.file)}: ${problem}`);
86359
86424
  const thresholds = await readPlanThresholds(root);
86360
- const roles = [...new Set([...set.required, ...found === undefined ? [] : manifestRosterRoles(found.manifest)])].sort();
86425
+ const roles = [...new Set([...set.required, ...found === undefined ? [] : await installedRosterRoles(root, found.manifest)])].sort();
86361
86426
  return { ...estimateRound(roles, found?.manifest, recorded?.depth ?? "unrecorded", thresholds), manifest: found === undefined ? undefined : path204.relative(root, found.file) };
86362
86427
  }
86363
86428
  function renderEstimate(estimate) {
@@ -86385,6 +86450,7 @@ var init_estimate = __esm(() => {
86385
86450
  init_fs();
86386
86451
  init_json();
86387
86452
  init_coverage();
86453
+ init_roster();
86388
86454
  init_role_switches();
86389
86455
  init_run_profile();
86390
86456
  init_slice();
@@ -86704,7 +86770,7 @@ async function packageReviewers(packageDir, manifest, options = {}) {
86704
86770
  throw new Error(`the required reviewers of ${packageDir} could not be read (${error instanceof Error ? error.message : String(error)}); fix the reviewer rules and re-run`, { cause: error });
86705
86771
  }
86706
86772
  if (manifest !== undefined)
86707
- for (const reviewer of manifestRosterRoles(manifest))
86773
+ for (const reviewer of await installedRosterRoles(root, manifest))
86708
86774
  names.add(reviewer);
86709
86775
  if (options.withDrivers === true)
86710
86776
  for (const reviewer of (await readDrivers(packageDir, root)).replies.reviewers)
@@ -86759,7 +86825,7 @@ var init_wave_drivers = __esm(() => {
86759
86825
  init_driver_read();
86760
86826
  init_fs();
86761
86827
  init_coverage();
86762
- init_round_split();
86828
+ init_roster();
86763
86829
  init_role_switches();
86764
86830
  init_estimate();
86765
86831
  });
@@ -88247,7 +88313,7 @@ function buildLedger(input) {
88247
88313
  run.scopeComplete = scopeComplete;
88248
88314
  runs.push(run);
88249
88315
  }
88250
- const fix = manifest === undefined ? { covered: [], gaps: [] } : fixSliceCoverage(manifest, readBy, dispositionsBy, skippedNames);
88316
+ const fix = manifest === undefined ? { covered: [], gaps: [] } : fixSliceCoverage(manifest, readBy, dispositionsBy, skippedNames, input.installed ?? new Set(manifest.slices.flatMap((slice) => slice.roles ?? [])));
88251
88317
  const fixCovered = new Set(fix.covered);
88252
88318
  gaps.push(...fix.gaps);
88253
88319
  const unresolvedRules = selected.filter((r) => input.ruleFiles[r] === undefined);
@@ -89241,6 +89307,216 @@ var init_readiness = __esm(() => {
89241
89307
  PARTIAL_LINE = /^Partial review:/;
89242
89308
  });
89243
89309
 
89310
+ // src/lib/live-render.ts
89311
+ function stripAnsi(text) {
89312
+ return text.replace(ANSI_RE, "");
89313
+ }
89314
+ function safeText(text) {
89315
+ return stripAnsi(text.replace(OSC_RE, "")).replace(CONTROL_RE, " ").replace(/ {2,}/g, " ").trim();
89316
+ }
89317
+ function isWideCodePoint(cp) {
89318
+ return cp >= 4352 && cp <= 4447 || cp >= 11904 && cp <= 12350 || cp >= 12353 && cp <= 13311 || cp >= 13312 && cp <= 19903 || cp >= 19968 && cp <= 40959 || cp >= 40960 && cp <= 42191 || cp >= 44032 && cp <= 55203 || cp >= 63744 && cp <= 64255 || cp >= 65072 && cp <= 65103 || cp >= 65280 && cp <= 65376 || cp >= 65504 && cp <= 65510 || cp >= 127744 && cp <= 129791 || cp >= 131072 && cp <= 262141;
89319
+ }
89320
+ function displayWidth(text) {
89321
+ const plain = stripAnsi(text);
89322
+ let width = 0;
89323
+ for (const ch of plain) {
89324
+ const cp = ch.codePointAt(0) ?? 0;
89325
+ width += isWideCodePoint(cp) ? 2 : 1;
89326
+ }
89327
+ return width;
89328
+ }
89329
+ function physicalRows(lines, cols) {
89330
+ if (cols <= 0) {
89331
+ return lines.length;
89332
+ }
89333
+ let rows = 0;
89334
+ for (const line of lines) {
89335
+ const width = displayWidth(line);
89336
+ rows += width === 0 ? 1 : Math.ceil(width / cols);
89337
+ }
89338
+ return rows;
89339
+ }
89340
+ function computeRepaint(opts) {
89341
+ const { prevRows, nextLines, cols, sync = false } = opts;
89342
+ let body = "";
89343
+ if (prevRows > 0) {
89344
+ body += "\r";
89345
+ if (prevRows > 1) {
89346
+ body += CURSOR_UP(prevRows - 1);
89347
+ }
89348
+ body += CLEAR_TO_END;
89349
+ }
89350
+ body += nextLines.join(`
89351
+ `);
89352
+ const output = sync ? `${SYNC_BEGIN}${body}${SYNC_END}` : body;
89353
+ return { output, rows: physicalRows(nextLines, cols) };
89354
+ }
89355
+
89356
+ class LiveMarkdownBlock {
89357
+ deps;
89358
+ pending = "";
89359
+ prevRows = 0;
89360
+ prevCols = 0;
89361
+ dirty = false;
89362
+ constructor(deps) {
89363
+ this.deps = deps;
89364
+ }
89365
+ append(text) {
89366
+ if (text.length === 0) {
89367
+ return;
89368
+ }
89369
+ this.pending += text;
89370
+ this.dirty = true;
89371
+ }
89372
+ flush() {
89373
+ if (!this.dirty) {
89374
+ return;
89375
+ }
89376
+ this.repaint();
89377
+ this.dirty = false;
89378
+ }
89379
+ finalize() {
89380
+ if (this.dirty) {
89381
+ this.repaint();
89382
+ this.dirty = false;
89383
+ }
89384
+ if (this.prevRows > 0 || this.pending.length > 0) {
89385
+ this.deps.out(`
89386
+ `);
89387
+ }
89388
+ this.pending = "";
89389
+ this.prevRows = 0;
89390
+ this.prevCols = 0;
89391
+ }
89392
+ repaint() {
89393
+ const cols = this.deps.cols();
89394
+ if (this.prevRows > 0 && cols !== this.prevCols) {
89395
+ this.deps.out(`
89396
+ `);
89397
+ this.prevRows = 0;
89398
+ }
89399
+ const lines = this.deps.render(this.pending).split(`
89400
+ `);
89401
+ const { output, rows } = computeRepaint({ prevRows: this.prevRows, nextLines: lines, cols, sync: this.deps.sync });
89402
+ this.deps.out(output);
89403
+ this.prevRows = rows;
89404
+ this.prevCols = cols;
89405
+ }
89406
+ }
89407
+ var ANSI_RE, OSC_RE, CONTROL_RE, CURSOR_UP = (n) => `\x1B[${n}A`, CLEAR_TO_END = "\x1B[0J", SYNC_BEGIN = "\x1B[?2026h", SYNC_END = "\x1B[?2026l";
89408
+ var init_live_render = __esm(() => {
89409
+ ANSI_RE = /\x1b\[[0-9;?]*[A-Za-z]/g;
89410
+ OSC_RE = /\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)?/g;
89411
+ CONTROL_RE = /[\u0000-\u001f\u007f-\u009f\u061c\u200e\u200f\u2028\u2029\u202a-\u202e\u2066-\u2069]+/g;
89412
+ });
89413
+
89414
+ // src/review/degradation.ts
89415
+ function isDegradationReviewer(reviewer) {
89416
+ return typeof reviewer === "string" && isBlastRadiusScopedFinding({ reviewer }, [DEGRADATION_REVIEWER]);
89417
+ }
89418
+ function hasEvidenceKind(finding) {
89419
+ const kind = finding.evidence_kind;
89420
+ return typeof kind === "string" && REVIEW_EVIDENCE_KINDS.includes(kind);
89421
+ }
89422
+ function omitEvidenceKind(finding) {
89423
+ const { evidence_kind: _dropped, ...rest } = finding;
89424
+ return rest;
89425
+ }
89426
+ function screenDegradationFindings(findings) {
89427
+ const retained = [];
89428
+ const downgraded = [];
89429
+ let screened = 0;
89430
+ for (const finding of findings) {
89431
+ if (!isDegradationReviewer(finding.reviewer)) {
89432
+ retained.push(finding);
89433
+ continue;
89434
+ }
89435
+ screened += 1;
89436
+ const outOfList = finding.evidence_kind !== undefined && !hasEvidenceKind(finding);
89437
+ const clean = outOfList ? omitEvidenceKind(finding) : finding;
89438
+ const severity = clean.severity;
89439
+ if (severity === undefined || !SCREENED_SEVERITIES.includes(severity) || hasEvidenceKind(clean)) {
89440
+ retained.push(clean);
89441
+ continue;
89442
+ }
89443
+ const from = severity;
89444
+ const marker = { rule: DEGRADATION_NO_EVIDENCE_RULE, original_severity: from };
89445
+ const lowered = { ...clean, severity: "info", evidence_screen: marker };
89446
+ if (lowered.blocking_merge === true) {
89447
+ lowered.blocking_merge = false;
89448
+ }
89449
+ retained.push(lowered);
89450
+ downgraded.push({
89451
+ finding: lowered,
89452
+ rule: DEGRADATION_NO_EVIDENCE_RULE,
89453
+ from,
89454
+ to: "info",
89455
+ detail: `${from} without evidence_kind (one of ${REVIEW_EVIDENCE_KINDS.join(", ")}) is unproven; kept as info`
89456
+ });
89457
+ }
89458
+ return { retained, downgraded, screened };
89459
+ }
89460
+ function degradationScreenRecord(result) {
89461
+ return {
89462
+ screened: result.screened,
89463
+ downgraded: result.downgraded.map((entry) => ({
89464
+ id: entry.finding.id ?? "(no id)",
89465
+ reviewer: entry.finding.reviewer ?? "(unknown)",
89466
+ from: entry.from,
89467
+ to: entry.to,
89468
+ rule: entry.rule,
89469
+ detail: entry.detail
89470
+ }))
89471
+ };
89472
+ }
89473
+ function escapeCell(value) {
89474
+ return safeText(value).replace(/[\\|`]/g, (ch) => `\\${ch}`).replace(/\[/g, "\\[").replace(/</g, "&lt;");
89475
+ }
89476
+ function renderDegradationScreenMarkdown(record) {
89477
+ const lines = [
89478
+ "## Degradation evidence screen",
89479
+ "",
89480
+ `rule: ${DEGRADATION_NO_EVIDENCE_RULE}`,
89481
+ `screened: ${record.screened}`,
89482
+ `downgraded: ${record.downgraded.length}`,
89483
+ ""
89484
+ ];
89485
+ if (record.downgraded.length === 0) {
89486
+ lines.push(record.screened === 0 ? "_no `review-degradation` finding in this round._" : "_every `major` or `blocker` from `review-degradation` carried an evidence kind._");
89487
+ lines.push("");
89488
+ return lines.join(`
89489
+ `);
89490
+ }
89491
+ lines.push("| finding | reviewer | rule | from | to | why |");
89492
+ lines.push("|---|---|---|---|---|---|");
89493
+ for (const entry of record.downgraded) {
89494
+ lines.push(`| ${escapeCell(entry.id)} | ${escapeCell(entry.reviewer)} | ${entry.rule} | ${entry.from} | ${entry.to} | ${escapeCell(entry.detail)} |`);
89495
+ }
89496
+ lines.push("");
89497
+ lines.push("Downgraded findings are kept, not deleted, and marked unproven: add an evidence kind to restore the severity.");
89498
+ lines.push("");
89499
+ return lines.join(`
89500
+ `);
89501
+ }
89502
+ function renderDegradationScreenLines(record) {
89503
+ if (record.screened === 0) {
89504
+ return [];
89505
+ }
89506
+ const lines = [`degradation screen: screened=${record.screened} downgraded=${record.downgraded.length}`];
89507
+ for (const entry of record.downgraded) {
89508
+ lines.push(` downgraded ${safeText(entry.id)} ${entry.from} -> ${entry.to} [${entry.rule}]: ${safeText(entry.detail)}`);
89509
+ }
89510
+ return lines;
89511
+ }
89512
+ var DEGRADATION_REVIEWER = "review-degradation", DEGRADATION_NO_EVIDENCE_RULE = "degradation-no-evidence", SCREENED_SEVERITIES;
89513
+ var init_degradation = __esm(() => {
89514
+ init_live_render();
89515
+ init_blast_radius();
89516
+ init_types4();
89517
+ SCREENED_SEVERITIES = ["blocker", "major"];
89518
+ });
89519
+
89244
89520
  // src/review/caps.ts
89245
89521
  function emptySeverityHistogram() {
89246
89522
  return { blocker: 0, major: 0, minor: 0, info: 0 };
@@ -89478,6 +89754,7 @@ var init_caps = __esm(() => {
89478
89754
  "review-architecture",
89479
89755
  "review-highload",
89480
89756
  "review-regression",
89757
+ "review-degradation",
89481
89758
  "review-core-boundaries",
89482
89759
  "code-boss-reviewer",
89483
89760
  "code-ai-review"
@@ -90061,6 +90338,17 @@ function mergeCluster(members) {
90061
90338
  finding.class_scope = { ...base, sites };
90062
90339
  }
90063
90340
  }
90341
+ const carriedClass = canonical.degradation_class ?? others.find((member) => member.degradation_class !== undefined)?.degradation_class;
90342
+ if (carriedClass !== undefined) {
90343
+ finding.degradation_class = carriedClass;
90344
+ }
90345
+ let carriedKind = canonical.evidence_kind;
90346
+ if (carriedKind === undefined && canonical.evidence_screen === undefined) {
90347
+ carriedKind = others.find((member) => member.evidence_kind !== undefined)?.evidence_kind;
90348
+ }
90349
+ if (carriedKind !== undefined) {
90350
+ finding.evidence_kind = carriedKind;
90351
+ }
90064
90352
  const higher = severityRule === "max" ? [] : others.filter((member) => !decides(member) && rank(member.severity) > rank(canonical.severity));
90065
90353
  const top = higher.length === 0 ? undefined : bestOf(higher);
90066
90354
  return {
@@ -90547,7 +90835,9 @@ async function createManagedReviewPackage(input) {
90547
90835
  const { result: verification, reclaimed: externalReclaims } = applyExternalVerdictRule(merged);
90548
90836
  const scoped = partitionExternalFindings(verification.retained);
90549
90837
  const scopeB = await screenScopeBFindings(input, packageDir, scoped.internal, passKeys);
90550
- const deduped = mergeDuplicates(scopeB.retained);
90838
+ const degradation = screenDegradationFindings(scopeB.retained);
90839
+ const degradationScreen = degradationScreenRecord(degradation);
90840
+ const deduped = mergeDuplicates(degradation.retained);
90551
90841
  const premiseSet = [...deduped.findings, ...scoped.external];
90552
90842
  const carriedHolds = premiseClaims === undefined ? carriedPremiseHolds(premiseSet, await readPreviousFindings(packageDir)) : undefined;
90553
90843
  if (carriedHolds?.ok === false)
@@ -90640,7 +90930,7 @@ async function createManagedReviewPackage(input) {
90640
90930
  findings,
90641
90931
  ...carriedHolds?.ok === true && carriedHolds.carried > 0 ? { carried: { holds: carriedHolds.carried, judges: carriedHolds.judges } } : {}
90642
90932
  });
90643
- await writeFileAtomic(path219.join(packageDir, "scope.md"), renderScope(input, flowMatch, at, verification, carried, caps, { externalReclaims, scopeBScreen: scopeB.record, filterStats, allFindings: findings }) + premiseBlock + renderDuplicatesMarkdown(deduped.merges) + renderChunkPassMarkdown(chunkPassIngest));
90933
+ await writeFileAtomic(path219.join(packageDir, "scope.md"), renderScope(input, flowMatch, at, verification, carried, caps, { externalReclaims, scopeBScreen: scopeB.record, degradationScreen, filterStats, allFindings: findings }) + premiseBlock + renderDuplicatesMarkdown(deduped.merges) + renderChunkPassMarkdown(chunkPassIngest));
90644
90934
  await writeFileAtomic(path219.join(packageDir, "coverage.md"), renderCoverage(coverage));
90645
90935
  await writeFileAtomic(path219.join(packageDir, "report.md"), withSpendSection(renderReport2(report, input.mode), input.research));
90646
90936
  await withOperatorDecisionsLock(packageDir, () => writeFileAtomic(path219.join(packageDir, "findings.json"), `${JSON.stringify(findings.map(toContractFinding), null, 2)}
@@ -90672,6 +90962,7 @@ async function createManagedReviewPackage(input) {
90672
90962
  filterStats,
90673
90963
  reviewNotes,
90674
90964
  scopeBScreen: scopeB.record,
90965
+ degradationScreen,
90675
90966
  duplicates: deduped.merges,
90676
90967
  chunkPass: chunkPassIngest
90677
90968
  };
@@ -91327,6 +91618,15 @@ function toContractFinding(finding) {
91327
91618
  if (finding.class_scope !== undefined) {
91328
91619
  record.class_scope = finding.class_scope;
91329
91620
  }
91621
+ if (finding.degradation_class !== undefined) {
91622
+ record.degradation_class = finding.degradation_class;
91623
+ }
91624
+ if (finding.evidence_kind !== undefined) {
91625
+ record.evidence_kind = finding.evidence_kind;
91626
+ }
91627
+ if (finding.evidence_screen !== undefined) {
91628
+ record.evidence_screen = finding.evidence_screen;
91629
+ }
91330
91630
  if (finding.global_id !== undefined) {
91331
91631
  record.global_id = finding.global_id;
91332
91632
  }
@@ -91585,10 +91885,24 @@ function parseLegacyReport(report, reviewers) {
91585
91885
  if (location.line !== undefined) {
91586
91886
  finding.line = location.line;
91587
91887
  }
91888
+ if (finding.reviewer === "review-degradation") {
91889
+ const degradationClass = closedListValue(block, ["class", "degradation class", "degradation_class"], REVIEW_DEGRADATION_CLASSES);
91890
+ if (degradationClass !== undefined) {
91891
+ finding.degradation_class = degradationClass;
91892
+ }
91893
+ const evidenceKind = closedListValue(block, ["evidence kind", "evidence_kind"], REVIEW_EVIDENCE_KINDS);
91894
+ if (evidenceKind !== undefined) {
91895
+ finding.evidence_kind = evidenceKind;
91896
+ }
91897
+ }
91588
91898
  findings.push(finding);
91589
91899
  }
91590
91900
  return findings;
91591
91901
  }
91902
+ function closedListValue(block, labels, allowed) {
91903
+ const value = labelledField(block, labels)?.trim().replace(/^[*_`]+|[*_`]+$/g, "").trim().toLowerCase();
91904
+ return allowed.find((candidate) => candidate === value);
91905
+ }
91592
91906
  function labelledField(block, labels) {
91593
91907
  const lines = block.split(`
91594
91908
  `);
@@ -91770,7 +92084,13 @@ async function readPreFilterScopeBlock(packageDir) {
91770
92084
  return extractPreFilterScopeBlock(await readFile114(scopePath, "utf8")) ?? undefined;
91771
92085
  }
91772
92086
  function renderScope(input, flowMatch, at, verification, carriedPreFilter, caps, extras = {}) {
91773
- const { externalReclaims = [], scopeBScreen = { source: "none", scopeBFindings: 0 }, filterStats, allFindings = [] } = extras;
92087
+ const {
92088
+ externalReclaims = [],
92089
+ scopeBScreen = { source: "none", scopeBFindings: 0 },
92090
+ degradationScreen = { screened: 0, downgraded: [] },
92091
+ filterStats,
92092
+ allFindings = []
92093
+ } = extras;
91774
92094
  const preFilter = input.scope?.counts ?? input.scopeCounts;
91775
92095
  const drops = input.scope?.drops;
91776
92096
  const head = `# Review Scope
@@ -91792,7 +92112,8 @@ ${renderStageCountsMarkdown({
91792
92112
  })}
91793
92113
 
91794
92114
  ${renderCapsMarkdown(caps)}
91795
- ${renderScopeBScreenMarkdown(scopeBScreen)}${filterStats === undefined ? "" : `
92115
+ ${renderScopeBScreenMarkdown(scopeBScreen)}
92116
+ ${renderDegradationScreenMarkdown(degradationScreen)}${filterStats === undefined ? "" : `
91796
92117
  ${renderFilterStatsMarkdown(filterStats)}`}${renderExternalReclaimsMarkdown(externalReclaims)}${renderPathMissingMarkdown(allFindings)}${renderQuoteUnlocatedMarkdown(allFindings)}`;
91797
92118
  return carriedPreFilter === undefined ? head : `${head.trimEnd()}
91798
92119
 
@@ -92030,6 +92351,7 @@ var init_managed = __esm(() => {
92030
92351
  init_verification();
92031
92352
  init_pr_comments();
92032
92353
  init_blast_radius();
92354
+ init_degradation();
92033
92355
  init_caps();
92034
92356
  init_filter_stats();
92035
92357
  init_dedupe();
@@ -92648,6 +92970,7 @@ var init_completion_gate = __esm(() => {
92648
92970
  "--no-prior-round",
92649
92971
  "--detail",
92650
92972
  "--brief",
92973
+ "--degradation",
92651
92974
  "--all",
92652
92975
  "--gating",
92653
92976
  "--style",
@@ -116856,14 +117179,14 @@ var init_registry2 = __esm(() => {
116856
117179
  label: "Codex",
116857
117180
  binary: "codex",
116858
117181
  detect: ["--version"],
116859
- versionPattern: "codex-cli (\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.]+)?)",
117182
+ versionPattern: "^codex-cli (\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?)[ \\t\\r\\n]*$(?![\\s\\S])",
116860
117183
  knownGoodRange: { min: "0.147.0" },
116861
117184
  sandboxModes: ["read-only", "worktree-write"],
116862
117185
  streamingInput: false,
116863
117186
  resumable: true,
116864
117187
  reportsCost: false,
116865
117188
  budgetFlag: false,
116866
- notes: "Resume requires NOT passing `--ephemeral`: an ephemeral thread fails resume with " + "`no rollout found for thread id \u2026 (code -32600)`. `codex exec resume` also takes a " + "narrower flag set than `codex exec` \u2014 no `-s`, no `-C` \u2014 so a read-only resume inherits " + "its sandbox, a write run re-asserts its confinement as `-c` overrides, and it must be " + "spawned with cwd already set to the worktree. Write mode needs codex 0.159.2 up to, but not including, 0.160.0."
117189
+ notes: "Resume requires NOT passing `--ephemeral`: an ephemeral thread fails resume with " + "`no rollout found for thread id \u2026 (code -32600)`. `codex exec resume` also takes a " + "narrower flag set than `codex exec` \u2014 no `-s`, no `-C` \u2014 so a read-only resume inherits " + "its sandbox, a write run re-asserts its confinement as `-c` overrides, and it must be " + "spawned with cwd already set to the worktree. Write mode needs codex 0.159.2 up to, but not including, 0.160.0, or exactly 0.164.0."
116867
117190
  },
116868
117191
  {
116869
117192
  id: "claude-cli",
@@ -116924,11 +117247,14 @@ function codecWriteVersionRefusal(entry, version) {
116924
117247
  if (verified === undefined)
116925
117248
  return;
116926
117249
  const range = `\`${entry.binary}\` ${verified.min} up to, but not including, ${verified.before}`;
116927
- const prefix = `${entry.label} write mode needs ${range}, the versions whose sandbox confinement was measured; `;
117250
+ const exact = verified.exact?.length ? `, or exactly ${verified.exact.join(", ")}` : "";
117251
+ const prefix = `${entry.label} write mode needs ${range}${exact}, the versions whose sandbox confinement was measured; `;
116928
117252
  if (version === undefined)
116929
117253
  return `${prefix}the installed version could not be read`;
116930
117254
  if (!/^\d+\.\d+\.\d+$/.test(version))
116931
117255
  return `${prefix}found ${version}, a pre-release`;
117256
+ if (verified.exact?.includes(version))
117257
+ return;
116932
117258
  if (compareVersions2(version, verified.min) < 0)
116933
117259
  return `${prefix}found ${version}, which is older`;
116934
117260
  if (compareVersions2(version, verified.before) >= 0) {
@@ -117015,7 +117341,7 @@ var init_dispatch2 = __esm(() => {
117015
117341
  IMPLEMENTED_ACP_SANDBOX_MODES = ["read-only", "worktree-write"];
117016
117342
  CODEC_WRITE_AGENT_IDS = ["claude-cli", "codex-cli"];
117017
117343
  CODEC_WRITE_VERIFIED_VERSIONS = {
117018
- "codex-cli": { min: "0.159.2", before: "0.160.0" }
117344
+ "codex-cli": { min: "0.159.2", before: "0.160.0", exact: ["0.164.0"] }
117019
117345
  };
117020
117346
  });
117021
117347
 
@@ -135660,7 +135986,8 @@ One schema per row: ${DISPOSITION_SCHEMA}`);
135660
135986
  const depth = recordedDepth?.depth;
135661
135987
  const required = await requiredReviewers(root, mode, files, { blastRadius, prContext: packageHasPrContext(packageDir), depth, enabledRoles: await packageEnabledRoles(packageDir) });
135662
135988
  const { payloadSlices, payloadDispatchCounts, ruleFiles, toolLogs: toolLogPaths, models, assignments, replies, dropped } = await readDrivers(inputDir, root);
135663
- const selected = [...new Set([...required.required, ...manifest === undefined ? [] : manifestRosterRoles(manifest)])].sort();
135989
+ const installed = await installedReviewerNames(root);
135990
+ const selected = [...new Set([...required.required, ...manifest === undefined ? [] : manifestRosterRoles(manifest, installed)])].sort();
135664
135991
  const skipped = parseSkips(rest).map((skip) => selected.includes(skip.reviewer) ? skip : { ...skip, reason: `${skip.reason} (not required this round)` });
135665
135992
  const decided = await skipDecision(packageDir, skipped.filter((skip) => selected.includes(skip.reviewer)).map((skip) => skip.reviewer));
135666
135993
  if (!decided.ok)
@@ -135706,6 +136033,7 @@ One schema per row: ${DISPOSITION_SCHEMA}`);
135706
136033
  manifestPath: path315.relative(process.cwd(), manifestPath),
135707
136034
  mutationRecorded: await packageHasMutationRecord(packageDir),
135708
136035
  skipped,
136036
+ installed,
135709
136037
  superseded: required.excluded.filter((entry) => entry.reason.startsWith("superseded by ") || entry.reason.startsWith("overlap group ")),
135710
136038
  supersedeNotes: required.notes,
135711
136039
  screened,
@@ -146893,6 +147221,94 @@ var init_provider_default = __esm(() => {
146893
147221
  init_providers();
146894
147222
  });
146895
147223
 
147224
+ // src/review/degradation-payload.ts
147225
+ function buildDegradationConsumers(input) {
147226
+ const cap = input.cap === undefined || !Number.isFinite(input.cap) ? DEFAULT_DEGRADATION_CONSUMER_CAP : Math.max(0, Math.trunc(input.cap));
147227
+ const inDiff = new Set([...input.changedFiles ?? [], ...input.changedExports.map((entry) => entry.file)]);
147228
+ const nodePaths = new Set(input.graph.nodes.map((node) => node.path));
147229
+ const best = new Map;
147230
+ const unresolved = [];
147231
+ const barrels = new Set(input.barrels ?? []);
147232
+ const targets = [...new Set([...input.changedExports.map((entry) => entry.file), ...barrels])];
147233
+ for (const target of targets) {
147234
+ if (!nodePaths.has(target)) {
147235
+ unresolved.push(target);
147236
+ continue;
147237
+ }
147238
+ const result = computeAffected(input.graph, target, { depth: 1, ranked: true });
147239
+ for (const dependent of result.ranked) {
147240
+ if (inDiff.has(dependent.path) || barrels.has(dependent.path) || best.has(dependent.path)) {
147241
+ continue;
147242
+ }
147243
+ best.set(dependent.path, { file: dependent.path, via: target, fanIn: dependent.fanIn });
147244
+ }
147245
+ }
147246
+ const ranked = [...best.values()].sort((a, b) => a.fanIn !== b.fanIn ? b.fanIn - a.fanIn : a.file < b.file ? -1 : a.file > b.file ? 1 : 0);
147247
+ const consumers = ranked.slice(0, cap);
147248
+ const dropped = ranked.slice(cap);
147249
+ const capped = dropped.length > 0;
147250
+ return {
147251
+ consumers,
147252
+ capped,
147253
+ dropped,
147254
+ cap,
147255
+ total: ranked.length,
147256
+ changedExports: input.changedExports.map((entry) => ({ file: entry.file, names: entry.names })),
147257
+ unresolved: [...new Set(unresolved)].sort(),
147258
+ summary: capped ? `the cap of ${cap} dropped ${dropped.length} of ${ranked.length} consumers; the first ${NOT_TRACED_LISTED} dropped ones are listed, the rest are counted, and none were traced` : `the cap of ${cap} dropped no consumer (${ranked.length} found)`
147259
+ };
147260
+ }
147261
+ function changedExportsOf(radius) {
147262
+ const seeds = radius.reach?.seeds;
147263
+ if (radius.reach?.mode === "symbol" && seeds !== undefined) {
147264
+ return seeds.filter((seed) => seed.names === "*" || seed.names.length > 0).map((seed) => ({ file: seed.file, names: seed.names === "*" ? "*" : [...seed.names].sort() })).sort((a, b) => a.file < b.file ? -1 : a.file > b.file ? 1 : 0);
147265
+ }
147266
+ const unresolved = new Set(radius.unresolved.map((entry) => entry.file));
147267
+ return radius.changedFiles.filter((file) => !unresolved.has(file)).map((file) => ({ file, names: "*" }));
147268
+ }
147269
+ function barrelsOf(radius) {
147270
+ return radius.reach?.mode === "symbol" ? [...new Set(radius.reach.barrels)].sort() : [];
147271
+ }
147272
+ function renderDegradationPayloadBrief(payload) {
147273
+ const lines = [];
147274
+ lines.push("SCOPE: degradation");
147275
+ lines.push("");
147276
+ lines.push("QUESTION: after this change, is the code worse than on the base branch: in behaviour, in defaults, in speed or in");
147277
+ lines.push("structure, and can that be shown? Scope is the diff plus the consumers below. The consumer list is the code");
147278
+ lines.push("graph's, not yours: trace those, and say so when you traced fewer.");
147279
+ lines.push("");
147280
+ lines.push(`CHANGED SHARED EXPORTS (${payload.changedExports.length}):`);
147281
+ for (const entry of payload.changedExports) {
147282
+ lines.push(` - ${safe(entry.file)} ${entry.names === "*" ? "(every export)" : entry.names.map(safe).join(", ")}`);
147283
+ }
147284
+ lines.push("");
147285
+ lines.push(`CONSUMERS (${payload.consumers.length} of ${payload.total}; the files that import the module holding each changed export, or a barrel re-exporting it; most imported first, cap ${payload.cap}):`);
147286
+ for (const consumer of payload.consumers) {
147287
+ lines.push(` - ${safe(consumer.file)} imports ${safe(consumer.via)} [fan-in ${consumer.fanIn}]`);
147288
+ }
147289
+ lines.push("");
147290
+ lines.push(`CAP: ${payload.summary}. The cap is one budget shared by all changed exports, not one per export.`);
147291
+ if (payload.dropped.length > 0) {
147292
+ const listed = payload.dropped.slice(0, NOT_TRACED_LISTED).map((consumer) => safe(consumer.file));
147293
+ const more = payload.dropped.length - listed.length;
147294
+ lines.push(`NOT TRACED (${payload.dropped.length}): ${listed.join(", ")}${more > 0 ? `, and ${more} more` : ""}`);
147295
+ }
147296
+ if (payload.unresolved.length > 0) {
147297
+ lines.push(`NOT ANSWERABLE (${payload.unresolved.length}): ${payload.unresolved.map(safe).join(", ")} absent from the code graph. Their consumers are unknown, not none.`);
147298
+ }
147299
+ lines.push("");
147300
+ return lines.join(`
147301
+ `);
147302
+ }
147303
+ function safe(text) {
147304
+ return text.replace(CONTROL_CHARACTERS2, (char) => `\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}`);
147305
+ }
147306
+ var DEFAULT_DEGRADATION_CONSUMER_CAP = 40, NOT_TRACED_LISTED = 20, CONTROL_CHARACTERS2;
147307
+ var init_degradation_payload = __esm(() => {
147308
+ init_affected();
147309
+ CONTROL_CHARACTERS2 = new RegExp("[\\u0000-\\u001f\\u007f-\\u009f\\u061c\\u200e\\u200f\\u2028\\u2029\\u202a-\\u202e\\u2066-\\u2069]", "g");
147310
+ });
147311
+
146896
147312
  // src/harness/routing/tier-rank-agent.ts
146897
147313
  import path340 from "path";
146898
147314
  function tierRankCacheFilePath(dir) {
@@ -148412,6 +148828,9 @@ async function runCreate(mode, args, createSeams = {}) {
148412
148828
  console.log(` exempted ${exemption.finding.id ?? "<unidentified>"} [${exemption.rule} not applied]: ${exemption.detail}`);
148413
148829
  }
148414
148830
  }
148831
+ for (const line of renderDegradationScreenLines(result.degradationScreen)) {
148832
+ console.log(line);
148833
+ }
148415
148834
  const chunkPass = result.chunkPass;
148416
148835
  if (chunkPass.brought.length > 0 || chunkPass.alreadyReported.length > 0) {
148417
148836
  console.log(`chunk-pass findings: brought_in=${chunkPass.brought.length} already_in_report=${chunkPass.alreadyReported.length}`);
@@ -149610,7 +150029,7 @@ async function runRequired(args) {
149610
150029
  depth: decision.depth
149611
150030
  });
149612
150031
  const sliceManifest = packageDir === undefined ? undefined : await readJsonFileOr(path343.join(packageDir, "slices", "manifest.json"), undefined);
149613
- const fixRoles = sliceManifest !== undefined && Array.isArray(sliceManifest.slices) ? manifestRosterRoles(sliceManifest).filter((role) => !baseSet.required.includes(role)) : [];
150032
+ const fixRoles = sliceManifest !== undefined && Array.isArray(sliceManifest.slices) ? (await installedRosterRoles(process.cwd(), sliceManifest)).filter((role) => !baseSet.required.includes(role)) : [];
149614
150033
  const set = fixRoles.length === 0 ? baseSet : { ...baseSet, required: [...baseSet.required, ...fixRoles].sort() };
149615
150034
  if (packageDir !== undefined) {
149616
150035
  const basis = await packageDepthBasis(packageDir);
@@ -150018,6 +150437,15 @@ async function runBlastRadius(args) {
150018
150437
  const maxFiles = parseNonNegativeInteger(optionValue(args, "--max-files"), "--max-files");
150019
150438
  const includeRelatedTests = !args.includes("--no-related-tests");
150020
150439
  const isFinalRound = args.includes("--final");
150440
+ const wantsDegradation = args.includes("--degradation");
150441
+ if (wantsDegradation) {
150442
+ const clash = ["--json", "--brief", "--out"].find((flag) => flag === "--out" ? optionValue(args, flag) !== undefined : args.includes(flag));
150443
+ if (clash !== undefined)
150444
+ throw new Error(`--degradation prints the review-degradation payload to stdout and cannot be combined with ${clash}; run blast-radius again for that output.`);
150445
+ } else if (args.some((arg) => arg === "--degradation-cap")) {
150446
+ throw new Error("--degradation-cap only applies together with --degradation.");
150447
+ }
150448
+ const degradationCap = wantsDegradation ? parseNonNegativeInteger(optionValue(args, "--degradation-cap"), "--degradation-cap") : undefined;
150021
150449
  const explicit = optionValue(args, "--changed");
150022
150450
  const ref = optionValue(args, "--ref") ?? optionValue(args, "--base");
150023
150451
  const reachMode = optionValue(args, "--reach") ?? "symbol";
@@ -150048,8 +150476,12 @@ async function runBlastRadius(args) {
150048
150476
  depth,
150049
150477
  maxFiles
150050
150478
  });
150051
- console.error(`recompute: ${decision.recompute ? "yes" : "no"} \u2014 ${decision.reason}`);
150052
- if (!decision.recompute && previous !== undefined) {
150479
+ const reuse = !decision.recompute && previous !== undefined;
150480
+ const recomputeForDegradation = reuse && wantsDegradation;
150481
+ const recompute = decision.recompute || recomputeForDegradation;
150482
+ const recomputeReason = recomputeForDegradation ? "the degradation payload needs the graph" : decision.reason;
150483
+ console.error(`recompute: ${recompute ? "yes" : "no"} \u2014 ${recomputeReason}`);
150484
+ if (reuse && !wantsDegradation) {
150053
150485
  await emitBlastRadius(previous, args);
150054
150486
  return;
150055
150487
  }
@@ -150092,7 +150524,14 @@ async function runBlastRadius(args) {
150092
150524
  textLinks,
150093
150525
  testCommand: detectTestCommand(projectRoot)
150094
150526
  });
150095
- await emitBlastRadius(radius, args);
150527
+ const degradationBrief = wantsDegradation ? renderDegradationPayloadBrief(buildDegradationConsumers({
150528
+ graph,
150529
+ changedExports: changedExportsOf(radius),
150530
+ changedFiles: radius.changedFiles,
150531
+ barrels: barrelsOf(radius),
150532
+ cap: degradationCap
150533
+ })) : undefined;
150534
+ await emitBlastRadius(radius, args, degradationBrief);
150096
150535
  const reasoned = (radius.counts.droppedUnrelatedSymbol ?? 0) + (radius.counts.droppedHubDamped ?? 0) + (radius.counts.droppedTextHub ?? 0) + (radius.counts.droppedTestConsumer ?? 0);
150097
150536
  if (reasoned > 0) {
150098
150537
  console.error(`omitted with a reason: ${radius.counts.droppedUnrelatedSymbol ?? 0} unrelated-symbol, ${radius.counts.droppedHubDamped ?? 0} hub-damped, ${radius.counts.droppedTextHub ?? 0} text-hub, ${radius.counts.droppedTestConsumer ?? 0} test-consumer. ` + "They are context, not owed a chunk pass.");
@@ -150106,7 +150545,7 @@ async function runBlastRadius(args) {
150106
150545
  }
150107
150546
  if (radius.counts.droppedByCap > 0) {
150108
150547
  console.error(`cap: ${radius.counts.droppedByCap} of ${radius.counts.candidates} candidate files were NOT reviewed (blast_radius_max_files=${radius.maxFiles}). They are listed in the record. ` + `Cut blast radius: run the waves \u2014 keryx review chunks plan --package ${packageOption ?? "<review>"}`);
150109
- if (packageOption !== undefined) {
150548
+ if (packageOption !== undefined && !wantsDegradation) {
150110
150549
  const planned = await recordRadiusAndPlan(resolvePackageDir(args, cwd), resolveProjectRoot2(cwd), radius, packageOption);
150111
150550
  console.error(planned);
150112
150551
  }
@@ -150115,8 +150554,10 @@ async function runBlastRadius(args) {
150115
150554
  console.error(`unresolved: ${radius.counts.changedFilesUnresolved} changed file(s) are absent from the code graph; their blast radius is unknown, not empty.`);
150116
150555
  }
150117
150556
  }
150118
- async function emitBlastRadius(radius, args) {
150119
- if (args.includes("--json")) {
150557
+ async function emitBlastRadius(radius, args, degradationBrief) {
150558
+ if (degradationBrief !== undefined) {
150559
+ console.log(degradationBrief);
150560
+ } else if (args.includes("--json")) {
150120
150561
  console.log(JSON.stringify(radius, null, 2));
150121
150562
  } else if (args.includes("--brief")) {
150122
150563
  console.log(renderBlastRadiusDispatchBrief(radius));
@@ -150673,14 +151114,19 @@ Writes mutation-<n>.json and .md to research-evidence/.`);
150673
151114
  function printBlastRadiusHelp() {
150674
151115
  console.log(`keryx review blast-radius [--ref <base> | --changed a,b] [--base <ref>] [--depth <n>] [--max-files <n>]
150675
151116
  [--no-related-tests] [--final] [--previous <blast-radius.json>]
150676
- [--package <review-dir>] [--reach symbol|file] [--no-text-links] [--json | --brief] [--out <file>]
151117
+ [--package <review-dir>] [--reach symbol|file] [--no-text-links] [--json | --brief | --degradation [--degradation-cap <n>]] [--out <file>]
150677
151118
 
150678
151119
  The files a change can reach: dependants of the changed files, up to --depth, capped at --max-files, with related tests.
150679
151120
  --reach symbol (default) follows the changed exported names: importers that use none of them, and files behind a
150680
151121
  high fan-in hub, are omitted with a reason instead of reviewed; --reach file walks every reverse import edge.
150681
151122
  Files that mention a string key the diff changed (i18n key, event, CSS class, config key) are added unless
150682
151123
  --no-text-links. With --package the changed files are base...head at the package's pinned head.
150683
- --package stores the record in the package; --previous/--final carry the dropped files across rounds.`);
151124
+ --package stores the record in the package; --previous/--final carry the dropped files across rounds.
151125
+ --degradation prints the review-degradation payload instead: the one-hop importers of the module that holds each changed
151126
+ shared export (and of a barrel that re-exports it), from the code graph, under one cap shared by all of them
151127
+ (--degradation-cap, default 40), with the cap stated and the files it dropped named (the first 20, then a count).
151128
+ It always recomputes from the graph, also with --previous, never rewrites the package's record, and is refused together
151129
+ with --json, --brief or --out; --degradation-cap is refused without --degradation.`);
150684
151130
  }
150685
151131
  function printCompleteHelp() {
150686
151132
  console.log(`keryx review complete <review-id-or-path> [--finding <id>] [--disposition <value>] [--evidence <text>]
@@ -151310,12 +151756,12 @@ var init_review = __esm(() => {
151310
151756
  init_context_collect();
151311
151757
  init_mutate();
151312
151758
  init_compare_reference();
151759
+ init_roster();
151313
151760
  init_coverage();
151314
151761
  init_role_switches();
151315
151762
  init_estimate_actual();
151316
151763
  init_estimate();
151317
151764
  init_json();
151318
- init_round_split();
151319
151765
  init_review_plan();
151320
151766
  init_review_plan_eval();
151321
151767
  init_coverage_complete();
@@ -151375,6 +151821,8 @@ var init_review = __esm(() => {
151375
151821
  init_external_providers();
151376
151822
  init_external_notice();
151377
151823
  init_blast_radius();
151824
+ init_degradation();
151825
+ init_degradation_payload();
151378
151826
  init_query();
151379
151827
  init_live_codex_models();
151380
151828
  init_select();
@@ -151514,7 +151962,9 @@ var init_review = __esm(() => {
151514
151962
  "--out",
151515
151963
  "--package",
151516
151964
  "--reach",
151517
- "--no-text-links"
151965
+ "--no-text-links",
151966
+ "--degradation",
151967
+ "--degradation-cap"
151518
151968
  ];
151519
151969
  TIER_FLAGS = [
151520
151970
  "--help",
@@ -160755,105 +161205,6 @@ var init_statusbar = __esm(() => {
160755
161205
  CSI = `${ESC}[`;
160756
161206
  });
160757
161207
 
160758
- // src/lib/live-render.ts
160759
- function stripAnsi(text) {
160760
- return text.replace(ANSI_RE, "");
160761
- }
160762
- function isWideCodePoint(cp) {
160763
- return cp >= 4352 && cp <= 4447 || cp >= 11904 && cp <= 12350 || cp >= 12353 && cp <= 13311 || cp >= 13312 && cp <= 19903 || cp >= 19968 && cp <= 40959 || cp >= 40960 && cp <= 42191 || cp >= 44032 && cp <= 55203 || cp >= 63744 && cp <= 64255 || cp >= 65072 && cp <= 65103 || cp >= 65280 && cp <= 65376 || cp >= 65504 && cp <= 65510 || cp >= 127744 && cp <= 129791 || cp >= 131072 && cp <= 262141;
160764
- }
160765
- function displayWidth(text) {
160766
- const plain = stripAnsi(text);
160767
- let width = 0;
160768
- for (const ch of plain) {
160769
- const cp = ch.codePointAt(0) ?? 0;
160770
- width += isWideCodePoint(cp) ? 2 : 1;
160771
- }
160772
- return width;
160773
- }
160774
- function physicalRows(lines, cols) {
160775
- if (cols <= 0) {
160776
- return lines.length;
160777
- }
160778
- let rows = 0;
160779
- for (const line of lines) {
160780
- const width = displayWidth(line);
160781
- rows += width === 0 ? 1 : Math.ceil(width / cols);
160782
- }
160783
- return rows;
160784
- }
160785
- function computeRepaint(opts) {
160786
- const { prevRows, nextLines, cols, sync = false } = opts;
160787
- let body = "";
160788
- if (prevRows > 0) {
160789
- body += "\r";
160790
- if (prevRows > 1) {
160791
- body += CURSOR_UP(prevRows - 1);
160792
- }
160793
- body += CLEAR_TO_END;
160794
- }
160795
- body += nextLines.join(`
160796
- `);
160797
- const output = sync ? `${SYNC_BEGIN}${body}${SYNC_END}` : body;
160798
- return { output, rows: physicalRows(nextLines, cols) };
160799
- }
160800
-
160801
- class LiveMarkdownBlock {
160802
- deps;
160803
- pending = "";
160804
- prevRows = 0;
160805
- prevCols = 0;
160806
- dirty = false;
160807
- constructor(deps) {
160808
- this.deps = deps;
160809
- }
160810
- append(text) {
160811
- if (text.length === 0) {
160812
- return;
160813
- }
160814
- this.pending += text;
160815
- this.dirty = true;
160816
- }
160817
- flush() {
160818
- if (!this.dirty) {
160819
- return;
160820
- }
160821
- this.repaint();
160822
- this.dirty = false;
160823
- }
160824
- finalize() {
160825
- if (this.dirty) {
160826
- this.repaint();
160827
- this.dirty = false;
160828
- }
160829
- if (this.prevRows > 0 || this.pending.length > 0) {
160830
- this.deps.out(`
160831
- `);
160832
- }
160833
- this.pending = "";
160834
- this.prevRows = 0;
160835
- this.prevCols = 0;
160836
- }
160837
- repaint() {
160838
- const cols = this.deps.cols();
160839
- if (this.prevRows > 0 && cols !== this.prevCols) {
160840
- this.deps.out(`
160841
- `);
160842
- this.prevRows = 0;
160843
- }
160844
- const lines = this.deps.render(this.pending).split(`
160845
- `);
160846
- const { output, rows } = computeRepaint({ prevRows: this.prevRows, nextLines: lines, cols, sync: this.deps.sync });
160847
- this.deps.out(output);
160848
- this.prevRows = rows;
160849
- this.prevCols = cols;
160850
- }
160851
- }
160852
- var ANSI_RE, CURSOR_UP = (n) => `\x1B[${n}A`, CLEAR_TO_END = "\x1B[0J", SYNC_BEGIN = "\x1B[?2026h", SYNC_END = "\x1B[?2026l";
160853
- var init_live_render = __esm(() => {
160854
- ANSI_RE = /\x1b\[[0-9;?]*[A-Za-z]/g;
160855
- });
160856
-
160857
161208
  // src/remote/rendering-mode.ts
160858
161209
  function isRenderMode(value) {
160859
161210
  return typeof value === "string" && RENDER_MODES.includes(value);
@@ -167231,7 +167582,15 @@ function unmountActiveTab(state) {
167231
167582
  state.scroll.scrollTop = 0;
167232
167583
  }
167233
167584
  function mountTab(state, input, tabId) {
167585
+ const target = state.tabs.find((tab) => tab.id === tabId);
167586
+ if (target === undefined)
167587
+ return;
167588
+ const previous = state.tabs.find((tab) => tab.id === state.active);
167234
167589
  unmountActiveTab(state);
167590
+ if (previous?.temporary && previous.id !== tabId) {
167591
+ state.tabs = state.tabs.filter((tab) => tab.id !== previous.id);
167592
+ state.temporaryParents.delete(previous.id);
167593
+ }
167235
167594
  state.active = tabId;
167236
167595
  paintTabs(state);
167237
167596
  if (input.footerFor !== undefined) {
@@ -167240,7 +167599,7 @@ function mountTab(state, input, tabId) {
167240
167599
  const availWidth = resolveModalAvailableWidth(state.chrome);
167241
167600
  const size = resolveModalPanelSize(availWidth, state.chrome.renderer.height, input.contentRows);
167242
167601
  const panelWidth = Math.min(availWidth, size.width);
167243
- const cleanup = input.renderTab(tabId, state.body, {
167602
+ const cleanup = (target.renderTab ?? input.renderTab)(tabId, state.body, {
167244
167603
  width: resolveModalInnerWidth(panelWidth) - MODAL_BODY_SCROLLBAR_COLS,
167245
167604
  height: modalBodyRows(size.height)
167246
167605
  });
@@ -167400,6 +167759,7 @@ function ensureHost(otui, chrome) {
167400
167759
  open: false,
167401
167760
  generation: 0,
167402
167761
  tabs: [],
167762
+ temporaryParents: new Map,
167403
167763
  active: "",
167404
167764
  tabCleanup: undefined,
167405
167765
  onClose: undefined,
@@ -167422,7 +167782,12 @@ function ensureHost(otui, chrome) {
167422
167782
  const focused = r.currentFocusedRenderable;
167423
167783
  const inBody = focused !== null && containsNode(state.scroll, focused);
167424
167784
  if (key.name === "escape") {
167425
- closeHost(state, { restoreFocus: true, runOnClose: true });
167785
+ const parent = state.temporaryParents.get(state.active);
167786
+ if (parent !== undefined && state.tabs.some((tab) => tab.id === parent)) {
167787
+ mountTab(state, state.input, parent);
167788
+ } else {
167789
+ closeHost(state, { restoreFocus: true, runOnClose: true });
167790
+ }
167426
167791
  key.preventDefault();
167427
167792
  key.stopPropagation();
167428
167793
  return;
@@ -167513,6 +167878,39 @@ function makeHandle(state, generation) {
167513
167878
  }
167514
167879
  mountTab(state, state.input, id);
167515
167880
  },
167881
+ addTab(tab, afterId) {
167882
+ if (state.generation !== generation || !state.open || state.input === undefined)
167883
+ return;
167884
+ if (state.tabs.some((entry) => entry.id === tab.id))
167885
+ return;
167886
+ const parent = afterId ?? state.temporaryParents.get(state.active) ?? state.active;
167887
+ const index = state.tabs.findIndex((entry) => entry.id === parent && !entry.temporary);
167888
+ if (index < 0)
167889
+ return;
167890
+ state.tabs.splice(index + 1, 0, { ...tab });
167891
+ if (tab.temporary)
167892
+ state.temporaryParents.set(tab.id, parent);
167893
+ mountTab(state, state.input, tab.id);
167894
+ state.chrome.renderer.requestRender();
167895
+ },
167896
+ removeTab(id) {
167897
+ if (state.generation !== generation || !state.open || state.input === undefined)
167898
+ return;
167899
+ const tab = state.tabs.find((entry) => entry.id === id);
167900
+ if (!tab?.temporary)
167901
+ return;
167902
+ if (state.active === id) {
167903
+ const parent = state.temporaryParents.get(id);
167904
+ if (parent === undefined)
167905
+ return;
167906
+ mountTab(state, state.input, parent);
167907
+ } else {
167908
+ state.tabs = state.tabs.filter((entry) => entry.id !== id);
167909
+ state.temporaryParents.delete(id);
167910
+ paintTabs(state);
167911
+ }
167912
+ state.chrome.renderer.requestRender();
167913
+ },
167516
167914
  activeTab() {
167517
167915
  return state.active;
167518
167916
  }
@@ -167545,7 +167943,8 @@ function openModal(otui, chrome, input) {
167545
167943
  state.generation += 1;
167546
167944
  const generation = state.generation;
167547
167945
  state.input = input;
167548
- state.tabs = input.tabs;
167946
+ state.tabs = input.tabs.map((tab) => ({ ...tab }));
167947
+ state.temporaryParents.clear();
167549
167948
  state.onClose = input.onClose;
167550
167949
  paintHeader(state, input.title);
167551
167950
  paintFooter(state, input.footer);
@@ -169445,6 +169844,44 @@ async function layersViewOf(dir, dispatch) {
169445
169844
  waiver: readCoverageWaiver(dispatch)
169446
169845
  };
169447
169846
  }
169847
+ async function readDegradationFindings(packageDir) {
169848
+ const parsed = await readJson14(path375.join(packageDir, "findings.json"));
169849
+ if (!Array.isArray(parsed))
169850
+ return [];
169851
+ const out = [];
169852
+ for (const raw of parsed) {
169853
+ if (raw === null || typeof raw !== "object")
169854
+ continue;
169855
+ const f = raw;
169856
+ const screen = f.evidence_screen !== null && typeof f.evidence_screen === "object" ? f.evidence_screen : undefined;
169857
+ const hasField = f.degradation_class !== undefined || f.evidence_kind !== undefined || screen !== undefined;
169858
+ if (!isDegradationReviewer(f.reviewer) && !hasField)
169859
+ continue;
169860
+ out.push({
169861
+ id: str4(f.id) ?? "?",
169862
+ severity: str4(f.severity) ?? "?",
169863
+ degradationClass: str4(f.degradation_class),
169864
+ evidenceKind: str4(f.evidence_kind),
169865
+ unproven: screen !== undefined,
169866
+ originalSeverity: str4(screen?.original_severity),
169867
+ file: str4(f.file)
169868
+ });
169869
+ }
169870
+ return out;
169871
+ }
169872
+ function degradationLines(findings) {
169873
+ if (findings === undefined || findings.length === 0)
169874
+ return [];
169875
+ const unproven = findings.filter((f) => f.unproven).length;
169876
+ const head = `degradation: ${findings.length} finding(s)${unproven > 0 ? `, ${unproven} unproven` : ""}`;
169877
+ return [
169878
+ head,
169879
+ ...findings.map((f) => {
169880
+ const proof = f.unproven ? `UNPROVEN${f.originalSeverity === undefined ? "" : ` (was ${f.originalSeverity})`}` : `evidence ${f.evidenceKind ?? "none"}`;
169881
+ return ` ${safeText(`${f.id} ${f.severity} ${f.degradationClass ?? "no class"}, ${proof}${f.file === undefined ? "" : ` ${f.file}`}`)}`;
169882
+ })
169883
+ ];
169884
+ }
169448
169885
  function pullLabel(review) {
169449
169886
  if (review.repo !== null && review.number !== null)
169450
169887
  return `${review.repo}#${review.number}`;
@@ -169460,10 +169897,11 @@ async function loadReviewDepth(cwd, listed) {
169460
169897
  const waiver = dispatch?.mutationWaiver;
169461
169898
  const mutationWaived = waiver !== null && typeof waiver === "object" && typeof waiver.operatorQuote === "string";
169462
169899
  const parity = await readReviewParity(review.dir);
169900
+ const degradation = await readDegradationFindings(review.dir);
169463
169901
  const timing = await packageEstimateLine(review.dir, { open: review.manifest.status !== "closed" });
169464
169902
  const base = { reviewId: review.reviewId, pull: pullLabel(review), selected, mutation: await mutationOf(review.dir), mutationWaived, layersView: await layersViewOf(review.dir, dispatch), readiness: await computeReadiness(review.dir).catch(() => {
169465
169903
  return;
169466
- }), ...parityIsEmpty(parity) ? {} : { parity }, ...timing === undefined ? {} : { timing } };
169904
+ }), ...parityIsEmpty(parity) ? {} : { parity }, ...degradation.length === 0 ? {} : { degradation }, ...timing === undefined ? {} : { timing } };
169467
169905
  if (mode === undefined) {
169468
169906
  rows.push({ ...base, mode: "unrecorded", required: [], excluded: [], missing: [], filesKnown: false });
169469
169907
  continue;
@@ -169522,7 +169960,7 @@ function layersDepthLines(view) {
169522
169960
  function formatReviewDepthRowLines(row) {
169523
169961
  const head = `${row.pull} ${row.reviewId} mode ${row.mode}`;
169524
169962
  const lines = [head];
169525
- const layers = [...readinessDepthLines(row.readiness), ...layersDepthLines(row.layersView), ...parityLines(row.parity), ...row.timing === undefined ? [] : [row.timing]].map((line) => ` ${line}`);
169963
+ const layers = [...readinessDepthLines(row.readiness), ...layersDepthLines(row.layersView), ...parityLines(row.parity), ...degradationLines(row.degradation), ...row.timing === undefined ? [] : [row.timing]].map((line) => ` ${line}`);
169526
169964
  if (row.mode === "unrecorded") {
169527
169965
  lines.push(" no dispatch record: the reviewer set is not known", ` ${mutationLine(row.mutation, row.mutationWaived)}`, ...layers);
169528
169966
  return lines;
@@ -169585,9 +170023,11 @@ ${blocks.join(`
169585
170023
  `)}
169586
170024
  `;
169587
170025
  }
169588
- var REVIEW_DEPTH_EMPTY = "No managed review has recorded a dispatch yet.", ZERO;
170026
+ var REVIEW_DEPTH_EMPTY = "No managed review has recorded a dispatch yet.", ZERO, str4 = (value) => typeof value === "string" && value !== "" ? value : undefined;
169589
170027
  var init_review_panel = __esm(() => {
170028
+ init_live_render();
169590
170029
  init_shell_quote();
170030
+ init_degradation();
169591
170031
  init_packages();
169592
170032
  init_coverage();
169593
170033
  init_role_switches();
@@ -177691,7 +178131,7 @@ function mountApprovalsSidebar(options) {
177691
178131
  if (known.has(view.approvalId))
177692
178132
  continue;
177693
178133
  known.add(view.approvalId);
177694
- notice(`Remote approval pending: ${view.summary.replace(CONTROL_CHARACTERS2, " ")} - ${APPROVALS_COMMAND} to answer
178134
+ notice(`Remote approval pending: ${view.summary.replace(CONTROL_CHARACTERS3, " ")} - ${APPROVALS_COMMAND} to answer
177695
178135
  `);
177696
178136
  }
177697
178137
  }
@@ -177721,7 +178161,7 @@ function mountApprovalsSidebar(options) {
177721
178161
  }
177722
178162
  };
177723
178163
  }
177724
- var CONTROL_CHARACTERS2, APPROVALS_POLL_MS = 3000;
178164
+ var CONTROL_CHARACTERS3, APPROVALS_POLL_MS = 3000;
177725
178165
  var init_approvals_sidebar = __esm(() => {
177726
178166
  init_agent_commands();
177727
178167
  init_serve_approvals_store();
@@ -177731,7 +178171,7 @@ var init_approvals_sidebar = __esm(() => {
177731
178171
  init_theme_repaint();
177732
178172
  init_theme_text();
177733
178173
  init_transcript_blocks();
177734
- CONTROL_CHARACTERS2 = /[\u0000-\u001f\u007f]/g;
178174
+ CONTROL_CHARACTERS3 = /[\u0000-\u001f\u007f]/g;
177735
178175
  });
177736
178176
 
177737
178177
  // src/intake/ports.ts
@@ -178662,11 +179102,11 @@ var init_actions = __esm(() => {
178662
179102
  function record6(value) {
178663
179103
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : undefined;
178664
179104
  }
178665
- function str4(value) {
179105
+ function str5(value) {
178666
179106
  return typeof value === "string" && value.length > 0 ? value : undefined;
178667
179107
  }
178668
179108
  function login2(value) {
178669
- return str4(record6(value)?.["login"]);
179109
+ return str5(record6(value)?.["login"]);
178670
179110
  }
178671
179111
  function rows2(stdout) {
178672
179112
  let data;
@@ -178689,18 +179129,18 @@ function parseAssignedIssues(repo, stdout) {
178689
179129
  const events = [];
178690
179130
  for (const r of list) {
178691
179131
  const n = numberOf(r);
178692
- const updated = str4(r["updatedAt"]);
179132
+ const updated = str5(r["updatedAt"]);
178693
179133
  if (n === undefined || updated === undefined)
178694
179134
  continue;
178695
- const url = str4(r["url"]);
179135
+ const url = str5(r["url"]);
178696
179136
  const author = login2(r["author"]);
178697
- const body = str4(r["body"]);
179137
+ const body = str5(r["body"]);
178698
179138
  events.push({
178699
179139
  key: `issue:${repo}#${n}`,
178700
179140
  kind: "issue",
178701
179141
  repo,
178702
179142
  ref: n,
178703
- title: str4(r["title"]) ?? `#${n}`,
179143
+ title: str5(r["title"]) ?? `#${n}`,
178704
179144
  ...url !== undefined ? { url } : {},
178705
179145
  stamp: updated,
178706
179146
  ...author !== undefined ? { author } : {},
@@ -178716,17 +179156,17 @@ function parseReviewRequests(repo, stdout) {
178716
179156
  const events = [];
178717
179157
  for (const r of list) {
178718
179158
  const n = numberOf(r);
178719
- const updated = str4(r["updatedAt"]);
179159
+ const updated = str5(r["updatedAt"]);
178720
179160
  if (n === undefined || updated === undefined)
178721
179161
  continue;
178722
- const url = str4(r["url"]);
179162
+ const url = str5(r["url"]);
178723
179163
  const author = login2(r["author"]);
178724
179164
  events.push({
178725
179165
  key: `review:${repo}#${n}`,
178726
179166
  kind: "review",
178727
179167
  repo,
178728
179168
  ref: n,
178729
- title: str4(r["title"]) ?? `#${n}`,
179169
+ title: str5(r["title"]) ?? `#${n}`,
178730
179170
  ...url !== undefined ? { url } : {},
178731
179171
  stamp: updated,
178732
179172
  ...author !== undefined ? { author } : {}
@@ -178744,12 +179184,12 @@ function parseOwnPrs(repo, stdout) {
178744
179184
  const n = numberOf(pr);
178745
179185
  if (n === undefined)
178746
179186
  continue;
178747
- const branch = str4(pr["headRefName"]);
179187
+ const branch = str5(pr["headRefName"]);
178748
179188
  if (branch !== undefined)
178749
179189
  branches.add(branch);
178750
179190
  const prAuthor = login2(pr["author"]);
178751
- const prTitle = str4(pr["title"]) ?? `#${n}`;
178752
- const prUrl = str4(pr["url"]);
179191
+ const prTitle = str5(pr["title"]) ?? `#${n}`;
179192
+ const prUrl = str5(pr["url"]);
178753
179193
  const raw = pr["comments"];
178754
179194
  if (!Array.isArray(raw))
178755
179195
  continue;
@@ -178757,15 +179197,15 @@ function parseOwnPrs(repo, stdout) {
178757
179197
  const r = record6(c);
178758
179198
  if (r === undefined)
178759
179199
  continue;
178760
- const id = str4(r["id"]);
178761
- const created = str4(r["createdAt"]);
179200
+ const id = str5(r["id"]);
179201
+ const created = str5(r["createdAt"]);
178762
179202
  if (id === undefined || created === undefined)
178763
179203
  continue;
178764
179204
  const author = login2(r["author"]);
178765
179205
  if (r["viewerDidAuthor"] === true || author !== undefined && author === prAuthor)
178766
179206
  continue;
178767
- const url = str4(r["url"]) ?? prUrl;
178768
- const body = str4(r["body"]);
179207
+ const url = str5(r["url"]) ?? prUrl;
179208
+ const body = str5(r["body"]);
178769
179209
  comments.push({
178770
179210
  key: `comment:${repo}#${n}:${id}`,
178771
179211
  kind: "comment",
@@ -178788,17 +179228,17 @@ function parseFailedRuns(repo, stdout, branches) {
178788
179228
  const events = [];
178789
179229
  for (const r of list) {
178790
179230
  const id = typeof r["databaseId"] === "number" ? String(r["databaseId"]) : undefined;
178791
- const created = str4(r["createdAt"]);
178792
- const branch = str4(r["headBranch"]);
179231
+ const created = str5(r["createdAt"]);
179232
+ const branch = str5(r["headBranch"]);
178793
179233
  if (id === undefined || created === undefined || branch === undefined || !branches.has(branch))
178794
179234
  continue;
178795
- const url = str4(r["url"]);
179235
+ const url = str5(r["url"]);
178796
179236
  events.push({
178797
179237
  key: `ci:${repo}:${id}`,
178798
179238
  kind: "ci",
178799
179239
  repo,
178800
179240
  ref: id,
178801
- title: `${str4(r["displayTitle"]) ?? "workflow run"} (${branch})`,
179241
+ title: `${str5(r["displayTitle"]) ?? "workflow run"} (${branch})`,
178802
179242
  ...url !== undefined ? { url } : {},
178803
179243
  stamp: created
178804
179244
  });
@@ -179607,7 +180047,7 @@ function resolveDeps(deps) {
179607
180047
  };
179608
180048
  }
179609
180049
  function clean2(text, max = 90) {
179610
- const flat = text.replace(CONTROL_CHARACTERS3, " ").replace(/\s+/g, " ").trim();
180050
+ const flat = text.replace(CONTROL_CHARACTERS4, " ").replace(/\s+/g, " ").trim();
179611
180051
  return flat.length <= max ? flat : `${flat.slice(0, Math.max(0, max - 1))}\u2026`;
179612
180052
  }
179613
180053
  function intakeActionsFor(card) {
@@ -180028,7 +180468,7 @@ function mountIntakeSidebar(options) {
180028
180468
  var INTAKE_COMMAND = "/intake", INTAKE_USAGE, INTAKE_POLL_MS = 5000, INTAKE_TABS, INTAKE_FOOTER, decideWithRealPorts = (root, cardId, action, options) => {
180029
180469
  ensureRealIntakePorts();
180030
180470
  return decideIntakeCard(root, cardId, action, options);
180031
- }, CONTROL_CHARACTERS3, ACTION_LABEL, KEY_ACTIONS, EMPTY4;
180471
+ }, CONTROL_CHARACTERS4, ACTION_LABEL, KEY_ACTIONS, EMPTY4;
180032
180472
  var init_intake_surface = __esm(() => {
180033
180473
  init_intake_ports();
180034
180474
  init_actions();
@@ -180057,7 +180497,7 @@ var init_intake_surface = __esm(() => {
180057
180497
  { key: "p", label: "\u043F\u0430\u0443\u0437\u0430/\u043F\u0443\u0441\u043A" },
180058
180498
  { key: "esc", label: "\u0437\u0430\u043A\u0440\u044B\u0442\u044C" }
180059
180499
  ];
180060
- CONTROL_CHARACTERS3 = /[\u0000-\u001f\u007f-\u009f]/g;
180500
+ CONTROL_CHARACTERS4 = /[\u0000-\u001f\u007f-\u009f]/g;
180061
180501
  ACTION_LABEL = {
180062
180502
  take: "\u0432\u0437\u044F\u0442\u044C",
180063
180503
  decline: "\u043E\u0442\u043A\u043B\u043E\u043D\u0438\u0442\u044C",
@@ -195655,9 +196095,11 @@ class ForegroundOperationOwner {
195655
196095
  }
195656
196096
  cancel(reason) {
195657
196097
  const controller = this.active?.controller;
195658
- if (controller !== undefined && !controller.signal.aborted) {
195659
- controller.abort(reason);
196098
+ if (controller === undefined || controller.signal.aborted) {
196099
+ return false;
195660
196100
  }
196101
+ controller.abort(reason);
196102
+ return true;
195661
196103
  }
195662
196104
  invalidateSession() {
195663
196105
  this.sessionGeneration += 1;
@@ -195688,6 +196130,10 @@ function createForegroundOperationOwner() {
195688
196130
  class ForegroundForceHandoff {
195689
196131
  pending = [];
195690
196132
  awaitingSettlement = false;
196133
+ handoffGeneration = 0;
196134
+ get generation() {
196135
+ return this.handoffGeneration;
196136
+ }
195691
196137
  enqueue(item) {
195692
196138
  this.pending.push(item);
195693
196139
  if (this.awaitingSettlement)
@@ -195696,8 +196142,19 @@ class ForegroundForceHandoff {
195696
196142
  return true;
195697
196143
  }
195698
196144
  clear() {
196145
+ this.handoffGeneration += 1;
196146
+ this.pending.length = 0;
196147
+ this.awaitingSettlement = false;
196148
+ }
196149
+ releaseAwaitingSettlement() {
196150
+ this.awaitingSettlement = false;
196151
+ }
196152
+ drainPending() {
196153
+ this.handoffGeneration += 1;
196154
+ const items = [...this.pending];
195699
196155
  this.pending.length = 0;
195700
196156
  this.awaitingSettlement = false;
196157
+ return items;
195701
196158
  }
195702
196159
  get isAwaitingSettlement() {
195703
196160
  return this.awaitingSettlement;
@@ -195733,21 +196190,32 @@ async function runAfterForegroundSettlement(owner, action) {
195733
196190
  await owner.settled();
195734
196191
  if (!owner.isDisposed && owner.generation === generation) {
195735
196192
  action();
196193
+ return true;
195736
196194
  }
196195
+ return false;
195737
196196
  }
195738
196197
  async function forceForegroundQueueItem(owner, handoff, item, deps) {
195739
196198
  const ownsSettlement = handoff.enqueue(item);
195740
- owner.cancel(deps.cancelReason ?? "queue item forced");
195741
- deps.announce?.();
196199
+ const handoffGeneration = handoff.generation;
196200
+ const interrupted = owner.cancel(deps.cancelReason ?? "queue item forced");
196201
+ deps.announce?.(interrupted);
195742
196202
  if (!ownsSettlement) {
195743
196203
  return;
195744
196204
  }
195745
- await runAfterForegroundSettlement(owner, () => {
196205
+ const settlementActionRan = await runAfterForegroundSettlement(owner, () => {
196206
+ if (deps.isBusy?.() === true) {
196207
+ handoff.releaseAwaitingSettlement();
196208
+ deps.resume?.();
196209
+ return;
196210
+ }
195746
196211
  const next = handoff.takeAfterSettlement();
195747
196212
  if (next !== undefined) {
195748
196213
  deps.run(next);
195749
196214
  }
195750
196215
  });
196216
+ if (!settlementActionRan && handoff.generation === handoffGeneration) {
196217
+ deps.requeue?.(handoff.drainPending());
196218
+ }
195751
196219
  }
195752
196220
  function withTurnScopedIo(io, turn) {
195753
196221
  const scoped = Object.create(io);
@@ -196912,6 +197380,46 @@ function openSidebarCard(otui, chrome, cardId, options) {
196912
197380
  const tab = tabOf(tabId);
196913
197381
  if (tab === undefined)
196914
197382
  return;
197383
+ if (options.snapshot().items?.[tabId] !== undefined) {
197384
+ const rows = new otui.BoxRenderable(chrome.renderer, {
197385
+ id: `sb-card-items-${tabId}`,
197386
+ flexDirection: "column",
197387
+ flexShrink: 0
197388
+ });
197389
+ body.add(rows);
197390
+ const paint = () => {
197391
+ for (const child of [...rows.getChildren()]) {
197392
+ rows.remove(child);
197393
+ child.destroy();
197394
+ }
197395
+ const snapshot = options.snapshot();
197396
+ rows.add(new otui.TextRenderable(chrome.renderer, {
197397
+ id: `sb-card-items-${tabId}-header`,
197398
+ content: snapshot.sections?.[tabId] ?? "Nothing to show yet.",
197399
+ fg: roleColor("text")
197400
+ }));
197401
+ for (const item of snapshot.items?.[tabId] ?? []) {
197402
+ rows.add(new otui.TextRenderable(chrome.renderer, {
197403
+ id: `sb-card-item-${tabId}-${item.id}`,
197404
+ content: item.text,
197405
+ fg: roleColor("text"),
197406
+ onMouseDown: () => {
197407
+ if (options.renderItem === undefined)
197408
+ return;
197409
+ handle?.addTab({
197410
+ id: `detail:${tabId}:${item.id}`,
197411
+ label: item.label,
197412
+ temporary: true,
197413
+ renderTab: (_id, target, context) => options.renderItem?.(tabId, item, target, context)
197414
+ }, tabId);
197415
+ }
197416
+ }));
197417
+ }
197418
+ chrome.renderer.requestRender();
197419
+ };
197420
+ paint();
197421
+ return options.subscribe?.(paint);
197422
+ }
196915
197423
  const lines = tab.lines(ctx.width);
196916
197424
  const parent = body;
196917
197425
  parent.add(new otui.TextRenderable(chrome.renderer, { id: `sb-card-detail-${tabId}`, content: lines.join(`
@@ -197002,8 +197510,9 @@ var init_sidebar_cards = __esm(() => {
197002
197510
  ],
197003
197511
  rows: [
197004
197512
  { sections: ["status"], required: true, fallback: "idle" },
197005
- { sections: ["plan"], required: false, fallback: "" },
197006
- { sections: ["subagents", "jobs"], required: true, fallback: "no workers" }
197513
+ { sections: ["subagents"], required: false, fallback: "" },
197514
+ { sections: ["jobs"], required: false, fallback: "" },
197515
+ { sections: ["plan"], required: false, fallback: "" }
197007
197516
  ]
197008
197517
  },
197009
197518
  {
@@ -197090,405 +197599,6 @@ var init_sidebar_cards = __esm(() => {
197090
197599
  ];
197091
197600
  });
197092
197601
 
197093
- // src/tui/side-worker.ts
197094
- function isSideWorkerId(id) {
197095
- return id.startsWith(SIDE_WORKER_ID_PREFIX);
197096
- }
197097
- function buildSideWorkerPrompt(opts) {
197098
- const maxRecent = opts.maxRecent ?? 10;
197099
- const recent = opts.recentHistory.slice(-maxRecent);
197100
- const lines = [
197101
- "## Live main-agent state (read-only snapshot)",
197102
- `phase: ${opts.snapshot.phase || "unknown"}`
197103
- ];
197104
- if (opts.snapshot.mainDetail !== undefined && opts.snapshot.mainDetail.length > 0) {
197105
- lines.push(`detail: ${opts.snapshot.mainDetail}`);
197106
- }
197107
- if (opts.snapshot.elapsedSec !== undefined) {
197108
- lines.push(`elapsed: ${opts.snapshot.elapsedSec.toFixed(1)}s`);
197109
- }
197110
- lines.push("", "## Recent main conversation (truncated; for context only)");
197111
- if (recent.length === 0) {
197112
- lines.push("(empty)");
197113
- } else {
197114
- for (const m of recent) {
197115
- const body = m.content.length > 400 ? `${m.content.slice(0, 397)}\u2026` : m.content;
197116
- lines.push(`### ${m.role}`, body, "");
197117
- }
197118
- }
197119
- lines.push("## Your task", "Answer the user's SIDE question below. Do NOT continue or take over the main task.", "Be brief. Prefer facts already visible above; use read-only tools only if essential.", "If the question is about what the main agent is doing, use the live snapshot.", "", "## Side question", opts.question.trim());
197120
- return lines.join(`
197121
- `);
197122
- }
197123
- function buildSideWorkerSystemInstruction(providerId, modelId) {
197124
- return `You are a keryx SIDE worker answering a user question while the MAIN agent is busy.
197125
- ` + `Rules:
197126
- ` + `- Answer ONLY the side question; do not continue the main task.
197127
- ` + `- Do not call shell_exec or any mutating tool. Read-only tools only if needed.
197128
- ` + `- Be concise (prefer bullets). Lead with the answer.
197129
- ` + `- Provider/model: ${providerId}/${modelId}.
197130
- `;
197131
- }
197132
- function sideWorkerLabel(seq) {
197133
- return `side-${seq}`;
197134
- }
197135
- var SIDE_WORKER_ID_PREFIX = "side:";
197136
-
197137
- // src/tui/main-queue.ts
197138
- function parseQueueCommand(args) {
197139
- const parts = args.trim().split(/\s+/).filter((part) => part.length > 0);
197140
- const action = parts[0]?.toLowerCase();
197141
- if (action !== "remove" && action !== "edit" && action !== "force") {
197142
- return;
197143
- }
197144
- const raw = parts[1];
197145
- if (raw === undefined) {
197146
- return { action, position: 1 };
197147
- }
197148
- if (!/^\d+$/.test(raw)) {
197149
- return;
197150
- }
197151
- const position = Number.parseInt(raw, 10);
197152
- if (position < 1) {
197153
- return;
197154
- }
197155
- return { action, position };
197156
- }
197157
- function formatMainQueueMarker(index) {
197158
- const n = index + 1;
197159
- return `> q${n} (${n})`;
197160
- }
197161
- function removeMainQueueItem(items, index) {
197162
- if (index < 0 || index >= items.length)
197163
- return [...items];
197164
- const next = [...items];
197165
- next.splice(index, 1);
197166
- return next;
197167
- }
197168
- function editMainQueueItem(items, index) {
197169
- if (index < 0 || index >= items.length)
197170
- return;
197171
- const rest = [...items];
197172
- const removed = rest.splice(index, 1)[0];
197173
- return { text: removed.question, rest, removed };
197174
- }
197175
- function reinsertMainQueueItem(items, at, item) {
197176
- const next = [...items];
197177
- const insertAt = Math.max(0, Math.min(at, next.length));
197178
- next.splice(insertAt, 0, item);
197179
- return next;
197180
- }
197181
- function pendingQueueEditFor(removed, at) {
197182
- return { id: removed.id, at, ...removed.source !== undefined ? { source: removed.source } : {} };
197183
- }
197184
- function reinsertEditedMainQueueItem(items, edit, question, displayQuestion) {
197185
- return reinsertMainQueueItem(items, edit.at, {
197186
- id: edit.id,
197187
- question,
197188
- displayQuestion,
197189
- ...edit.source !== undefined ? { source: edit.source } : {}
197190
- });
197191
- }
197192
- function dropQueuedBySource(items, source) {
197193
- const kept = [];
197194
- const dropped = [];
197195
- for (const item of items) {
197196
- (item.source === source ? dropped : kept).push(item);
197197
- }
197198
- return { kept, dropped };
197199
- }
197200
- function drainIdleMainQueue(items, options) {
197201
- if (!options.isIdle())
197202
- return false;
197203
- const next = options.takeForced() ?? items.shift();
197204
- if (next === undefined)
197205
- return false;
197206
- options.dispatch(next);
197207
- return true;
197208
- }
197209
- function drainNext(options) {
197210
- for (let budget = options.pending();budget > 0; budget--) {
197211
- const before = options.pending();
197212
- const next = options.take();
197213
- options.paint();
197214
- if (next === undefined)
197215
- return "empty";
197216
- options.run(next);
197217
- if (options.isBusy())
197218
- return "started";
197219
- if (options.pending() >= before)
197220
- return "exhausted";
197221
- }
197222
- return "empty";
197223
- }
197224
- function createMainQueueDrain(deps) {
197225
- let detached = 0;
197226
- const workInFlight = () => deps.turnWorkInFlight() || detached > 0;
197227
- const drain = () => {
197228
- const outcome = drainNext({
197229
- take: () => deps.forced.takeNext() ?? deps.queue().shift(),
197230
- pending: () => deps.forced.pendingCount + deps.queue().length,
197231
- paint: deps.paint,
197232
- run: deps.run,
197233
- isBusy: workInFlight
197234
- });
197235
- if (outcome === "empty")
197236
- deps.onSettle();
197237
- return outcome;
197238
- };
197239
- const drainWhenIdle = () => {
197240
- if (workInFlight())
197241
- return;
197242
- drain();
197243
- };
197244
- return {
197245
- onForegroundOperationComplete() {
197246
- if (deps.forced.isAwaitingSettlement)
197247
- return;
197248
- drain();
197249
- },
197250
- onTurnSettle() {
197251
- if (deps.forced.isAwaitingSettlement)
197252
- return;
197253
- drain();
197254
- },
197255
- onLeaseRelease: drainWhenIdle,
197256
- detach(work) {
197257
- detached += 1;
197258
- const end = () => {
197259
- detached -= 1;
197260
- drainWhenIdle();
197261
- };
197262
- work.then(end, (error) => {
197263
- try {
197264
- deps.onDetachedFailure(error);
197265
- } catch {}
197266
- end();
197267
- });
197268
- },
197269
- workInFlight
197270
- };
197271
- }
197272
- function describePendingCall(tool, inputJson) {
197273
- let shown = tool;
197274
- try {
197275
- const parsed = JSON.parse(inputJson);
197276
- if (parsed !== null && typeof parsed === "object") {
197277
- const command = parsed.command;
197278
- if (typeof command === "string" && command.trim().length > 0)
197279
- shown = command;
197280
- }
197281
- } catch {}
197282
- const oneLine = shown.replace(/\s+/g, " ").trim();
197283
- return oneLine.length > PENDING_CALL_CHARS ? `${oneLine.slice(0, PENDING_CALL_CHARS - 1)}\u2026` : oneLine;
197284
- }
197285
- function queueWaitReason(wait) {
197286
- return wait.kind === "turn" ? "waits for the current turn" : `waits for approval of: ${wait.call}`;
197287
- }
197288
- function formatQueueRow(index, text, wait) {
197289
- return `${formatMainQueueMarker(index)} ${text} \u2014 queued \xB7 ${queueWaitReason(wait)}`;
197290
- }
197291
- function formatQueueListing(texts, wait) {
197292
- if (texts.length === 0)
197293
- return ["main queue is empty."];
197294
- return [...texts.map((text, index) => formatQueueRow(index, text, wait)), QUEUE_FORCE_HELP];
197295
- }
197296
- var QUEUE_FORCE_HELP = "[Force] interrupts the current turn and runs this message now \xB7 [Edit] moves it back to the composer \xB7 [Delete] removes it", PENDING_CALL_CHARS = 60;
197297
-
197298
- // src/tui/main-queue-view.ts
197299
- function buildMainQueueRow(otui, r, params) {
197300
- const theme = getTheme();
197301
- const row = new otui.BoxRenderable(r, { id: `mq-${params.id}`, width: "100%", flexDirection: "row" });
197302
- row.add(new otui.TextRenderable(r, {
197303
- id: `mq-${params.id}-t`,
197304
- flexGrow: 1,
197305
- minWidth: 0,
197306
- content: otui.t`${dimChunk(otui, formatQueueRow(params.index, params.text, params.wait))}`
197307
- }));
197308
- const force = smallActionButton(otui, r, "Force", `mq-force-${params.id}`, theme.focus, params.onForce);
197309
- const edit = smallActionButton(otui, r, "Edit", `mq-edit-${params.id}`, theme.text, params.onEdit);
197310
- const del = smallActionButton(otui, r, "Delete", `mq-del-${params.id}`, theme.error, params.onDelete);
197311
- row.add(force.box);
197312
- row.add(edit.box);
197313
- row.add(del.box);
197314
- return {
197315
- box: row,
197316
- buttons: { force, edit, delete: del },
197317
- setRowActive: (active) => {
197318
- row.backgroundColor = active ? theme.highlight : undefined;
197319
- }
197320
- };
197321
- }
197322
- function buildMainQueueHelp(otui, r) {
197323
- return new otui.TextRenderable(r, {
197324
- id: "mq-help",
197325
- width: "100%",
197326
- content: otui.t`${dimChunk(otui, QUEUE_FORCE_HELP)}`
197327
- });
197328
- }
197329
- var init_main_queue_view = __esm(() => {
197330
- init_action_button();
197331
- init_theme();
197332
- init_theme_text();
197333
- });
197334
-
197335
- // src/tui/remote-queue-wiring.ts
197336
- function createRemoteQueueWiring(deps) {
197337
- let pendingEdit;
197338
- return {
197339
- removeMainQueue(index) {
197340
- const queue = deps.getQueue();
197341
- if (index < 0 || index >= queue.length)
197342
- return;
197343
- const removed = queue[index];
197344
- if (removed?.source === TG_SOURCE) {
197345
- deps.getBridge()?.queuedLineRemoved(removed.question);
197346
- }
197347
- deps.setQueue(removeMainQueueItem(queue, index));
197348
- deps.paint();
197349
- },
197350
- editMainQueue(index) {
197351
- const edited = editMainQueueItem(deps.getQueue(), index);
197352
- if (edited === undefined)
197353
- return;
197354
- deps.setQueue(edited.rest);
197355
- pendingEdit = pendingQueueEditFor(edited.removed, index);
197356
- deps.composer.setText(edited.text);
197357
- deps.composer.focus();
197358
- deps.paint();
197359
- },
197360
- requeuePendingEdit(line, displayLine) {
197361
- if (pendingEdit === undefined)
197362
- return false;
197363
- const edit = pendingEdit;
197364
- pendingEdit = undefined;
197365
- deps.setQueue(reinsertEditedMainQueueItem(deps.getQueue(), edit, line, displayLine));
197366
- deps.paint();
197367
- return true;
197368
- },
197369
- hasPendingEdit: () => pendingEdit !== undefined,
197370
- stopRemoteForSessionSwitch() {
197371
- const bridge = deps.getBridge();
197372
- if (bridge?.keepingTopic === true) {
197373
- bridge.sessionLeaving();
197374
- return;
197375
- }
197376
- if (bridge?.active !== true)
197377
- return;
197378
- const closing = bridge.disable();
197379
- deps.say(`\u25C7 remote control is turning off: the session changed, and its Telegram topic is being archived. Turn it on again with /remote-control <name>.
197380
- `);
197381
- closing.then(() => deps.say(`\u25C7 remote control off: the topic is archived.
197382
- `));
197383
- },
197384
- dropQueuedTelegramLines() {
197385
- const { kept, dropped } = dropQueuedBySource(deps.getQueue(), TG_SOURCE);
197386
- if (dropped.length === 0)
197387
- return [];
197388
- deps.setQueue(kept);
197389
- deps.paint();
197390
- return dropped.map((item) => item.question);
197391
- },
197392
- enqueueTelegramLine(text) {
197393
- const queue = deps.getQueue();
197394
- queue.push({ id: deps.nextId(), question: text, displayQuestion: deps.summarize(text), source: TG_SOURCE });
197395
- deps.paint();
197396
- deps.say(`\u25C7 a line from Telegram is queued as q${queue.length}.
197397
- `);
197398
- },
197399
- runTelegramLine(text) {
197400
- deps.runLine(text, "operator", TG_SOURCE);
197401
- },
197402
- runQueued(item) {
197403
- deps.runLine(item.question, "operator", item.source);
197404
- },
197405
- turnStarted(source) {
197406
- deps.getBridge()?.turnStarted(source);
197407
- },
197408
- assistantText(text) {
197409
- deps.getBridge()?.assistantText(text);
197410
- },
197411
- wrapOnToolCall(base) {
197412
- return (...args) => {
197413
- deps.getBridge()?.toolCall(typeof args[0] === "string" ? args[0] : undefined);
197414
- base?.(...args);
197415
- };
197416
- },
197417
- turnSettled(outcome) {
197418
- const bridge = deps.getBridge();
197419
- return bridge === undefined ? Promise.resolve() : bridge.turnSettled({ failed: outcome.failed || outcome.aborted });
197420
- }
197421
- };
197422
- }
197423
- var init_remote_queue_wiring = __esm(() => {
197424
- init_shell_bridge();
197425
- });
197426
-
197427
- // src/tui/turn-loop.ts
197428
- function createTurnLoop(deps) {
197429
- let queue = [];
197430
- let seq = 0;
197431
- const nextId = () => `mq${seq++}`;
197432
- const enqueue = (line, displayLine, source) => {
197433
- queue.push({ id: nextId(), question: line, displayQuestion: displayLine, ...source !== undefined ? { source } : {} });
197434
- deps.paint();
197435
- return queue.length;
197436
- };
197437
- const remote = createRemoteQueueWiring({
197438
- ...deps.remote,
197439
- getQueue: () => queue,
197440
- setQueue: (next) => {
197441
- queue = next;
197442
- },
197443
- nextId,
197444
- paint: deps.paint,
197445
- say: deps.say,
197446
- runLine: deps.runLine
197447
- });
197448
- const drain = createMainQueueDrain({
197449
- forced: deps.forced,
197450
- queue: () => queue,
197451
- paint: deps.paint,
197452
- run: (next) => remote.runQueued(next),
197453
- turnWorkInFlight: deps.turnWorkInFlight,
197454
- onSettle: deps.onSettle,
197455
- onDetachedFailure: deps.onDetachedFailure
197456
- });
197457
- return {
197458
- get queue() {
197459
- return queue;
197460
- },
197461
- replaceQueue(next) {
197462
- queue = next;
197463
- },
197464
- enqueue,
197465
- enqueueWhileBusy(line, displayLine, commandName, source) {
197466
- if (deferredBusyAction(line, commandName) !== "queue")
197467
- return false;
197468
- const position = enqueue(line, displayLine, source);
197469
- deps.say(`${queuedCommandNotice(position)}
197470
- `);
197471
- return true;
197472
- },
197473
- drainAfterRecipientChoice(isIdle) {
197474
- drainIdleMainQueue(queue, {
197475
- isIdle,
197476
- takeForced: () => deps.forced.takeNext(),
197477
- dispatch: (next) => {
197478
- deps.paint();
197479
- remote.runQueued(next);
197480
- }
197481
- });
197482
- },
197483
- remote,
197484
- drain
197485
- };
197486
- }
197487
- var init_turn_loop = __esm(() => {
197488
- init_busy_dispatch();
197489
- init_remote_queue_wiring();
197490
- });
197491
-
197492
197602
  // src/tui/worker-fleet.ts
197493
197603
  function clip8(s, max) {
197494
197604
  if (s.length <= max) {
@@ -198084,6 +198194,818 @@ var init_subagent_inspector = __esm(() => {
198084
198194
  ];
198085
198195
  });
198086
198196
 
198197
+ // src/tui/background-job-session.ts
198198
+ function appendBounded(output, chunk) {
198199
+ const combined = output + chunk;
198200
+ return combined.length > MAX_BACKGROUND_JOB_OUTPUT_CHARS ? combined.slice(-MAX_BACKGROUND_JOB_OUTPUT_CHARS) : combined;
198201
+ }
198202
+ function clip10(s, max) {
198203
+ if (max <= 0) {
198204
+ return "";
198205
+ }
198206
+ if (s.length <= max) {
198207
+ return s;
198208
+ }
198209
+ return `${s.slice(0, Math.max(1, max - 1))}\u2026`;
198210
+ }
198211
+ function formatJobListHeader(count) {
198212
+ return `Background Jobs ${count}`;
198213
+ }
198214
+ function formatJobRow(entry, width = SIDEBAR_TEXT_WIDTH) {
198215
+ const glyph = STATUS_GLYPH4[entry.status];
198216
+ const budget = Math.max(1, width - glyph.length - 1);
198217
+ const cmd = clip10(entry.command, budget);
198218
+ return clip10(`${glyph} ${cmd}`, width);
198219
+ }
198220
+ function formatJobDuration(entry, now) {
198221
+ const started = Date.parse(entry.startedAt);
198222
+ const ended = entry.endedAt !== undefined ? Date.parse(entry.endedAt) : now;
198223
+ return formatDuration(ended - started);
198224
+ }
198225
+ function formatJobMeta(entry, now = Date.now()) {
198226
+ const rows = [
198227
+ ["Id", entry.jobId],
198228
+ ["Pid", String(entry.pid)],
198229
+ ["Status", entry.status],
198230
+ ["Command", entry.command],
198231
+ ["Started", entry.startedAt],
198232
+ ["Ended", entry.endedAt ?? "\u2014"],
198233
+ ["Duration", formatJobDuration(entry, now)],
198234
+ ["Exit code", entry.exitCode !== undefined ? String(entry.exitCode) : "\u2014"]
198235
+ ];
198236
+ if (entry.killReason !== undefined) {
198237
+ rows.push(["Kill reason", entry.killReason]);
198238
+ }
198239
+ const width = rows.reduce((max, [label]) => Math.max(max, label.length), 0);
198240
+ return rows.map(([label, value]) => `${label.padEnd(width)} ${value}`).join(`
198241
+ `);
198242
+ }
198243
+ function formatJobOutput(entry) {
198244
+ return entry.output.length > 0 ? entry.output : "(no output yet)";
198245
+ }
198246
+
198247
+ class BackgroundJobStore {
198248
+ jobs = new Map;
198249
+ pending = new Map;
198250
+ listeners = new Set;
198251
+ apply(event) {
198252
+ switch (event.type) {
198253
+ case "start":
198254
+ this.pending.set(event.jobId, {
198255
+ jobId: event.jobId,
198256
+ command: event.command,
198257
+ pid: event.pid,
198258
+ status: "running",
198259
+ startedAt: event.startedAt,
198260
+ output: ""
198261
+ });
198262
+ return;
198263
+ case "phase": {
198264
+ const entry = this.pending.get(event.jobId);
198265
+ if (entry === undefined) {
198266
+ return;
198267
+ }
198268
+ this.pending.delete(event.jobId);
198269
+ this.jobs.set(event.jobId, entry);
198270
+ this.emit({ id: event.jobId, kind: "start" });
198271
+ return;
198272
+ }
198273
+ case "output": {
198274
+ const hidden = this.pending.get(event.jobId);
198275
+ if (hidden !== undefined) {
198276
+ hidden.output = appendBounded(hidden.output, event.chunk);
198277
+ return;
198278
+ }
198279
+ const current = this.jobs.get(event.jobId);
198280
+ if (current === undefined) {
198281
+ return;
198282
+ }
198283
+ current.output = appendBounded(current.output, event.chunk);
198284
+ this.emit({ id: event.jobId, kind: "output" });
198285
+ return;
198286
+ }
198287
+ case "exit": {
198288
+ if (this.pending.delete(event.jobId)) {
198289
+ return;
198290
+ }
198291
+ const current = this.jobs.get(event.jobId);
198292
+ if (current === undefined) {
198293
+ return;
198294
+ }
198295
+ current.status = event.status;
198296
+ current.endedAt = event.endedAt;
198297
+ if (event.exitCode !== undefined) {
198298
+ current.exitCode = event.exitCode;
198299
+ }
198300
+ if (event.killReason !== undefined) {
198301
+ current.killReason = event.killReason;
198302
+ }
198303
+ this.emit({ id: event.jobId, kind: "exit" });
198304
+ return;
198305
+ }
198306
+ }
198307
+ }
198308
+ get(jobId) {
198309
+ return this.jobs.get(jobId);
198310
+ }
198311
+ list() {
198312
+ return [...this.jobs.values()];
198313
+ }
198314
+ subscribe(listener) {
198315
+ this.listeners.add(listener);
198316
+ return () => {
198317
+ this.listeners.delete(listener);
198318
+ };
198319
+ }
198320
+ removeAll() {
198321
+ this.pending.clear();
198322
+ if (this.jobs.size === 0) {
198323
+ return;
198324
+ }
198325
+ const ids = [...this.jobs.keys()];
198326
+ this.jobs.clear();
198327
+ for (const id of ids) {
198328
+ this.emit({ id, kind: "exit" });
198329
+ }
198330
+ }
198331
+ emit(hint) {
198332
+ for (const listener of this.listeners) {
198333
+ try {
198334
+ listener(hint);
198335
+ } catch {}
198336
+ }
198337
+ }
198338
+ }
198339
+ var MAX_BACKGROUND_JOB_OUTPUT_CHARS = 20000, STATUS_GLYPH4;
198340
+ var init_background_job_session = __esm(() => {
198341
+ init_shell_chrome();
198342
+ init_duration_format();
198343
+ STATUS_GLYPH4 = {
198344
+ running: "\u25D0",
198345
+ completed: "\u25CF",
198346
+ failed: "\u25B2",
198347
+ killed: "\u2717"
198348
+ };
198349
+ });
198350
+
198351
+ // src/tui/background-job-inspector.ts
198352
+ function resolveTarget(otui, renderer, body) {
198353
+ if (otui === undefined || otui === null || body === undefined || body === null) {
198354
+ return;
198355
+ }
198356
+ const parent = body;
198357
+ if (parent.add === undefined) {
198358
+ return;
198359
+ }
198360
+ const scrollCtor = otui.ScrollBoxRenderable;
198361
+ if (scrollCtor === undefined) {
198362
+ return { add: parent.add.bind(parent) };
198363
+ }
198364
+ const scroll = new scrollCtor(renderer, {
198365
+ id: "job-inspector-scroll",
198366
+ flexGrow: 1,
198367
+ minHeight: 0,
198368
+ scrollY: true,
198369
+ stickyScroll: true,
198370
+ stickyStart: "bottom",
198371
+ contentOptions: { flexDirection: "column" }
198372
+ });
198373
+ parent.add(scroll);
198374
+ const target = scroll.content ?? scroll;
198375
+ if (target.add === undefined) {
198376
+ return;
198377
+ }
198378
+ return { add: target.add.bind(target) };
198379
+ }
198380
+ function paintContent3(otui, renderer, body, content) {
198381
+ const target = resolveTarget(otui, renderer, body);
198382
+ const textCtor = otui.TextRenderable;
198383
+ if (target === undefined || textCtor === undefined) {
198384
+ return;
198385
+ }
198386
+ const node = new textCtor(renderer, { id: "job-inspector-output", content });
198387
+ target.add(node);
198388
+ return node;
198389
+ }
198390
+ function paintMetaTab(otui, renderer, body, entry, registry, jobId) {
198391
+ const target = resolveTarget(otui, renderer, body);
198392
+ const textCtor = otui.TextRenderable;
198393
+ if (target === undefined || textCtor === undefined) {
198394
+ return;
198395
+ }
198396
+ const metaNode = new textCtor(renderer, { id: "job-inspector-meta", content: formatJobMeta(entry) });
198397
+ target.add(metaNode);
198398
+ const killStatusNode = new textCtor(renderer, { id: "job-inspector-kill-status", content: "" });
198399
+ const killNode = new textCtor(renderer, {
198400
+ id: "job-inspector-kill",
198401
+ content: "[ Kill ]",
198402
+ onMouseDown: () => {
198403
+ registry.kill(jobId).then((result) => {
198404
+ if (!result.ok) {
198405
+ killStatusNode.content = `Kill failed: ${result.error}`;
198406
+ }
198407
+ });
198408
+ }
198409
+ });
198410
+ target.add(killNode);
198411
+ target.add(killStatusNode);
198412
+ return metaNode;
198413
+ }
198414
+ function inspectorTitleForJob(entry) {
198415
+ return entry.command;
198416
+ }
198417
+ function presentJobInspector(openModalFn, otui, chrome, options) {
198418
+ const entry = options.store.get(options.id);
198419
+ if (entry === undefined) {
198420
+ return;
198421
+ }
198422
+ let outputNode;
198423
+ let metaNode;
198424
+ let unsubscribe;
198425
+ const refresh = (hint) => {
198426
+ if (hint !== undefined && hint.id !== options.id) {
198427
+ return;
198428
+ }
198429
+ const current = options.store.get(options.id);
198430
+ if (current === undefined) {
198431
+ if (outputNode !== undefined) {
198432
+ outputNode.content = GONE_TEXT;
198433
+ }
198434
+ if (metaNode !== undefined) {
198435
+ metaNode.content = GONE_TEXT;
198436
+ }
198437
+ return;
198438
+ }
198439
+ if (outputNode !== undefined) {
198440
+ outputNode.content = formatJobOutput(current);
198441
+ }
198442
+ if (metaNode !== undefined) {
198443
+ metaNode.content = formatJobMeta(current);
198444
+ }
198445
+ };
198446
+ const handle = openModalFn(otui, chrome, {
198447
+ title: inspectorTitleForJob(entry),
198448
+ tabs: [
198449
+ { id: "output", label: "Output" },
198450
+ { id: "meta", label: "Meta" }
198451
+ ],
198452
+ initialTab: "output",
198453
+ footer: JOB_INSPECTOR_FOOTER,
198454
+ renderTab: (tabId, body) => {
198455
+ const renderer = options.renderer ?? chrome?.renderer;
198456
+ const current = options.store.get(options.id);
198457
+ if (current === undefined) {
198458
+ paintContent3(otui, renderer, body, GONE_TEXT);
198459
+ return;
198460
+ }
198461
+ if (tabId === "meta") {
198462
+ metaNode = paintMetaTab(otui, renderer, body, current, options.registry, options.id);
198463
+ outputNode = undefined;
198464
+ return;
198465
+ }
198466
+ outputNode = paintContent3(otui, renderer, body, formatJobOutput(current));
198467
+ metaNode = undefined;
198468
+ },
198469
+ onClose: () => {
198470
+ unsubscribe?.();
198471
+ }
198472
+ });
198473
+ if (handle === undefined) {
198474
+ return;
198475
+ }
198476
+ unsubscribe = options.store.subscribe(refresh);
198477
+ return handle;
198478
+ }
198479
+ function openJobInspector(otui, chrome, options) {
198480
+ return presentJobInspector((hostOtui, hostChrome, input) => openModal(hostOtui, hostChrome, input), otui, chrome, options);
198481
+ }
198482
+ function paintBackgroundJobSidebar(otui, renderer, parent, entries, options) {
198483
+ const box = parent;
198484
+ const ctor = otui.TextRenderable;
198485
+ if (box.add === undefined || ctor === undefined) {
198486
+ return;
198487
+ }
198488
+ if (box.getChildren !== undefined && box.remove !== undefined) {
198489
+ for (const child of [...box.getChildren()]) {
198490
+ box.remove(child);
198491
+ }
198492
+ }
198493
+ if (entries.length === 0) {
198494
+ return;
198495
+ }
198496
+ const width = options.width ?? SIDEBAR_TEXT_WIDTH;
198497
+ box.add(new ctor(renderer, {
198498
+ id: "sb-jobs-h",
198499
+ content: formatJobListHeader(entries.length)
198500
+ }));
198501
+ for (const entry of entries) {
198502
+ const id = entry.jobId;
198503
+ box.add(new ctor(renderer, {
198504
+ id: `sb-job-${id}`,
198505
+ content: formatJobRow(entry, width),
198506
+ onMouseDown: () => {
198507
+ options.onOpen(id);
198508
+ }
198509
+ }));
198510
+ }
198511
+ }
198512
+ var JOB_INSPECTOR_FOOTER, GONE_TEXT = "Background job is gone.";
198513
+ var init_background_job_inspector = __esm(() => {
198514
+ init_modal_host();
198515
+ init_shell_chrome();
198516
+ init_background_job_session();
198517
+ JOB_INSPECTOR_FOOTER = [
198518
+ { key: "\u2190/\u2192", label: "tabs" },
198519
+ { key: "k", label: "kill" },
198520
+ { key: "esc", label: "close" }
198521
+ ];
198522
+ });
198523
+
198524
+ // src/tui/sidebar-work.ts
198525
+ function counts2(label, statuses, done) {
198526
+ const n = (status) => statuses.filter((value) => value === status).length;
198527
+ const failures = n("failed") + n("killed");
198528
+ return `${label} ${n("running")}/${n(done)}` + (n("queued") > 0 ? ` \xB7 ${n("queued")} queued` : "") + (n("blocked") > 0 ? ` \xB7 ${n("blocked")} blocked` : "") + (failures > 0 ? ` \xB7 ${failures} failed` : "");
198529
+ }
198530
+ function workSidebarSnapshot(agents, jobs) {
198531
+ return {
198532
+ sections: {
198533
+ ...agents.length > 0 ? { subagents: counts2("Subagents", agents.map((a) => a.status), "done") } : {},
198534
+ ...jobs.length > 0 ? { jobs: counts2("Jobs", jobs.map((j) => j.status), "completed") } : {}
198535
+ },
198536
+ items: {
198537
+ subagents: agents.map((a) => ({ id: a.id, label: a.label, text: `${a.status} \xB7 ${a.label}` })),
198538
+ jobs: jobs.map((j) => ({ id: j.jobId, label: j.command, text: `${j.status} \xB7 ${j.command}` }))
198539
+ }
198540
+ };
198541
+ }
198542
+ function embeddedInspector(otui, chrome, body) {
198543
+ let dispose = () => {};
198544
+ const open = (_otui, _chrome, input) => {
198545
+ const parent = body;
198546
+ const nav = new otui.BoxRenderable(chrome.renderer, { id: "embedded-inspector-tabs", flexDirection: "row", flexShrink: 0 });
198547
+ const content = new otui.BoxRenderable(chrome.renderer, { id: "embedded-inspector-content", flexDirection: "column", flexGrow: 1, minHeight: 0 });
198548
+ parent.add(nav);
198549
+ parent.add(content);
198550
+ let active = input.initialTab ?? input.tabs[0]?.id ?? "";
198551
+ let cleanup;
198552
+ const buttons = new Map;
198553
+ const mount = (id) => {
198554
+ if (!input.tabs.some((tab) => tab.id === id))
198555
+ return;
198556
+ cleanup?.();
198557
+ cleanup = undefined;
198558
+ for (const child of [...content.getChildren()]) {
198559
+ content.remove(child);
198560
+ child.destroy();
198561
+ }
198562
+ active = id;
198563
+ for (const [tabId, button] of buttons)
198564
+ button.fg = roleColor(tabId === id ? "text" : "muted");
198565
+ const result = input.renderTab(id, content);
198566
+ if (typeof result === "function")
198567
+ cleanup = result;
198568
+ chrome.renderer.requestRender();
198569
+ };
198570
+ for (const tab of input.tabs) {
198571
+ const button = new otui.TextRenderable(chrome.renderer, {
198572
+ id: `embedded-inspector-tab-${tab.id}`,
198573
+ content: `[ ${tab.label} ] `,
198574
+ onMouseDown: () => mount(tab.id)
198575
+ });
198576
+ buttons.set(tab.id, button);
198577
+ nav.add(button);
198578
+ }
198579
+ mount(active);
198580
+ let closed = false;
198581
+ dispose = () => {
198582
+ if (closed)
198583
+ return;
198584
+ closed = true;
198585
+ cleanup?.();
198586
+ input.onClose?.();
198587
+ };
198588
+ return { close: () => dispose(), setTab: mount, activeTab: () => active };
198589
+ };
198590
+ return { open, dispose: () => dispose() };
198591
+ }
198592
+ function renderWorkInspector(otui, chrome, body, section, id, options) {
198593
+ const embedded = embeddedInspector(otui, chrome, body);
198594
+ const handle = section === "subagents" ? presentSubagentInspector(embedded.open, otui, chrome, { store: options.agents, id, renderer: chrome.renderer }) : presentJobInspector(embedded.open, otui, chrome, { store: options.jobs, id, registry: options.registry, renderer: chrome.renderer });
198595
+ if (handle === undefined) {
198596
+ body.add(new otui.TextRenderable(chrome.renderer, {
198597
+ id: "embedded-inspector-gone",
198598
+ content: "This worker is gone.",
198599
+ fg: roleColor("muted")
198600
+ }));
198601
+ }
198602
+ return embedded.dispose;
198603
+ }
198604
+ var init_sidebar_work = __esm(() => {
198605
+ init_subagent_inspector();
198606
+ init_background_job_inspector();
198607
+ init_theme();
198608
+ });
198609
+
198610
+ // src/tui/side-worker.ts
198611
+ function isSideWorkerId(id) {
198612
+ return id.startsWith(SIDE_WORKER_ID_PREFIX);
198613
+ }
198614
+ function buildSideWorkerPrompt(opts) {
198615
+ const maxRecent = opts.maxRecent ?? 10;
198616
+ const recent = opts.recentHistory.slice(-maxRecent);
198617
+ const lines = [
198618
+ "## Live main-agent state (read-only snapshot)",
198619
+ `phase: ${opts.snapshot.phase || "unknown"}`
198620
+ ];
198621
+ if (opts.snapshot.mainDetail !== undefined && opts.snapshot.mainDetail.length > 0) {
198622
+ lines.push(`detail: ${opts.snapshot.mainDetail}`);
198623
+ }
198624
+ if (opts.snapshot.elapsedSec !== undefined) {
198625
+ lines.push(`elapsed: ${opts.snapshot.elapsedSec.toFixed(1)}s`);
198626
+ }
198627
+ lines.push("", "## Recent main conversation (truncated; for context only)");
198628
+ if (recent.length === 0) {
198629
+ lines.push("(empty)");
198630
+ } else {
198631
+ for (const m of recent) {
198632
+ const body = m.content.length > 400 ? `${m.content.slice(0, 397)}\u2026` : m.content;
198633
+ lines.push(`### ${m.role}`, body, "");
198634
+ }
198635
+ }
198636
+ lines.push("## Your task", "Answer the user's SIDE question below. Do NOT continue or take over the main task.", "Be brief. Prefer facts already visible above; use read-only tools only if essential.", "If the question is about what the main agent is doing, use the live snapshot.", "", "## Side question", opts.question.trim());
198637
+ return lines.join(`
198638
+ `);
198639
+ }
198640
+ function buildSideWorkerSystemInstruction(providerId, modelId) {
198641
+ return `You are a keryx SIDE worker answering a user question while the MAIN agent is busy.
198642
+ ` + `Rules:
198643
+ ` + `- Answer ONLY the side question; do not continue the main task.
198644
+ ` + `- Do not call shell_exec or any mutating tool. Read-only tools only if needed.
198645
+ ` + `- Be concise (prefer bullets). Lead with the answer.
198646
+ ` + `- Provider/model: ${providerId}/${modelId}.
198647
+ `;
198648
+ }
198649
+ function sideWorkerLabel(seq) {
198650
+ return `side-${seq}`;
198651
+ }
198652
+ var SIDE_WORKER_ID_PREFIX = "side:";
198653
+
198654
+ // src/tui/main-queue.ts
198655
+ function parseQueueCommand(args) {
198656
+ const parts = args.trim().split(/\s+/).filter((part) => part.length > 0);
198657
+ const action = parts[0]?.toLowerCase();
198658
+ if (action !== "remove" && action !== "edit" && action !== "force") {
198659
+ return;
198660
+ }
198661
+ const raw = parts[1];
198662
+ if (raw === undefined) {
198663
+ return { action, position: 1 };
198664
+ }
198665
+ if (!/^\d+$/.test(raw)) {
198666
+ return;
198667
+ }
198668
+ const position = Number.parseInt(raw, 10);
198669
+ if (position < 1) {
198670
+ return;
198671
+ }
198672
+ return { action, position };
198673
+ }
198674
+ function formatMainQueueMarker(index) {
198675
+ const n = index + 1;
198676
+ return `> q${n} (${n})`;
198677
+ }
198678
+ function removeMainQueueItem(items, index) {
198679
+ if (index < 0 || index >= items.length)
198680
+ return [...items];
198681
+ const next = [...items];
198682
+ next.splice(index, 1);
198683
+ return next;
198684
+ }
198685
+ function editMainQueueItem(items, index) {
198686
+ if (index < 0 || index >= items.length)
198687
+ return;
198688
+ const rest = [...items];
198689
+ const removed = rest.splice(index, 1)[0];
198690
+ return { text: removed.question, rest, removed };
198691
+ }
198692
+ function reinsertMainQueueItem(items, at, item) {
198693
+ const next = [...items];
198694
+ const insertAt = Math.max(0, Math.min(at, next.length));
198695
+ next.splice(insertAt, 0, item);
198696
+ return next;
198697
+ }
198698
+ function pendingQueueEditFor(removed, at) {
198699
+ return { id: removed.id, at, ...removed.source !== undefined ? { source: removed.source } : {} };
198700
+ }
198701
+ function reinsertEditedMainQueueItem(items, edit, question, displayQuestion) {
198702
+ return reinsertMainQueueItem(items, edit.at, {
198703
+ id: edit.id,
198704
+ question,
198705
+ displayQuestion,
198706
+ ...edit.source !== undefined ? { source: edit.source } : {}
198707
+ });
198708
+ }
198709
+ function dropQueuedBySource(items, source) {
198710
+ const kept = [];
198711
+ const dropped = [];
198712
+ for (const item of items) {
198713
+ (item.source === source ? dropped : kept).push(item);
198714
+ }
198715
+ return { kept, dropped };
198716
+ }
198717
+ function drainIdleMainQueue(items, options) {
198718
+ if (!options.isIdle())
198719
+ return false;
198720
+ const next = options.takeForced() ?? items.shift();
198721
+ if (next === undefined)
198722
+ return false;
198723
+ options.dispatch(next);
198724
+ return true;
198725
+ }
198726
+ function drainNext(options) {
198727
+ for (let budget = options.pending();budget > 0; budget--) {
198728
+ const before = options.pending();
198729
+ const next = options.take();
198730
+ options.paint();
198731
+ if (next === undefined)
198732
+ return "empty";
198733
+ options.run(next);
198734
+ if (options.isBusy())
198735
+ return "started";
198736
+ if (options.pending() >= before)
198737
+ return "exhausted";
198738
+ }
198739
+ return "empty";
198740
+ }
198741
+ function createMainQueueDrain(deps) {
198742
+ let detached = 0;
198743
+ const workInFlight = () => deps.turnWorkInFlight() || detached > 0;
198744
+ const drain = () => {
198745
+ const outcome = drainNext({
198746
+ take: () => deps.forced.takeNext() ?? deps.queue().shift(),
198747
+ pending: () => deps.forced.pendingCount + deps.queue().length,
198748
+ paint: deps.paint,
198749
+ run: deps.run,
198750
+ isBusy: workInFlight
198751
+ });
198752
+ if (outcome === "empty")
198753
+ deps.onSettle();
198754
+ return outcome;
198755
+ };
198756
+ const drainWhenIdle = () => {
198757
+ if (workInFlight())
198758
+ return;
198759
+ drain();
198760
+ };
198761
+ return {
198762
+ onForegroundOperationComplete() {
198763
+ if (deps.forced.isAwaitingSettlement)
198764
+ return;
198765
+ drain();
198766
+ },
198767
+ onTurnSettle() {
198768
+ if (deps.forced.isAwaitingSettlement)
198769
+ return;
198770
+ drain();
198771
+ },
198772
+ onLeaseRelease: drainWhenIdle,
198773
+ detach(work) {
198774
+ detached += 1;
198775
+ const end = () => {
198776
+ detached -= 1;
198777
+ drainWhenIdle();
198778
+ };
198779
+ work.then(end, (error) => {
198780
+ try {
198781
+ deps.onDetachedFailure(error);
198782
+ } catch {}
198783
+ end();
198784
+ });
198785
+ },
198786
+ workInFlight
198787
+ };
198788
+ }
198789
+ function describePendingCall(tool, inputJson) {
198790
+ let shown = tool;
198791
+ try {
198792
+ const parsed = JSON.parse(inputJson);
198793
+ if (parsed !== null && typeof parsed === "object") {
198794
+ const command = parsed.command;
198795
+ if (typeof command === "string" && command.trim().length > 0)
198796
+ shown = command;
198797
+ }
198798
+ } catch {}
198799
+ const oneLine = shown.replace(/\s+/g, " ").trim();
198800
+ return oneLine.length > PENDING_CALL_CHARS ? `${oneLine.slice(0, PENDING_CALL_CHARS - 1)}\u2026` : oneLine;
198801
+ }
198802
+ function queueWaitReason(wait) {
198803
+ return wait.kind === "turn" ? "waits for the current turn" : `waits for approval of: ${wait.call}`;
198804
+ }
198805
+ function formatQueueRow(index, text, wait) {
198806
+ return `${formatMainQueueMarker(index)} ${text} \u2014 queued \xB7 ${queueWaitReason(wait)}`;
198807
+ }
198808
+ function formatQueueListing(texts, wait) {
198809
+ if (texts.length === 0)
198810
+ return ["main queue is empty."];
198811
+ return [...texts.map((text, index) => formatQueueRow(index, text, wait)), QUEUE_FORCE_HELP];
198812
+ }
198813
+ var QUEUE_FORCE_HELP = "[Force] interrupts the current turn and runs this message now \xB7 [Edit] moves it back to the composer \xB7 [Delete] removes it", PENDING_CALL_CHARS = 60;
198814
+
198815
+ // src/tui/main-queue-view.ts
198816
+ function buildMainQueueRow(otui, r, params) {
198817
+ const theme = getTheme();
198818
+ const row = new otui.BoxRenderable(r, { id: `mq-${params.id}`, width: "100%", flexDirection: "row" });
198819
+ row.add(new otui.TextRenderable(r, {
198820
+ id: `mq-${params.id}-t`,
198821
+ flexGrow: 1,
198822
+ minWidth: 0,
198823
+ content: otui.t`${dimChunk(otui, formatQueueRow(params.index, params.text, params.wait))}`
198824
+ }));
198825
+ const force = smallActionButton(otui, r, "Force", `mq-force-${params.id}`, theme.focus, params.onForce);
198826
+ const edit = smallActionButton(otui, r, "Edit", `mq-edit-${params.id}`, theme.text, params.onEdit);
198827
+ const del = smallActionButton(otui, r, "Delete", `mq-del-${params.id}`, theme.error, params.onDelete);
198828
+ row.add(force.box);
198829
+ row.add(edit.box);
198830
+ row.add(del.box);
198831
+ return {
198832
+ box: row,
198833
+ buttons: { force, edit, delete: del },
198834
+ setRowActive: (active) => {
198835
+ row.backgroundColor = active ? theme.highlight : undefined;
198836
+ }
198837
+ };
198838
+ }
198839
+ function buildMainQueueHelp(otui, r) {
198840
+ return new otui.TextRenderable(r, {
198841
+ id: "mq-help",
198842
+ width: "100%",
198843
+ content: otui.t`${dimChunk(otui, QUEUE_FORCE_HELP)}`
198844
+ });
198845
+ }
198846
+ var init_main_queue_view = __esm(() => {
198847
+ init_action_button();
198848
+ init_theme();
198849
+ init_theme_text();
198850
+ });
198851
+
198852
+ // src/tui/remote-queue-wiring.ts
198853
+ function createRemoteQueueWiring(deps) {
198854
+ let pendingEdit;
198855
+ return {
198856
+ removeMainQueue(index) {
198857
+ const queue = deps.getQueue();
198858
+ if (index < 0 || index >= queue.length)
198859
+ return;
198860
+ const removed = queue[index];
198861
+ if (removed?.source === TG_SOURCE) {
198862
+ deps.getBridge()?.queuedLineRemoved(removed.question);
198863
+ }
198864
+ deps.setQueue(removeMainQueueItem(queue, index));
198865
+ deps.paint();
198866
+ },
198867
+ editMainQueue(index) {
198868
+ const edited = editMainQueueItem(deps.getQueue(), index);
198869
+ if (edited === undefined)
198870
+ return;
198871
+ deps.setQueue(edited.rest);
198872
+ pendingEdit = pendingQueueEditFor(edited.removed, index);
198873
+ deps.composer.setText(edited.text);
198874
+ deps.composer.focus();
198875
+ deps.paint();
198876
+ },
198877
+ requeuePendingEdit(line, displayLine) {
198878
+ if (pendingEdit === undefined)
198879
+ return false;
198880
+ const edit = pendingEdit;
198881
+ pendingEdit = undefined;
198882
+ deps.setQueue(reinsertEditedMainQueueItem(deps.getQueue(), edit, line, displayLine));
198883
+ deps.paint();
198884
+ return true;
198885
+ },
198886
+ hasPendingEdit: () => pendingEdit !== undefined,
198887
+ stopRemoteForSessionSwitch() {
198888
+ const bridge = deps.getBridge();
198889
+ if (bridge?.keepingTopic === true) {
198890
+ bridge.sessionLeaving();
198891
+ return;
198892
+ }
198893
+ if (bridge?.active !== true)
198894
+ return;
198895
+ const closing = bridge.disable();
198896
+ deps.say(`\u25C7 remote control is turning off: the session changed, and its Telegram topic is being archived. Turn it on again with /remote-control <name>.
198897
+ `);
198898
+ closing.then(() => deps.say(`\u25C7 remote control off: the topic is archived.
198899
+ `));
198900
+ },
198901
+ dropQueuedTelegramLines() {
198902
+ const { kept, dropped } = dropQueuedBySource(deps.getQueue(), TG_SOURCE);
198903
+ if (dropped.length === 0)
198904
+ return [];
198905
+ deps.setQueue(kept);
198906
+ deps.paint();
198907
+ return dropped.map((item) => item.question);
198908
+ },
198909
+ enqueueTelegramLine(text) {
198910
+ const queue = deps.getQueue();
198911
+ queue.push({ id: deps.nextId(), question: text, displayQuestion: deps.summarize(text), source: TG_SOURCE });
198912
+ deps.paint();
198913
+ deps.say(`\u25C7 a line from Telegram is queued as q${queue.length}.
198914
+ `);
198915
+ },
198916
+ runTelegramLine(text) {
198917
+ deps.runLine(text, "operator", TG_SOURCE);
198918
+ },
198919
+ runQueued(item) {
198920
+ deps.runLine(item.question, "operator", item.source);
198921
+ },
198922
+ turnStarted(source) {
198923
+ deps.getBridge()?.turnStarted(source);
198924
+ },
198925
+ assistantText(text) {
198926
+ deps.getBridge()?.assistantText(text);
198927
+ },
198928
+ wrapOnToolCall(base) {
198929
+ return (...args) => {
198930
+ deps.getBridge()?.toolCall(typeof args[0] === "string" ? args[0] : undefined);
198931
+ base?.(...args);
198932
+ };
198933
+ },
198934
+ turnSettled(outcome) {
198935
+ const bridge = deps.getBridge();
198936
+ return bridge === undefined ? Promise.resolve() : bridge.turnSettled({ failed: outcome.failed || outcome.aborted });
198937
+ }
198938
+ };
198939
+ }
198940
+ var init_remote_queue_wiring = __esm(() => {
198941
+ init_shell_bridge();
198942
+ });
198943
+
198944
+ // src/tui/turn-loop.ts
198945
+ function createTurnLoop(deps) {
198946
+ let queue = [];
198947
+ let seq = 0;
198948
+ const nextId = () => `mq${seq++}`;
198949
+ const enqueue = (line, displayLine, source) => {
198950
+ queue.push({ id: nextId(), question: line, displayQuestion: displayLine, ...source !== undefined ? { source } : {} });
198951
+ deps.paint();
198952
+ return queue.length;
198953
+ };
198954
+ const remote = createRemoteQueueWiring({
198955
+ ...deps.remote,
198956
+ getQueue: () => queue,
198957
+ setQueue: (next) => {
198958
+ queue = next;
198959
+ },
198960
+ nextId,
198961
+ paint: deps.paint,
198962
+ say: deps.say,
198963
+ runLine: deps.runLine
198964
+ });
198965
+ const drain = createMainQueueDrain({
198966
+ forced: deps.forced,
198967
+ queue: () => queue,
198968
+ paint: deps.paint,
198969
+ run: (next) => remote.runQueued(next),
198970
+ turnWorkInFlight: deps.turnWorkInFlight,
198971
+ onSettle: deps.onSettle,
198972
+ onDetachedFailure: deps.onDetachedFailure
198973
+ });
198974
+ return {
198975
+ get queue() {
198976
+ return queue;
198977
+ },
198978
+ replaceQueue(next) {
198979
+ queue = next;
198980
+ },
198981
+ enqueue,
198982
+ enqueueWhileBusy(line, displayLine, commandName, source) {
198983
+ if (deferredBusyAction(line, commandName) !== "queue")
198984
+ return false;
198985
+ const position = enqueue(line, displayLine, source);
198986
+ deps.say(`${queuedCommandNotice(position)}
198987
+ `);
198988
+ return true;
198989
+ },
198990
+ drainAfterRecipientChoice(isIdle) {
198991
+ drainIdleMainQueue(queue, {
198992
+ isIdle,
198993
+ takeForced: () => deps.forced.takeNext(),
198994
+ dispatch: (next) => {
198995
+ deps.paint();
198996
+ remote.runQueued(next);
198997
+ }
198998
+ });
198999
+ },
199000
+ remote,
199001
+ drain
199002
+ };
199003
+ }
199004
+ var init_turn_loop = __esm(() => {
199005
+ init_busy_dispatch();
199006
+ init_remote_queue_wiring();
199007
+ });
199008
+
198087
199009
  // src/tui/addressee-queue.ts
198088
199010
  function emptyAddresseeQueues() {
198089
199011
  return new Map;
@@ -198866,7 +199788,7 @@ function renderExternalTranscript(events, options = {}) {
198866
199788
  if (options.summary !== false && events.length > 0) {
198867
199789
  lines.push("", formatTranscriptFooter(foldExternalTranscript(events)));
198868
199790
  }
198869
- return options.width === undefined ? lines : lines.map((line) => clip10(line, options.width ?? 0));
199791
+ return options.width === undefined ? lines : lines.map((line) => clip11(line, options.width ?? 0));
198870
199792
  }
198871
199793
  function formatExternalWork(events, options = {}) {
198872
199794
  return renderExternalTranscript(events, options).join(`
@@ -199031,7 +199953,7 @@ function describeSkips(skippedLines) {
199031
199953
  function plural5(count, singular, pluralForm) {
199032
199954
  return `${count} ${count === 1 ? singular : pluralForm ?? `${singular}s`}`;
199033
199955
  }
199034
- function clip10(value, width) {
199956
+ function clip11(value, width) {
199035
199957
  if (width <= 0 || value.length <= width)
199036
199958
  return value;
199037
199959
  return `${value.slice(0, Math.max(1, width - 1))}\u2026`;
@@ -199061,7 +199983,7 @@ var init_external_transcript = __esm(() => {
199061
199983
  });
199062
199984
 
199063
199985
  // src/tui/external-inspector.ts
199064
- function paintContent3(otui, renderer, body, id, content) {
199986
+ function paintContent4(otui, renderer, body, id, content) {
199065
199987
  if (otui === undefined || otui === null || body === undefined || body === null)
199066
199988
  return;
199067
199989
  const parent = body;
@@ -199134,18 +200056,18 @@ function presentExternalInspector(openModalFn, otui, chrome, options) {
199134
200056
  const renderer = options.renderer ?? chrome?.renderer;
199135
200057
  const current = options.store.get(options.id);
199136
200058
  if (current === undefined) {
199137
- paintContent3(otui, renderer, body, "external-inspector-body", EXTERNAL_RUN_GONE);
200059
+ paintContent4(otui, renderer, body, "external-inspector-body", EXTERNAL_RUN_GONE);
199138
200060
  return;
199139
200061
  }
199140
200062
  if (tabId === "meta") {
199141
- metaNode = paintContent3(otui, renderer, body, "external-inspector-meta", formatExternalMeta(current));
200063
+ metaNode = paintContent4(otui, renderer, body, "external-inspector-meta", formatExternalMeta(current));
199142
200064
  return;
199143
200065
  }
199144
200066
  if (tabId === "command") {
199145
- commandNode = paintContent3(otui, renderer, body, "external-inspector-command", formatExternalCommand(current));
200067
+ commandNode = paintContent4(otui, renderer, body, "external-inspector-command", formatExternalCommand(current));
199146
200068
  return;
199147
200069
  }
199148
- workNode = paintContent3(otui, renderer, body, "external-inspector-work", formatExternalWork(current.events));
200070
+ workNode = paintContent4(otui, renderer, body, "external-inspector-work", formatExternalWork(current.events));
199149
200071
  },
199150
200072
  onClose: () => {
199151
200073
  unsubscribe?.();
@@ -199169,333 +200091,6 @@ var init_external_inspector = __esm(() => {
199169
200091
  ];
199170
200092
  });
199171
200093
 
199172
- // src/tui/background-job-session.ts
199173
- function appendBounded(output, chunk) {
199174
- const combined = output + chunk;
199175
- return combined.length > MAX_BACKGROUND_JOB_OUTPUT_CHARS ? combined.slice(-MAX_BACKGROUND_JOB_OUTPUT_CHARS) : combined;
199176
- }
199177
- function clip11(s, max) {
199178
- if (max <= 0) {
199179
- return "";
199180
- }
199181
- if (s.length <= max) {
199182
- return s;
199183
- }
199184
- return `${s.slice(0, Math.max(1, max - 1))}\u2026`;
199185
- }
199186
- function formatJobListHeader(count) {
199187
- return `Background Jobs ${count}`;
199188
- }
199189
- function formatJobRow(entry, width = SIDEBAR_TEXT_WIDTH) {
199190
- const glyph = STATUS_GLYPH4[entry.status];
199191
- const budget = Math.max(1, width - glyph.length - 1);
199192
- const cmd = clip11(entry.command, budget);
199193
- return clip11(`${glyph} ${cmd}`, width);
199194
- }
199195
- function formatJobDuration(entry, now) {
199196
- const started = Date.parse(entry.startedAt);
199197
- const ended = entry.endedAt !== undefined ? Date.parse(entry.endedAt) : now;
199198
- return formatDuration(ended - started);
199199
- }
199200
- function formatJobMeta(entry, now = Date.now()) {
199201
- const rows = [
199202
- ["Id", entry.jobId],
199203
- ["Pid", String(entry.pid)],
199204
- ["Status", entry.status],
199205
- ["Command", entry.command],
199206
- ["Started", entry.startedAt],
199207
- ["Ended", entry.endedAt ?? "\u2014"],
199208
- ["Duration", formatJobDuration(entry, now)],
199209
- ["Exit code", entry.exitCode !== undefined ? String(entry.exitCode) : "\u2014"]
199210
- ];
199211
- if (entry.killReason !== undefined) {
199212
- rows.push(["Kill reason", entry.killReason]);
199213
- }
199214
- const width = rows.reduce((max, [label]) => Math.max(max, label.length), 0);
199215
- return rows.map(([label, value]) => `${label.padEnd(width)} ${value}`).join(`
199216
- `);
199217
- }
199218
- function formatJobOutput(entry) {
199219
- return entry.output.length > 0 ? entry.output : "(no output yet)";
199220
- }
199221
-
199222
- class BackgroundJobStore {
199223
- jobs = new Map;
199224
- pending = new Map;
199225
- listeners = new Set;
199226
- apply(event) {
199227
- switch (event.type) {
199228
- case "start":
199229
- this.pending.set(event.jobId, {
199230
- jobId: event.jobId,
199231
- command: event.command,
199232
- pid: event.pid,
199233
- status: "running",
199234
- startedAt: event.startedAt,
199235
- output: ""
199236
- });
199237
- return;
199238
- case "phase": {
199239
- const entry = this.pending.get(event.jobId);
199240
- if (entry === undefined) {
199241
- return;
199242
- }
199243
- this.pending.delete(event.jobId);
199244
- this.jobs.set(event.jobId, entry);
199245
- this.emit({ id: event.jobId, kind: "start" });
199246
- return;
199247
- }
199248
- case "output": {
199249
- const hidden = this.pending.get(event.jobId);
199250
- if (hidden !== undefined) {
199251
- hidden.output = appendBounded(hidden.output, event.chunk);
199252
- return;
199253
- }
199254
- const current = this.jobs.get(event.jobId);
199255
- if (current === undefined) {
199256
- return;
199257
- }
199258
- current.output = appendBounded(current.output, event.chunk);
199259
- this.emit({ id: event.jobId, kind: "output" });
199260
- return;
199261
- }
199262
- case "exit": {
199263
- if (this.pending.delete(event.jobId)) {
199264
- return;
199265
- }
199266
- const current = this.jobs.get(event.jobId);
199267
- if (current === undefined) {
199268
- return;
199269
- }
199270
- current.status = event.status;
199271
- current.endedAt = event.endedAt;
199272
- if (event.exitCode !== undefined) {
199273
- current.exitCode = event.exitCode;
199274
- }
199275
- if (event.killReason !== undefined) {
199276
- current.killReason = event.killReason;
199277
- }
199278
- this.emit({ id: event.jobId, kind: "exit" });
199279
- return;
199280
- }
199281
- }
199282
- }
199283
- get(jobId) {
199284
- return this.jobs.get(jobId);
199285
- }
199286
- list() {
199287
- return [...this.jobs.values()];
199288
- }
199289
- subscribe(listener) {
199290
- this.listeners.add(listener);
199291
- return () => {
199292
- this.listeners.delete(listener);
199293
- };
199294
- }
199295
- removeAll() {
199296
- this.pending.clear();
199297
- if (this.jobs.size === 0) {
199298
- return;
199299
- }
199300
- const ids = [...this.jobs.keys()];
199301
- this.jobs.clear();
199302
- for (const id of ids) {
199303
- this.emit({ id, kind: "exit" });
199304
- }
199305
- }
199306
- emit(hint) {
199307
- for (const listener of this.listeners) {
199308
- try {
199309
- listener(hint);
199310
- } catch {}
199311
- }
199312
- }
199313
- }
199314
- var MAX_BACKGROUND_JOB_OUTPUT_CHARS = 20000, STATUS_GLYPH4;
199315
- var init_background_job_session = __esm(() => {
199316
- init_shell_chrome();
199317
- init_duration_format();
199318
- STATUS_GLYPH4 = {
199319
- running: "\u25D0",
199320
- completed: "\u25CF",
199321
- failed: "\u25B2",
199322
- killed: "\u2717"
199323
- };
199324
- });
199325
-
199326
- // src/tui/background-job-inspector.ts
199327
- function resolveTarget(otui, renderer, body) {
199328
- if (otui === undefined || otui === null || body === undefined || body === null) {
199329
- return;
199330
- }
199331
- const parent = body;
199332
- if (parent.add === undefined) {
199333
- return;
199334
- }
199335
- const scrollCtor = otui.ScrollBoxRenderable;
199336
- if (scrollCtor === undefined) {
199337
- return { add: parent.add.bind(parent) };
199338
- }
199339
- const scroll = new scrollCtor(renderer, {
199340
- id: "job-inspector-scroll",
199341
- flexGrow: 1,
199342
- minHeight: 0,
199343
- scrollY: true,
199344
- stickyScroll: true,
199345
- stickyStart: "bottom",
199346
- contentOptions: { flexDirection: "column" }
199347
- });
199348
- parent.add(scroll);
199349
- const target = scroll.content ?? scroll;
199350
- if (target.add === undefined) {
199351
- return;
199352
- }
199353
- return { add: target.add.bind(target) };
199354
- }
199355
- function paintContent4(otui, renderer, body, content) {
199356
- const target = resolveTarget(otui, renderer, body);
199357
- const textCtor = otui.TextRenderable;
199358
- if (target === undefined || textCtor === undefined) {
199359
- return;
199360
- }
199361
- const node = new textCtor(renderer, { id: "job-inspector-output", content });
199362
- target.add(node);
199363
- return node;
199364
- }
199365
- function paintMetaTab(otui, renderer, body, entry, registry, jobId) {
199366
- const target = resolveTarget(otui, renderer, body);
199367
- const textCtor = otui.TextRenderable;
199368
- if (target === undefined || textCtor === undefined) {
199369
- return;
199370
- }
199371
- const metaNode = new textCtor(renderer, { id: "job-inspector-meta", content: formatJobMeta(entry) });
199372
- target.add(metaNode);
199373
- const killStatusNode = new textCtor(renderer, { id: "job-inspector-kill-status", content: "" });
199374
- const killNode = new textCtor(renderer, {
199375
- id: "job-inspector-kill",
199376
- content: "[ Kill ]",
199377
- onMouseDown: () => {
199378
- registry.kill(jobId).then((result) => {
199379
- if (!result.ok) {
199380
- killStatusNode.content = `Kill failed: ${result.error}`;
199381
- }
199382
- });
199383
- }
199384
- });
199385
- target.add(killNode);
199386
- target.add(killStatusNode);
199387
- return metaNode;
199388
- }
199389
- function inspectorTitleForJob(entry) {
199390
- return entry.command;
199391
- }
199392
- function presentJobInspector(openModalFn, otui, chrome, options) {
199393
- const entry = options.store.get(options.id);
199394
- if (entry === undefined) {
199395
- return;
199396
- }
199397
- let outputNode;
199398
- let metaNode;
199399
- let unsubscribe;
199400
- const refresh = (hint) => {
199401
- if (hint !== undefined && hint.id !== options.id) {
199402
- return;
199403
- }
199404
- const current = options.store.get(options.id);
199405
- if (current === undefined) {
199406
- if (outputNode !== undefined) {
199407
- outputNode.content = GONE_TEXT;
199408
- }
199409
- if (metaNode !== undefined) {
199410
- metaNode.content = GONE_TEXT;
199411
- }
199412
- return;
199413
- }
199414
- if (outputNode !== undefined) {
199415
- outputNode.content = formatJobOutput(current);
199416
- }
199417
- if (metaNode !== undefined) {
199418
- metaNode.content = formatJobMeta(current);
199419
- }
199420
- };
199421
- const handle = openModalFn(otui, chrome, {
199422
- title: inspectorTitleForJob(entry),
199423
- tabs: [
199424
- { id: "output", label: "Output" },
199425
- { id: "meta", label: "Meta" }
199426
- ],
199427
- initialTab: "output",
199428
- footer: JOB_INSPECTOR_FOOTER,
199429
- renderTab: (tabId, body) => {
199430
- const renderer = options.renderer ?? chrome?.renderer;
199431
- const current = options.store.get(options.id);
199432
- if (current === undefined) {
199433
- paintContent4(otui, renderer, body, GONE_TEXT);
199434
- return;
199435
- }
199436
- if (tabId === "meta") {
199437
- metaNode = paintMetaTab(otui, renderer, body, current, options.registry, options.id);
199438
- outputNode = undefined;
199439
- return;
199440
- }
199441
- outputNode = paintContent4(otui, renderer, body, formatJobOutput(current));
199442
- metaNode = undefined;
199443
- },
199444
- onClose: () => {
199445
- unsubscribe?.();
199446
- }
199447
- });
199448
- if (handle === undefined) {
199449
- return;
199450
- }
199451
- unsubscribe = options.store.subscribe(refresh);
199452
- return handle;
199453
- }
199454
- function openJobInspector(otui, chrome, options) {
199455
- return presentJobInspector((hostOtui, hostChrome, input) => openModal(hostOtui, hostChrome, input), otui, chrome, options);
199456
- }
199457
- function paintBackgroundJobSidebar(otui, renderer, parent, entries, options) {
199458
- const box = parent;
199459
- const ctor = otui.TextRenderable;
199460
- if (box.add === undefined || ctor === undefined) {
199461
- return;
199462
- }
199463
- if (box.getChildren !== undefined && box.remove !== undefined) {
199464
- for (const child of [...box.getChildren()]) {
199465
- box.remove(child);
199466
- }
199467
- }
199468
- if (entries.length === 0) {
199469
- return;
199470
- }
199471
- const width = options.width ?? SIDEBAR_TEXT_WIDTH;
199472
- box.add(new ctor(renderer, {
199473
- id: "sb-jobs-h",
199474
- content: formatJobListHeader(entries.length)
199475
- }));
199476
- for (const entry of entries) {
199477
- const id = entry.jobId;
199478
- box.add(new ctor(renderer, {
199479
- id: `sb-job-${id}`,
199480
- content: formatJobRow(entry, width),
199481
- onMouseDown: () => {
199482
- options.onOpen(id);
199483
- }
199484
- }));
199485
- }
199486
- }
199487
- var JOB_INSPECTOR_FOOTER, GONE_TEXT = "Background job is gone.";
199488
- var init_background_job_inspector = __esm(() => {
199489
- init_modal_host();
199490
- init_shell_chrome();
199491
- init_background_job_session();
199492
- JOB_INSPECTOR_FOOTER = [
199493
- { key: "\u2190/\u2192", label: "tabs" },
199494
- { key: "k", label: "kill" },
199495
- { key: "esc", label: "close" }
199496
- ];
199497
- });
199498
-
199499
200094
  // src/tui/execution-plan-panel.ts
199500
200095
  function truncate7(text, width) {
199501
200096
  if (width <= 0)
@@ -203455,8 +204050,12 @@ async function launchTuiAgentShell(opts) {
203455
204050
  sections[section] = text;
203456
204051
  }
203457
204052
  const hold = leaseView()?.banner();
204053
+ const work = workSidebarSnapshot(sessions.list(), jobs.list());
204054
+ delete sections.subagents;
204055
+ delete sections.jobs;
203458
204056
  return {
203459
- sections,
204057
+ sections: { ...sections, ...work.sections },
204058
+ items: work.items,
203460
204059
  approvalsPending: liveApprovals?.pending() ?? 0,
203461
204060
  ...hold !== undefined ? { hold } : {},
203462
204061
  ...updateAvailableVersion !== undefined ? { updateAvailable: updateAvailableVersion } : {},
@@ -203469,7 +204068,23 @@ async function launchTuiAgentShell(opts) {
203469
204068
  geometry: sidebarGeometry,
203470
204069
  snapshot: () => cardSnapshot,
203471
204070
  onOpenCard: (cardId) => {
203472
- openSidebarCard(otui, chrome, cardId, { snapshot: sidebarSnapshot, runCommand: (command) => runLine(command) });
204071
+ openSidebarCard(otui, chrome, cardId, {
204072
+ snapshot: sidebarSnapshot,
204073
+ runCommand: (command) => runLine(command),
204074
+ subscribe: (refresh) => {
204075
+ const offAgents = sessions.subscribe(refresh);
204076
+ const offJobs = jobs.subscribe(refresh);
204077
+ return () => {
204078
+ offAgents();
204079
+ offJobs();
204080
+ };
204081
+ },
204082
+ renderItem: (section, item, body) => renderWorkInspector(otui, chrome, body, section, item.id, {
204083
+ agents: sessions,
204084
+ jobs,
204085
+ registry: { kill: (id) => deps.jobRegistry !== undefined ? deps.jobRegistry.kill(id) : Promise.resolve({ ok: false, error: "Job registry is unavailable." }) }
204086
+ })
204087
+ });
203473
204088
  }
203474
204089
  });
203475
204090
  const tickSidebarCards = () => {
@@ -204668,7 +205283,14 @@ async function launchTuiAgentShell(opts) {
204668
205283
  foregroundOperation.invalidateSession();
204669
205284
  io.resetStream();
204670
205285
  blockIo.resetReasoningLiveState();
204671
- forceHandoff.clear();
205286
+ const parked = forceHandoff.drainPending();
205287
+ if (parked.length > 0) {
205288
+ turnLoop.replaceQueue([...parked, ...turnLoop.queue]);
205289
+ paintMainQueue();
205290
+ io.onSystem?.(parked.length === 1 ? `\u25C7 the forced line is back in the main queue.
205291
+ ` : `\u25C7 ${parked.length} forced lines are back in the main queue.
205292
+ `);
205293
+ }
204672
205294
  suggestionGate.cancel();
204673
205295
  if (sessionPersistTimer !== undefined) {
204674
205296
  clearTimeout(sessionPersistTimer);
@@ -206102,8 +206724,18 @@ ${formatThemeList(getThemeId())}`);
206102
206724
  turnLoop.replaceQueue(removeMainQueueItem(turnLoop.queue, index));
206103
206725
  paintMainQueue();
206104
206726
  forceForegroundQueueItem(foregroundOperation, forceHandoff, item, {
206105
- announce: () => io.onSystem?.(`\u25C7 main turn interrupted \u2014 q${index + 1} will run next.
206727
+ announce: (interrupted) => io.onSystem?.(interrupted ? `\u25C7 main turn interrupted \u2014 q${index + 1} will run next.
206728
+ ` : `\u25C7 q${index + 1} will run next.
206106
206729
  `),
206730
+ isBusy: () => chrome.isBusy(),
206731
+ resume: () => drainSites.onLeaseRelease(),
206732
+ requeue: (items) => {
206733
+ if (foregroundOperation.isDisposed)
206734
+ return;
206735
+ turnLoop.replaceQueue([...items, ...turnLoop.queue]);
206736
+ paintMainQueue();
206737
+ drainSites.onLeaseRelease();
206738
+ },
206107
206739
  run: (next) => remoteQueue.runQueued(next)
206108
206740
  });
206109
206741
  };
@@ -208052,6 +208684,7 @@ var init_tui_shell = __esm(() => {
208052
208684
  init_shell_chrome();
208053
208685
  init_sidebar_geometry();
208054
208686
  init_sidebar_cards();
208687
+ init_sidebar_work();
208055
208688
  init_main_queue_view();
208056
208689
  init_turn_loop();
208057
208690
  init_queue_nav();
@@ -235628,7 +236261,7 @@ function buildAcpSessionTools(input) {
235628
236261
  ];
235629
236262
  }
235630
236263
  var ACP_CLIENT_MCP_TOOL_NAMES;
235631
- var init_roster = __esm(() => {
236264
+ var init_roster2 = __esm(() => {
235632
236265
  init_apply_patch_tool();
235633
236266
  init_interactive_tools();
235634
236267
  init_shell_exec_tool();
@@ -236855,7 +237488,7 @@ var init_server2 = __esm(() => {
236855
237488
  init_session();
236856
237489
  init_agent_io();
236857
237490
  init_commands2();
236858
- init_roster();
237491
+ init_roster2();
236859
237492
  init_client_requests();
236860
237493
  init_dispatch();
236861
237494
  init_framing();
@@ -248409,7 +249042,7 @@ var init_agents_catalog = __esm(() => {
248409
249042
  });
248410
249043
 
248411
249044
  // src/harness/monitor/reduce.ts
248412
- function str5(value) {
249045
+ function str6(value) {
248413
249046
  return typeof value === "string" && value.length > 0 ? value : undefined;
248414
249047
  }
248415
249048
  function num3(value) {
@@ -248418,8 +249051,8 @@ function num3(value) {
248418
249051
  function modelFrom(data) {
248419
249052
  if (data === undefined)
248420
249053
  return;
248421
- const model = str5(data.model);
248422
- const provider = str5(data.provider);
249054
+ const model = str6(data.model);
249055
+ const provider = str6(data.provider);
248423
249056
  if (provider !== undefined && model !== undefined)
248424
249057
  return `${provider}/${model}`;
248425
249058
  return model ?? provider;
@@ -248467,7 +249100,7 @@ function reduceAgents(events) {
248467
249100
  const model = modelFrom(event.data);
248468
249101
  if (model !== undefined)
248469
249102
  rec.model = model;
248470
- const source = str5(event.data?.source);
249103
+ const source = str6(event.data?.source);
248471
249104
  if (source !== undefined)
248472
249105
  rec.source = source;
248473
249106
  const budgetRemaining = num3(event.data?.budgetRemaining ?? undefined);