@aperant/framework 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -480,12 +480,12 @@ The detector returns:
480
480
  ```json
481
481
  {
482
482
  "i18n": { "engage": true, "signals": [{ "kind": "...", "detail": "...", "tier": "strong|weak" }], "reason": "strong: locale-file src/locales/fr.json" },
483
- "xplatform": { "engage": false, "signals": [], "reason": "no signals" }
483
+ "crossplatform": { "engage": false, "signals": [], "reason": "no signals" }
484
484
  }
485
485
  ```
486
486
 
487
487
  Engagement precedence (the detector encodes it; you just read the result):
488
- config `pr_review.specialists.{i18n,xplatform}` ∈ `auto|always|never` (default
488
+ config `pr_review.specialists.{i18n,crossplatform}` ∈ `auto|always|never` (default
489
489
  `auto`) is checked FIRST and is absolute — `always` engages with no scan,
490
490
  `never` skips with no scan (even when the diff introduces the first locale
491
491
  file). Otherwise `auto` defers to the STRONG/WEAK tier scan.
@@ -500,17 +500,17 @@ verbatim, e.g. `config override: never`):
500
500
  // metadata.json (additive key)
501
501
  "conditional_specialists": {
502
502
  "i18n": { "engaged": true, "reason": "strong: locale-file src/locales/fr.json" },
503
- "xplatform": { "engaged": false, "reason": "no signals" }
503
+ "crossplatform": { "engaged": false, "reason": "no signals" }
504
504
  }
505
505
  ```
506
506
 
507
507
  ```
508
508
  i18n: ENGAGED (strong: locale-file src/locales/fr.json)
509
- xplatform: SKIPPED (no platform signals; config: auto)
509
+ crossplatform: SKIPPED (no platform signals; config: auto)
510
510
  ```
511
511
 
512
- Store `result.i18n.engage` and `result.xplatform.engage` as `ENGAGE_I18N` /
513
- `ENGAGE_XPLATFORM` for Phase 3.
512
+ Store `result.i18n.engage` and `result.crossplatform.engage` as `ENGAGE_I18N` /
513
+ `ENGAGE_CROSSPLATFORM` for Phase 3.
514
514
 
515
515
  **R3: pin the reviewed head SHA (PR reviews only).** After `gh pr diff` fetches into `pr-review-${REVIEW_ID}`, capture the SHA and pass it to `pr-review start-round`. All subsequent evidence reads within this round should reference this pinned SHA, not a fresh `gh pr view` call — otherwise a mid-round force-push invalidates cited line numbers.
516
516
 
@@ -839,7 +839,7 @@ DO NOT return full error output in your response -- write it to the file.
839
839
  node packages/framework/bin/apt-tools.mjs workflow advance . --id ${REVIEW_ID} --phase specialist-review
840
840
  ```
841
841
 
842
- **CRITICAL: Spawn all SELECTED review agents (6-8) in ONE message using parallel Agent tool calls.** The 6 core specialists ALWAYS spawn. Additionally append the **i18n** agent when `ENGAGE_I18N` is true and the **cross-platform** agent when `ENGAGE_XPLATFORM` is true (from Step 4b). So the set is 6, 7, or 8 agents — all in the SAME message.
842
+ **CRITICAL: Spawn all SELECTED review agents (6-8) in ONE message using parallel Agent tool calls.** The 6 core specialists ALWAYS spawn. Additionally append the **i18n** agent when `ENGAGE_I18N` is true and the **cross-platform** agent when `ENGAGE_CROSSPLATFORM` is true (from Step 4b). So the set is 6, 7, or 8 agents — all in the SAME message.
843
843
 
844
844
  Each agent receives:
845
845
  1. The changed file list
@@ -909,10 +909,10 @@ Agent(
909
909
  prompt: "{CONSTRUCTED_PROMPT_WITH_FILLED_PLACEHOLDERS}",
910
910
  )
911
911
 
912
- # Append ONLY if ENGAGE_XPLATFORM is true:
912
+ # Append ONLY if ENGAGE_CROSSPLATFORM is true:
913
913
  Agent(
914
- name: "xplatform-reviewer",
915
- subagent_type: "apt-pr-review-xplatform",
914
+ name: "crossplatform-reviewer",
915
+ subagent_type: "apt-pr-review-crossplatform",
916
916
  description: "Cross-platform review of PR changes",
917
917
  prompt: "{CONSTRUCTED_PROMPT_WITH_FILLED_PLACEHOLDERS}",
918
918
  )
@@ -1027,7 +1027,7 @@ Review Directory: {REVIEW_DIR}
1027
1027
 
1028
1028
  Read all finding files from {REVIEW_DIR}/findings/ (excluding validated.md if it exists).
1029
1029
  The core finding files are: security.md, quality.md, logic.md, codebase-fit.md, test-integrity.md, performance.md.
1030
- Additionally read i18n.md and/or xplatform.md when present (the conditional specialists write them only when engaged).
1030
+ Additionally read i18n.md and/or crossplatform.md when present (the conditional specialists write them only when engaged).
1031
1031
 
1032
1032
  For each finding in each file, run the hypothesis-validation process from your instructions.
1033
1033
 
@@ -1935,7 +1935,7 @@ Fix agents self-report their outcome via a status line (`FIXED: X | FAILED: Y |
1935
1935
  | `review.max_iterations` | number | 4 | Maximum fix-review iterations before stopping. |
1936
1936
  | `review.optional_agents` | string[] | [] | Reserved for future use. Additional specialist agents to spawn beyond the core 6. |
1937
1937
  | `pr_review.specialists.i18n` | `"auto"\|"always"\|"never"` | `"auto"` | Override for the conditional i18n specialist. `always` forces engage, `never` forces skip (over the detector AND over a diff-introduced signal), `auto`/absent defers to `detect-specialists`. Override is the TOP of the precedence chain (ID-01a). |
1938
- | `pr_review.specialists.xplatform` | `"auto"\|"always"\|"never"` | `"auto"` | Override for the conditional cross-platform specialist. Same semantics as `pr_review.specialists.i18n`. |
1938
+ | `pr_review.specialists.crossplatform` | `"auto"\|"always"\|"never"` | `"auto"` | Override for the conditional cross-platform specialist. Same semantics as `pr_review.specialists.i18n`. |
1939
1939
 
1940
1940
  ---
1941
1941
 
@@ -1964,7 +1964,7 @@ Agent(
1964
1964
  | Test-integrity reviewer | `.claude/agents/apt-pr-review-test-integrity.md` | Read, Write, Grep, Glob | 3 |
1965
1965
  | Performance reviewer | `.claude/agents/apt-pr-review-performance.md` | Read, Write, Grep, Glob | 3 |
1966
1966
  | i18n reviewer **(conditional)** | `.claude/agents/apt-pr-review-i18n.md` | Read, Write, Grep, Glob | 3 — spawned only when `detect-specialists` engages i18n (or config `always`) |
1967
- | Cross-platform reviewer **(conditional)** | `.claude/agents/apt-pr-review-xplatform.md` | Read, Write, Grep, Glob | 3 — spawned only when `detect-specialists` engages xplatform (or config `always`) |
1967
+ | Cross-platform reviewer **(conditional)** | `.claude/agents/apt-pr-review-crossplatform.md` | Read, Write, Grep, Glob | 3 — spawned only when `detect-specialists` engages crossplatform (or config `always`) |
1968
1968
  | Finding validator | `.claude/agents/apt-pr-review-validator.md` | Read, Write, Grep, Glob | 4 |
1969
1969
  | Fix agent (x3) | `.claude/agents/apt-pr-review-fixer.md` | Read, Write, Edit, Bash, Grep, Glob | 6 |
1970
1970
  | Self-reviewer | `.claude/agents/apt-pr-review-self-reviewer.md` | Read, Write, Edit, Bash, Grep, Glob | 7 |
@@ -23,6 +23,7 @@ import {
23
23
  mkdirSync,
24
24
  readdirSync,
25
25
  readFileSync,
26
+ unlinkSync,
26
27
  writeFileSync,
27
28
  } from 'node:fs'
28
29
  import { basename, dirname, join, resolve } from 'node:path'
@@ -1276,6 +1277,34 @@ export async function cmdInit(projectDir, extraArgs = []) {
1276
1277
  }
1277
1278
  }
1278
1279
 
1280
+ // Orphan-removal sweep — the cross-platform PR-review agent was renamed
1281
+ // from `apt-pr-review-xplatform` to `apt-pr-review-crossplatform`. The
1282
+ // install pipeline writes the current agent set + a fresh manifest but
1283
+ // has NO stale-removal pass (readManifest is consumed only by
1284
+ // skill-installs.mjs + check-version.mjs, never pipeline.mjs), so a
1285
+ // rename leaves the OLD-named file orphaned in every active runtime
1286
+ // root. Targeted, best-effort unlink of that ONE basename from each
1287
+ // requested runtime's install root (the agent lands under different
1288
+ // per-runtime subdirs: claude→agents, pi→skills, windsurf→workflows).
1289
+ // Mirrors runLegacyCleanup's conservative idempotent posture; ENOENT
1290
+ // and any other unlink error is swallowed.
1291
+ const ORPHANED_AGENT_BASENAME = 'apt-pr-review-xplatform.md'
1292
+ const ORPHAN_AGENT_SUBDIRS = ['agents', 'skills', 'workflows', 'rules']
1293
+ for (const runtimeId of effectiveAnswers.runtimes) {
1294
+ const runtime = getRuntime(runtimeId)
1295
+ if (!runtime?.installRoot) continue
1296
+ for (const subdir of ORPHAN_AGENT_SUBDIRS) {
1297
+ const orphanPath = join(installTarget, runtime.installRoot, subdir, ORPHANED_AGENT_BASENAME)
1298
+ if (existsSync(orphanPath)) {
1299
+ try {
1300
+ unlinkSync(orphanPath)
1301
+ } catch {
1302
+ // best-effort — a missing/locked orphan must not fail init
1303
+ }
1304
+ }
1305
+ }
1306
+ }
1307
+
1279
1308
  const results = await installRuntimes(
1280
1309
  effectiveAnswers.runtimes,
1281
1310
  canonicalRoot,
@@ -2,7 +2,7 @@
2
2
  * commands/pr-review/detect-specialists.mjs — deterministic detector for the
3
3
  * two CONDITIONAL /apt:pr-review specialists (i18n + cross-platform).
4
4
  *
5
- * Restores the 2.8 `i18n-reviewer` + `xplatform` perspectives but gates their
5
+ * Restores the 2.8 `i18n-reviewer` + `crossplatform` perspectives but gates their
6
6
  * Phase-3 spawn on a reproducible, language/platform/ecosystem-agnostic scan
7
7
  * with a config override on top. The SKILL orchestrator NEVER decides
8
8
  * engagement by reading the diff itself — it calls this detector and obeys the
@@ -11,7 +11,7 @@
11
11
  * Engagement model (spec ID-01/ID-01a/ID-02/ID-02a/ID-03/ID-03a/ID-03b):
12
12
  *
13
13
  * 1. Config check FIRST and short-circuit (ID-01a). `.aperant/config.json`
14
- * pr_review.specialists.{i18n,xplatform} ∈ {auto|always|never}, default
14
+ * pr_review.specialists.{i18n,crossplatform} ∈ {auto|always|never}, default
15
15
  * auto. `always` → engage (no scan); `never` → skip (no scan); only
16
16
  * `auto`/absent proceeds. Config is the TOP of the precedence chain — a
17
17
  * diff-introduced STRONG signal does NOT override `never`.
@@ -44,7 +44,7 @@ import { err, ok } from '../../util/result.mjs'
44
44
  * @typedef {{ engage: boolean, signals: Signal[], reason: string }} SpecialistDecision
45
45
  */
46
46
 
47
- // Manifests scanned for dependency-substring i18n/xplatform library markers.
47
+ // Manifests scanned for dependency-substring i18n/crossplatform library markers.
48
48
  // No YAML/TOML parse — substring/regex on the raw text, mirroring
49
49
  // toolchain-detect.mjs:isBerryNodeLinker (R2).
50
50
  const MANIFEST_FILES = [
@@ -334,10 +334,10 @@ function scanSourceForI18nTokens(detailLabel, text) {
334
334
  return found
335
335
  }
336
336
 
337
- // ── xplatform matchers ──────────────────────────────────────────────────────
337
+ // ── crossplatform matchers ──────────────────────────────────────────────────────
338
338
 
339
339
  // Cross-platform app frameworks (STRONG) matched as manifest substrings.
340
- const XPL_FRAMEWORK_SUBSTRINGS = [
340
+ const CROSSPLATFORM_FRAMEWORK_SUBSTRINGS = [
341
341
  'electron',
342
342
  '@tauri-apps',
343
343
  'react-native',
@@ -358,13 +358,13 @@ const XPL_FRAMEWORK_SUBSTRINGS = [
358
358
  * @param {string} text
359
359
  * @returns {Array<{kind:string, detail:string, tier:string}>}
360
360
  */
361
- function scanManifestForXplatform(manifestName, text) {
361
+ function scanManifestForCrossplatform(manifestName, text) {
362
362
  const found = []
363
363
  const lower = text.toLowerCase()
364
- for (const fw of XPL_FRAMEWORK_SUBSTRINGS) {
364
+ for (const fw of CROSSPLATFORM_FRAMEWORK_SUBSTRINGS) {
365
365
  if (lower.includes(fw)) {
366
366
  found.push({
367
- kind: 'xplatform-framework',
367
+ kind: 'crossplatform-framework',
368
368
  detail: `${fw} in ${manifestName}`,
369
369
  tier: 'strong',
370
370
  })
@@ -378,7 +378,7 @@ function scanManifestForXplatform(manifestName, text) {
378
378
 
379
379
  // Compile-time platform guards (STRONG) — source-token regexes spanning
380
380
  // Rust / Go / C/C++ / CMake / Swift.
381
- const XPL_COMPILE_GUARD_RES = [
381
+ const CROSSPLATFORM_COMPILE_GUARD_RES = [
382
382
  { re: /#\[cfg\(target_os/, label: '#[cfg(target_os' },
383
383
  { re: /\bcfg!\s*\(\s*windows\b/, label: 'cfg!(windows)' },
384
384
  { re: /\/\/go:build\s+(windows|darwin|linux)/, label: '//go:build <os>' },
@@ -393,7 +393,7 @@ const XPL_COMPILE_GUARD_RES = [
393
393
  ]
394
394
 
395
395
  // Lone runtime platform checks (WEAK).
396
- const XPL_WEAK_TOKEN_RES = [
396
+ const CROSSPLATFORM_WEAK_TOKEN_RES = [
397
397
  { re: /\bprocess\.platform\b/, label: 'process.platform' },
398
398
  { re: /\bos\.platform\s*\(/, label: 'os.platform()' },
399
399
  { re: /\bsys\.platform\b/, label: 'sys.platform' },
@@ -405,28 +405,28 @@ const XPL_WEAK_TOKEN_RES = [
405
405
  ]
406
406
 
407
407
  /**
408
- * Scan one source blob for xplatform compile-guard (STRONG) + lone runtime
408
+ * Scan one source blob for crossplatform compile-guard (STRONG) + lone runtime
409
409
  * (WEAK) tokens.
410
410
  *
411
411
  * @param {string} detailLabel
412
412
  * @param {string} text
413
413
  * @returns {Array<{kind:string, detail:string, tier:string}>}
414
414
  */
415
- function scanSourceForXplatformTokens(detailLabel, text) {
415
+ function scanSourceForCrossplatformTokens(detailLabel, text) {
416
416
  const found = []
417
- for (const { re, label } of XPL_COMPILE_GUARD_RES) {
417
+ for (const { re, label } of CROSSPLATFORM_COMPILE_GUARD_RES) {
418
418
  if (re.test(text)) {
419
419
  found.push({
420
- kind: 'xplatform-guard',
420
+ kind: 'crossplatform-guard',
421
421
  detail: `${label} in ${detailLabel}`,
422
422
  tier: 'strong',
423
423
  })
424
424
  }
425
425
  }
426
- for (const { re, label } of XPL_WEAK_TOKEN_RES) {
426
+ for (const { re, label } of CROSSPLATFORM_WEAK_TOKEN_RES) {
427
427
  if (re.test(text)) {
428
428
  found.push({
429
- kind: 'xplatform-runtime',
429
+ kind: 'crossplatform-runtime',
430
430
  detail: `${label} in ${detailLabel}`,
431
431
  tier: 'weak',
432
432
  })
@@ -496,7 +496,7 @@ function detectCiMatrix(workflowName, text) {
496
496
  }
497
497
  if (osFamilies.size > 1) {
498
498
  return {
499
- kind: 'xplatform-ci-matrix',
499
+ kind: 'crossplatform-ci-matrix',
500
500
  detail: `multi-OS CI matrix in ${workflowName} (${osFamilies.size} OS families)`,
501
501
  tier: 'strong',
502
502
  }
@@ -519,7 +519,7 @@ function detectPlatformDirSiblings(relPaths) {
519
519
  const hasLinuxDir = relPaths.some((p) => p.split('/').includes('linux'))
520
520
  if (hasWindowsDir && hasMacosDir && hasLinuxDir) {
521
521
  found.push({
522
- kind: 'xplatform-dir-siblings',
522
+ kind: 'crossplatform-dir-siblings',
523
523
  detail: 'platform dir siblings windows/ + macos/ + linux/ co-present',
524
524
  tier: 'strong',
525
525
  })
@@ -531,7 +531,7 @@ function detectPlatformDirSiblings(relPaths) {
531
531
  })
532
532
  if (hasXcodeproj && hasGradle) {
533
533
  found.push({
534
- kind: 'xplatform-dir-siblings',
534
+ kind: 'crossplatform-dir-siblings',
535
535
  detail: '*.xcodeproj + build.gradle co-presence',
536
536
  tier: 'strong',
537
537
  })
@@ -545,12 +545,14 @@ function detectPlatformDirSiblings(relPaths) {
545
545
  * @param {string[]} relPaths
546
546
  * @returns {Array<{kind:string, detail:string, tier:string}>}
547
547
  */
548
- function detectWeakXplatformFiles(relPaths) {
548
+ function detectWeakCrossplatformFiles(relPaths) {
549
549
  const found = []
550
550
  for (const p of relPaths) {
551
551
  const ext = extname(p.toLowerCase())
552
- if (ext === '.plist') found.push({ kind: 'xplatform-file', detail: `plist ${p}`, tier: 'weak' })
553
- else if (ext === '.rc') found.push({ kind: 'xplatform-file', detail: `rc ${p}`, tier: 'weak' })
552
+ if (ext === '.plist')
553
+ found.push({ kind: 'crossplatform-file', detail: `plist ${p}`, tier: 'weak' })
554
+ else if (ext === '.rc')
555
+ found.push({ kind: 'crossplatform-file', detail: `rc ${p}`, tier: 'weak' })
554
556
  }
555
557
  return found
556
558
  }
@@ -731,7 +733,7 @@ function applyScope(baselineSignals, diffSignals, diffTouchedPaths, scope) {
731
733
  * Read `.aperant/config.json:pr_review.specialists` (best-effort).
732
734
  *
733
735
  * @param {string} root
734
- * @returns {{ i18n?: string, xplatform?: string }}
736
+ * @returns {{ i18n?: string, crossplatform?: string }}
735
737
  */
736
738
  function readSpecialistConfig(root) {
737
739
  try {
@@ -759,15 +761,15 @@ function normalizeMode(v) {
759
761
  /**
760
762
  * The core detector. Pure over its inputs (reads the filesystem at `root` and
761
763
  * the supplied `diffText`); returns the full
762
- * `{ i18n, xplatform }` decision object.
764
+ * `{ i18n, crossplatform }` decision object.
763
765
  *
764
766
  * @param {object} opts
765
767
  * @param {string} opts.root absolute project dir
766
768
  * @param {string} [opts.diffText] unified-diff text (optional)
767
769
  * @param {'repo'|'diff'} [opts.scope] default 'repo'
768
- * @param {{i18n?:string, xplatform?:string}} [opts.config] override config
770
+ * @param {{i18n?:string, crossplatform?:string}} [opts.config] override config
769
771
  * (read from .aperant/config.json when omitted)
770
- * @returns {{ i18n: SpecialistDecision, xplatform: SpecialistDecision }}
772
+ * @returns {{ i18n: SpecialistDecision, crossplatform: SpecialistDecision }}
771
773
  */
772
774
  export function detectSpecialists(opts) {
773
775
  const root = resolve(opts.root)
@@ -776,7 +778,7 @@ export function detectSpecialists(opts) {
776
778
  const diffText = opts.diffText ?? ''
777
779
 
778
780
  const i18nMode = normalizeMode(config.i18n)
779
- const xplatformMode = normalizeMode(config.xplatform)
781
+ const crossplatformMode = normalizeMode(config.crossplatform)
780
782
 
781
783
  // Parse diff once (shared by both specialists).
782
784
  const diff = diffText ? parseUnifiedDiff(diffText) : { touchedPaths: [], addedText: '' }
@@ -791,17 +793,17 @@ export function detectSpecialists(opts) {
791
793
  collectDiff: collectI18nDiff,
792
794
  })
793
795
 
794
- // ── xplatform ───────────────────────────────────────────────────────────
795
- const xplatform = decideSpecialist({
796
- mode: xplatformMode,
796
+ // ── crossplatform ───────────────────────────────────────────────────────────
797
+ const crossplatform = decideSpecialist({
798
+ mode: crossplatformMode,
797
799
  root,
798
800
  diff,
799
801
  scope,
800
- collectBaseline: collectXplatformBaseline,
801
- collectDiff: collectXplatformDiff,
802
+ collectBaseline: collectCrossplatformBaseline,
803
+ collectDiff: collectCrossplatformDiff,
802
804
  })
803
805
 
804
- return { i18n, xplatform }
806
+ return { i18n, crossplatform }
805
807
  }
806
808
 
807
809
  /**
@@ -880,7 +882,7 @@ function collectI18nDiff(diff) {
880
882
  return dedupeSignals(signals)
881
883
  }
882
884
 
883
- function collectXplatformBaseline(root) {
885
+ function collectCrossplatformBaseline(root) {
884
886
  const signals = []
885
887
  const files = walkFiles(root)
886
888
 
@@ -889,12 +891,12 @@ function collectXplatformBaseline(root) {
889
891
  const abs = join(root, manifest)
890
892
  if (!existsSync(abs)) continue
891
893
  const text = readSafe(abs)
892
- if (text) signals.push(...scanManifestForXplatform(manifest, text))
894
+ if (text) signals.push(...scanManifestForCrossplatform(manifest, text))
893
895
  }
894
896
  // pubspec.yaml present → Flutter (its presence alone is the flutter marker).
895
897
  if (existsSync(join(root, 'pubspec.yaml'))) {
896
898
  signals.push({
897
- kind: 'xplatform-framework',
899
+ kind: 'crossplatform-framework',
898
900
  detail: 'flutter (pubspec.yaml present)',
899
901
  tier: 'strong',
900
902
  })
@@ -930,9 +932,13 @@ function collectXplatformBaseline(root) {
930
932
  ) {
931
933
  const text = readSafe(join(root, rel))
932
934
  if (text) {
933
- for (const { re, label } of XPL_COMPILE_GUARD_RES) {
935
+ for (const { re, label } of CROSSPLATFORM_COMPILE_GUARD_RES) {
934
936
  if (re.test(text)) {
935
- signals.push({ kind: 'xplatform-guard', detail: `${label} in ${rel}`, tier: 'strong' })
937
+ signals.push({
938
+ kind: 'crossplatform-guard',
939
+ detail: `${label} in ${rel}`,
940
+ tier: 'strong',
941
+ })
936
942
  }
937
943
  }
938
944
  }
@@ -941,25 +947,25 @@ function collectXplatformBaseline(root) {
941
947
 
942
948
  // Platform dir siblings + weak files.
943
949
  signals.push(...detectPlatformDirSiblings(files))
944
- signals.push(...detectWeakXplatformFiles(files))
950
+ signals.push(...detectWeakCrossplatformFiles(files))
945
951
 
946
952
  return dedupeSignals(signals)
947
953
  }
948
954
 
949
- function collectXplatformDiff(diff) {
955
+ function collectCrossplatformDiff(diff) {
950
956
  const signals = []
951
957
  if (diff.addedText) {
952
958
  const lowerAdded = diff.addedText.toLowerCase()
953
- for (const fw of XPL_FRAMEWORK_SUBSTRINGS) {
959
+ for (const fw of CROSSPLATFORM_FRAMEWORK_SUBSTRINGS) {
954
960
  if (lowerAdded.includes(fw)) {
955
961
  signals.push({
956
- kind: 'xplatform-framework',
962
+ kind: 'crossplatform-framework',
957
963
  detail: `${fw} (diff added line)`,
958
964
  tier: 'strong',
959
965
  })
960
966
  }
961
967
  }
962
- signals.push(...scanSourceForXplatformTokens('diff', diff.addedText))
968
+ signals.push(...scanSourceForCrossplatformTokens('diff', diff.addedText))
963
969
  }
964
970
  // CI matrix introduced in the diff.
965
971
  for (const rel of diff.touchedPaths) {
@@ -968,7 +974,7 @@ function collectXplatformDiff(diff) {
968
974
  if (m) signals.push(m)
969
975
  }
970
976
  }
971
- signals.push(...detectWeakXplatformFiles(diff.touchedPaths))
977
+ signals.push(...detectWeakCrossplatformFiles(diff.touchedPaths))
972
978
  return dedupeSignals(signals)
973
979
  }
974
980
 
@@ -1018,6 +1024,6 @@ export function cmdDetectSpecialists(projectDir, extraArgs = []) {
1018
1024
  command: 'pr-review-detect-specialists',
1019
1025
  scope,
1020
1026
  i18n: result.i18n,
1021
- xplatform: result.xplatform,
1027
+ crossplatform: result.crossplatform,
1022
1028
  })
1023
1029
  }
@@ -181,6 +181,7 @@ export const LEGACY_INSTALL_PATHS = Object.freeze({
181
181
  'agents/apt-improver.md',
182
182
  'agents/apt-planner.md',
183
183
  'agents/apt-pr-review-codebase-fit.md',
184
+ 'agents/apt-pr-review-crossplatform.md',
184
185
  'agents/apt-pr-review-fixer.md',
185
186
  'agents/apt-pr-review-i18n.md',
186
187
  'agents/apt-pr-review-logic.md',