@tiangong-ai/cli 0.0.31 → 0.0.33
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 +140 -98
- package/dist/research/orchestration.js +519 -21
- package/dist/research/orchestration.js.map +1 -1
- package/dist/research/workspace/acquisition.d.ts +71 -0
- package/dist/research/workspace/acquisition.js +593 -0
- package/dist/research/workspace/acquisition.js.map +1 -0
- package/dist/research/workspace/artifacts.d.ts +43 -0
- package/dist/research/workspace/artifacts.js +544 -0
- package/dist/research/workspace/artifacts.js.map +1 -0
- package/dist/research/workspace/broker.d.ts +5 -0
- package/dist/research/workspace/broker.js +318 -43
- 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/discovery-planning.d.ts +25 -0
- package/dist/research/workspace/discovery-planning.js +106 -0
- package/dist/research/workspace/discovery-planning.js.map +1 -0
- package/dist/research/workspace/discovery-status.d.ts +46 -0
- package/dist/research/workspace/discovery-status.js +183 -0
- package/dist/research/workspace/discovery-status.js.map +1 -0
- package/dist/research/workspace/discovery.d.ts +25 -0
- package/dist/research/workspace/discovery.js +268 -0
- package/dist/research/workspace/discovery.js.map +1 -0
- package/dist/research/workspace/downloads.d.ts +76 -0
- package/dist/research/workspace/downloads.js +274 -0
- package/dist/research/workspace/downloads.js.map +1 -0
- package/dist/research/workspace/evidence-ledger.d.ts +52 -0
- package/dist/research/workspace/evidence-ledger.js +487 -0
- package/dist/research/workspace/evidence-ledger.js.map +1 -0
- package/dist/research/workspace/evidence.d.ts +1 -0
- package/dist/research/workspace/evidence.js +2 -0
- package/dist/research/workspace/evidence.js.map +1 -1
- package/dist/research/workspace/input-plan.js +4 -0
- package/dist/research/workspace/input-plan.js.map +1 -1
- package/dist/research/workspace/native-activity.d.ts +65 -0
- package/dist/research/workspace/native-activity.js +153 -0
- package/dist/research/workspace/native-activity.js.map +1 -0
- package/dist/research/workspace/preflight.d.ts +18 -0
- package/dist/research/workspace/preflight.js +47 -20
- package/dist/research/workspace/preflight.js.map +1 -1
- package/dist/research/workspace/projects.d.ts +3 -1
- package/dist/research/workspace/projects.js +357 -5
- package/dist/research/workspace/projects.js.map +1 -1
- package/dist/research/workspace/runtime.d.ts +171 -3
- package/dist/research/workspace/runtime.js +1582 -77
- package/dist/research/workspace/runtime.js.map +1 -1
- package/dist/research/workspace/sanitization.js +28 -6
- package/dist/research/workspace/sanitization.js.map +1 -1
- package/dist/research/workspace/schemas.d.ts +3 -0
- package/dist/research/workspace/schemas.js +206 -33
- package/dist/research/workspace/schemas.js.map +1 -1
- package/dist/research/workspace/setup-catalog.js +2 -2
- package/dist/research/workspace/setup-wizard.js +45 -17
- package/dist/research/workspace/setup-wizard.js.map +1 -1
- package/dist/research/workspace/setup.d.ts +27 -5
- package/dist/research/workspace/setup.js +400 -85
- package/dist/research/workspace/setup.js.map +1 -1
- package/dist/research/workspace/types.d.ts +38 -3
- package/dist/research/workspace/workspace.d.ts +2 -0
- package/dist/research/workspace/workspace.js +128 -26
- package/dist/research/workspace/workspace.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
-
import { chmod, link, lstat, open, readFile, rm } from "node:fs/promises";
|
|
3
|
+
import { chmod, link, lstat, open, readFile, realpath, rm } from "node:fs/promises";
|
|
4
4
|
import { hostname, homedir, platform } from "node:os";
|
|
5
5
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
6
6
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
@@ -30,10 +30,10 @@ const BRAVE_PROFILE_SKILLS = {
|
|
|
30
30
|
const ADAPTER_ENV_KEY = "TIANGONG_RESEARCH_ADAPTER_CREDENTIALS_JSON";
|
|
31
31
|
const MAX_COMMAND_OUTPUT_BYTES = 1024 * 1024;
|
|
32
32
|
export async function createResearchSetupPlan(input) {
|
|
33
|
-
const root =
|
|
34
|
-
await assertWorkspaceDirectory(root);
|
|
33
|
+
const root = await resolveResearchSetupWorkspacePath(input.workspace);
|
|
35
34
|
const scope = input.scope ?? "project";
|
|
36
35
|
const agents = normalizeAgents(input.agents ?? ["codex"]);
|
|
36
|
+
const agentRoutes = normalizeAgentRoutes(input.agentRoutes);
|
|
37
37
|
if (!input.confirmNetworkDownloads &&
|
|
38
38
|
input.skillIds.length + BRAVE_PROFILE_SKILLS[input.evidenceProfile].length) {
|
|
39
39
|
throw setupError({
|
|
@@ -88,6 +88,18 @@ export async function createResearchSetupPlan(input) {
|
|
|
88
88
|
const selected = resolveSetupSkills([
|
|
89
89
|
...new Set([...BRAVE_PROFILE_SKILLS[input.evidenceProfile], ...input.skillIds]),
|
|
90
90
|
]);
|
|
91
|
+
const producerInstallTarget = agentRoutes.producerAgent === "codex" ? "codex" : "claude-code";
|
|
92
|
+
if (selected.some((skill) => skill.role === "orchestrator") &&
|
|
93
|
+
!agents.includes(producerInstallTarget)) {
|
|
94
|
+
throw setupError({
|
|
95
|
+
code: "RESEARCH_SETUP_SELECTION_INVALID",
|
|
96
|
+
step: "selection",
|
|
97
|
+
reason: `The native ${agentRoutes.producerAgent} producer requires the orchestrator Skill in its project Skill root.`,
|
|
98
|
+
minimumAction: `Include ${producerInstallTarget} in --agents, or choose the other native producer.`,
|
|
99
|
+
retryCommand: "tiangong-ai research setup plan --help",
|
|
100
|
+
exitCode: 2,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
91
103
|
if (selected.some((skill) => skill.role === "evidence-capability") && !agents.includes("codex")) {
|
|
92
104
|
throw setupError({
|
|
93
105
|
code: "RESEARCH_SETUP_SELECTION_INVALID",
|
|
@@ -102,7 +114,6 @@ export async function createResearchSetupPlan(input) {
|
|
|
102
114
|
validateLicenseAcceptances(selected, input.acceptedLicenseIds);
|
|
103
115
|
const settings = normalizedSettings(selected, input.settings ?? {});
|
|
104
116
|
const credentialSources = normalizedCredentialSources(selected, input.credentialEnvironment ?? {});
|
|
105
|
-
const agentRoutes = normalizeAgentRoutes(input.agentRoutes);
|
|
106
117
|
const targets = plannedInstallTargets(root, scope, agents, input.environment ?? process.env, input.targetRoots);
|
|
107
118
|
const selectedSources = [...new Set(selected.map((skill) => skill.sourceId))]
|
|
108
119
|
.map(setupSource)
|
|
@@ -433,11 +444,11 @@ export async function applyResearchSetupPlan(planPath, options = {}) {
|
|
|
433
444
|
});
|
|
434
445
|
state = await updateSetupState(root, {
|
|
435
446
|
...state,
|
|
436
|
-
status: report.
|
|
437
|
-
? "
|
|
438
|
-
: report.
|
|
447
|
+
status: report.researchReadiness === "BLOCKED"
|
|
448
|
+
? "blocked"
|
|
449
|
+
: report.overallReadiness === "PARTIALLY_READY"
|
|
439
450
|
? "partially-ready"
|
|
440
|
-
: "
|
|
451
|
+
: "ready",
|
|
441
452
|
currentStep: null,
|
|
442
453
|
completedSteps: [...new Set([...state.completedSteps, "doctor"])],
|
|
443
454
|
});
|
|
@@ -691,41 +702,85 @@ export async function doctorResearchSetup(workspace, options = {}) {
|
|
|
691
702
|
return `${command} is executable (${sanitizeResearchText(result.stdout.trim()).slice(0, 120)}).`;
|
|
692
703
|
});
|
|
693
704
|
}
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
705
|
+
if (platform() === "win32") {
|
|
706
|
+
const production = plan.workspace.mode === "production-research";
|
|
707
|
+
checks.push({
|
|
708
|
+
id: "platform-sandbox",
|
|
709
|
+
category: "runtime",
|
|
710
|
+
scope: "research-core",
|
|
711
|
+
status: production ? "fail" : "warn",
|
|
712
|
+
detail: production
|
|
713
|
+
? "Production research execution is unsupported on Windows because no approved capsule sandbox is available."
|
|
714
|
+
: "Windows can inspect and smoke-test setup state, but native research execution requires an approved macOS or Linux capsule sandbox.",
|
|
715
|
+
minimumAction: production
|
|
716
|
+
? "Run production research on macOS with sandbox-exec or Linux with Bubblewrap."
|
|
717
|
+
: "Use macOS or Linux before switching this workspace to production-research mode.",
|
|
718
|
+
blocking: production,
|
|
719
|
+
requiredFor: production ? ["setup", "research-core"] : [],
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
else {
|
|
723
|
+
await setupDoctorCheck(checks, "platform-sandbox", "runtime", async () => {
|
|
724
|
+
if (platform() === "darwin") {
|
|
725
|
+
const info = await lstat("/usr/bin/sandbox-exec").catch(() => undefined);
|
|
726
|
+
if (!info?.isFile())
|
|
727
|
+
throw new Error("/usr/bin/sandbox-exec is unavailable");
|
|
728
|
+
return "macOS sandbox-exec is available.";
|
|
729
|
+
}
|
|
730
|
+
if (platform() === "linux") {
|
|
731
|
+
const result = await runner({
|
|
732
|
+
command: "bwrap",
|
|
733
|
+
args: ["--version"],
|
|
734
|
+
cwd: root,
|
|
735
|
+
environment: installerEnvironment(environment),
|
|
736
|
+
timeoutMs: 15_000,
|
|
737
|
+
});
|
|
738
|
+
if (result.exitCode !== 0)
|
|
739
|
+
throw new Error("Bubblewrap is unavailable");
|
|
740
|
+
return "Linux Bubblewrap is available.";
|
|
741
|
+
}
|
|
742
|
+
throw new Error("Research execution is unsupported on this platform");
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
checks.push({
|
|
746
|
+
id: "agent.native-producer",
|
|
747
|
+
category: "agent",
|
|
748
|
+
scope: "research-core",
|
|
749
|
+
status: "pass",
|
|
750
|
+
detail: `Producer work is bound to the current interactive ${plan.agentRoutes.producerAgent} host; setup will not launch it as a child process.`,
|
|
751
|
+
minimumAction: null,
|
|
752
|
+
blocking: true,
|
|
753
|
+
requiredFor: ["setup", "research-core"],
|
|
754
|
+
});
|
|
755
|
+
const reviewerCommand = plan.agentRoutes.reviewerAgent;
|
|
756
|
+
await setupDoctorCheck(checks, `agent.${reviewerCommand}.reviewer`, "agent", async () => {
|
|
757
|
+
const result = await runner({
|
|
758
|
+
command: reviewerCommand,
|
|
759
|
+
args: ["--version"],
|
|
760
|
+
cwd: root,
|
|
761
|
+
environment: agentDoctorEnvironment(environment),
|
|
762
|
+
timeoutMs: 30_000,
|
|
763
|
+
});
|
|
764
|
+
if (result.exitCode !== 0)
|
|
765
|
+
throw new Error(`${reviewerCommand} is not executable`);
|
|
766
|
+
return `${reviewerCommand} reviewer CLI is executable (${sanitizeResearchText(result.stdout.trim()).slice(0, 160)}).`;
|
|
767
|
+
});
|
|
768
|
+
await setupDoctorCheck(checks, "agent-route-config", "agent", async () => {
|
|
769
|
+
const config = await loadWorkspaceConfig(root);
|
|
770
|
+
if (config.producer.executionMode !== "native-host" ||
|
|
771
|
+
config.reviewer.executionMode !== "headless-cli" ||
|
|
772
|
+
config.producer.agent === config.reviewer.agent) {
|
|
773
|
+
throw new Error("Workspace must bind producer=native-host and reviewer=headless-cli on different agent families.");
|
|
700
774
|
}
|
|
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.";
|
|
775
|
+
if (config.mode === "production-research" &&
|
|
776
|
+
(!config.producer.model ||
|
|
777
|
+
!config.reviewer.model ||
|
|
778
|
+
!config.producer.pricing ||
|
|
779
|
+
!config.reviewer.pricing)) {
|
|
780
|
+
throw new Error("Production agent models and reviewed pricing are incomplete.");
|
|
712
781
|
}
|
|
713
|
-
|
|
782
|
+
return `${config.producer.agent}(native-host) -> ${config.reviewer.agent}(headless-cli).`;
|
|
714
783
|
});
|
|
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
784
|
const selected = plan.selection.skillIds.map(setupSkill);
|
|
730
785
|
const installations = await inspectSelectedInstallations(plan, selected, environment);
|
|
731
786
|
for (const installation of installations) {
|
|
@@ -802,10 +857,15 @@ export async function doctorResearchSetup(workspace, options = {}) {
|
|
|
802
857
|
}
|
|
803
858
|
await appendDependencyChecks(checks, selected, runner, root, environment);
|
|
804
859
|
let capabilityDoctor = null;
|
|
860
|
+
const blockingBeforeLive = checks
|
|
861
|
+
.map((check) => normalizeSetupDoctorCheck(check, selected))
|
|
862
|
+
.filter((check) => check.blocking && check.status === "fail")
|
|
863
|
+
.map((check) => check.id);
|
|
864
|
+
const runRequiredLive = options.live === true && blockingBeforeLive.length === 0;
|
|
805
865
|
if (selected.some((skill) => skill.role === "evidence-capability")) {
|
|
806
866
|
try {
|
|
807
867
|
capabilityDoctor = await doctorExternalCapabilities(root, {
|
|
808
|
-
live:
|
|
868
|
+
live: runRequiredLive,
|
|
809
869
|
fetcher,
|
|
810
870
|
sleeper,
|
|
811
871
|
});
|
|
@@ -829,7 +889,20 @@ export async function doctorResearchSetup(workspace, options = {}) {
|
|
|
829
889
|
});
|
|
830
890
|
}
|
|
831
891
|
}
|
|
832
|
-
if (options.live) {
|
|
892
|
+
if (options.live === true && !runRequiredLive) {
|
|
893
|
+
checks.push({
|
|
894
|
+
id: "live.required-capabilities.skipped",
|
|
895
|
+
category: "live-check",
|
|
896
|
+
scope: "evidence",
|
|
897
|
+
status: "fail",
|
|
898
|
+
detail: "Required live capability probes were not started because a static blocking prerequisite failed.",
|
|
899
|
+
minimumAction: "Resolve the static blocking prerequisites, then rerun live doctor checks.",
|
|
900
|
+
blocking: true,
|
|
901
|
+
requiredFor: ["setup", "research-core"],
|
|
902
|
+
skippedBecause: blockingBeforeLive.join(", "),
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
if (runRequiredLive) {
|
|
833
906
|
await appendCompanionLiveChecks(checks, {
|
|
834
907
|
plan,
|
|
835
908
|
selected,
|
|
@@ -839,13 +912,45 @@ export async function doctorResearchSetup(workspace, options = {}) {
|
|
|
839
912
|
allowSyntheticUnstructureUpload: options.allowSyntheticUnstructureUpload === true,
|
|
840
913
|
});
|
|
841
914
|
}
|
|
915
|
+
else if (options.live === true &&
|
|
916
|
+
selected.some((skill) => ["input-preprocessor", "acquisition-adapter", "post-closure-authoring"].includes(skill.role))) {
|
|
917
|
+
checks.push({
|
|
918
|
+
id: "live.optional-components.skipped",
|
|
919
|
+
category: "live-check",
|
|
920
|
+
status: "warn",
|
|
921
|
+
detail: "Optional component diagnostics were skipped after a static blocking prerequisite failed.",
|
|
922
|
+
minimumAction: "Resolve the blocking prerequisites before retrying optional diagnostics.",
|
|
923
|
+
blocking: false,
|
|
924
|
+
requiredFor: [],
|
|
925
|
+
skippedBecause: blockingBeforeLive.join(", "),
|
|
926
|
+
});
|
|
927
|
+
}
|
|
842
928
|
let workspaceDoctor = null;
|
|
929
|
+
const blockingPrerequisiteFailures = checks
|
|
930
|
+
.map((check) => normalizeSetupDoctorCheck(check, selected))
|
|
931
|
+
.filter((check) => check.blocking && check.status === "fail")
|
|
932
|
+
.map((check) => check.id);
|
|
933
|
+
const runAgentSmoke = options.agentSmoke === true && blockingPrerequisiteFailures.length === 0;
|
|
934
|
+
if (options.agentSmoke === true && !runAgentSmoke) {
|
|
935
|
+
checks.push({
|
|
936
|
+
id: "agent-reviewer-smoke.skipped",
|
|
937
|
+
category: "agent",
|
|
938
|
+
scope: "review",
|
|
939
|
+
status: "fail",
|
|
940
|
+
detail: "The paid reviewer smoke was not started because a zero/low-cost blocking prerequisite failed.",
|
|
941
|
+
minimumAction: "Resolve the listed blocking prerequisites, then rerun the explicitly confirmed reviewer smoke.",
|
|
942
|
+
blocking: true,
|
|
943
|
+
requiredFor: ["setup", "research-core"],
|
|
944
|
+
skippedBecause: blockingPrerequisiteFailures.join(", "),
|
|
945
|
+
});
|
|
946
|
+
}
|
|
843
947
|
try {
|
|
844
948
|
workspaceDoctor = await doctorResearchWorkspace(root, {
|
|
845
|
-
agentSmoke:
|
|
846
|
-
capabilitySmoke:
|
|
949
|
+
agentSmoke: runAgentSmoke,
|
|
950
|
+
capabilitySmoke: runRequiredLive,
|
|
847
951
|
environment,
|
|
848
952
|
capabilityFetcher: fetcher,
|
|
953
|
+
...(capabilityDoctor === null ? {} : { capabilityDoctorResult: capabilityDoctor }),
|
|
849
954
|
...(options.executor === undefined ? {} : { executor: options.executor }),
|
|
850
955
|
});
|
|
851
956
|
const requiredRuntimeChecks = options.agentSmoke === true || options.live === true;
|
|
@@ -888,9 +993,16 @@ export async function doctorResearchSetup(workspace, options = {}) {
|
|
|
888
993
|
: `Run tiangong-ai research setup doctor --live --workspace ${root} --json after reviewing quota impact.`,
|
|
889
994
|
});
|
|
890
995
|
}
|
|
891
|
-
const
|
|
996
|
+
const scopedChecks = checks.map((check) => normalizeSetupDoctorCheck(check, selected));
|
|
997
|
+
const researchReadiness = scopedChecks.some((check) => check.blocking && check.status === "fail")
|
|
892
998
|
? "BLOCKED"
|
|
893
|
-
:
|
|
999
|
+
: "READY";
|
|
1000
|
+
const preprocessingReadiness = setupDomainReadiness(scopedChecks, "preprocessing");
|
|
1001
|
+
const acquisitionReadiness = setupDomainReadiness(scopedChecks, "acquisition");
|
|
1002
|
+
const authoringReadiness = setupDomainReadiness(scopedChecks, "authoring");
|
|
1003
|
+
const overallReadiness = researchReadiness === "BLOCKED"
|
|
1004
|
+
? "BLOCKED"
|
|
1005
|
+
: scopedChecks.some((check) => check.status !== "pass")
|
|
894
1006
|
? "PARTIALLY_READY"
|
|
895
1007
|
: "READY";
|
|
896
1008
|
const setupSecrets = [
|
|
@@ -908,14 +1020,19 @@ export async function doctorResearchSetup(workspace, options = {}) {
|
|
|
908
1020
|
planSha256: plan.planSha256,
|
|
909
1021
|
checkedAt: new Date().toISOString(),
|
|
910
1022
|
mode: options.live ? "live" : "static",
|
|
911
|
-
readiness,
|
|
912
|
-
|
|
1023
|
+
readiness: researchReadiness,
|
|
1024
|
+
researchReadiness,
|
|
1025
|
+
preprocessingReadiness,
|
|
1026
|
+
acquisitionReadiness,
|
|
1027
|
+
authoringReadiness,
|
|
1028
|
+
overallReadiness,
|
|
1029
|
+
checks: scopedChecks,
|
|
913
1030
|
capabilityDoctor,
|
|
914
1031
|
workspaceDoctor,
|
|
915
1032
|
summary: {
|
|
916
|
-
pass:
|
|
917
|
-
warn:
|
|
918
|
-
fail:
|
|
1033
|
+
pass: scopedChecks.filter((check) => check.status === "pass").length,
|
|
1034
|
+
warn: scopedChecks.filter((check) => check.status === "warn").length,
|
|
1035
|
+
fail: scopedChecks.filter((check) => check.status === "fail").length,
|
|
919
1036
|
},
|
|
920
1037
|
}, setupSecrets);
|
|
921
1038
|
await writeJsonAtomic(paths.setupReport, report);
|
|
@@ -1206,10 +1323,20 @@ function assertPlanMatchesCatalog(plan) {
|
|
|
1206
1323
|
}
|
|
1207
1324
|
}
|
|
1208
1325
|
function validAgentRoutes(value) {
|
|
1209
|
-
const allowed = new Set([
|
|
1326
|
+
const allowed = new Set([
|
|
1327
|
+
"producerAgent",
|
|
1328
|
+
"reviewerAgent",
|
|
1329
|
+
"producerModel",
|
|
1330
|
+
"reviewerModel",
|
|
1331
|
+
"producerPricing",
|
|
1332
|
+
"reviewerPricing",
|
|
1333
|
+
]);
|
|
1210
1334
|
if (Object.keys(value).some((key) => !allowed.has(key)))
|
|
1211
1335
|
return false;
|
|
1212
|
-
if (
|
|
1336
|
+
if ((value.producerAgent !== "codex" && value.producerAgent !== "claude") ||
|
|
1337
|
+
(value.reviewerAgent !== "codex" && value.reviewerAgent !== "claude") ||
|
|
1338
|
+
value.producerAgent === value.reviewerAgent ||
|
|
1339
|
+
!(value.producerModel === null || typeof value.producerModel === "string") ||
|
|
1213
1340
|
!(value.reviewerModel === null || typeof value.reviewerModel === "string")) {
|
|
1214
1341
|
return false;
|
|
1215
1342
|
}
|
|
@@ -1318,13 +1445,39 @@ function normalizedCredentialSources(selected, supplied) {
|
|
|
1318
1445
|
.sort((left, right) => left.id.localeCompare(right.id));
|
|
1319
1446
|
}
|
|
1320
1447
|
function normalizeAgentRoutes(value) {
|
|
1448
|
+
const producerAgent = normalizeAgentKind(value?.producerAgent ?? "codex", "producer");
|
|
1449
|
+
const reviewerAgent = normalizeAgentKind(value?.reviewerAgent ?? (producerAgent === "codex" ? "claude" : "codex"), "reviewer");
|
|
1450
|
+
if (producerAgent === reviewerAgent) {
|
|
1451
|
+
throw setupError({
|
|
1452
|
+
code: "RESEARCH_SETUP_AGENT_ROUTE_INVALID",
|
|
1453
|
+
step: "agent-route",
|
|
1454
|
+
reason: "The native producer and independent reviewer must use different agent families.",
|
|
1455
|
+
minimumAction: "Choose Codex + Claude Code in either producer/reviewer order.",
|
|
1456
|
+
retryCommand: "tiangong-ai research setup plan --help",
|
|
1457
|
+
exitCode: 2,
|
|
1458
|
+
});
|
|
1459
|
+
}
|
|
1321
1460
|
return {
|
|
1461
|
+
producerAgent,
|
|
1462
|
+
reviewerAgent,
|
|
1322
1463
|
producerModel: normalizeNullableIdentifier(value?.producerModel),
|
|
1323
1464
|
reviewerModel: normalizeNullableIdentifier(value?.reviewerModel),
|
|
1324
1465
|
producerPricing: normalizePricing(value?.producerPricing),
|
|
1325
1466
|
reviewerPricing: normalizePricing(value?.reviewerPricing),
|
|
1326
1467
|
};
|
|
1327
1468
|
}
|
|
1469
|
+
function normalizeAgentKind(value, label) {
|
|
1470
|
+
if (value === "codex" || value === "claude")
|
|
1471
|
+
return value;
|
|
1472
|
+
throw setupError({
|
|
1473
|
+
code: "RESEARCH_SETUP_AGENT_ROUTE_INVALID",
|
|
1474
|
+
step: "agent-route",
|
|
1475
|
+
reason: `${label} agent must be codex or claude.`,
|
|
1476
|
+
minimumAction: "Choose Codex or Claude Code and keep the reviewer on the other family.",
|
|
1477
|
+
retryCommand: "tiangong-ai research setup plan --help",
|
|
1478
|
+
exitCode: 2,
|
|
1479
|
+
});
|
|
1480
|
+
}
|
|
1328
1481
|
function normalizePricing(value) {
|
|
1329
1482
|
if (value === undefined || value === null)
|
|
1330
1483
|
return null;
|
|
@@ -1474,33 +1627,29 @@ async function ensureSetupWorkspace(plan) {
|
|
|
1474
1627
|
await initializeResearchWorkspace(plan.workspace.path, plan.workspace.name, plan.workspace.mode);
|
|
1475
1628
|
}
|
|
1476
1629
|
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
1630
|
const paths = workspacePaths(plan.workspace.path);
|
|
1484
1631
|
const config = await loadWorkspaceConfig(plan.workspace.path);
|
|
1485
1632
|
const updated = {
|
|
1486
1633
|
...config,
|
|
1487
|
-
producer:
|
|
1488
|
-
|
|
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
|
-
},
|
|
1634
|
+
producer: setupAgentRoute(config.producer, plan.agentRoutes.producerAgent, "native-host", plan.agentRoutes.producerModel, plan.agentRoutes.producerPricing),
|
|
1635
|
+
reviewer: setupAgentRoute(config.reviewer, plan.agentRoutes.reviewerAgent, "headless-cli", plan.agentRoutes.reviewerModel, plan.agentRoutes.reviewerPricing),
|
|
1501
1636
|
};
|
|
1502
1637
|
await writeJsonAtomic(paths.config, updated);
|
|
1503
1638
|
}
|
|
1639
|
+
function setupAgentRoute(current, agent, executionMode, plannedModel, plannedPricing) {
|
|
1640
|
+
const sameAgent = current.agent === agent;
|
|
1641
|
+
return {
|
|
1642
|
+
agent,
|
|
1643
|
+
executionMode,
|
|
1644
|
+
binary: agent === "codex" ? "codex" : "claude",
|
|
1645
|
+
model: plannedModel ?? (sameAgent ? current.model : null),
|
|
1646
|
+
effort: sameAgent ? (current.effort ?? "low") : "low",
|
|
1647
|
+
...(agent === "codex" ? { verbosity: sameAgent ? (current.verbosity ?? "low") : "low" } : {}),
|
|
1648
|
+
...((plannedPricing ?? (sameAgent ? current.pricing : undefined)) === undefined
|
|
1649
|
+
? {}
|
|
1650
|
+
: { pricing: plannedPricing ?? current.pricing }),
|
|
1651
|
+
};
|
|
1652
|
+
}
|
|
1504
1653
|
async function inspectSelectedInstallations(plan, selected, _environment) {
|
|
1505
1654
|
const results = [];
|
|
1506
1655
|
for (const agent of plan.install.agents) {
|
|
@@ -2161,6 +2310,12 @@ async function runAcademicPaperCompanion(input) {
|
|
|
2161
2310
|
}
|
|
2162
2311
|
const result = results[0];
|
|
2163
2312
|
if (execution.exitCode !== 0 || result.success !== true) {
|
|
2313
|
+
const sourcesTried = Array.isArray(result.sources_tried)
|
|
2314
|
+
? result.sources_tried
|
|
2315
|
+
.filter((source) => typeof source === "string")
|
|
2316
|
+
.map((source) => sanitizeResearchText(source).slice(0, 100))
|
|
2317
|
+
: [];
|
|
2318
|
+
const adapterError = sanitizeResearchRecord(isObject(result.error) ? result.error : {});
|
|
2164
2319
|
if (result.success === false &&
|
|
2165
2320
|
isObject(result.browser_handoff) &&
|
|
2166
2321
|
result.file === null &&
|
|
@@ -2170,7 +2325,7 @@ async function runAcademicPaperCompanion(input) {
|
|
|
2170
2325
|
skillId: input.skill.id,
|
|
2171
2326
|
skillTreeSha256: input.skill.expectedTreeSha256,
|
|
2172
2327
|
querySha256: sha256Text(doi ?? title),
|
|
2173
|
-
sourcesTried
|
|
2328
|
+
sourcesTried,
|
|
2174
2329
|
artifactCommitted: false,
|
|
2175
2330
|
});
|
|
2176
2331
|
return {
|
|
@@ -2181,20 +2336,32 @@ async function runAcademicPaperCompanion(input) {
|
|
|
2181
2336
|
skillId: input.skill.id,
|
|
2182
2337
|
role: input.skill.role,
|
|
2183
2338
|
artifactCommitted: false,
|
|
2184
|
-
sourcesTried
|
|
2185
|
-
error:
|
|
2339
|
+
sourcesTried,
|
|
2340
|
+
error: adapterError,
|
|
2186
2341
|
provenance: companionProvenance(input.plan, input.skill),
|
|
2187
2342
|
next: "Automatic legal OA sources were exhausted. Follow the installed academic-paper-download browser-handoff reference explicitly; no browser is launched or selected automatically.",
|
|
2188
2343
|
};
|
|
2189
2344
|
}
|
|
2345
|
+
const adapterCode = typeof adapterError.code === "string"
|
|
2346
|
+
? sanitizeResearchText(adapterError.code).trim().slice(0, 100)
|
|
2347
|
+
: "unknown-adapter-error";
|
|
2348
|
+
const adapterMessage = typeof adapterError.message === "string"
|
|
2349
|
+
? sanitizeResearchText(adapterError.message).trim().slice(0, 500)
|
|
2350
|
+
: "";
|
|
2190
2351
|
throw setupError({
|
|
2191
2352
|
code: "RESEARCH_SETUP_COMPANION_COMMAND_FAILED",
|
|
2192
2353
|
step: "companion-paper-download",
|
|
2193
|
-
reason: `The pinned paper adapter
|
|
2194
|
-
minimumAction:
|
|
2354
|
+
reason: `The pinned paper adapter failed (${adapterCode}; exit status ${execution.exitCode}).`,
|
|
2355
|
+
minimumAction: adapterMessage ||
|
|
2356
|
+
sanitizeResearchText(execution.stderr).trim().slice(0, 500) ||
|
|
2195
2357
|
"Inspect the structured adapter error and verify its pinned Python dependencies.",
|
|
2196
2358
|
retryCommand: `tiangong-ai research setup doctor --workspace ${input.root} --json`,
|
|
2197
2359
|
exitCode: 3,
|
|
2360
|
+
diagnostics: {
|
|
2361
|
+
adapterError,
|
|
2362
|
+
sourcesTried,
|
|
2363
|
+
artifactCommitted: false,
|
|
2364
|
+
},
|
|
2198
2365
|
});
|
|
2199
2366
|
}
|
|
2200
2367
|
const artifactPath = requireContainedArtifactPath(result.file, outputDirectory, "file");
|
|
@@ -2565,6 +2732,84 @@ async function archiveSetupGeneration(root) {
|
|
|
2565
2732
|
}
|
|
2566
2733
|
return prior.planSha256;
|
|
2567
2734
|
}
|
|
2735
|
+
function normalizeSetupDoctorCheck(check, selected) {
|
|
2736
|
+
const componentIds = check.componentIds ?? setupCheckComponentIds(check, selected);
|
|
2737
|
+
const scope = check.scope ?? setupCheckScope(check, selected, componentIds);
|
|
2738
|
+
const blocking = check.blocking ?? ["research-core", "evidence", "review"].includes(scope);
|
|
2739
|
+
return {
|
|
2740
|
+
...check,
|
|
2741
|
+
scope,
|
|
2742
|
+
componentIds,
|
|
2743
|
+
requiredFor: check.requiredFor ??
|
|
2744
|
+
(blocking
|
|
2745
|
+
? ["setup", "research-core"]
|
|
2746
|
+
: componentIds.map((componentId) => `component:${componentId}`)),
|
|
2747
|
+
blocking,
|
|
2748
|
+
componentGate: check.componentGate ?? check.id !== "live.semantic-scholar",
|
|
2749
|
+
};
|
|
2750
|
+
}
|
|
2751
|
+
function setupCheckComponentIds(check, selected) {
|
|
2752
|
+
if (check.id === "live.semantic-scholar")
|
|
2753
|
+
return ["tiangong.academic-paper-download"];
|
|
2754
|
+
if (check.id === "live.tiangong-unstructure") {
|
|
2755
|
+
return ["tiangong.document-granular-decompose"];
|
|
2756
|
+
}
|
|
2757
|
+
if (check.id.startsWith("skill.")) {
|
|
2758
|
+
return selected.filter((skill) => check.id.endsWith(`.${skill.id}`)).map((skill) => skill.id);
|
|
2759
|
+
}
|
|
2760
|
+
if (check.id.startsWith("setting.")) {
|
|
2761
|
+
const id = check.id.slice("setting.".length);
|
|
2762
|
+
const setting = RESEARCH_SETUP_SETTINGS.find((candidate) => candidate.id === id);
|
|
2763
|
+
return selected
|
|
2764
|
+
.filter((skill) => setting?.requiredBy.includes(skill.id))
|
|
2765
|
+
.map((skill) => skill.id);
|
|
2766
|
+
}
|
|
2767
|
+
if (check.id.startsWith("credential.")) {
|
|
2768
|
+
const id = check.id.slice("credential.".length);
|
|
2769
|
+
const credential = RESEARCH_SETUP_CREDENTIALS.find((candidate) => candidate.id === id);
|
|
2770
|
+
return selected
|
|
2771
|
+
.filter((skill) => credential?.requiredBy.includes(skill.id))
|
|
2772
|
+
.map((skill) => skill.id);
|
|
2773
|
+
}
|
|
2774
|
+
if (check.id.startsWith("dependency.")) {
|
|
2775
|
+
const id = check.id.slice("dependency.".length);
|
|
2776
|
+
return selected
|
|
2777
|
+
.filter((skill) => skill.dependencies.some((item) => item.id === id))
|
|
2778
|
+
.map((skill) => skill.id);
|
|
2779
|
+
}
|
|
2780
|
+
return [];
|
|
2781
|
+
}
|
|
2782
|
+
function setupCheckScope(check, selected, componentIds) {
|
|
2783
|
+
if (check.id === "live.semantic-scholar")
|
|
2784
|
+
return "acquisition";
|
|
2785
|
+
if (check.id === "live.tiangong-unstructure")
|
|
2786
|
+
return "preprocessing";
|
|
2787
|
+
if (check.category === "evidence-capability" || check.id.includes("capability")) {
|
|
2788
|
+
return "evidence";
|
|
2789
|
+
}
|
|
2790
|
+
if (check.category === "agent" || check.id.includes("attestation"))
|
|
2791
|
+
return "review";
|
|
2792
|
+
const roles = componentIds
|
|
2793
|
+
.map((id) => selected.find((skill) => skill.id === id)?.role)
|
|
2794
|
+
.filter((role) => Boolean(role));
|
|
2795
|
+
if (roles.includes("evidence-capability"))
|
|
2796
|
+
return "evidence";
|
|
2797
|
+
if (roles.includes("input-preprocessor"))
|
|
2798
|
+
return "preprocessing";
|
|
2799
|
+
if (roles.includes("acquisition-adapter"))
|
|
2800
|
+
return "acquisition";
|
|
2801
|
+
if (roles.includes("post-closure-authoring"))
|
|
2802
|
+
return "authoring";
|
|
2803
|
+
return "research-core";
|
|
2804
|
+
}
|
|
2805
|
+
function setupDomainReadiness(checks, scope) {
|
|
2806
|
+
const matching = checks.filter((check) => check.scope === scope);
|
|
2807
|
+
if (matching.length === 0)
|
|
2808
|
+
return "NOT_REQUIRED";
|
|
2809
|
+
if (matching.some((check) => check.blocking && check.status === "fail"))
|
|
2810
|
+
return "BLOCKED";
|
|
2811
|
+
return matching.some((check) => check.status !== "pass") ? "DEGRADED" : "READY";
|
|
2812
|
+
}
|
|
2568
2813
|
function requireAbsoluteWorkspace(value) {
|
|
2569
2814
|
if (!value || !isAbsolute(value) || /[\0\r\n]/.test(value)) {
|
|
2570
2815
|
throw setupError({
|
|
@@ -2578,9 +2823,25 @@ function requireAbsoluteWorkspace(value) {
|
|
|
2578
2823
|
}
|
|
2579
2824
|
return resolve(value);
|
|
2580
2825
|
}
|
|
2581
|
-
async function
|
|
2826
|
+
export async function resolveResearchSetupWorkspacePath(value, options = {}) {
|
|
2827
|
+
const root = requireAbsoluteWorkspace(value);
|
|
2582
2828
|
const info = await lstat(root).catch(() => undefined);
|
|
2583
|
-
if (
|
|
2829
|
+
if (info) {
|
|
2830
|
+
if (!info.isDirectory() || info.isSymbolicLink()) {
|
|
2831
|
+
throw setupError({
|
|
2832
|
+
code: "RESEARCH_SETUP_WORKSPACE_INVALID",
|
|
2833
|
+
step: "workspace",
|
|
2834
|
+
reason: "Setup workspace must exist as a regular non-symlink directory.",
|
|
2835
|
+
minimumAction: `Create the directory explicitly, then retry with --workspace ${root}.`,
|
|
2836
|
+
retryCommand: "tiangong-ai research setup --help",
|
|
2837
|
+
exitCode: 2,
|
|
2838
|
+
});
|
|
2839
|
+
}
|
|
2840
|
+
const canonicalRoot = await realpath(root);
|
|
2841
|
+
await assertNoSymlinkedExistingPath(canonicalRoot);
|
|
2842
|
+
return canonicalRoot;
|
|
2843
|
+
}
|
|
2844
|
+
if (!options.allowMissingLeaf) {
|
|
2584
2845
|
throw setupError({
|
|
2585
2846
|
code: "RESEARCH_SETUP_WORKSPACE_INVALID",
|
|
2586
2847
|
step: "workspace",
|
|
@@ -2590,7 +2851,24 @@ async function assertWorkspaceDirectory(root) {
|
|
|
2590
2851
|
exitCode: 2,
|
|
2591
2852
|
});
|
|
2592
2853
|
}
|
|
2593
|
-
|
|
2854
|
+
const requestedParent = dirname(root);
|
|
2855
|
+
const canonicalParent = await realpath(requestedParent).catch(() => undefined);
|
|
2856
|
+
const parentInfo = canonicalParent
|
|
2857
|
+
? await lstat(canonicalParent).catch(() => undefined)
|
|
2858
|
+
: undefined;
|
|
2859
|
+
if (!canonicalParent || !parentInfo?.isDirectory() || parentInfo.isSymbolicLink()) {
|
|
2860
|
+
throw setupError({
|
|
2861
|
+
code: "RESEARCH_SETUP_WORKSPACE_INVALID",
|
|
2862
|
+
step: "workspace",
|
|
2863
|
+
reason: "The parent of a new setup workspace must exist as a regular directory.",
|
|
2864
|
+
minimumAction: `Create the parent directory explicitly, then retry with --workspace ${root}.`,
|
|
2865
|
+
retryCommand: "tiangong-ai research setup --help",
|
|
2866
|
+
exitCode: 2,
|
|
2867
|
+
});
|
|
2868
|
+
}
|
|
2869
|
+
const canonicalRoot = join(canonicalParent, basename(root));
|
|
2870
|
+
await assertNoSymlinkedExistingPath(canonicalRoot);
|
|
2871
|
+
return canonicalRoot;
|
|
2594
2872
|
}
|
|
2595
2873
|
function normalizedWorkspaceName(value) {
|
|
2596
2874
|
const normalized = value.trim();
|
|
@@ -2995,15 +3273,36 @@ async function appendDependencyChecks(checks, selected, runner, root, environmen
|
|
|
2995
3273
|
async function appendCompanionLiveChecks(checks, input) {
|
|
2996
3274
|
if (input.selected.some((skill) => skill.id === "tiangong.academic-paper-download")) {
|
|
2997
3275
|
await appendSemanticScholarLiveCheck(checks, input);
|
|
3276
|
+
const semanticScholar = checks.findLast((check) => check.id === "live.semantic-scholar");
|
|
3277
|
+
checks.push({
|
|
3278
|
+
id: "companion.tiangong.academic-paper-download",
|
|
3279
|
+
category: "live-check",
|
|
3280
|
+
scope: "acquisition",
|
|
3281
|
+
componentIds: ["tiangong.academic-paper-download"],
|
|
3282
|
+
status: semanticScholar?.status === "pass" ? "pass" : "warn",
|
|
3283
|
+
detail: semanticScholar?.status === "pass"
|
|
3284
|
+
? "OA resolver diagnostics: Unpaywall=unknown, Semantic Scholar=ready, arXiv=unknown; the deterministic resolver order is unchanged."
|
|
3285
|
+
: "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.",
|
|
3286
|
+
minimumAction: semanticScholar?.status === "pass"
|
|
3287
|
+
? null
|
|
3288
|
+
: "Retry the resolver diagnostic later or configure its optional key; unrelated research remains authorized and no standalone evidence fallback is permitted.",
|
|
3289
|
+
blocking: false,
|
|
3290
|
+
componentGate: false,
|
|
3291
|
+
requiredFor: ["component:tiangong.academic-paper-download"],
|
|
3292
|
+
});
|
|
2998
3293
|
}
|
|
2999
3294
|
if (input.selected.some((skill) => skill.id === "tiangong.document-granular-decompose")) {
|
|
3000
3295
|
if (!input.allowSyntheticUnstructureUpload) {
|
|
3001
3296
|
checks.push({
|
|
3002
3297
|
id: "live.tiangong-unstructure",
|
|
3003
3298
|
category: "live-check",
|
|
3299
|
+
scope: "preprocessing",
|
|
3300
|
+
componentIds: ["tiangong.document-granular-decompose"],
|
|
3004
3301
|
status: "warn",
|
|
3005
3302
|
detail: "Synthetic document upload was not explicitly authorized, so no document was sent.",
|
|
3006
3303
|
minimumAction: "Rerun setup doctor with the separate synthetic-upload confirmation after reviewing service cost and data policy.",
|
|
3304
|
+
blocking: false,
|
|
3305
|
+
componentGate: true,
|
|
3007
3306
|
});
|
|
3008
3307
|
}
|
|
3009
3308
|
else {
|
|
@@ -3068,9 +3367,13 @@ async function appendSemanticScholarLiveCheck(checks, input) {
|
|
|
3068
3367
|
checks.push({
|
|
3069
3368
|
id: "live.semantic-scholar",
|
|
3070
3369
|
category: "live-check",
|
|
3370
|
+
scope: "acquisition",
|
|
3371
|
+
componentIds: ["tiangong.academic-paper-download"],
|
|
3071
3372
|
status: "fail",
|
|
3072
3373
|
detail: "Semantic Scholar returned a redirect; credential-bearing redirects are not followed.",
|
|
3073
|
-
minimumAction: "Verify the fixed Semantic Scholar endpoint and network policy
|
|
3374
|
+
minimumAction: "Verify the fixed Semantic Scholar endpoint and network policy before relying on that resolver; do not fall back to a standalone evidence wrapper.",
|
|
3375
|
+
blocking: false,
|
|
3376
|
+
componentGate: false,
|
|
3074
3377
|
diagnostics: {
|
|
3075
3378
|
code: "PROVIDER_REDIRECT_REJECTED",
|
|
3076
3379
|
executionMode: "setup-doctor",
|
|
@@ -3090,13 +3393,17 @@ async function appendSemanticScholarLiveCheck(checks, input) {
|
|
|
3090
3393
|
checks.push({
|
|
3091
3394
|
id: "live.semantic-scholar",
|
|
3092
3395
|
category: "live-check",
|
|
3396
|
+
scope: "acquisition",
|
|
3397
|
+
componentIds: ["tiangong.academic-paper-download"],
|
|
3093
3398
|
status: "fail",
|
|
3094
3399
|
detail: sanitizeResearchText(`Semantic Scholar live check returned HTTP ${response.status}${detail ? `: ${detail}` : ""}.`, apiKey ? [apiKey] : []),
|
|
3095
3400
|
minimumAction: rateLimited
|
|
3096
|
-
? `Wait for the provider quota window${retryAfterSeconds === null ? "" : ` (Retry-After ${retryAfterSeconds}s)`}, then rerun
|
|
3401
|
+
? `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
3402
|
: authenticationFailure
|
|
3098
3403
|
? "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
|
|
3404
|
+
: "Verify Semantic Scholar availability and the fixed API contract before relying on that resolver; do not downgrade the research workflow.",
|
|
3405
|
+
blocking: false,
|
|
3406
|
+
componentGate: false,
|
|
3100
3407
|
diagnostics: {
|
|
3101
3408
|
code: authenticationFailure
|
|
3102
3409
|
? "PROVIDER_AUTHENTICATION_FAILED"
|
|
@@ -3116,20 +3423,28 @@ async function appendSemanticScholarLiveCheck(checks, input) {
|
|
|
3116
3423
|
checks.push({
|
|
3117
3424
|
id: "live.semantic-scholar",
|
|
3118
3425
|
category: "live-check",
|
|
3426
|
+
scope: "acquisition",
|
|
3427
|
+
componentIds: ["tiangong.academic-paper-download"],
|
|
3119
3428
|
status: "pass",
|
|
3120
3429
|
detail: apiKey
|
|
3121
3430
|
? "Semantic Scholar accepted the configured optional API key."
|
|
3122
3431
|
: "Semantic Scholar public API is reachable without an optional API key.",
|
|
3123
3432
|
minimumAction: null,
|
|
3433
|
+
blocking: false,
|
|
3434
|
+
componentGate: false,
|
|
3124
3435
|
});
|
|
3125
3436
|
}
|
|
3126
3437
|
catch (error) {
|
|
3127
3438
|
checks.push({
|
|
3128
3439
|
id: "live.semantic-scholar",
|
|
3129
3440
|
category: "live-check",
|
|
3441
|
+
scope: "acquisition",
|
|
3442
|
+
componentIds: ["tiangong.academic-paper-download"],
|
|
3130
3443
|
status: "fail",
|
|
3131
3444
|
detail: sanitizeResearchText(error instanceof Error ? error.message : String(error), apiKey ? [apiKey] : []),
|
|
3132
|
-
minimumAction: "Restore provider connectivity
|
|
3445
|
+
minimumAction: "Restore provider connectivity before relying on Semantic Scholar; unrelated research remains available and no standalone fallback is authorized.",
|
|
3446
|
+
blocking: false,
|
|
3447
|
+
componentGate: false,
|
|
3133
3448
|
diagnostics: {
|
|
3134
3449
|
code: "PROVIDER_TRANSPORT_FAILED",
|
|
3135
3450
|
executionMode: "setup-doctor",
|