@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.
Files changed (38) hide show
  1. package/AGENTS.md +2 -2
  2. package/README.md +127 -97
  3. package/dist/edge-search.js +57 -16
  4. package/dist/edge-search.js.map +1 -1
  5. package/dist/http.js +17 -1
  6. package/dist/http.js.map +1 -1
  7. package/dist/research/orchestration.js +153 -4
  8. package/dist/research/orchestration.js.map +1 -1
  9. package/dist/research/workspace/broker.d.ts +5 -0
  10. package/dist/research/workspace/broker.js +130 -19
  11. package/dist/research/workspace/broker.js.map +1 -1
  12. package/dist/research/workspace/companion-readiness.d.ts +11 -0
  13. package/dist/research/workspace/companion-readiness.js +75 -0
  14. package/dist/research/workspace/companion-readiness.js.map +1 -0
  15. package/dist/research/workspace/external-skills.d.ts +95 -0
  16. package/dist/research/workspace/external-skills.js +79 -25
  17. package/dist/research/workspace/external-skills.js.map +1 -1
  18. package/dist/research/workspace/preflight.d.ts +22 -0
  19. package/dist/research/workspace/preflight.js +54 -5
  20. package/dist/research/workspace/preflight.js.map +1 -1
  21. package/dist/research/workspace/projects.js +27 -0
  22. package/dist/research/workspace/projects.js.map +1 -1
  23. package/dist/research/workspace/runtime.d.ts +68 -2
  24. package/dist/research/workspace/runtime.js +652 -14
  25. package/dist/research/workspace/runtime.js.map +1 -1
  26. package/dist/research/workspace/setup-catalog.d.ts +20 -2
  27. package/dist/research/workspace/setup-catalog.js +202 -5
  28. package/dist/research/workspace/setup-catalog.js.map +1 -1
  29. package/dist/research/workspace/setup-wizard.js +35 -13
  30. package/dist/research/workspace/setup-wizard.js.map +1 -1
  31. package/dist/research/workspace/setup.d.ts +30 -5
  32. package/dist/research/workspace/setup.js +500 -114
  33. package/dist/research/workspace/setup.js.map +1 -1
  34. package/dist/research/workspace/types.d.ts +9 -0
  35. package/dist/research/workspace/workspace.d.ts +2 -0
  36. package/dist/research/workspace/workspace.js +83 -20
  37. package/dist/research/workspace/workspace.js.map +1 -1
  38. 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, configureTiangongSciCapability, doctorExternalCapabilities, EXTERNAL_SKILL_CONTEXT_PROFILE, EXTERNAL_SKILL_MEDIA_PROFILE, EXTERNAL_SKILL_PROFILE, reconcileSetupManagedCapabilities, } from "./external-skills.js";
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.readiness === "READY"
437
- ? "ready"
438
- : report.readiness === "PARTIALLY_READY"
448
+ status: report.researchReadiness === "BLOCKED"
449
+ ? "blocked"
450
+ : report.overallReadiness === "PARTIALLY_READY"
439
451
  ? "partially-ready"
440
- : "blocked",
452
+ : "ready",
441
453
  currentStep: null,
442
454
  completedSteps: [...new Set([...state.completedSteps, "doctor"])],
443
455
  });
@@ -691,41 +703,85 @@ export async function doctorResearchSetup(workspace, options = {}) {
691
703
  return `${command} is executable (${sanitizeResearchText(result.stdout.trim()).slice(0, 120)}).`;
692
704
  });
693
705
  }
694
- await setupDoctorCheck(checks, "platform-sandbox", "runtime", async () => {
695
- if (platform() === "darwin") {
696
- const info = await lstat("/usr/bin/sandbox-exec").catch(() => undefined);
697
- if (!info?.isFile())
698
- throw new Error("/usr/bin/sandbox-exec is unavailable");
699
- return "macOS sandbox-exec is available.";
706
+ if (platform() === "win32") {
707
+ const production = plan.workspace.mode === "production-research";
708
+ checks.push({
709
+ id: "platform-sandbox",
710
+ category: "runtime",
711
+ scope: "research-core",
712
+ status: production ? "fail" : "warn",
713
+ detail: production
714
+ ? "Production research execution is unsupported on Windows because no approved capsule sandbox is available."
715
+ : "Windows can inspect and smoke-test setup state, but native research execution requires an approved macOS or Linux capsule sandbox.",
716
+ minimumAction: production
717
+ ? "Run production research on macOS with sandbox-exec or Linux with Bubblewrap."
718
+ : "Use macOS or Linux before switching this workspace to production-research mode.",
719
+ blocking: production,
720
+ requiredFor: production ? ["setup", "research-core"] : [],
721
+ });
722
+ }
723
+ else {
724
+ await setupDoctorCheck(checks, "platform-sandbox", "runtime", async () => {
725
+ if (platform() === "darwin") {
726
+ const info = await lstat("/usr/bin/sandbox-exec").catch(() => undefined);
727
+ if (!info?.isFile())
728
+ throw new Error("/usr/bin/sandbox-exec is unavailable");
729
+ return "macOS sandbox-exec is available.";
730
+ }
731
+ if (platform() === "linux") {
732
+ const result = await runner({
733
+ command: "bwrap",
734
+ args: ["--version"],
735
+ cwd: root,
736
+ environment: installerEnvironment(environment),
737
+ timeoutMs: 15_000,
738
+ });
739
+ if (result.exitCode !== 0)
740
+ throw new Error("Bubblewrap is unavailable");
741
+ return "Linux Bubblewrap is available.";
742
+ }
743
+ throw new Error("Research execution is unsupported on this platform");
744
+ });
745
+ }
746
+ checks.push({
747
+ id: "agent.native-producer",
748
+ category: "agent",
749
+ scope: "research-core",
750
+ status: "pass",
751
+ detail: `Producer work is bound to the current interactive ${plan.agentRoutes.producerAgent} host; setup will not launch it as a child process.`,
752
+ minimumAction: null,
753
+ blocking: true,
754
+ requiredFor: ["setup", "research-core"],
755
+ });
756
+ const reviewerCommand = plan.agentRoutes.reviewerAgent;
757
+ await setupDoctorCheck(checks, `agent.${reviewerCommand}.reviewer`, "agent", async () => {
758
+ const result = await runner({
759
+ command: reviewerCommand,
760
+ args: ["--version"],
761
+ cwd: root,
762
+ environment: agentDoctorEnvironment(environment),
763
+ timeoutMs: 30_000,
764
+ });
765
+ if (result.exitCode !== 0)
766
+ throw new Error(`${reviewerCommand} is not executable`);
767
+ return `${reviewerCommand} reviewer CLI is executable (${sanitizeResearchText(result.stdout.trim()).slice(0, 160)}).`;
768
+ });
769
+ await setupDoctorCheck(checks, "agent-route-config", "agent", async () => {
770
+ const config = await loadWorkspaceConfig(root);
771
+ if (config.producer.executionMode !== "native-host" ||
772
+ config.reviewer.executionMode !== "headless-cli" ||
773
+ config.producer.agent === config.reviewer.agent) {
774
+ throw new Error("Workspace must bind producer=native-host and reviewer=headless-cli on different agent families.");
700
775
  }
701
- if (platform() === "linux") {
702
- const result = await runner({
703
- command: "bwrap",
704
- args: ["--version"],
705
- cwd: root,
706
- environment: installerEnvironment(environment),
707
- timeoutMs: 15_000,
708
- });
709
- if (result.exitCode !== 0)
710
- throw new Error("Bubblewrap is unavailable");
711
- return "Linux Bubblewrap is available.";
776
+ if (config.mode === "production-research" &&
777
+ (!config.producer.model ||
778
+ !config.reviewer.model ||
779
+ !config.producer.pricing ||
780
+ !config.reviewer.pricing)) {
781
+ throw new Error("Production agent models and reviewed pricing are incomplete.");
712
782
  }
713
- throw new Error("Research execution is unsupported on this platform");
783
+ return `${config.producer.agent}(native-host) -> ${config.reviewer.agent}(headless-cli).`;
714
784
  });
715
- for (const command of ["codex", "claude"]) {
716
- await setupDoctorCheck(checks, `agent.${command}`, "agent", async () => {
717
- const result = await runner({
718
- command,
719
- args: ["--version"],
720
- cwd: root,
721
- environment: agentDoctorEnvironment(environment),
722
- timeoutMs: 30_000,
723
- });
724
- if (result.exitCode !== 0)
725
- throw new Error(`${command} is not executable`);
726
- return `${command} is executable (${sanitizeResearchText(result.stdout.trim()).slice(0, 160)}).`;
727
- });
728
- }
729
785
  const selected = plan.selection.skillIds.map(setupSkill);
730
786
  const installations = await inspectSelectedInstallations(plan, selected, environment);
731
787
  for (const installation of installations) {
@@ -802,10 +858,15 @@ export async function doctorResearchSetup(workspace, options = {}) {
802
858
  }
803
859
  await appendDependencyChecks(checks, selected, runner, root, environment);
804
860
  let capabilityDoctor = null;
861
+ const blockingBeforeLive = checks
862
+ .map((check) => normalizeSetupDoctorCheck(check, selected))
863
+ .filter((check) => check.blocking && check.status === "fail")
864
+ .map((check) => check.id);
865
+ const runRequiredLive = options.live === true && blockingBeforeLive.length === 0;
805
866
  if (selected.some((skill) => skill.role === "evidence-capability")) {
806
867
  try {
807
868
  capabilityDoctor = await doctorExternalCapabilities(root, {
808
- live: options.live === true,
869
+ live: runRequiredLive,
809
870
  fetcher,
810
871
  sleeper,
811
872
  });
@@ -829,7 +890,20 @@ export async function doctorResearchSetup(workspace, options = {}) {
829
890
  });
830
891
  }
831
892
  }
832
- if (options.live) {
893
+ if (options.live === true && !runRequiredLive) {
894
+ checks.push({
895
+ id: "live.required-capabilities.skipped",
896
+ category: "live-check",
897
+ scope: "evidence",
898
+ status: "fail",
899
+ detail: "Required live capability probes were not started because a static blocking prerequisite failed.",
900
+ minimumAction: "Resolve the static blocking prerequisites, then rerun live doctor checks.",
901
+ blocking: true,
902
+ requiredFor: ["setup", "research-core"],
903
+ skippedBecause: blockingBeforeLive.join(", "),
904
+ });
905
+ }
906
+ if (runRequiredLive) {
833
907
  await appendCompanionLiveChecks(checks, {
834
908
  plan,
835
909
  selected,
@@ -839,13 +913,45 @@ export async function doctorResearchSetup(workspace, options = {}) {
839
913
  allowSyntheticUnstructureUpload: options.allowSyntheticUnstructureUpload === true,
840
914
  });
841
915
  }
916
+ else if (options.live === true &&
917
+ selected.some((skill) => ["input-preprocessor", "acquisition-adapter", "post-closure-authoring"].includes(skill.role))) {
918
+ checks.push({
919
+ id: "live.optional-components.skipped",
920
+ category: "live-check",
921
+ status: "warn",
922
+ detail: "Optional component diagnostics were skipped after a static blocking prerequisite failed.",
923
+ minimumAction: "Resolve the blocking prerequisites before retrying optional diagnostics.",
924
+ blocking: false,
925
+ requiredFor: [],
926
+ skippedBecause: blockingBeforeLive.join(", "),
927
+ });
928
+ }
842
929
  let workspaceDoctor = null;
930
+ const blockingPrerequisiteFailures = checks
931
+ .map((check) => normalizeSetupDoctorCheck(check, selected))
932
+ .filter((check) => check.blocking && check.status === "fail")
933
+ .map((check) => check.id);
934
+ const runAgentSmoke = options.agentSmoke === true && blockingPrerequisiteFailures.length === 0;
935
+ if (options.agentSmoke === true && !runAgentSmoke) {
936
+ checks.push({
937
+ id: "agent-reviewer-smoke.skipped",
938
+ category: "agent",
939
+ scope: "review",
940
+ status: "fail",
941
+ detail: "The paid reviewer smoke was not started because a zero/low-cost blocking prerequisite failed.",
942
+ minimumAction: "Resolve the listed blocking prerequisites, then rerun the explicitly confirmed reviewer smoke.",
943
+ blocking: true,
944
+ requiredFor: ["setup", "research-core"],
945
+ skippedBecause: blockingPrerequisiteFailures.join(", "),
946
+ });
947
+ }
843
948
  try {
844
949
  workspaceDoctor = await doctorResearchWorkspace(root, {
845
- agentSmoke: options.agentSmoke === true,
846
- capabilitySmoke: options.live === true,
950
+ agentSmoke: runAgentSmoke,
951
+ capabilitySmoke: runRequiredLive,
847
952
  environment,
848
953
  capabilityFetcher: fetcher,
954
+ ...(capabilityDoctor === null ? {} : { capabilityDoctorResult: capabilityDoctor }),
849
955
  ...(options.executor === undefined ? {} : { executor: options.executor }),
850
956
  });
851
957
  const requiredRuntimeChecks = options.agentSmoke === true || options.live === true;
@@ -888,9 +994,16 @@ export async function doctorResearchSetup(workspace, options = {}) {
888
994
  : `Run tiangong-ai research setup doctor --live --workspace ${root} --json after reviewing quota impact.`,
889
995
  });
890
996
  }
891
- const readiness = checks.some((check) => check.status === "fail")
997
+ const scopedChecks = checks.map((check) => normalizeSetupDoctorCheck(check, selected));
998
+ const researchReadiness = scopedChecks.some((check) => check.blocking && check.status === "fail")
999
+ ? "BLOCKED"
1000
+ : "READY";
1001
+ const preprocessingReadiness = setupDomainReadiness(scopedChecks, "preprocessing");
1002
+ const acquisitionReadiness = setupDomainReadiness(scopedChecks, "acquisition");
1003
+ const authoringReadiness = setupDomainReadiness(scopedChecks, "authoring");
1004
+ const overallReadiness = researchReadiness === "BLOCKED"
892
1005
  ? "BLOCKED"
893
- : checks.some((check) => check.status === "warn")
1006
+ : scopedChecks.some((check) => check.status !== "pass")
894
1007
  ? "PARTIALLY_READY"
895
1008
  : "READY";
896
1009
  const setupSecrets = [
@@ -908,14 +1021,19 @@ export async function doctorResearchSetup(workspace, options = {}) {
908
1021
  planSha256: plan.planSha256,
909
1022
  checkedAt: new Date().toISOString(),
910
1023
  mode: options.live ? "live" : "static",
911
- readiness,
912
- checks,
1024
+ readiness: researchReadiness,
1025
+ researchReadiness,
1026
+ preprocessingReadiness,
1027
+ acquisitionReadiness,
1028
+ authoringReadiness,
1029
+ overallReadiness,
1030
+ checks: scopedChecks,
913
1031
  capabilityDoctor,
914
1032
  workspaceDoctor,
915
1033
  summary: {
916
- pass: checks.filter((check) => check.status === "pass").length,
917
- warn: checks.filter((check) => check.status === "warn").length,
918
- fail: checks.filter((check) => check.status === "fail").length,
1034
+ pass: scopedChecks.filter((check) => check.status === "pass").length,
1035
+ warn: scopedChecks.filter((check) => check.status === "warn").length,
1036
+ fail: scopedChecks.filter((check) => check.status === "fail").length,
919
1037
  },
920
1038
  }, setupSecrets);
921
1039
  await writeJsonAtomic(paths.setupReport, report);
@@ -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: drift.length > 0 || plan.cli.version !== packageVersion(),
977
- cliVersionDrift: plan.cli.version === packageVersion()
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: drift.length
987
- ? "Use the newer CLI to create and review a replacement immutable plan; upgrade never runs a floating skills update."
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(["producerModel", "reviewerModel", "producerPricing", "reviewerPricing"]);
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 (!(value.producerModel === null || typeof value.producerModel === "string") ||
1337
+ if ((value.producerAgent !== "codex" && value.producerAgent !== "claude") ||
1338
+ (value.reviewerAgent !== "codex" && value.reviewerAgent !== "claude") ||
1339
+ value.producerAgent === value.reviewerAgent ||
1340
+ !(value.producerModel === null || typeof value.producerModel === "string") ||
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
- ...config.producer,
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 hasTiangongSci = plan.selection.skillIds.includes("tiangong.kb-sci-search");
1763
- const codexRoot = hasBraveProfile || hasTiangongSci ? plannedTargetRoot(plan, "codex") : null;
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
- if (hasTiangongSci) {
1772
- const skill = setupSkill("tiangong.kb-sci-search");
1946
+ for (const database of tiangongDatabases) {
1947
+ const skill = setupSkill(database.skillId);
1773
1948
  const source = setupSource(skill.sourceId);
1774
- await configureTiangongSciCapability({
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: "first-party.tiangong.kb-sci-search",
1959
+ catalogId: database.catalogId,
1784
1960
  },
1785
- endpoint: plan.settings["tiangong.sci.endpoint"],
1786
- ...(plan.settings["tiangong.sci.region"] === undefined
1961
+ endpoint: plan.settings[`${database.settingPrefix}.endpoint`],
1962
+ ...(plan.settings[`${database.settingPrefix}.region`] === undefined
1787
1963
  ? {}
1788
- : { region: plan.settings["tiangong.sci.region"] }),
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
- ...(hasTiangongSci ? ["database.tiangong.sci-search"] : []),
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: `Resolve ${id}, then rerun research setup doctor.`,
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 setupDoctorCheck(checks, "live.semantic-scholar", "live-check", async () => {
2965
- const apiKey = input.adapterCredentials.get("semantic-scholar.api-key");
2966
- const headers = new Headers({ Accept: "application/json" });
2967
- if (apiKey)
2968
- headers.set("x-api-key", apiKey);
2969
- const response = await input.fetcher("https://api.semanticscholar.org/graph/v1/paper/search?query=reproducible%20research&limit=1&fields=paperId", {
2970
- method: "GET",
2971
- headers,
2972
- redirect: "manual",
2973
- signal: AbortSignal.timeout(30_000),
2974
- });
2975
- if (response.status >= 300 && response.status < 400) {
2976
- throw new Error("Semantic Scholar returned a redirect; credential-bearing redirects are not followed.");
2977
- }
2978
- if (!response.ok) {
2979
- const detail = await boundedResponseText(response, 2_000, apiKey ? [apiKey] : []);
2980
- throw new Error(`Semantic Scholar live check returned HTTP ${response.status}${detail ? `: ${detail}` : ""}.`);
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 "";