@tiangong-ai/cli 0.0.32 → 0.0.34
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 +7 -2
- package/README.md +72 -19
- package/dist/research/commands.js +58 -1
- package/dist/research/commands.js.map +1 -1
- package/dist/research/orchestration.js +388 -31
- 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.js +265 -56
- package/dist/research/workspace/broker.js.map +1 -1
- package/dist/research/workspace/constants.d.ts +1 -0
- package/dist/research/workspace/constants.js +4 -2
- package/dist/research/workspace/constants.js.map +1 -1
- package/dist/research/workspace/context.js +106 -11
- package/dist/research/workspace/context.js.map +1 -1
- 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 +5 -0
- package/dist/research/workspace/preflight.js +37 -17
- package/dist/research/workspace/preflight.js.map +1 -1
- package/dist/research/workspace/projects.d.ts +3 -1
- package/dist/research/workspace/projects.js +346 -5
- package/dist/research/workspace/projects.js.map +1 -1
- package/dist/research/workspace/runtime.d.ts +106 -4
- package/dist/research/workspace/runtime.js +947 -80
- 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 +5 -5
- package/dist/research/workspace/setup-invocation.d.ts +11 -0
- package/dist/research/workspace/setup-invocation.js +34 -0
- package/dist/research/workspace/setup-invocation.js.map +1 -0
- package/dist/research/workspace/setup-wizard.js +10 -4
- package/dist/research/workspace/setup-wizard.js.map +1 -1
- package/dist/research/workspace/setup.d.ts +55 -7
- package/dist/research/workspace/setup.js +603 -32
- package/dist/research/workspace/setup.js.map +1 -1
- package/dist/research/workspace/types.d.ts +52 -3
- package/dist/research/workspace/workspace.js +45 -6
- package/dist/research/workspace/workspace.js.map +1 -1
- package/package.json +4 -2
|
@@ -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, mkdir, open, readFile, readdir, realpath, rename, 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";
|
|
@@ -13,8 +13,11 @@ import { appendJournalEvent } from "./journal.js";
|
|
|
13
13
|
import { configuredResearchSecrets, isSensitiveEnvironmentName, sanitizeResearchRecord, sanitizeResearchText, } from "./sanitization.js";
|
|
14
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
|
-
import { packageVersion } from "./constants.js";
|
|
16
|
+
import { packageRoot, packageVersion, RESEARCH_PACKAGE_NAME } from "./constants.js";
|
|
17
|
+
import { exactResearchCliCommand, pinResearchCliCommand, researchSetupApplyCommand, researchSetupRetryCommand, } from "./setup-invocation.js";
|
|
17
18
|
import { doctorResearchWorkspace, initializeResearchWorkspace, loadWorkspaceConfig, } from "./workspace.js";
|
|
19
|
+
const RECOVERY_SKILL_NAME = "tiangong-auto-research-recovery";
|
|
20
|
+
const RECOVERY_SHIM_MARKER = ".tiangong-recovery-shim.json";
|
|
18
21
|
const BRAVE_PROFILE_SKILLS = {
|
|
19
22
|
none: [],
|
|
20
23
|
[EXTERNAL_SKILL_PROFILE]: ["brave.web-search", "brave.news-search"],
|
|
@@ -30,8 +33,7 @@ const BRAVE_PROFILE_SKILLS = {
|
|
|
30
33
|
const ADAPTER_ENV_KEY = "TIANGONG_RESEARCH_ADAPTER_CREDENTIALS_JSON";
|
|
31
34
|
const MAX_COMMAND_OUTPUT_BYTES = 1024 * 1024;
|
|
32
35
|
export async function createResearchSetupPlan(input) {
|
|
33
|
-
const root =
|
|
34
|
-
await assertWorkspaceDirectory(root);
|
|
36
|
+
const root = await resolveResearchSetupWorkspacePath(input.workspace);
|
|
35
37
|
const scope = input.scope ?? "project";
|
|
36
38
|
const agents = normalizeAgents(input.agents ?? ["codex"]);
|
|
37
39
|
const agentRoutes = normalizeAgentRoutes(input.agentRoutes);
|
|
@@ -330,6 +332,11 @@ export async function applyResearchSetupPlan(planPath, options = {}) {
|
|
|
330
332
|
state = await startSetupStep(root, state, "credentials");
|
|
331
333
|
await configurePlanCredentials(plan, environment);
|
|
332
334
|
state = await completeSetupStep(root, state, "credentials");
|
|
335
|
+
if (plan.selection.skillIds.includes("tiangong.auto-research")) {
|
|
336
|
+
state = await startSetupStep(root, state, "recovery-shim");
|
|
337
|
+
await installResearchSetupRecoveryShims(plan);
|
|
338
|
+
state = await completeSetupStep(root, state, "recovery-shim");
|
|
339
|
+
}
|
|
333
340
|
state = await startSetupStep(root, state, "installation-preflight");
|
|
334
341
|
const selected = plan.selection.skillIds.map(setupSkill);
|
|
335
342
|
const installInspection = await inspectSelectedInstallations(plan, selected, environment);
|
|
@@ -356,7 +363,12 @@ export async function applyResearchSetupPlan(planPath, options = {}) {
|
|
|
356
363
|
].sort();
|
|
357
364
|
const sourceDirectories = new Map();
|
|
358
365
|
for (const sourceId of requiredSourceIds) {
|
|
359
|
-
|
|
366
|
+
try {
|
|
367
|
+
sourceDirectories.set(sourceId, await ensureSetupSourceCheckout(plan, sourceId, runner, installerEnvironment(environment)));
|
|
368
|
+
}
|
|
369
|
+
catch (error) {
|
|
370
|
+
throw await annotateSetupSourceCheckoutFailure(error, plan, sourceId);
|
|
371
|
+
}
|
|
360
372
|
}
|
|
361
373
|
state = await completeSetupStep(root, state, "source-checkout");
|
|
362
374
|
state = await startSetupStep(root, state, "skill-install");
|
|
@@ -397,6 +409,11 @@ export async function applyResearchSetupPlan(planPath, options = {}) {
|
|
|
397
409
|
else {
|
|
398
410
|
state = await completeSetupStep(root, state, "skill-install");
|
|
399
411
|
}
|
|
412
|
+
if (plan.selection.skillIds.includes("tiangong.auto-research")) {
|
|
413
|
+
state = await startSetupStep(root, state, "recovery-shim-cleanup");
|
|
414
|
+
await removeResearchSetupRecoveryShims(plan);
|
|
415
|
+
state = await completeSetupStep(root, state, "recovery-shim-cleanup");
|
|
416
|
+
}
|
|
400
417
|
state = await startSetupStep(root, state, "capability-configuration");
|
|
401
418
|
await configureSelectedCapabilities(plan, environment);
|
|
402
419
|
await reconcilePlanCredentialStores(plan);
|
|
@@ -479,12 +496,31 @@ export async function applyResearchSetupPlan(planPath, options = {}) {
|
|
|
479
496
|
}
|
|
480
497
|
}
|
|
481
498
|
export async function inspectResearchSetupStatus(workspace, environment = process.env) {
|
|
482
|
-
const
|
|
499
|
+
const requestedRoot = requireAbsoluteWorkspace(resolve(workspace));
|
|
500
|
+
const requestedPaths = workspacePaths(requestedRoot);
|
|
501
|
+
const plan = await loadAndVerifyResearchSetupPlan(requestedPaths.setupPlan);
|
|
502
|
+
const canonicalRequestedRoot = await realpath(requestedRoot).catch(() => requestedRoot);
|
|
503
|
+
if (canonicalRequestedRoot !== plan.workspace.path) {
|
|
504
|
+
throw setupError({
|
|
505
|
+
code: "RESEARCH_SETUP_WORKSPACE_INVALID",
|
|
506
|
+
step: "workspace",
|
|
507
|
+
reason: "The setup plan is bound to a different canonical workspace path.",
|
|
508
|
+
minimumAction: "Run setup status against the exact workspace recorded in the setup plan.",
|
|
509
|
+
retryCommand: researchSetupApplyCommand({
|
|
510
|
+
version: plan.cli.version,
|
|
511
|
+
planPath: workspacePaths(plan.workspace.path).setupPlan,
|
|
512
|
+
}),
|
|
513
|
+
exitCode: 2,
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
const root = plan.workspace.path;
|
|
483
517
|
const paths = workspacePaths(root);
|
|
484
|
-
const
|
|
485
|
-
const state =
|
|
518
|
+
const storedState = await loadSetupState(root, plan.planSha256);
|
|
519
|
+
const state = setupStateForOutput(storedState, plan, root);
|
|
486
520
|
const selected = plan.selection.skillIds.map(setupSkill);
|
|
487
521
|
const installations = await inspectSelectedInstallations(plan, selected, environment);
|
|
522
|
+
const credentialReadiness = await inspectSetupCredentialReadiness(plan);
|
|
523
|
+
const provenance = await inspectSetupProvenance(plan, installations, environment);
|
|
488
524
|
const report = (await pathExists(paths.setupReport))
|
|
489
525
|
? await readJsonFile(paths.setupReport, "Research setup report")
|
|
490
526
|
: null;
|
|
@@ -502,16 +538,420 @@ export async function inspectResearchSetupStatus(workspace, environment = proces
|
|
|
502
538
|
},
|
|
503
539
|
state,
|
|
504
540
|
installations,
|
|
541
|
+
credentialReadiness,
|
|
542
|
+
provenance,
|
|
505
543
|
report,
|
|
506
|
-
next: state
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
544
|
+
next: setupNextAction(plan, state, root),
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
function setupStateForOutput(state, plan, root) {
|
|
548
|
+
if (!state.lastError)
|
|
549
|
+
return state;
|
|
550
|
+
return {
|
|
551
|
+
...state,
|
|
552
|
+
lastError: {
|
|
553
|
+
...state.lastError,
|
|
554
|
+
retryCommand: researchSetupRetryCommand({
|
|
555
|
+
version: plan.cli.version,
|
|
556
|
+
workspace: root,
|
|
557
|
+
step: state.lastError.step,
|
|
558
|
+
}),
|
|
559
|
+
},
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
function setupNextAction(plan, state, root) {
|
|
563
|
+
if (state.status === "ready")
|
|
564
|
+
return null;
|
|
565
|
+
if (state.status === "pending") {
|
|
566
|
+
return {
|
|
567
|
+
action: "apply",
|
|
568
|
+
minimumAction: "Apply the reviewed immutable setup plan.",
|
|
569
|
+
retryCommand: researchSetupApplyCommand({
|
|
570
|
+
version: plan.cli.version,
|
|
571
|
+
planPath: workspacePaths(root).setupPlan,
|
|
572
|
+
}),
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
if (state.status === "blocked" && state.lastError) {
|
|
576
|
+
return {
|
|
577
|
+
action: "retry",
|
|
578
|
+
minimumAction: state.lastError.minimumAction,
|
|
579
|
+
retryCommand: state.lastError.retryCommand,
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
if (state.status === "applying") {
|
|
583
|
+
return {
|
|
584
|
+
action: "inspect",
|
|
585
|
+
minimumAction: "Inspect the active setup attempt; do not start a competing apply.",
|
|
586
|
+
retryCommand: exactResearchCliCommand(["research", "setup", "status", "--workspace", root, "--json"], plan.cli.version),
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
return {
|
|
590
|
+
action: "doctor",
|
|
591
|
+
minimumAction: "Run setup doctor and resolve every reported missing readiness item.",
|
|
592
|
+
retryCommand: exactResearchCliCommand(["research", "setup", "doctor", "--workspace", root, "--json"], plan.cli.version),
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
async function inspectSetupCredentialReadiness(plan) {
|
|
596
|
+
const definitions = selectedCredentialDefinitions(plan);
|
|
597
|
+
const brokerIds = definitions
|
|
598
|
+
.filter((definition) => definition.storage === "broker")
|
|
599
|
+
.map((definition) => definition.id);
|
|
600
|
+
const configuredBroker = new Set((await loadCapabilityCredentialMapForIds(plan.workspace.path, brokerIds, {
|
|
601
|
+
ignoreUndeclared: true,
|
|
602
|
+
})).keys());
|
|
603
|
+
const configuredAdapter = await loadAdapterCredentials(plan.workspace.path, definitions, {
|
|
604
|
+
ignoreUndeclared: true,
|
|
605
|
+
});
|
|
606
|
+
const configuredIds = definitions
|
|
607
|
+
.filter((definition) => definition.storage === "broker"
|
|
608
|
+
? configuredBroker.has(definition.id)
|
|
609
|
+
: configuredAdapter.has(definition.id))
|
|
610
|
+
.map((definition) => definition.id)
|
|
611
|
+
.sort();
|
|
612
|
+
const configured = new Set(configuredIds);
|
|
613
|
+
return {
|
|
614
|
+
valuesEmitted: false,
|
|
615
|
+
configuredIds,
|
|
616
|
+
missingRequiredIds: definitions
|
|
617
|
+
.filter((definition) => definition.required && !configured.has(definition.id))
|
|
618
|
+
.map((definition) => definition.id)
|
|
619
|
+
.sort(),
|
|
620
|
+
scopes: [
|
|
621
|
+
...new Set(definitions
|
|
622
|
+
.filter((definition) => configured.has(definition.id))
|
|
623
|
+
.map((definition) => definition.storage)),
|
|
624
|
+
].sort(),
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
async function inspectSetupProvenance(plan, installations, environment) {
|
|
628
|
+
const orchestratorSelected = plan.selection.skillIds.includes("tiangong.auto-research");
|
|
629
|
+
const orchestratorInstallations = installations
|
|
630
|
+
.filter((installation) => installation.skillId === "tiangong.auto-research")
|
|
631
|
+
.map((installation) => ({
|
|
632
|
+
agent: installation.agent,
|
|
633
|
+
path: installation.path,
|
|
634
|
+
status: installation.status,
|
|
635
|
+
observedTreeSha256: installation.observedTreeSha256,
|
|
636
|
+
}));
|
|
637
|
+
return {
|
|
638
|
+
effectiveCli: {
|
|
639
|
+
packageName: RESEARCH_PACKAGE_NAME,
|
|
640
|
+
packageVersion: packageVersion(),
|
|
641
|
+
packageRoot: packageRoot(),
|
|
642
|
+
invocationMode: "exact-npx",
|
|
643
|
+
commandPrefix: exactResearchCliCommand([], plan.cli.version),
|
|
644
|
+
},
|
|
645
|
+
ambientCli: await findAmbientExecutable(environment, "tiangong-ai"),
|
|
646
|
+
ambientSkillConflicts: await inspectAmbientProjectSkillConflicts(plan, environment),
|
|
647
|
+
recoveryShims: await inspectResearchSetupRecoveryShims(plan),
|
|
648
|
+
selectedOrchestrator: orchestratorSelected
|
|
649
|
+
? {
|
|
650
|
+
skillId: "tiangong.auto-research",
|
|
651
|
+
scope: plan.install.scope,
|
|
652
|
+
preferredPath: orchestratorInstallations.find((installation) => installation.status === "installed")
|
|
653
|
+
?.path ?? null,
|
|
654
|
+
installations: orchestratorInstallations,
|
|
655
|
+
}
|
|
656
|
+
: null,
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
function recoveryShimPath(plan, agent) {
|
|
660
|
+
return join(setupTargetRoot({
|
|
661
|
+
workspace: plan.workspace.path,
|
|
662
|
+
scope: "project",
|
|
663
|
+
agent,
|
|
664
|
+
}), RECOVERY_SKILL_NAME);
|
|
665
|
+
}
|
|
666
|
+
function recoveryShimMarker(plan, agent) {
|
|
667
|
+
return {
|
|
668
|
+
schemaVersion: 1,
|
|
669
|
+
kind: "tiangong-auto-research-recovery-shim",
|
|
670
|
+
planSha256: plan.planSha256,
|
|
671
|
+
cliVersion: plan.cli.version,
|
|
672
|
+
workspace: plan.workspace.path,
|
|
673
|
+
agent,
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
function recoveryShimInstructions(marker) {
|
|
677
|
+
const inspectCommand = exactResearchCliCommand(["research", "context", "inspect", "--path", marker.workspace, "--json"], marker.cliVersion);
|
|
678
|
+
const statusCommand = exactResearchCliCommand(["research", "setup", "status", "--workspace", marker.workspace, "--json"], marker.cliVersion);
|
|
679
|
+
return `---
|
|
680
|
+
name: ${RECOVERY_SKILL_NAME}
|
|
681
|
+
description: Recovery-only routing for an explicitly reviewed Tiangong Auto Research setup that is pending, applying, or blocked. Use when a research request occurs under this workspace before the full project orchestrator is installed. Never use for research execution or standalone evidence search.
|
|
682
|
+
---
|
|
683
|
+
|
|
684
|
+
# Tiangong Auto Research recovery-only shim
|
|
685
|
+
|
|
686
|
+
This CLI-generated Skill is bound to setup plan \`${marker.planSha256}\`. It exists only
|
|
687
|
+
until the full external \`tiangong-auto-research\` Skill matches its reviewed tree hash.
|
|
688
|
+
|
|
689
|
+
Never run research or standalone evidence from this shim. Do not read, copy, print, or
|
|
690
|
+
edit credentials, setup state, locks, manifests, or the immutable plan.
|
|
691
|
+
|
|
692
|
+
First run the exact-version read-only preflight:
|
|
693
|
+
|
|
694
|
+
\`\`\`bash
|
|
695
|
+
${inspectCommand}
|
|
696
|
+
\`\`\`
|
|
697
|
+
|
|
698
|
+
If the context is managed, inspect the structured setup state:
|
|
699
|
+
|
|
700
|
+
\`\`\`bash
|
|
701
|
+
${statusCommand}
|
|
702
|
+
\`\`\`
|
|
703
|
+
|
|
704
|
+
For \`pending\` or \`blocked\`, execute only the returned \`setup.next.retryCommand\`.
|
|
705
|
+
For \`applying\`, report the active step and do not start a competing apply. Stop after
|
|
706
|
+
reporting any new blocker. Never fall through to a global Skill, ambient CLI, or
|
|
707
|
+
standalone provider credential.
|
|
708
|
+
`;
|
|
709
|
+
}
|
|
710
|
+
function serializedRecoveryShimMarker(marker) {
|
|
711
|
+
return `${JSON.stringify(marker, null, 2)}\n`;
|
|
712
|
+
}
|
|
713
|
+
async function inspectRecoveryShim(path, workspace, agent, expectedPlanSha256) {
|
|
714
|
+
const info = await lstat(path).catch(() => undefined);
|
|
715
|
+
if (!info)
|
|
716
|
+
return { status: "missing", marker: null };
|
|
717
|
+
if (!info.isDirectory() || info.isSymbolicLink())
|
|
718
|
+
return { status: "blocked", marker: null };
|
|
719
|
+
try {
|
|
720
|
+
const entries = (await readdir(path)).sort();
|
|
721
|
+
if (canonicalJson(entries) !== canonicalJson([RECOVERY_SHIM_MARKER, "SKILL.md"].sort())) {
|
|
722
|
+
return { status: "drifted", marker: null };
|
|
723
|
+
}
|
|
724
|
+
const markerPath = join(path, RECOVERY_SHIM_MARKER);
|
|
725
|
+
const skillPath = join(path, "SKILL.md");
|
|
726
|
+
const [markerInfo, skillInfo, markerText, skillText] = await Promise.all([
|
|
727
|
+
lstat(markerPath),
|
|
728
|
+
lstat(skillPath),
|
|
729
|
+
readFile(markerPath, "utf8"),
|
|
730
|
+
readFile(skillPath, "utf8"),
|
|
731
|
+
]);
|
|
732
|
+
if (!markerInfo.isFile() ||
|
|
733
|
+
markerInfo.isSymbolicLink() ||
|
|
734
|
+
!skillInfo.isFile() ||
|
|
735
|
+
skillInfo.isSymbolicLink()) {
|
|
736
|
+
return { status: "blocked", marker: null };
|
|
737
|
+
}
|
|
738
|
+
const value = JSON.parse(markerText);
|
|
739
|
+
if (!isObject(value) ||
|
|
740
|
+
value.schemaVersion !== 1 ||
|
|
741
|
+
value.kind !== "tiangong-auto-research-recovery-shim" ||
|
|
742
|
+
typeof value.planSha256 !== "string" ||
|
|
743
|
+
!/^[0-9a-f]{64}$/.test(value.planSha256) ||
|
|
744
|
+
typeof value.cliVersion !== "string" ||
|
|
745
|
+
!/^\d+\.\d+\.\d+$/.test(value.cliVersion) ||
|
|
746
|
+
value.workspace !== workspace ||
|
|
747
|
+
value.agent !== agent) {
|
|
748
|
+
return { status: "drifted", marker: null };
|
|
749
|
+
}
|
|
750
|
+
const marker = value;
|
|
751
|
+
if (markerText !== serializedRecoveryShimMarker(marker) ||
|
|
752
|
+
skillText !== recoveryShimInstructions(marker)) {
|
|
753
|
+
return { status: "drifted", marker: null };
|
|
754
|
+
}
|
|
755
|
+
return {
|
|
756
|
+
status: marker.planSha256 === expectedPlanSha256 ? "installed" : "stale",
|
|
757
|
+
marker,
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
catch {
|
|
761
|
+
return { status: "blocked", marker: null };
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
async function writeRecoveryShimDirectory(path, marker) {
|
|
765
|
+
const temporary = join(dirname(path), `.${RECOVERY_SKILL_NAME}.${process.pid}.${randomUUID()}.tmp`);
|
|
766
|
+
await mkdir(temporary, { mode: 0o700 });
|
|
767
|
+
try {
|
|
768
|
+
await writeTextAtomic(join(temporary, "SKILL.md"), recoveryShimInstructions(marker), 0o444);
|
|
769
|
+
await writeTextAtomic(join(temporary, RECOVERY_SHIM_MARKER), serializedRecoveryShimMarker(marker), 0o444);
|
|
770
|
+
return temporary;
|
|
771
|
+
}
|
|
772
|
+
catch (error) {
|
|
773
|
+
await rm(temporary, { recursive: true, force: true });
|
|
774
|
+
throw error;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
async function installResearchSetupRecoveryShims(plan) {
|
|
778
|
+
for (const agent of plan.install.agents) {
|
|
779
|
+
const path = recoveryShimPath(plan, agent);
|
|
780
|
+
const parent = dirname(path);
|
|
781
|
+
await assertNoSymlinkedExistingPath(parent, plan.workspace.path);
|
|
782
|
+
await ensureDirectory(parent);
|
|
783
|
+
await assertNoSymlinkedExistingPath(parent, plan.workspace.path);
|
|
784
|
+
const inspection = await inspectRecoveryShim(path, plan.workspace.path, agent, plan.planSha256);
|
|
785
|
+
if (inspection.status === "installed")
|
|
786
|
+
continue;
|
|
787
|
+
if (inspection.status === "drifted" || inspection.status === "blocked") {
|
|
788
|
+
throw setupError({
|
|
789
|
+
code: "RESEARCH_SETUP_RECOVERY_SHIM_UNSAFE",
|
|
790
|
+
step: "recovery-shim",
|
|
791
|
+
reason: `The recovery Skill destination is not an exact CLI-owned shim for ${agent}.`,
|
|
792
|
+
minimumAction: "Review the reported project Skill directory. Setup will not overwrite or delete ambiguous bytes.",
|
|
793
|
+
retryCommand: exactResearchCliCommand(["research", "setup", "status", "--workspace", plan.workspace.path, "--json"], plan.cli.version),
|
|
794
|
+
exitCode: 3,
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
const temporary = await writeRecoveryShimDirectory(path, recoveryShimMarker(plan, agent));
|
|
798
|
+
try {
|
|
799
|
+
if (inspection.status === "missing") {
|
|
800
|
+
await rename(temporary, path);
|
|
801
|
+
}
|
|
802
|
+
else {
|
|
803
|
+
const backup = `${path}.${process.pid}.${randomUUID()}.previous`;
|
|
804
|
+
await rename(path, backup);
|
|
805
|
+
try {
|
|
806
|
+
await rename(temporary, path);
|
|
807
|
+
await rm(backup, { recursive: true, force: true });
|
|
808
|
+
}
|
|
809
|
+
catch (error) {
|
|
810
|
+
if (!(await pathExists(path)))
|
|
811
|
+
await rename(backup, path).catch(() => undefined);
|
|
812
|
+
throw error;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
catch (error) {
|
|
817
|
+
await rm(temporary, { recursive: true, force: true });
|
|
818
|
+
throw error;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
async function removeResearchSetupRecoveryShims(plan) {
|
|
823
|
+
for (const agent of plan.install.agents) {
|
|
824
|
+
const path = recoveryShimPath(plan, agent);
|
|
825
|
+
const inspection = await inspectRecoveryShim(path, plan.workspace.path, agent, plan.planSha256);
|
|
826
|
+
if (inspection.status === "missing")
|
|
827
|
+
continue;
|
|
828
|
+
if (inspection.status !== "installed") {
|
|
829
|
+
throw setupError({
|
|
830
|
+
code: "RESEARCH_SETUP_RECOVERY_SHIM_UNSAFE",
|
|
831
|
+
step: "recovery-shim-cleanup",
|
|
832
|
+
reason: `The recovery Skill changed before verified cleanup for ${agent}.`,
|
|
833
|
+
minimumAction: "Review the recovery Skill directory. Setup removes only its exact plan-bound generated bytes.",
|
|
834
|
+
retryCommand: exactResearchCliCommand(["research", "setup", "status", "--workspace", plan.workspace.path, "--json"], plan.cli.version),
|
|
835
|
+
exitCode: 3,
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
await rm(path, { recursive: true, force: false });
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
async function inspectResearchSetupRecoveryShims(plan) {
|
|
842
|
+
if (!plan.selection.skillIds.includes("tiangong.auto-research"))
|
|
843
|
+
return [];
|
|
844
|
+
const results = [];
|
|
845
|
+
for (const agent of plan.install.agents) {
|
|
846
|
+
const path = recoveryShimPath(plan, agent);
|
|
847
|
+
const inspection = await inspectRecoveryShim(path, plan.workspace.path, agent, plan.planSha256);
|
|
848
|
+
if (inspection.status === "missing")
|
|
849
|
+
continue;
|
|
850
|
+
results.push({
|
|
851
|
+
agent,
|
|
852
|
+
path,
|
|
853
|
+
status: inspection.status === "stale" ? "drifted" : inspection.status,
|
|
854
|
+
planSha256: inspection.marker?.planSha256 ?? null,
|
|
855
|
+
cliVersion: inspection.marker?.cliVersion ?? null,
|
|
856
|
+
recoveryOnly: true,
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
return results;
|
|
860
|
+
}
|
|
861
|
+
async function inspectAmbientProjectSkillConflicts(plan, environment) {
|
|
862
|
+
if (plan.install.scope !== "project")
|
|
863
|
+
return [];
|
|
864
|
+
const conflicts = [];
|
|
865
|
+
for (const agent of plan.install.agents) {
|
|
866
|
+
const globalRoot = setupTargetRoot({
|
|
867
|
+
workspace: plan.workspace.path,
|
|
868
|
+
scope: "global",
|
|
869
|
+
agent,
|
|
870
|
+
environment,
|
|
871
|
+
});
|
|
872
|
+
for (const skillId of plan.selection.skillIds) {
|
|
873
|
+
const skill = setupSkill(skillId);
|
|
874
|
+
const path = join(globalRoot, skill.skillName);
|
|
875
|
+
const info = await lstat(path).catch(() => undefined);
|
|
876
|
+
if (!info)
|
|
877
|
+
continue;
|
|
878
|
+
let status = "blocked";
|
|
879
|
+
let observedTreeSha256 = null;
|
|
880
|
+
if (info.isDirectory() && !info.isSymbolicLink()) {
|
|
881
|
+
try {
|
|
882
|
+
observedTreeSha256 = await hashRegularTree(path);
|
|
883
|
+
status = observedTreeSha256 === skill.expectedTreeSha256 ? "matching" : "drifted";
|
|
884
|
+
}
|
|
885
|
+
catch {
|
|
886
|
+
status = "blocked";
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
conflicts.push({
|
|
890
|
+
skillId: skill.id,
|
|
891
|
+
skillName: skill.skillName,
|
|
892
|
+
agent,
|
|
893
|
+
path,
|
|
894
|
+
status,
|
|
895
|
+
observedTreeSha256,
|
|
896
|
+
expectedTreeSha256: skill.expectedTreeSha256,
|
|
897
|
+
unmanagedPathCliFallback: await containsUnmanagedPathCliFallback(path),
|
|
898
|
+
ignoredByProjectScope: true,
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
return conflicts;
|
|
903
|
+
}
|
|
904
|
+
async function containsUnmanagedPathCliFallback(root) {
|
|
905
|
+
const state = { inspectedFiles: 0 };
|
|
906
|
+
const inspectDirectory = async (directory, depth) => {
|
|
907
|
+
if (depth > 4 || state.inspectedFiles >= 100)
|
|
908
|
+
return false;
|
|
909
|
+
const entries = await readdir(directory, { withFileTypes: true }).catch(() => []);
|
|
910
|
+
for (const entry of entries) {
|
|
911
|
+
if (entry.isSymbolicLink())
|
|
912
|
+
continue;
|
|
913
|
+
const path = join(directory, entry.name);
|
|
914
|
+
if (entry.isDirectory()) {
|
|
915
|
+
if (await inspectDirectory(path, depth + 1))
|
|
916
|
+
return true;
|
|
917
|
+
continue;
|
|
918
|
+
}
|
|
919
|
+
if (!entry.isFile() || !/\.(?:c?js|mjs|py|sh)$/.test(entry.name))
|
|
920
|
+
continue;
|
|
921
|
+
state.inspectedFiles += 1;
|
|
922
|
+
const info = await lstat(path).catch(() => undefined);
|
|
923
|
+
if (!info?.isFile() || info.isSymbolicLink() || info.size > 256 * 1024)
|
|
924
|
+
continue;
|
|
925
|
+
const content = await readFile(path, "utf8").catch(() => "");
|
|
926
|
+
if (content.includes("TIANGONG_AI_CLI:-tiangong-ai"))
|
|
927
|
+
return true;
|
|
928
|
+
}
|
|
929
|
+
return false;
|
|
514
930
|
};
|
|
931
|
+
return inspectDirectory(root, 0);
|
|
932
|
+
}
|
|
933
|
+
async function findAmbientExecutable(environment, executable) {
|
|
934
|
+
const pathValue = environment.PATH;
|
|
935
|
+
if (!pathValue)
|
|
936
|
+
return null;
|
|
937
|
+
const suffixes = process.platform === "win32" ? [".cmd", ".exe", ""] : [""];
|
|
938
|
+
for (const directory of pathValue.split(process.platform === "win32" ? ";" : ":")) {
|
|
939
|
+
if (!directory)
|
|
940
|
+
continue;
|
|
941
|
+
for (const suffix of suffixes) {
|
|
942
|
+
const candidate = join(directory, `${executable}${suffix}`);
|
|
943
|
+
const info = await lstat(candidate).catch(() => undefined);
|
|
944
|
+
if (!info || (!info.isFile() && !info.isSymbolicLink()))
|
|
945
|
+
continue;
|
|
946
|
+
const resolved = await realpath(candidate).catch(() => undefined);
|
|
947
|
+
if (!resolved)
|
|
948
|
+
continue;
|
|
949
|
+
const resolvedInfo = await lstat(resolved).catch(() => undefined);
|
|
950
|
+
if (resolvedInfo?.isFile() && !resolvedInfo.isSymbolicLink())
|
|
951
|
+
return { path: resolved, ignoredByExactInvocation: true };
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
return null;
|
|
515
955
|
}
|
|
516
956
|
export async function setResearchSetupCredentialFromEnvironment(input) {
|
|
517
957
|
const root = requireAbsoluteWorkspace(input.workspace);
|
|
@@ -795,6 +1235,26 @@ export async function doctorResearchSetup(workspace, options = {}) {
|
|
|
795
1235
|
: "Restore the pinned Skill bytes; setup will not overwrite a drifted or symlinked directory.",
|
|
796
1236
|
});
|
|
797
1237
|
}
|
|
1238
|
+
const provenance = await inspectSetupProvenance(plan, installations, environment);
|
|
1239
|
+
for (const conflict of provenance.ambientSkillConflicts) {
|
|
1240
|
+
const projectInstallation = installations.find((installation) => installation.agent === conflict.agent && installation.skillId === conflict.skillId);
|
|
1241
|
+
const projectInstalled = projectInstallation?.status === "installed";
|
|
1242
|
+
checks.push({
|
|
1243
|
+
id: `skill-scope.${conflict.agent}.${conflict.skillId}`,
|
|
1244
|
+
category: "skill-installation",
|
|
1245
|
+
scope: "research-core",
|
|
1246
|
+
componentIds: [conflict.skillId],
|
|
1247
|
+
status: projectInstalled ? "warn" : "fail",
|
|
1248
|
+
detail: projectInstalled
|
|
1249
|
+
? `A global same-name Skill exists at ${conflict.path}, but the verified project copy is authoritative and the global copy is ignored.`
|
|
1250
|
+
: `SKILL_SCOPE_FALLBACK_UNSAFE: the project copy is not verified while a global same-name Skill exists at ${conflict.path}${conflict.unmanagedPathCliFallback ? " and contains an unmanaged PATH CLI fallback" : ""}.`,
|
|
1251
|
+
minimumAction: projectInstalled
|
|
1252
|
+
? "Remove or update the ignored global copy during separate owner-approved maintenance if it is no longer needed."
|
|
1253
|
+
: "Resume the exact setup plan until the project Skill matches its reviewed tree; do not use the global fallback.",
|
|
1254
|
+
blocking: !projectInstalled,
|
|
1255
|
+
requiredFor: projectInstalled ? [] : ["setup", "research-core"],
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
798
1258
|
for (const setting of requiredSettingsForSkills(selected)) {
|
|
799
1259
|
const configured = plan.settings[setting.id];
|
|
800
1260
|
checks.push({
|
|
@@ -1762,7 +2222,7 @@ async function ensureSetupSourceCheckout(plan, sourceId, runner, environment) {
|
|
|
1762
2222
|
const source = plan.sources.find((candidate) => candidate.id === sourceId);
|
|
1763
2223
|
if (!source)
|
|
1764
2224
|
throw planCatalogDrift(`missing source ${sourceId}`);
|
|
1765
|
-
const checkout =
|
|
2225
|
+
const checkout = setupSourceCheckoutPath(plan, sourceId);
|
|
1766
2226
|
await assertNoSymlinkedExistingPath(dirname(checkout), plan.workspace.path);
|
|
1767
2227
|
let createdCheckout = false;
|
|
1768
2228
|
if (!(await pathExists(checkout))) {
|
|
@@ -1875,6 +2335,42 @@ async function ensureSetupSourceCheckout(plan, sourceId, runner, environment) {
|
|
|
1875
2335
|
}
|
|
1876
2336
|
return checkout;
|
|
1877
2337
|
}
|
|
2338
|
+
function setupSourceCheckoutPath(plan, sourceId) {
|
|
2339
|
+
const source = plan.sources.find((candidate) => candidate.id === sourceId);
|
|
2340
|
+
if (!source)
|
|
2341
|
+
throw planCatalogDrift(`missing source ${sourceId}`);
|
|
2342
|
+
return join(workspacePaths(plan.workspace.path).setupSources, `${source.id}-${source.immutableRef.slice(0, 12)}`);
|
|
2343
|
+
}
|
|
2344
|
+
async function annotateSetupSourceCheckoutFailure(error, plan, sourceId) {
|
|
2345
|
+
if (!(error instanceof CliError) || error.code !== "RESEARCH_SETUP_COMMAND_FAILED")
|
|
2346
|
+
return error;
|
|
2347
|
+
const source = plan.sources.find((candidate) => candidate.id === sourceId);
|
|
2348
|
+
if (!source)
|
|
2349
|
+
return error;
|
|
2350
|
+
const details = isObject(error.details) ? error.details : {};
|
|
2351
|
+
const checkout = setupSourceCheckoutPath(plan, sourceId);
|
|
2352
|
+
return setupError({
|
|
2353
|
+
code: error.code,
|
|
2354
|
+
step: "source-checkout",
|
|
2355
|
+
reason: typeof details.reason === "string" ? details.reason : sanitizeResearchText(error.message),
|
|
2356
|
+
minimumAction: typeof details.minimumAction === "string"
|
|
2357
|
+
? details.minimumAction
|
|
2358
|
+
: "Resolve the source transport failure, then retry only the recorded source-checkout step.",
|
|
2359
|
+
retryCommand: researchSetupRetryCommand({
|
|
2360
|
+
version: plan.cli.version,
|
|
2361
|
+
workspace: plan.workspace.path,
|
|
2362
|
+
step: "source-checkout",
|
|
2363
|
+
}),
|
|
2364
|
+
exitCode: error.exitCode,
|
|
2365
|
+
diagnostics: {
|
|
2366
|
+
sourceId: source.id,
|
|
2367
|
+
repository: source.repository,
|
|
2368
|
+
immutableRef: source.immutableRef,
|
|
2369
|
+
cacheState: (await pathExists(checkout)) ? "partial" : "absent",
|
|
2370
|
+
safeToRetry: true,
|
|
2371
|
+
},
|
|
2372
|
+
});
|
|
2373
|
+
}
|
|
1878
2374
|
async function configureDeterministicSourceCheckout(checkout, runner, cwd, environment) {
|
|
1879
2375
|
for (const [key, value] of [
|
|
1880
2376
|
["core.autocrlf", "false"],
|
|
@@ -2311,6 +2807,12 @@ async function runAcademicPaperCompanion(input) {
|
|
|
2311
2807
|
}
|
|
2312
2808
|
const result = results[0];
|
|
2313
2809
|
if (execution.exitCode !== 0 || result.success !== true) {
|
|
2810
|
+
const sourcesTried = Array.isArray(result.sources_tried)
|
|
2811
|
+
? result.sources_tried
|
|
2812
|
+
.filter((source) => typeof source === "string")
|
|
2813
|
+
.map((source) => sanitizeResearchText(source).slice(0, 100))
|
|
2814
|
+
: [];
|
|
2815
|
+
const adapterError = sanitizeResearchRecord(isObject(result.error) ? result.error : {});
|
|
2314
2816
|
if (result.success === false &&
|
|
2315
2817
|
isObject(result.browser_handoff) &&
|
|
2316
2818
|
result.file === null &&
|
|
@@ -2320,7 +2822,7 @@ async function runAcademicPaperCompanion(input) {
|
|
|
2320
2822
|
skillId: input.skill.id,
|
|
2321
2823
|
skillTreeSha256: input.skill.expectedTreeSha256,
|
|
2322
2824
|
querySha256: sha256Text(doi ?? title),
|
|
2323
|
-
sourcesTried
|
|
2825
|
+
sourcesTried,
|
|
2324
2826
|
artifactCommitted: false,
|
|
2325
2827
|
});
|
|
2326
2828
|
return {
|
|
@@ -2331,20 +2833,32 @@ async function runAcademicPaperCompanion(input) {
|
|
|
2331
2833
|
skillId: input.skill.id,
|
|
2332
2834
|
role: input.skill.role,
|
|
2333
2835
|
artifactCommitted: false,
|
|
2334
|
-
sourcesTried
|
|
2335
|
-
error:
|
|
2836
|
+
sourcesTried,
|
|
2837
|
+
error: adapterError,
|
|
2336
2838
|
provenance: companionProvenance(input.plan, input.skill),
|
|
2337
2839
|
next: "Automatic legal OA sources were exhausted. Follow the installed academic-paper-download browser-handoff reference explicitly; no browser is launched or selected automatically.",
|
|
2338
2840
|
};
|
|
2339
2841
|
}
|
|
2842
|
+
const adapterCode = typeof adapterError.code === "string"
|
|
2843
|
+
? sanitizeResearchText(adapterError.code).trim().slice(0, 100)
|
|
2844
|
+
: "unknown-adapter-error";
|
|
2845
|
+
const adapterMessage = typeof adapterError.message === "string"
|
|
2846
|
+
? sanitizeResearchText(adapterError.message).trim().slice(0, 500)
|
|
2847
|
+
: "";
|
|
2340
2848
|
throw setupError({
|
|
2341
2849
|
code: "RESEARCH_SETUP_COMPANION_COMMAND_FAILED",
|
|
2342
2850
|
step: "companion-paper-download",
|
|
2343
|
-
reason: `The pinned paper adapter
|
|
2344
|
-
minimumAction:
|
|
2851
|
+
reason: `The pinned paper adapter failed (${adapterCode}; exit status ${execution.exitCode}).`,
|
|
2852
|
+
minimumAction: adapterMessage ||
|
|
2853
|
+
sanitizeResearchText(execution.stderr).trim().slice(0, 500) ||
|
|
2345
2854
|
"Inspect the structured adapter error and verify its pinned Python dependencies.",
|
|
2346
2855
|
retryCommand: `tiangong-ai research setup doctor --workspace ${input.root} --json`,
|
|
2347
2856
|
exitCode: 3,
|
|
2857
|
+
diagnostics: {
|
|
2858
|
+
adapterError,
|
|
2859
|
+
sourcesTried,
|
|
2860
|
+
artifactCommitted: false,
|
|
2861
|
+
},
|
|
2348
2862
|
});
|
|
2349
2863
|
}
|
|
2350
2864
|
const artifactPath = requireContainedArtifactPath(result.file, outputDirectory, "file");
|
|
@@ -2637,7 +3151,14 @@ async function loadSetupState(root, planSha256) {
|
|
|
2637
3151
|
typeof value.attempts !== "number" ||
|
|
2638
3152
|
!Number.isInteger(value.attempts) ||
|
|
2639
3153
|
typeof value.updatedAt !== "string" ||
|
|
2640
|
-
!(value.lastError === null ||
|
|
3154
|
+
!(value.lastError === null ||
|
|
3155
|
+
(isObject(value.lastError) &&
|
|
3156
|
+
typeof value.lastError.code === "string" &&
|
|
3157
|
+
typeof value.lastError.step === "string" &&
|
|
3158
|
+
typeof value.lastError.reason === "string" &&
|
|
3159
|
+
typeof value.lastError.minimumAction === "string" &&
|
|
3160
|
+
typeof value.lastError.retryCommand === "string" &&
|
|
3161
|
+
(value.lastError.diagnostics === undefined || isObject(value.lastError.diagnostics))))) {
|
|
2641
3162
|
throw setupError({
|
|
2642
3163
|
code: "RESEARCH_SETUP_STATE_INVALID",
|
|
2643
3164
|
step: "state",
|
|
@@ -2806,9 +3327,25 @@ function requireAbsoluteWorkspace(value) {
|
|
|
2806
3327
|
}
|
|
2807
3328
|
return resolve(value);
|
|
2808
3329
|
}
|
|
2809
|
-
async function
|
|
3330
|
+
export async function resolveResearchSetupWorkspacePath(value, options = {}) {
|
|
3331
|
+
const root = requireAbsoluteWorkspace(value);
|
|
2810
3332
|
const info = await lstat(root).catch(() => undefined);
|
|
2811
|
-
if (
|
|
3333
|
+
if (info) {
|
|
3334
|
+
if (!info.isDirectory() || info.isSymbolicLink()) {
|
|
3335
|
+
throw setupError({
|
|
3336
|
+
code: "RESEARCH_SETUP_WORKSPACE_INVALID",
|
|
3337
|
+
step: "workspace",
|
|
3338
|
+
reason: "Setup workspace must exist as a regular non-symlink directory.",
|
|
3339
|
+
minimumAction: `Create the directory explicitly, then retry with --workspace ${root}.`,
|
|
3340
|
+
retryCommand: "tiangong-ai research setup --help",
|
|
3341
|
+
exitCode: 2,
|
|
3342
|
+
});
|
|
3343
|
+
}
|
|
3344
|
+
const canonicalRoot = await realpath(root);
|
|
3345
|
+
await assertNoSymlinkedExistingPath(canonicalRoot);
|
|
3346
|
+
return canonicalRoot;
|
|
3347
|
+
}
|
|
3348
|
+
if (!options.allowMissingLeaf) {
|
|
2812
3349
|
throw setupError({
|
|
2813
3350
|
code: "RESEARCH_SETUP_WORKSPACE_INVALID",
|
|
2814
3351
|
step: "workspace",
|
|
@@ -2818,7 +3355,24 @@ async function assertWorkspaceDirectory(root) {
|
|
|
2818
3355
|
exitCode: 2,
|
|
2819
3356
|
});
|
|
2820
3357
|
}
|
|
2821
|
-
|
|
3358
|
+
const requestedParent = dirname(root);
|
|
3359
|
+
const canonicalParent = await realpath(requestedParent).catch(() => undefined);
|
|
3360
|
+
const parentInfo = canonicalParent
|
|
3361
|
+
? await lstat(canonicalParent).catch(() => undefined)
|
|
3362
|
+
: undefined;
|
|
3363
|
+
if (!canonicalParent || !parentInfo?.isDirectory() || parentInfo.isSymbolicLink()) {
|
|
3364
|
+
throw setupError({
|
|
3365
|
+
code: "RESEARCH_SETUP_WORKSPACE_INVALID",
|
|
3366
|
+
step: "workspace",
|
|
3367
|
+
reason: "The parent of a new setup workspace must exist as a regular directory.",
|
|
3368
|
+
minimumAction: `Create the parent directory explicitly, then retry with --workspace ${root}.`,
|
|
3369
|
+
retryCommand: "tiangong-ai research setup --help",
|
|
3370
|
+
exitCode: 2,
|
|
3371
|
+
});
|
|
3372
|
+
}
|
|
3373
|
+
const canonicalRoot = join(canonicalParent, basename(root));
|
|
3374
|
+
await assertNoSymlinkedExistingPath(canonicalRoot);
|
|
3375
|
+
return canonicalRoot;
|
|
2822
3376
|
}
|
|
2823
3377
|
function normalizedWorkspaceName(value) {
|
|
2824
3378
|
const normalized = value.trim();
|
|
@@ -2890,6 +3444,15 @@ function setupMutations(root, targets, selected) {
|
|
|
2890
3444
|
reason: "Initialize or verify the auditable research workspace control plane.",
|
|
2891
3445
|
},
|
|
2892
3446
|
];
|
|
3447
|
+
if (selected.some((skill) => skill.id === "tiangong.auto-research")) {
|
|
3448
|
+
for (const target of targets) {
|
|
3449
|
+
mutations.push({
|
|
3450
|
+
step: "recovery-shim",
|
|
3451
|
+
target: join(setupTargetRoot({ workspace: root, scope: "project", agent: target.agent }), RECOVERY_SKILL_NAME),
|
|
3452
|
+
reason: "Create a plan-bound recovery-only routing Skill until the full external orchestrator is verified.",
|
|
3453
|
+
});
|
|
3454
|
+
}
|
|
3455
|
+
}
|
|
2893
3456
|
for (const target of targets) {
|
|
2894
3457
|
for (const skill of selected) {
|
|
2895
3458
|
mutations.push({
|
|
@@ -3466,16 +4029,20 @@ function syntheticPdfText() {
|
|
|
3466
4029
|
function setupFailure(error, fallbackStep, root) {
|
|
3467
4030
|
if (error instanceof CliError && isObject(error.details)) {
|
|
3468
4031
|
const details = sanitizeResearchRecord(error.details);
|
|
4032
|
+
const step = typeof details.step === "string" ? details.step : fallbackStep;
|
|
3469
4033
|
return {
|
|
3470
4034
|
code: error.code,
|
|
3471
|
-
step
|
|
4035
|
+
step,
|
|
3472
4036
|
reason: typeof details.reason === "string" ? details.reason : sanitizeResearchText(error.message),
|
|
3473
4037
|
minimumAction: typeof details.minimumAction === "string"
|
|
3474
4038
|
? details.minimumAction
|
|
3475
4039
|
: "Resolve the reported setup error and retry the exact recorded step.",
|
|
3476
|
-
retryCommand:
|
|
3477
|
-
|
|
3478
|
-
:
|
|
4040
|
+
retryCommand: researchSetupRetryCommand({
|
|
4041
|
+
version: packageVersion(),
|
|
4042
|
+
workspace: root,
|
|
4043
|
+
step,
|
|
4044
|
+
}),
|
|
4045
|
+
...(isObject(details.diagnostics) ? { diagnostics: details.diagnostics } : {}),
|
|
3479
4046
|
};
|
|
3480
4047
|
}
|
|
3481
4048
|
return {
|
|
@@ -3483,7 +4050,11 @@ function setupFailure(error, fallbackStep, root) {
|
|
|
3483
4050
|
step: fallbackStep,
|
|
3484
4051
|
reason: sanitizeResearchText(error instanceof Error ? error.message : String(error)),
|
|
3485
4052
|
minimumAction: "Inspect the sanitized setup status, correct the failure, and retry the exact recorded step.",
|
|
3486
|
-
retryCommand:
|
|
4053
|
+
retryCommand: researchSetupRetryCommand({
|
|
4054
|
+
version: packageVersion(),
|
|
4055
|
+
workspace: root,
|
|
4056
|
+
step: fallbackStep,
|
|
4057
|
+
}),
|
|
3487
4058
|
};
|
|
3488
4059
|
}
|
|
3489
4060
|
function setupError(input) {
|
|
@@ -3491,7 +4062,7 @@ function setupError(input) {
|
|
|
3491
4062
|
step: input.step,
|
|
3492
4063
|
reason: input.reason,
|
|
3493
4064
|
minimumAction: input.minimumAction,
|
|
3494
|
-
retryCommand: input.retryCommand,
|
|
4065
|
+
retryCommand: pinResearchCliCommand(input.retryCommand),
|
|
3495
4066
|
...(input.diagnostics === undefined ? {} : { diagnostics: input.diagnostics }),
|
|
3496
4067
|
});
|
|
3497
4068
|
return new CliError(sanitizeResearchText(input.reason), {
|