@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.
- package/AGENTS.md +2 -2
- package/README.md +97 -89
- package/dist/research/orchestration.js +144 -3
- package/dist/research/orchestration.js.map +1 -1
- package/dist/research/workspace/broker.d.ts +5 -0
- package/dist/research/workspace/broker.js +70 -4
- 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/preflight.d.ts +13 -0
- package/dist/research/workspace/preflight.js +10 -3
- package/dist/research/workspace/preflight.js.map +1 -1
- package/dist/research/workspace/projects.js +11 -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.js +2 -2
- 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 +23 -4
- package/dist/research/workspace/setup.js +339 -74
- package/dist/research/workspace/setup.js.map +1 -1
- package/dist/research/workspace/types.d.ts +7 -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 +1 -1
|
@@ -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);
|
|
@@ -1206,10 +1324,20 @@ function assertPlanMatchesCatalog(plan) {
|
|
|
1206
1324
|
}
|
|
1207
1325
|
}
|
|
1208
1326
|
function validAgentRoutes(value) {
|
|
1209
|
-
const allowed = new Set([
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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",
|