@tiangong-ai/cli 0.0.30 → 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.
- package/AGENTS.md +2 -2
- package/README.md +127 -97
- package/dist/edge-search.js +57 -16
- package/dist/edge-search.js.map +1 -1
- package/dist/http.js +17 -1
- package/dist/http.js.map +1 -1
- package/dist/research/orchestration.js +153 -4
- package/dist/research/orchestration.js.map +1 -1
- package/dist/research/workspace/broker.d.ts +5 -0
- package/dist/research/workspace/broker.js +130 -19
- package/dist/research/workspace/broker.js.map +1 -1
- package/dist/research/workspace/companion-readiness.d.ts +11 -0
- package/dist/research/workspace/companion-readiness.js +75 -0
- package/dist/research/workspace/companion-readiness.js.map +1 -0
- package/dist/research/workspace/external-skills.d.ts +95 -0
- package/dist/research/workspace/external-skills.js +79 -25
- package/dist/research/workspace/external-skills.js.map +1 -1
- package/dist/research/workspace/preflight.d.ts +22 -0
- package/dist/research/workspace/preflight.js +54 -5
- package/dist/research/workspace/preflight.js.map +1 -1
- package/dist/research/workspace/projects.js +27 -0
- package/dist/research/workspace/projects.js.map +1 -1
- package/dist/research/workspace/runtime.d.ts +68 -2
- package/dist/research/workspace/runtime.js +652 -14
- package/dist/research/workspace/runtime.js.map +1 -1
- package/dist/research/workspace/setup-catalog.d.ts +20 -2
- package/dist/research/workspace/setup-catalog.js +202 -5
- package/dist/research/workspace/setup-catalog.js.map +1 -1
- package/dist/research/workspace/setup-wizard.js +35 -13
- package/dist/research/workspace/setup-wizard.js.map +1 -1
- package/dist/research/workspace/setup.d.ts +30 -5
- package/dist/research/workspace/setup.js +500 -114
- package/dist/research/workspace/setup.js.map +1 -1
- package/dist/research/workspace/types.d.ts +9 -0
- package/dist/research/workspace/workspace.d.ts +2 -0
- package/dist/research/workspace/workspace.js +83 -20
- package/dist/research/workspace/workspace.js.map +1 -1
- package/package.json +2 -1
|
@@ -8,10 +8,10 @@ import { CliError } from "../../errors.js";
|
|
|
8
8
|
import { loadCapabilityDeclarations } from "./capabilities.js";
|
|
9
9
|
import { inspectResearchContext } from "./context.js";
|
|
10
10
|
import { inspectCapabilityCredentialEnvironment, loadCapabilityCredentialMapForIds, reconcileCapabilityCredentialEnvironment, setCapabilityCredentialValue, } from "./credentials.js";
|
|
11
|
-
import { configureExternalSkillProfile,
|
|
11
|
+
import { configureExternalSkillProfile, configureTiangongDatabaseCapability, doctorExternalCapabilities, EXTERNAL_SKILL_CONTEXT_PROFILE, EXTERNAL_SKILL_MEDIA_PROFILE, EXTERNAL_SKILL_PROFILE, reconcileSetupManagedCapabilities, } from "./external-skills.js";
|
|
12
12
|
import { appendJournalEvent } from "./journal.js";
|
|
13
13
|
import { configuredResearchSecrets, isSensitiveEnvironmentName, sanitizeResearchRecord, sanitizeResearchText, } from "./sanitization.js";
|
|
14
|
-
import { inspectResearchSetupCatalog, RESEARCH_SETUP_CREDENTIALS, RESEARCH_SETUP_INSTALLER, RESEARCH_SETUP_SETTINGS, RESEARCH_SETUP_SKILLS, resolveSetupSkills, setupSkill, setupSource, setupTargetRoot, } from "./setup-catalog.js";
|
|
14
|
+
import { inspectResearchSetupCatalog, RESEARCH_SETUP_CREDENTIALS, RESEARCH_SETUP_INSTALLER, RESEARCH_SETUP_SETTINGS, RESEARCH_SETUP_SKILLS, resolveSetupSkills, setupSkill, setupSource, setupTargetRoot, verifyResearchSetupRuntimeContract, } from "./setup-catalog.js";
|
|
15
15
|
import { acquireFileLock, canonicalJson, ensureDirectory, fileSize, hashRegularTree, isObject, pathExists, REGULAR_TREE_HASH_ALGORITHM, readJsonFile, sha256File, sha256Text, workspacePaths, writeJsonAtomic, writeTextAtomic, } from "./storage.js";
|
|
16
16
|
import { packageVersion } from "./constants.js";
|
|
17
17
|
import { doctorResearchWorkspace, initializeResearchWorkspace, loadWorkspaceConfig, } from "./workspace.js";
|
|
@@ -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.
|
|
437
|
-
? "
|
|
438
|
-
: report.
|
|
448
|
+
status: report.researchReadiness === "BLOCKED"
|
|
449
|
+
? "blocked"
|
|
450
|
+
: report.overallReadiness === "PARTIALLY_READY"
|
|
439
451
|
? "partially-ready"
|
|
440
|
-
: "
|
|
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
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
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 (
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
846
|
-
capabilitySmoke:
|
|
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
|
|
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
|
-
:
|
|
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
|
-
|
|
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:
|
|
917
|
-
warn:
|
|
918
|
-
fail:
|
|
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);
|
|
@@ -969,22 +1087,24 @@ export async function checkResearchSetupUpdates(workspace, environment = process
|
|
|
969
1087
|
});
|
|
970
1088
|
}
|
|
971
1089
|
}
|
|
1090
|
+
const cliVersionDrift = plan.cli.version === packageVersion()
|
|
1091
|
+
? null
|
|
1092
|
+
: { planned: plan.cli.version, active: packageVersion() };
|
|
1093
|
+
const updateAvailable = drift.length > 0 || cliVersionDrift !== null;
|
|
972
1094
|
return {
|
|
973
1095
|
schemaVersion: 1,
|
|
974
1096
|
workspace: root,
|
|
975
1097
|
checkedAt: new Date().toISOString(),
|
|
976
|
-
updateAvailable
|
|
977
|
-
cliVersionDrift
|
|
978
|
-
? null
|
|
979
|
-
: { planned: plan.cli.version, active: packageVersion() },
|
|
1098
|
+
updateAvailable,
|
|
1099
|
+
cliVersionDrift,
|
|
980
1100
|
drift,
|
|
981
1101
|
currentInstaller: RESEARCH_SETUP_INSTALLER,
|
|
982
1102
|
catalog,
|
|
983
1103
|
policy: {
|
|
984
1104
|
automaticUpdate: false,
|
|
985
1105
|
floatingUpdate: false,
|
|
986
|
-
minimumAction:
|
|
987
|
-
? "Use the
|
|
1106
|
+
minimumAction: updateAvailable
|
|
1107
|
+
? "Use the active exact CLI release to create, review, and apply a replacement immutable plan; upgrade never runs a floating CLI or Skills update."
|
|
988
1108
|
: "No catalog migration is required. Installed tree drift is reported separately by setup status/doctor.",
|
|
989
1109
|
},
|
|
990
1110
|
};
|
|
@@ -1204,10 +1324,20 @@ function assertPlanMatchesCatalog(plan) {
|
|
|
1204
1324
|
}
|
|
1205
1325
|
}
|
|
1206
1326
|
function validAgentRoutes(value) {
|
|
1207
|
-
const allowed = new Set([
|
|
1327
|
+
const allowed = new Set([
|
|
1328
|
+
"producerAgent",
|
|
1329
|
+
"reviewerAgent",
|
|
1330
|
+
"producerModel",
|
|
1331
|
+
"reviewerModel",
|
|
1332
|
+
"producerPricing",
|
|
1333
|
+
"reviewerPricing",
|
|
1334
|
+
]);
|
|
1208
1335
|
if (Object.keys(value).some((key) => !allowed.has(key)))
|
|
1209
1336
|
return false;
|
|
1210
|
-
if (
|
|
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") ||
|
|
1211
1341
|
!(value.reviewerModel === null || typeof value.reviewerModel === "string")) {
|
|
1212
1342
|
return false;
|
|
1213
1343
|
}
|
|
@@ -1316,13 +1446,39 @@ function normalizedCredentialSources(selected, supplied) {
|
|
|
1316
1446
|
.sort((left, right) => left.id.localeCompare(right.id));
|
|
1317
1447
|
}
|
|
1318
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
|
+
}
|
|
1319
1461
|
return {
|
|
1462
|
+
producerAgent,
|
|
1463
|
+
reviewerAgent,
|
|
1320
1464
|
producerModel: normalizeNullableIdentifier(value?.producerModel),
|
|
1321
1465
|
reviewerModel: normalizeNullableIdentifier(value?.reviewerModel),
|
|
1322
1466
|
producerPricing: normalizePricing(value?.producerPricing),
|
|
1323
1467
|
reviewerPricing: normalizePricing(value?.reviewerPricing),
|
|
1324
1468
|
};
|
|
1325
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
|
+
}
|
|
1326
1482
|
function normalizePricing(value) {
|
|
1327
1483
|
if (value === undefined || value === null)
|
|
1328
1484
|
return null;
|
|
@@ -1472,33 +1628,29 @@ async function ensureSetupWorkspace(plan) {
|
|
|
1472
1628
|
await initializeResearchWorkspace(plan.workspace.path, plan.workspace.name, plan.workspace.mode);
|
|
1473
1629
|
}
|
|
1474
1630
|
async function configureAgentRoutes(plan) {
|
|
1475
|
-
if (!plan.agentRoutes.producerModel &&
|
|
1476
|
-
!plan.agentRoutes.reviewerModel &&
|
|
1477
|
-
!plan.agentRoutes.producerPricing &&
|
|
1478
|
-
!plan.agentRoutes.reviewerPricing) {
|
|
1479
|
-
return;
|
|
1480
|
-
}
|
|
1481
1631
|
const paths = workspacePaths(plan.workspace.path);
|
|
1482
1632
|
const config = await loadWorkspaceConfig(plan.workspace.path);
|
|
1483
1633
|
const updated = {
|
|
1484
1634
|
...config,
|
|
1485
|
-
producer:
|
|
1486
|
-
|
|
1487
|
-
...(plan.agentRoutes.producerModel === null ? {} : { model: plan.agentRoutes.producerModel }),
|
|
1488
|
-
...(plan.agentRoutes.producerPricing === null
|
|
1489
|
-
? {}
|
|
1490
|
-
: { pricing: plan.agentRoutes.producerPricing }),
|
|
1491
|
-
},
|
|
1492
|
-
reviewer: {
|
|
1493
|
-
...config.reviewer,
|
|
1494
|
-
...(plan.agentRoutes.reviewerModel === null ? {} : { model: plan.agentRoutes.reviewerModel }),
|
|
1495
|
-
...(plan.agentRoutes.reviewerPricing === null
|
|
1496
|
-
? {}
|
|
1497
|
-
: { pricing: plan.agentRoutes.reviewerPricing }),
|
|
1498
|
-
},
|
|
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),
|
|
1499
1637
|
};
|
|
1500
1638
|
await writeJsonAtomic(paths.config, updated);
|
|
1501
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
|
+
}
|
|
1502
1654
|
async function inspectSelectedInstallations(plan, selected, _environment) {
|
|
1503
1655
|
const results = [];
|
|
1504
1656
|
for (const agent of plan.install.agents) {
|
|
@@ -1719,6 +1871,7 @@ async function ensureSetupSourceCheckout(plan, sourceId, runner, environment) {
|
|
|
1719
1871
|
},
|
|
1720
1872
|
});
|
|
1721
1873
|
}
|
|
1874
|
+
await verifyResearchSetupRuntimeContract(sourcePath, setupSkill(skill.id));
|
|
1722
1875
|
}
|
|
1723
1876
|
return checkout;
|
|
1724
1877
|
}
|
|
@@ -1759,8 +1912,30 @@ async function installSetupSkills(input) {
|
|
|
1759
1912
|
}
|
|
1760
1913
|
async function configureSelectedCapabilities(plan, _environment) {
|
|
1761
1914
|
const hasBraveProfile = plan.selection.evidenceProfile !== "none";
|
|
1762
|
-
const
|
|
1763
|
-
|
|
1915
|
+
const tiangongDatabases = [
|
|
1916
|
+
{
|
|
1917
|
+
kind: "sci",
|
|
1918
|
+
skillId: "tiangong.kb-sci-search",
|
|
1919
|
+
settingPrefix: "tiangong.sci",
|
|
1920
|
+
catalogId: "first-party.tiangong.kb-sci-search",
|
|
1921
|
+
capabilityId: "database.tiangong.sci-search",
|
|
1922
|
+
},
|
|
1923
|
+
{
|
|
1924
|
+
kind: "report",
|
|
1925
|
+
skillId: "tiangong.kb-report-search",
|
|
1926
|
+
settingPrefix: "tiangong.report",
|
|
1927
|
+
catalogId: "first-party.tiangong.kb-report-search",
|
|
1928
|
+
capabilityId: "database.tiangong.report-search",
|
|
1929
|
+
},
|
|
1930
|
+
{
|
|
1931
|
+
kind: "patent",
|
|
1932
|
+
skillId: "tiangong.kb-patent-search",
|
|
1933
|
+
settingPrefix: "tiangong.patent",
|
|
1934
|
+
catalogId: "first-party.tiangong.kb-patent-search",
|
|
1935
|
+
capabilityId: "database.tiangong.patent-search",
|
|
1936
|
+
},
|
|
1937
|
+
].filter((entry) => plan.selection.skillIds.includes(entry.skillId));
|
|
1938
|
+
const codexRoot = hasBraveProfile || tiangongDatabases.length ? plannedTargetRoot(plan, "codex") : null;
|
|
1764
1939
|
if (plan.selection.evidenceProfile !== "none") {
|
|
1765
1940
|
await configureExternalSkillProfile({
|
|
1766
1941
|
workspace: plan.workspace.path,
|
|
@@ -1768,10 +1943,11 @@ async function configureSelectedCapabilities(plan, _environment) {
|
|
|
1768
1943
|
skillRoot: codexRoot,
|
|
1769
1944
|
});
|
|
1770
1945
|
}
|
|
1771
|
-
|
|
1772
|
-
const skill = setupSkill(
|
|
1946
|
+
for (const database of tiangongDatabases) {
|
|
1947
|
+
const skill = setupSkill(database.skillId);
|
|
1773
1948
|
const source = setupSource(skill.sourceId);
|
|
1774
|
-
await
|
|
1949
|
+
await configureTiangongDatabaseCapability({
|
|
1950
|
+
kind: database.kind,
|
|
1775
1951
|
workspace: plan.workspace.path,
|
|
1776
1952
|
skillPath: join(codexRoot, skill.skillName),
|
|
1777
1953
|
source: {
|
|
@@ -1780,19 +1956,19 @@ async function configureSelectedCapabilities(plan, _environment) {
|
|
|
1780
1956
|
immutableRef: source.immutableRef,
|
|
1781
1957
|
expectedTreeSha256: skill.expectedTreeSha256,
|
|
1782
1958
|
license: "MIT",
|
|
1783
|
-
catalogId:
|
|
1959
|
+
catalogId: database.catalogId,
|
|
1784
1960
|
},
|
|
1785
|
-
endpoint: plan.settings[
|
|
1786
|
-
...(plan.settings[
|
|
1961
|
+
endpoint: plan.settings[`${database.settingPrefix}.endpoint`],
|
|
1962
|
+
...(plan.settings[`${database.settingPrefix}.region`] === undefined
|
|
1787
1963
|
? {}
|
|
1788
|
-
: { region: plan.settings[
|
|
1964
|
+
: { region: plan.settings[`${database.settingPrefix}.region`] }),
|
|
1789
1965
|
});
|
|
1790
1966
|
}
|
|
1791
1967
|
await reconcileSetupManagedCapabilities({
|
|
1792
1968
|
workspace: plan.workspace.path,
|
|
1793
1969
|
selectedCapabilityIds: [
|
|
1794
1970
|
...BRAVE_PROFILE_SKILLS[plan.selection.evidenceProfile].map(setupManagedCapabilityId),
|
|
1795
|
-
...(
|
|
1971
|
+
...tiangongDatabases.map((database) => database.capabilityId),
|
|
1796
1972
|
],
|
|
1797
1973
|
});
|
|
1798
1974
|
}
|
|
@@ -1887,6 +2063,14 @@ async function assertRequiredCredentialPreflight(plan, environment) {
|
|
|
1887
2063
|
.join(", ")}), then retry this exact step.`,
|
|
1888
2064
|
retryCommand: `tiangong-ai research setup retry --step credential-preflight --workspace ${plan.workspace.path} --json`,
|
|
1889
2065
|
exitCode: 3,
|
|
2066
|
+
diagnostics: {
|
|
2067
|
+
executionMode: "setup-preflight",
|
|
2068
|
+
credentialScope: [
|
|
2069
|
+
...new Set(failures.map((failure) => definitions.find((definition) => definition.id === failure.id).storage)),
|
|
2070
|
+
].join("+"),
|
|
2071
|
+
networkAttempted: false,
|
|
2072
|
+
missingCredentialIds: failures.map((failure) => failure.id),
|
|
2073
|
+
},
|
|
1890
2074
|
});
|
|
1891
2075
|
}
|
|
1892
2076
|
}
|
|
@@ -2531,6 +2715,84 @@ async function archiveSetupGeneration(root) {
|
|
|
2531
2715
|
}
|
|
2532
2716
|
return prior.planSha256;
|
|
2533
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
|
+
}
|
|
2534
2796
|
function requireAbsoluteWorkspace(value) {
|
|
2535
2797
|
if (!value || !isAbsolute(value) || /[\0\r\n]/.test(value)) {
|
|
2536
2798
|
throw setupError({
|
|
@@ -2885,7 +3147,7 @@ function commandFailure(step, command, result, root, environment) {
|
|
|
2885
3147
|
exitCode: 3,
|
|
2886
3148
|
});
|
|
2887
3149
|
}
|
|
2888
|
-
async function setupDoctorCheck(checks, id, category, callback) {
|
|
3150
|
+
async function setupDoctorCheck(checks, id, category, callback, failureMinimumAction = `Resolve ${id}, then rerun research setup doctor.`) {
|
|
2889
3151
|
try {
|
|
2890
3152
|
checks.push({
|
|
2891
3153
|
id,
|
|
@@ -2901,7 +3163,7 @@ async function setupDoctorCheck(checks, id, category, callback) {
|
|
|
2901
3163
|
category,
|
|
2902
3164
|
status: "fail",
|
|
2903
3165
|
detail: sanitizeResearchText(error instanceof Error ? error.message : String(error)),
|
|
2904
|
-
minimumAction:
|
|
3166
|
+
minimumAction: failureMinimumAction,
|
|
2905
3167
|
});
|
|
2906
3168
|
}
|
|
2907
3169
|
}
|
|
@@ -2955,34 +3217,28 @@ async function appendDependencyChecks(checks, selected, runner, root, environmen
|
|
|
2955
3217
|
return `${dependency.requirement} is active.`;
|
|
2956
3218
|
}
|
|
2957
3219
|
throw new Error(`No automatic dependency check is declared for ${dependency.id}. ${dependency.minimumAction}`);
|
|
2958
|
-
});
|
|
3220
|
+
}, dependency.minimumAction);
|
|
2959
3221
|
}
|
|
2960
3222
|
}
|
|
2961
3223
|
async function appendCompanionLiveChecks(checks, input) {
|
|
2962
|
-
void input.sleeper;
|
|
2963
3224
|
if (input.selected.some((skill) => skill.id === "tiangong.academic-paper-download")) {
|
|
2964
|
-
await
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
}
|
|
2982
|
-
await response.body?.cancel().catch(() => undefined);
|
|
2983
|
-
return apiKey
|
|
2984
|
-
? "Semantic Scholar accepted the configured optional API key."
|
|
2985
|
-
: "Semantic Scholar public API is reachable without an optional API key.";
|
|
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"],
|
|
2986
3242
|
});
|
|
2987
3243
|
}
|
|
2988
3244
|
if (input.selected.some((skill) => skill.id === "tiangong.document-granular-decompose")) {
|
|
@@ -2990,9 +3246,13 @@ async function appendCompanionLiveChecks(checks, input) {
|
|
|
2990
3246
|
checks.push({
|
|
2991
3247
|
id: "live.tiangong-unstructure",
|
|
2992
3248
|
category: "live-check",
|
|
3249
|
+
scope: "preprocessing",
|
|
3250
|
+
componentIds: ["tiangong.document-granular-decompose"],
|
|
2993
3251
|
status: "warn",
|
|
2994
3252
|
detail: "Synthetic document upload was not explicitly authorized, so no document was sent.",
|
|
2995
3253
|
minimumAction: "Rerun setup doctor with the separate synthetic-upload confirmation after reviewing service cost and data policy.",
|
|
3254
|
+
blocking: false,
|
|
3255
|
+
componentGate: true,
|
|
2996
3256
|
});
|
|
2997
3257
|
}
|
|
2998
3258
|
else {
|
|
@@ -3029,6 +3289,132 @@ async function appendCompanionLiveChecks(checks, input) {
|
|
|
3029
3289
|
}
|
|
3030
3290
|
}
|
|
3031
3291
|
}
|
|
3292
|
+
async function appendSemanticScholarLiveCheck(checks, input) {
|
|
3293
|
+
const apiKey = input.adapterCredentials.get("semantic-scholar.api-key");
|
|
3294
|
+
const headers = new Headers({ Accept: "application/json" });
|
|
3295
|
+
if (apiKey)
|
|
3296
|
+
headers.set("x-api-key", apiKey);
|
|
3297
|
+
const url = "https://api.semanticscholar.org/graph/v1/paper/search?query=reproducible%20research&limit=1&fields=paperId";
|
|
3298
|
+
try {
|
|
3299
|
+
let response;
|
|
3300
|
+
let retryAfterSeconds = null;
|
|
3301
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
3302
|
+
response = await input.fetcher(url, {
|
|
3303
|
+
method: "GET",
|
|
3304
|
+
headers,
|
|
3305
|
+
redirect: "manual",
|
|
3306
|
+
signal: AbortSignal.timeout(30_000),
|
|
3307
|
+
});
|
|
3308
|
+
if (response.status !== 429 || attempt === 1)
|
|
3309
|
+
break;
|
|
3310
|
+
retryAfterSeconds = setupRetryAfterSeconds(response.headers.get("retry-after"));
|
|
3311
|
+
await response.body?.cancel().catch(() => undefined);
|
|
3312
|
+
await input.sleeper(Math.min(5, retryAfterSeconds ?? 1) * 1_000);
|
|
3313
|
+
}
|
|
3314
|
+
retryAfterSeconds ??= setupRetryAfterSeconds(response.headers.get("retry-after"));
|
|
3315
|
+
if (response.status >= 300 && response.status < 400) {
|
|
3316
|
+
await response.body?.cancel().catch(() => undefined);
|
|
3317
|
+
checks.push({
|
|
3318
|
+
id: "live.semantic-scholar",
|
|
3319
|
+
category: "live-check",
|
|
3320
|
+
scope: "acquisition",
|
|
3321
|
+
componentIds: ["tiangong.academic-paper-download"],
|
|
3322
|
+
status: "fail",
|
|
3323
|
+
detail: "Semantic Scholar returned a redirect; credential-bearing redirects are not followed.",
|
|
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,
|
|
3327
|
+
diagnostics: {
|
|
3328
|
+
code: "PROVIDER_REDIRECT_REJECTED",
|
|
3329
|
+
executionMode: "setup-doctor",
|
|
3330
|
+
credentialScope: "adapter",
|
|
3331
|
+
networkAttempted: true,
|
|
3332
|
+
httpStatus: response.status,
|
|
3333
|
+
},
|
|
3334
|
+
});
|
|
3335
|
+
return;
|
|
3336
|
+
}
|
|
3337
|
+
if (!response.ok) {
|
|
3338
|
+
const authenticationFailure = response.status === 401 || response.status === 403;
|
|
3339
|
+
const rateLimited = response.status === 429;
|
|
3340
|
+
const detail = authenticationFailure
|
|
3341
|
+
? ""
|
|
3342
|
+
: await boundedResponseText(response, 2_000, apiKey ? [apiKey] : []);
|
|
3343
|
+
checks.push({
|
|
3344
|
+
id: "live.semantic-scholar",
|
|
3345
|
+
category: "live-check",
|
|
3346
|
+
scope: "acquisition",
|
|
3347
|
+
componentIds: ["tiangong.academic-paper-download"],
|
|
3348
|
+
status: "fail",
|
|
3349
|
+
detail: sanitizeResearchText(`Semantic Scholar live check returned HTTP ${response.status}${detail ? `: ${detail}` : ""}.`, apiKey ? [apiKey] : []),
|
|
3350
|
+
minimumAction: rateLimited
|
|
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.`
|
|
3352
|
+
: authenticationFailure
|
|
3353
|
+
? "Replace or remove the optional Semantic Scholar API key, verify provider entitlement, and rerun setup doctor; do not expose the key in output."
|
|
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,
|
|
3357
|
+
diagnostics: {
|
|
3358
|
+
code: authenticationFailure
|
|
3359
|
+
? "PROVIDER_AUTHENTICATION_FAILED"
|
|
3360
|
+
: rateLimited
|
|
3361
|
+
? "PROVIDER_RATE_LIMITED"
|
|
3362
|
+
: "PROVIDER_REQUEST_FAILED",
|
|
3363
|
+
executionMode: "setup-doctor",
|
|
3364
|
+
credentialScope: "adapter",
|
|
3365
|
+
networkAttempted: true,
|
|
3366
|
+
httpStatus: response.status,
|
|
3367
|
+
retryAfterSeconds,
|
|
3368
|
+
},
|
|
3369
|
+
});
|
|
3370
|
+
return;
|
|
3371
|
+
}
|
|
3372
|
+
await response.body?.cancel().catch(() => undefined);
|
|
3373
|
+
checks.push({
|
|
3374
|
+
id: "live.semantic-scholar",
|
|
3375
|
+
category: "live-check",
|
|
3376
|
+
scope: "acquisition",
|
|
3377
|
+
componentIds: ["tiangong.academic-paper-download"],
|
|
3378
|
+
status: "pass",
|
|
3379
|
+
detail: apiKey
|
|
3380
|
+
? "Semantic Scholar accepted the configured optional API key."
|
|
3381
|
+
: "Semantic Scholar public API is reachable without an optional API key.",
|
|
3382
|
+
minimumAction: null,
|
|
3383
|
+
blocking: false,
|
|
3384
|
+
componentGate: false,
|
|
3385
|
+
});
|
|
3386
|
+
}
|
|
3387
|
+
catch (error) {
|
|
3388
|
+
checks.push({
|
|
3389
|
+
id: "live.semantic-scholar",
|
|
3390
|
+
category: "live-check",
|
|
3391
|
+
scope: "acquisition",
|
|
3392
|
+
componentIds: ["tiangong.academic-paper-download"],
|
|
3393
|
+
status: "fail",
|
|
3394
|
+
detail: sanitizeResearchText(error instanceof Error ? error.message : String(error), apiKey ? [apiKey] : []),
|
|
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,
|
|
3398
|
+
diagnostics: {
|
|
3399
|
+
code: "PROVIDER_TRANSPORT_FAILED",
|
|
3400
|
+
executionMode: "setup-doctor",
|
|
3401
|
+
credentialScope: "adapter",
|
|
3402
|
+
networkAttempted: true,
|
|
3403
|
+
},
|
|
3404
|
+
});
|
|
3405
|
+
}
|
|
3406
|
+
}
|
|
3407
|
+
function setupRetryAfterSeconds(value) {
|
|
3408
|
+
if (!value)
|
|
3409
|
+
return null;
|
|
3410
|
+
const seconds = Number(value);
|
|
3411
|
+
if (Number.isFinite(seconds) && seconds >= 0)
|
|
3412
|
+
return Math.ceil(seconds);
|
|
3413
|
+
const timestamp = Date.parse(value);
|
|
3414
|
+
if (!Number.isFinite(timestamp))
|
|
3415
|
+
return null;
|
|
3416
|
+
return Math.max(0, Math.ceil((timestamp - Date.now()) / 1_000));
|
|
3417
|
+
}
|
|
3032
3418
|
async function boundedResponseText(response, maximumBytes, secrets) {
|
|
3033
3419
|
if (!response.body)
|
|
3034
3420
|
return "";
|