@tiangong-ai/cli 0.0.31 → 0.0.32

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.
@@ -34,6 +34,7 @@ export async function createResearchSetupPlan(input) {
34
34
  await assertWorkspaceDirectory(root);
35
35
  const scope = input.scope ?? "project";
36
36
  const agents = normalizeAgents(input.agents ?? ["codex"]);
37
+ const agentRoutes = normalizeAgentRoutes(input.agentRoutes);
37
38
  if (!input.confirmNetworkDownloads &&
38
39
  input.skillIds.length + BRAVE_PROFILE_SKILLS[input.evidenceProfile].length) {
39
40
  throw setupError({
@@ -88,6 +89,18 @@ export async function createResearchSetupPlan(input) {
88
89
  const selected = resolveSetupSkills([
89
90
  ...new Set([...BRAVE_PROFILE_SKILLS[input.evidenceProfile], ...input.skillIds]),
90
91
  ]);
92
+ const producerInstallTarget = agentRoutes.producerAgent === "codex" ? "codex" : "claude-code";
93
+ if (selected.some((skill) => skill.role === "orchestrator") &&
94
+ !agents.includes(producerInstallTarget)) {
95
+ throw setupError({
96
+ code: "RESEARCH_SETUP_SELECTION_INVALID",
97
+ step: "selection",
98
+ reason: `The native ${agentRoutes.producerAgent} producer requires the orchestrator Skill in its project Skill root.`,
99
+ minimumAction: `Include ${producerInstallTarget} in --agents, or choose the other native producer.`,
100
+ retryCommand: "tiangong-ai research setup plan --help",
101
+ exitCode: 2,
102
+ });
103
+ }
91
104
  if (selected.some((skill) => skill.role === "evidence-capability") && !agents.includes("codex")) {
92
105
  throw setupError({
93
106
  code: "RESEARCH_SETUP_SELECTION_INVALID",
@@ -102,7 +115,6 @@ export async function createResearchSetupPlan(input) {
102
115
  validateLicenseAcceptances(selected, input.acceptedLicenseIds);
103
116
  const settings = normalizedSettings(selected, input.settings ?? {});
104
117
  const credentialSources = normalizedCredentialSources(selected, input.credentialEnvironment ?? {});
105
- const agentRoutes = normalizeAgentRoutes(input.agentRoutes);
106
118
  const targets = plannedInstallTargets(root, scope, agents, input.environment ?? process.env, input.targetRoots);
107
119
  const selectedSources = [...new Set(selected.map((skill) => skill.sourceId))]
108
120
  .map(setupSource)
@@ -433,11 +445,11 @@ export async function applyResearchSetupPlan(planPath, options = {}) {
433
445
  });
434
446
  state = await updateSetupState(root, {
435
447
  ...state,
436
- status: report.readiness === "READY"
437
- ? "ready"
438
- : report.readiness === "PARTIALLY_READY"
448
+ status: report.researchReadiness === "BLOCKED"
449
+ ? "blocked"
450
+ : report.overallReadiness === "PARTIALLY_READY"
439
451
  ? "partially-ready"
440
- : "blocked",
452
+ : "ready",
441
453
  currentStep: null,
442
454
  completedSteps: [...new Set([...state.completedSteps, "doctor"])],
443
455
  });
@@ -691,41 +703,85 @@ export async function doctorResearchSetup(workspace, options = {}) {
691
703
  return `${command} is executable (${sanitizeResearchText(result.stdout.trim()).slice(0, 120)}).`;
692
704
  });
693
705
  }
694
- await setupDoctorCheck(checks, "platform-sandbox", "runtime", async () => {
695
- if (platform() === "darwin") {
696
- const info = await lstat("/usr/bin/sandbox-exec").catch(() => undefined);
697
- if (!info?.isFile())
698
- throw new Error("/usr/bin/sandbox-exec is unavailable");
699
- return "macOS sandbox-exec is available.";
706
+ if (platform() === "win32") {
707
+ const production = plan.workspace.mode === "production-research";
708
+ checks.push({
709
+ id: "platform-sandbox",
710
+ category: "runtime",
711
+ scope: "research-core",
712
+ status: production ? "fail" : "warn",
713
+ detail: production
714
+ ? "Production research execution is unsupported on Windows because no approved capsule sandbox is available."
715
+ : "Windows can inspect and smoke-test setup state, but native research execution requires an approved macOS or Linux capsule sandbox.",
716
+ minimumAction: production
717
+ ? "Run production research on macOS with sandbox-exec or Linux with Bubblewrap."
718
+ : "Use macOS or Linux before switching this workspace to production-research mode.",
719
+ blocking: production,
720
+ requiredFor: production ? ["setup", "research-core"] : [],
721
+ });
722
+ }
723
+ else {
724
+ await setupDoctorCheck(checks, "platform-sandbox", "runtime", async () => {
725
+ if (platform() === "darwin") {
726
+ const info = await lstat("/usr/bin/sandbox-exec").catch(() => undefined);
727
+ if (!info?.isFile())
728
+ throw new Error("/usr/bin/sandbox-exec is unavailable");
729
+ return "macOS sandbox-exec is available.";
730
+ }
731
+ if (platform() === "linux") {
732
+ const result = await runner({
733
+ command: "bwrap",
734
+ args: ["--version"],
735
+ cwd: root,
736
+ environment: installerEnvironment(environment),
737
+ timeoutMs: 15_000,
738
+ });
739
+ if (result.exitCode !== 0)
740
+ throw new Error("Bubblewrap is unavailable");
741
+ return "Linux Bubblewrap is available.";
742
+ }
743
+ throw new Error("Research execution is unsupported on this platform");
744
+ });
745
+ }
746
+ checks.push({
747
+ id: "agent.native-producer",
748
+ category: "agent",
749
+ scope: "research-core",
750
+ status: "pass",
751
+ detail: `Producer work is bound to the current interactive ${plan.agentRoutes.producerAgent} host; setup will not launch it as a child process.`,
752
+ minimumAction: null,
753
+ blocking: true,
754
+ requiredFor: ["setup", "research-core"],
755
+ });
756
+ const reviewerCommand = plan.agentRoutes.reviewerAgent;
757
+ await setupDoctorCheck(checks, `agent.${reviewerCommand}.reviewer`, "agent", async () => {
758
+ const result = await runner({
759
+ command: reviewerCommand,
760
+ args: ["--version"],
761
+ cwd: root,
762
+ environment: agentDoctorEnvironment(environment),
763
+ timeoutMs: 30_000,
764
+ });
765
+ if (result.exitCode !== 0)
766
+ throw new Error(`${reviewerCommand} is not executable`);
767
+ return `${reviewerCommand} reviewer CLI is executable (${sanitizeResearchText(result.stdout.trim()).slice(0, 160)}).`;
768
+ });
769
+ await setupDoctorCheck(checks, "agent-route-config", "agent", async () => {
770
+ const config = await loadWorkspaceConfig(root);
771
+ if (config.producer.executionMode !== "native-host" ||
772
+ config.reviewer.executionMode !== "headless-cli" ||
773
+ config.producer.agent === config.reviewer.agent) {
774
+ throw new Error("Workspace must bind producer=native-host and reviewer=headless-cli on different agent families.");
700
775
  }
701
- if (platform() === "linux") {
702
- const result = await runner({
703
- command: "bwrap",
704
- args: ["--version"],
705
- cwd: root,
706
- environment: installerEnvironment(environment),
707
- timeoutMs: 15_000,
708
- });
709
- if (result.exitCode !== 0)
710
- throw new Error("Bubblewrap is unavailable");
711
- return "Linux Bubblewrap is available.";
776
+ if (config.mode === "production-research" &&
777
+ (!config.producer.model ||
778
+ !config.reviewer.model ||
779
+ !config.producer.pricing ||
780
+ !config.reviewer.pricing)) {
781
+ throw new Error("Production agent models and reviewed pricing are incomplete.");
712
782
  }
713
- throw new Error("Research execution is unsupported on this platform");
783
+ return `${config.producer.agent}(native-host) -> ${config.reviewer.agent}(headless-cli).`;
714
784
  });
715
- for (const command of ["codex", "claude"]) {
716
- await setupDoctorCheck(checks, `agent.${command}`, "agent", async () => {
717
- const result = await runner({
718
- command,
719
- args: ["--version"],
720
- cwd: root,
721
- environment: agentDoctorEnvironment(environment),
722
- timeoutMs: 30_000,
723
- });
724
- if (result.exitCode !== 0)
725
- throw new Error(`${command} is not executable`);
726
- return `${command} is executable (${sanitizeResearchText(result.stdout.trim()).slice(0, 160)}).`;
727
- });
728
- }
729
785
  const selected = plan.selection.skillIds.map(setupSkill);
730
786
  const installations = await inspectSelectedInstallations(plan, selected, environment);
731
787
  for (const installation of installations) {
@@ -802,10 +858,15 @@ export async function doctorResearchSetup(workspace, options = {}) {
802
858
  }
803
859
  await appendDependencyChecks(checks, selected, runner, root, environment);
804
860
  let capabilityDoctor = null;
861
+ const blockingBeforeLive = checks
862
+ .map((check) => normalizeSetupDoctorCheck(check, selected))
863
+ .filter((check) => check.blocking && check.status === "fail")
864
+ .map((check) => check.id);
865
+ const runRequiredLive = options.live === true && blockingBeforeLive.length === 0;
805
866
  if (selected.some((skill) => skill.role === "evidence-capability")) {
806
867
  try {
807
868
  capabilityDoctor = await doctorExternalCapabilities(root, {
808
- live: options.live === true,
869
+ live: runRequiredLive,
809
870
  fetcher,
810
871
  sleeper,
811
872
  });
@@ -829,7 +890,20 @@ export async function doctorResearchSetup(workspace, options = {}) {
829
890
  });
830
891
  }
831
892
  }
832
- if (options.live) {
893
+ if (options.live === true && !runRequiredLive) {
894
+ checks.push({
895
+ id: "live.required-capabilities.skipped",
896
+ category: "live-check",
897
+ scope: "evidence",
898
+ status: "fail",
899
+ detail: "Required live capability probes were not started because a static blocking prerequisite failed.",
900
+ minimumAction: "Resolve the static blocking prerequisites, then rerun live doctor checks.",
901
+ blocking: true,
902
+ requiredFor: ["setup", "research-core"],
903
+ skippedBecause: blockingBeforeLive.join(", "),
904
+ });
905
+ }
906
+ if (runRequiredLive) {
833
907
  await appendCompanionLiveChecks(checks, {
834
908
  plan,
835
909
  selected,
@@ -839,13 +913,45 @@ export async function doctorResearchSetup(workspace, options = {}) {
839
913
  allowSyntheticUnstructureUpload: options.allowSyntheticUnstructureUpload === true,
840
914
  });
841
915
  }
916
+ else if (options.live === true &&
917
+ selected.some((skill) => ["input-preprocessor", "acquisition-adapter", "post-closure-authoring"].includes(skill.role))) {
918
+ checks.push({
919
+ id: "live.optional-components.skipped",
920
+ category: "live-check",
921
+ status: "warn",
922
+ detail: "Optional component diagnostics were skipped after a static blocking prerequisite failed.",
923
+ minimumAction: "Resolve the blocking prerequisites before retrying optional diagnostics.",
924
+ blocking: false,
925
+ requiredFor: [],
926
+ skippedBecause: blockingBeforeLive.join(", "),
927
+ });
928
+ }
842
929
  let workspaceDoctor = null;
930
+ const blockingPrerequisiteFailures = checks
931
+ .map((check) => normalizeSetupDoctorCheck(check, selected))
932
+ .filter((check) => check.blocking && check.status === "fail")
933
+ .map((check) => check.id);
934
+ const runAgentSmoke = options.agentSmoke === true && blockingPrerequisiteFailures.length === 0;
935
+ if (options.agentSmoke === true && !runAgentSmoke) {
936
+ checks.push({
937
+ id: "agent-reviewer-smoke.skipped",
938
+ category: "agent",
939
+ scope: "review",
940
+ status: "fail",
941
+ detail: "The paid reviewer smoke was not started because a zero/low-cost blocking prerequisite failed.",
942
+ minimumAction: "Resolve the listed blocking prerequisites, then rerun the explicitly confirmed reviewer smoke.",
943
+ blocking: true,
944
+ requiredFor: ["setup", "research-core"],
945
+ skippedBecause: blockingPrerequisiteFailures.join(", "),
946
+ });
947
+ }
843
948
  try {
844
949
  workspaceDoctor = await doctorResearchWorkspace(root, {
845
- agentSmoke: options.agentSmoke === true,
846
- capabilitySmoke: options.live === true,
950
+ agentSmoke: runAgentSmoke,
951
+ capabilitySmoke: runRequiredLive,
847
952
  environment,
848
953
  capabilityFetcher: fetcher,
954
+ ...(capabilityDoctor === null ? {} : { capabilityDoctorResult: capabilityDoctor }),
849
955
  ...(options.executor === undefined ? {} : { executor: options.executor }),
850
956
  });
851
957
  const requiredRuntimeChecks = options.agentSmoke === true || options.live === true;
@@ -888,9 +994,16 @@ export async function doctorResearchSetup(workspace, options = {}) {
888
994
  : `Run tiangong-ai research setup doctor --live --workspace ${root} --json after reviewing quota impact.`,
889
995
  });
890
996
  }
891
- const readiness = checks.some((check) => check.status === "fail")
997
+ const scopedChecks = checks.map((check) => normalizeSetupDoctorCheck(check, selected));
998
+ const researchReadiness = scopedChecks.some((check) => check.blocking && check.status === "fail")
999
+ ? "BLOCKED"
1000
+ : "READY";
1001
+ const preprocessingReadiness = setupDomainReadiness(scopedChecks, "preprocessing");
1002
+ const acquisitionReadiness = setupDomainReadiness(scopedChecks, "acquisition");
1003
+ const authoringReadiness = setupDomainReadiness(scopedChecks, "authoring");
1004
+ const overallReadiness = researchReadiness === "BLOCKED"
892
1005
  ? "BLOCKED"
893
- : checks.some((check) => check.status === "warn")
1006
+ : scopedChecks.some((check) => check.status !== "pass")
894
1007
  ? "PARTIALLY_READY"
895
1008
  : "READY";
896
1009
  const setupSecrets = [
@@ -908,14 +1021,19 @@ export async function doctorResearchSetup(workspace, options = {}) {
908
1021
  planSha256: plan.planSha256,
909
1022
  checkedAt: new Date().toISOString(),
910
1023
  mode: options.live ? "live" : "static",
911
- readiness,
912
- checks,
1024
+ readiness: researchReadiness,
1025
+ researchReadiness,
1026
+ preprocessingReadiness,
1027
+ acquisitionReadiness,
1028
+ authoringReadiness,
1029
+ overallReadiness,
1030
+ checks: scopedChecks,
913
1031
  capabilityDoctor,
914
1032
  workspaceDoctor,
915
1033
  summary: {
916
- pass: checks.filter((check) => check.status === "pass").length,
917
- warn: checks.filter((check) => check.status === "warn").length,
918
- fail: checks.filter((check) => check.status === "fail").length,
1034
+ pass: scopedChecks.filter((check) => check.status === "pass").length,
1035
+ warn: scopedChecks.filter((check) => check.status === "warn").length,
1036
+ fail: scopedChecks.filter((check) => check.status === "fail").length,
919
1037
  },
920
1038
  }, setupSecrets);
921
1039
  await writeJsonAtomic(paths.setupReport, report);
@@ -1206,10 +1324,20 @@ function assertPlanMatchesCatalog(plan) {
1206
1324
  }
1207
1325
  }
1208
1326
  function validAgentRoutes(value) {
1209
- const allowed = new Set(["producerModel", "reviewerModel", "producerPricing", "reviewerPricing"]);
1327
+ const allowed = new Set([
1328
+ "producerAgent",
1329
+ "reviewerAgent",
1330
+ "producerModel",
1331
+ "reviewerModel",
1332
+ "producerPricing",
1333
+ "reviewerPricing",
1334
+ ]);
1210
1335
  if (Object.keys(value).some((key) => !allowed.has(key)))
1211
1336
  return false;
1212
- if (!(value.producerModel === null || typeof value.producerModel === "string") ||
1337
+ if ((value.producerAgent !== "codex" && value.producerAgent !== "claude") ||
1338
+ (value.reviewerAgent !== "codex" && value.reviewerAgent !== "claude") ||
1339
+ value.producerAgent === value.reviewerAgent ||
1340
+ !(value.producerModel === null || typeof value.producerModel === "string") ||
1213
1341
  !(value.reviewerModel === null || typeof value.reviewerModel === "string")) {
1214
1342
  return false;
1215
1343
  }
@@ -1318,13 +1446,39 @@ function normalizedCredentialSources(selected, supplied) {
1318
1446
  .sort((left, right) => left.id.localeCompare(right.id));
1319
1447
  }
1320
1448
  function normalizeAgentRoutes(value) {
1449
+ const producerAgent = normalizeAgentKind(value?.producerAgent ?? "codex", "producer");
1450
+ const reviewerAgent = normalizeAgentKind(value?.reviewerAgent ?? (producerAgent === "codex" ? "claude" : "codex"), "reviewer");
1451
+ if (producerAgent === reviewerAgent) {
1452
+ throw setupError({
1453
+ code: "RESEARCH_SETUP_AGENT_ROUTE_INVALID",
1454
+ step: "agent-route",
1455
+ reason: "The native producer and independent reviewer must use different agent families.",
1456
+ minimumAction: "Choose Codex + Claude Code in either producer/reviewer order.",
1457
+ retryCommand: "tiangong-ai research setup plan --help",
1458
+ exitCode: 2,
1459
+ });
1460
+ }
1321
1461
  return {
1462
+ producerAgent,
1463
+ reviewerAgent,
1322
1464
  producerModel: normalizeNullableIdentifier(value?.producerModel),
1323
1465
  reviewerModel: normalizeNullableIdentifier(value?.reviewerModel),
1324
1466
  producerPricing: normalizePricing(value?.producerPricing),
1325
1467
  reviewerPricing: normalizePricing(value?.reviewerPricing),
1326
1468
  };
1327
1469
  }
1470
+ function normalizeAgentKind(value, label) {
1471
+ if (value === "codex" || value === "claude")
1472
+ return value;
1473
+ throw setupError({
1474
+ code: "RESEARCH_SETUP_AGENT_ROUTE_INVALID",
1475
+ step: "agent-route",
1476
+ reason: `${label} agent must be codex or claude.`,
1477
+ minimumAction: "Choose Codex or Claude Code and keep the reviewer on the other family.",
1478
+ retryCommand: "tiangong-ai research setup plan --help",
1479
+ exitCode: 2,
1480
+ });
1481
+ }
1328
1482
  function normalizePricing(value) {
1329
1483
  if (value === undefined || value === null)
1330
1484
  return null;
@@ -1474,33 +1628,29 @@ async function ensureSetupWorkspace(plan) {
1474
1628
  await initializeResearchWorkspace(plan.workspace.path, plan.workspace.name, plan.workspace.mode);
1475
1629
  }
1476
1630
  async function configureAgentRoutes(plan) {
1477
- if (!plan.agentRoutes.producerModel &&
1478
- !plan.agentRoutes.reviewerModel &&
1479
- !plan.agentRoutes.producerPricing &&
1480
- !plan.agentRoutes.reviewerPricing) {
1481
- return;
1482
- }
1483
1631
  const paths = workspacePaths(plan.workspace.path);
1484
1632
  const config = await loadWorkspaceConfig(plan.workspace.path);
1485
1633
  const updated = {
1486
1634
  ...config,
1487
- producer: {
1488
- ...config.producer,
1489
- ...(plan.agentRoutes.producerModel === null ? {} : { model: plan.agentRoutes.producerModel }),
1490
- ...(plan.agentRoutes.producerPricing === null
1491
- ? {}
1492
- : { pricing: plan.agentRoutes.producerPricing }),
1493
- },
1494
- reviewer: {
1495
- ...config.reviewer,
1496
- ...(plan.agentRoutes.reviewerModel === null ? {} : { model: plan.agentRoutes.reviewerModel }),
1497
- ...(plan.agentRoutes.reviewerPricing === null
1498
- ? {}
1499
- : { pricing: plan.agentRoutes.reviewerPricing }),
1500
- },
1635
+ producer: setupAgentRoute(config.producer, plan.agentRoutes.producerAgent, "native-host", plan.agentRoutes.producerModel, plan.agentRoutes.producerPricing),
1636
+ reviewer: setupAgentRoute(config.reviewer, plan.agentRoutes.reviewerAgent, "headless-cli", plan.agentRoutes.reviewerModel, plan.agentRoutes.reviewerPricing),
1501
1637
  };
1502
1638
  await writeJsonAtomic(paths.config, updated);
1503
1639
  }
1640
+ function setupAgentRoute(current, agent, executionMode, plannedModel, plannedPricing) {
1641
+ const sameAgent = current.agent === agent;
1642
+ return {
1643
+ agent,
1644
+ executionMode,
1645
+ binary: agent === "codex" ? "codex" : "claude",
1646
+ model: plannedModel ?? (sameAgent ? current.model : null),
1647
+ effort: sameAgent ? (current.effort ?? "low") : "low",
1648
+ ...(agent === "codex" ? { verbosity: sameAgent ? (current.verbosity ?? "low") : "low" } : {}),
1649
+ ...((plannedPricing ?? (sameAgent ? current.pricing : undefined)) === undefined
1650
+ ? {}
1651
+ : { pricing: plannedPricing ?? current.pricing }),
1652
+ };
1653
+ }
1504
1654
  async function inspectSelectedInstallations(plan, selected, _environment) {
1505
1655
  const results = [];
1506
1656
  for (const agent of plan.install.agents) {
@@ -2565,6 +2715,84 @@ async function archiveSetupGeneration(root) {
2565
2715
  }
2566
2716
  return prior.planSha256;
2567
2717
  }
2718
+ function normalizeSetupDoctorCheck(check, selected) {
2719
+ const componentIds = check.componentIds ?? setupCheckComponentIds(check, selected);
2720
+ const scope = check.scope ?? setupCheckScope(check, selected, componentIds);
2721
+ const blocking = check.blocking ?? ["research-core", "evidence", "review"].includes(scope);
2722
+ return {
2723
+ ...check,
2724
+ scope,
2725
+ componentIds,
2726
+ requiredFor: check.requiredFor ??
2727
+ (blocking
2728
+ ? ["setup", "research-core"]
2729
+ : componentIds.map((componentId) => `component:${componentId}`)),
2730
+ blocking,
2731
+ componentGate: check.componentGate ?? check.id !== "live.semantic-scholar",
2732
+ };
2733
+ }
2734
+ function setupCheckComponentIds(check, selected) {
2735
+ if (check.id === "live.semantic-scholar")
2736
+ return ["tiangong.academic-paper-download"];
2737
+ if (check.id === "live.tiangong-unstructure") {
2738
+ return ["tiangong.document-granular-decompose"];
2739
+ }
2740
+ if (check.id.startsWith("skill.")) {
2741
+ return selected.filter((skill) => check.id.endsWith(`.${skill.id}`)).map((skill) => skill.id);
2742
+ }
2743
+ if (check.id.startsWith("setting.")) {
2744
+ const id = check.id.slice("setting.".length);
2745
+ const setting = RESEARCH_SETUP_SETTINGS.find((candidate) => candidate.id === id);
2746
+ return selected
2747
+ .filter((skill) => setting?.requiredBy.includes(skill.id))
2748
+ .map((skill) => skill.id);
2749
+ }
2750
+ if (check.id.startsWith("credential.")) {
2751
+ const id = check.id.slice("credential.".length);
2752
+ const credential = RESEARCH_SETUP_CREDENTIALS.find((candidate) => candidate.id === id);
2753
+ return selected
2754
+ .filter((skill) => credential?.requiredBy.includes(skill.id))
2755
+ .map((skill) => skill.id);
2756
+ }
2757
+ if (check.id.startsWith("dependency.")) {
2758
+ const id = check.id.slice("dependency.".length);
2759
+ return selected
2760
+ .filter((skill) => skill.dependencies.some((item) => item.id === id))
2761
+ .map((skill) => skill.id);
2762
+ }
2763
+ return [];
2764
+ }
2765
+ function setupCheckScope(check, selected, componentIds) {
2766
+ if (check.id === "live.semantic-scholar")
2767
+ return "acquisition";
2768
+ if (check.id === "live.tiangong-unstructure")
2769
+ return "preprocessing";
2770
+ if (check.category === "evidence-capability" || check.id.includes("capability")) {
2771
+ return "evidence";
2772
+ }
2773
+ if (check.category === "agent" || check.id.includes("attestation"))
2774
+ return "review";
2775
+ const roles = componentIds
2776
+ .map((id) => selected.find((skill) => skill.id === id)?.role)
2777
+ .filter((role) => Boolean(role));
2778
+ if (roles.includes("evidence-capability"))
2779
+ return "evidence";
2780
+ if (roles.includes("input-preprocessor"))
2781
+ return "preprocessing";
2782
+ if (roles.includes("acquisition-adapter"))
2783
+ return "acquisition";
2784
+ if (roles.includes("post-closure-authoring"))
2785
+ return "authoring";
2786
+ return "research-core";
2787
+ }
2788
+ function setupDomainReadiness(checks, scope) {
2789
+ const matching = checks.filter((check) => check.scope === scope);
2790
+ if (matching.length === 0)
2791
+ return "NOT_REQUIRED";
2792
+ if (matching.some((check) => check.blocking && check.status === "fail"))
2793
+ return "BLOCKED";
2794
+ return matching.some((check) => check.status !== "pass") ? "DEGRADED" : "READY";
2795
+ }
2568
2796
  function requireAbsoluteWorkspace(value) {
2569
2797
  if (!value || !isAbsolute(value) || /[\0\r\n]/.test(value)) {
2570
2798
  throw setupError({
@@ -2995,15 +3223,36 @@ async function appendDependencyChecks(checks, selected, runner, root, environmen
2995
3223
  async function appendCompanionLiveChecks(checks, input) {
2996
3224
  if (input.selected.some((skill) => skill.id === "tiangong.academic-paper-download")) {
2997
3225
  await appendSemanticScholarLiveCheck(checks, input);
3226
+ const semanticScholar = checks.findLast((check) => check.id === "live.semantic-scholar");
3227
+ checks.push({
3228
+ id: "companion.tiangong.academic-paper-download",
3229
+ category: "live-check",
3230
+ scope: "acquisition",
3231
+ componentIds: ["tiangong.academic-paper-download"],
3232
+ status: semanticScholar?.status === "pass" ? "pass" : "warn",
3233
+ detail: semanticScholar?.status === "pass"
3234
+ ? "OA resolver diagnostics: Unpaywall=unknown, Semantic Scholar=ready, arXiv=unknown; the deterministic resolver order is unchanged."
3235
+ : "OA resolver diagnostics: Unpaywall=unknown, Semantic Scholar=degraded, arXiv=unknown. The adapter remains available and actual acquisition will stop or request explicit browser handoff only after its ordered OA sources are exhausted.",
3236
+ minimumAction: semanticScholar?.status === "pass"
3237
+ ? null
3238
+ : "Retry the resolver diagnostic later or configure its optional key; unrelated research remains authorized and no standalone evidence fallback is permitted.",
3239
+ blocking: false,
3240
+ componentGate: false,
3241
+ requiredFor: ["component:tiangong.academic-paper-download"],
3242
+ });
2998
3243
  }
2999
3244
  if (input.selected.some((skill) => skill.id === "tiangong.document-granular-decompose")) {
3000
3245
  if (!input.allowSyntheticUnstructureUpload) {
3001
3246
  checks.push({
3002
3247
  id: "live.tiangong-unstructure",
3003
3248
  category: "live-check",
3249
+ scope: "preprocessing",
3250
+ componentIds: ["tiangong.document-granular-decompose"],
3004
3251
  status: "warn",
3005
3252
  detail: "Synthetic document upload was not explicitly authorized, so no document was sent.",
3006
3253
  minimumAction: "Rerun setup doctor with the separate synthetic-upload confirmation after reviewing service cost and data policy.",
3254
+ blocking: false,
3255
+ componentGate: true,
3007
3256
  });
3008
3257
  }
3009
3258
  else {
@@ -3068,9 +3317,13 @@ async function appendSemanticScholarLiveCheck(checks, input) {
3068
3317
  checks.push({
3069
3318
  id: "live.semantic-scholar",
3070
3319
  category: "live-check",
3320
+ scope: "acquisition",
3321
+ componentIds: ["tiangong.academic-paper-download"],
3071
3322
  status: "fail",
3072
3323
  detail: "Semantic Scholar returned a redirect; credential-bearing redirects are not followed.",
3073
- minimumAction: "Verify the fixed Semantic Scholar endpoint and network policy, then rerun setup doctor; do not fall back to a standalone source wrapper.",
3324
+ minimumAction: "Verify the fixed Semantic Scholar endpoint and network policy before relying on that resolver; do not fall back to a standalone evidence wrapper.",
3325
+ blocking: false,
3326
+ componentGate: false,
3074
3327
  diagnostics: {
3075
3328
  code: "PROVIDER_REDIRECT_REJECTED",
3076
3329
  executionMode: "setup-doctor",
@@ -3090,13 +3343,17 @@ async function appendSemanticScholarLiveCheck(checks, input) {
3090
3343
  checks.push({
3091
3344
  id: "live.semantic-scholar",
3092
3345
  category: "live-check",
3346
+ scope: "acquisition",
3347
+ componentIds: ["tiangong.academic-paper-download"],
3093
3348
  status: "fail",
3094
3349
  detail: sanitizeResearchText(`Semantic Scholar live check returned HTTP ${response.status}${detail ? `: ${detail}` : ""}.`, apiKey ? [apiKey] : []),
3095
3350
  minimumAction: rateLimited
3096
- ? `Wait for the provider quota window${retryAfterSeconds === null ? "" : ` (Retry-After ${retryAfterSeconds}s)`}, then rerun setup doctor. The production workflow remains blocked and must not downgrade to standalone search.`
3351
+ ? `Wait for the provider quota window${retryAfterSeconds === null ? "" : ` (Retry-After ${retryAfterSeconds}s)`}, then rerun this optional resolver diagnostic. Unrelated research remains available and must not downgrade to standalone search.`
3097
3352
  : authenticationFailure
3098
3353
  ? "Replace or remove the optional Semantic Scholar API key, verify provider entitlement, and rerun setup doctor; do not expose the key in output."
3099
- : "Verify Semantic Scholar availability and the fixed API contract, then rerun setup doctor; do not downgrade the research workflow.",
3354
+ : "Verify Semantic Scholar availability and the fixed API contract before relying on that resolver; do not downgrade the research workflow.",
3355
+ blocking: false,
3356
+ componentGate: false,
3100
3357
  diagnostics: {
3101
3358
  code: authenticationFailure
3102
3359
  ? "PROVIDER_AUTHENTICATION_FAILED"
@@ -3116,20 +3373,28 @@ async function appendSemanticScholarLiveCheck(checks, input) {
3116
3373
  checks.push({
3117
3374
  id: "live.semantic-scholar",
3118
3375
  category: "live-check",
3376
+ scope: "acquisition",
3377
+ componentIds: ["tiangong.academic-paper-download"],
3119
3378
  status: "pass",
3120
3379
  detail: apiKey
3121
3380
  ? "Semantic Scholar accepted the configured optional API key."
3122
3381
  : "Semantic Scholar public API is reachable without an optional API key.",
3123
3382
  minimumAction: null,
3383
+ blocking: false,
3384
+ componentGate: false,
3124
3385
  });
3125
3386
  }
3126
3387
  catch (error) {
3127
3388
  checks.push({
3128
3389
  id: "live.semantic-scholar",
3129
3390
  category: "live-check",
3391
+ scope: "acquisition",
3392
+ componentIds: ["tiangong.academic-paper-download"],
3130
3393
  status: "fail",
3131
3394
  detail: sanitizeResearchText(error instanceof Error ? error.message : String(error), apiKey ? [apiKey] : []),
3132
- minimumAction: "Restore provider connectivity, then rerun setup doctor; the production workflow must remain blocked instead of using a standalone fallback.",
3395
+ minimumAction: "Restore provider connectivity before relying on Semantic Scholar; unrelated research remains available and no standalone fallback is authorized.",
3396
+ blocking: false,
3397
+ componentGate: false,
3133
3398
  diagnostics: {
3134
3399
  code: "PROVIDER_TRANSPORT_FAILED",
3135
3400
  executionMode: "setup-doctor",