@tryaura/aura-cli 0.3.0 → 0.3.2

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.
@@ -1,5 +1,5 @@
1
- import { A as displayPath, C as isRecord$4, D as SHARED_INSTRUCTIONS_TEMPLATE, E as AuraManifestError, S as errorMessage$1, T as resolveAuraManifestPath, _ as assertAuraManifestWritable, a as planDesiredMcpConvergence, b as parseAuraManifest, c as rememberMcpConvergence, d as sharedSkillsRoot, g as hashContent, h as stripLegacyManagedBlock, k as pluralize, m as planSharedSkillTreeUpdate, n as appendInstructionFragments, p as managedContentRevisionStatus, r as forgetMcpPlans, t as planSharedInstructionLink, u as planSkillDeployment, v as createAuraManifestWriteOperation, w as AURA_MANIFEST_PATH, x as errorCode, y as createEmptyAuraManifest } from "./shared-link-plan-C57dSoid.js";
2
- import { COMMAND_NOT_FOUND_EXIT_CODE, DEFAULT_EXEC_TIMEOUT_MS, DEFAULT_HTTP_TIMEOUT_MS, MAX_EXEC_OUTPUT_CHARACTERS, MAX_EXEC_TIMEOUT_MS, MAX_HTTP_RESPONSE_BYTES, MAX_HTTP_TIMEOUT_MS, McpWriteError, NOT_EXECUTABLE_EXIT_CODE, OUTPUT_LIMIT_EXIT_CODE, SHARED_INSTRUCTIONS_TEMPLATE_TOKEN, TIMEOUT_EXIT_CODE, defineOwnProperty, detectExecutable, hasMcpRedaction, jsonPropertyPath, mcpEnvironmentVariableNames, mcpServerNameProblem, normalizeMcpServerDefinition, parseMcpServerDefinition, parseMcpServerManifest, parseSkillFrontmatter, parseSkillReferences, resolveMcpSecretNameCollisions, resolveSkillDirectory, splitSourceLines } from "@tryaura/aura-sdk";
1
+ import { A as displayPath, C as isRecord$4, D as SHARED_INSTRUCTIONS_TEMPLATE, E as AuraManifestError, S as errorMessage$1, T as resolveAuraManifestPath, _ as assertAuraManifestWritable, a as planDesiredMcpConvergence, b as parseAuraManifest, c as rememberMcpConvergence, d as sharedSkillsRoot, g as hashContent, h as stripLegacyManagedBlock, k as pluralize, m as planSharedSkillTreeUpdate, n as appendInstructionFragments, p as managedContentRevisionStatus, r as forgetMcpPlans, t as planSharedInstructionLink, u as planSkillDeployment, v as createAuraManifestWriteOperation, w as AURA_MANIFEST_PATH, x as errorCode, y as createEmptyAuraManifest } from "./shared-link-plan-DMijyUdG.js";
2
+ import { COMMAND_NOT_FOUND_EXIT_CODE, DEFAULT_EXEC_TIMEOUT_MS, DEFAULT_HTTP_TIMEOUT_MS, MAX_EXEC_OUTPUT_CHARACTERS, MAX_EXEC_TIMEOUT_MS, MAX_HTTP_RESPONSE_BYTES, MAX_HTTP_TIMEOUT_MS, McpWriteError, NOT_EXECUTABLE_EXIT_CODE, OUTPUT_LIMIT_EXIT_CODE, SHARED_INSTRUCTIONS_TEMPLATE_TOKEN, TIMEOUT_EXIT_CODE, defineOwnProperty, detectExecutable, hasMcpRedaction, jsonPropertyPath, mcpEnvironmentVariableNames, mcpServerNameProblem, normalizeMcpServerDefinition, parseMcpServerDefinition, parseMcpServerManifest, parseMcpServerManifestValue, parseSkillFrontmatter, parseSkillReferences, resolveMcpSecretNameCollisions, resolveSkillDirectory, splitSourceLines } from "@tryaura/aura-sdk";
3
3
  import { basename, delimiter, dirname, isAbsolute, join, posix, relative, resolve, sep, win32 } from "node:path";
4
4
  import { Buffer as Buffer$1, isUtf8 } from "node:buffer";
5
5
  import { createHash, randomUUID, timingSafeEqual } from "node:crypto";
@@ -10,7 +10,7 @@ import { access, chmod, link, lstat, mkdir, open, opendir, readFile, readdir, re
10
10
  import { applyPatch, createTwoFilesPatch, structuredPatch } from "diff";
11
11
  import { coerce, parse, satisfies, valid, validRange } from "semver";
12
12
  import { isDeepStrictEqual } from "node:util";
13
- import { fileURLToPath } from "node:url";
13
+ import { fileURLToPath, pathToFileURL } from "node:url";
14
14
  import { EnvHttpProxyAgent, fetch as fetch$1 } from "undici";
15
15
  import { gunzipSync } from "node:zlib";
16
16
  import process$1 from "node:process";
@@ -931,25 +931,21 @@ function backupRoot(homeDir) {
931
931
  /**
932
932
  * Determines where a plan may write.
933
933
  *
934
- * The workspace is always one root. The rest are derived from what the detected adapters declared
935
- * as global-scope configuration, so the writable surface of the home directory is exactly the
934
+ * Roots are derived only from Aura's shared home directory and what detected adapters declared as
935
+ * global-scope configuration, so the writable surface of the home directory is exactly the
936
936
  * applications Aura found rather than a hardcoded guess. A declared file contributes its own
937
937
  * directory — `~/.claude/CLAUDE.md` yields `~/.claude`, not `~` — which keeps an adapter that reads
938
938
  * something under a shared directory such as `~/.config/<app>` from opening `~/.config` wholesale.
939
939
  */
940
- function resolveAllowedRoots(model, managedHomeRoots) {
940
+ function resolveAllowedRoots(model) {
941
941
  const homeDir = resolve(model.homeDir);
942
942
  const roots = /* @__PURE__ */ new Map();
943
- add(roots, {
944
- exact: false,
945
- path: resolve(model.projectRoot ?? model.cwd)
946
- });
947
943
  const sharedDirectory = dirname(resolve(model.sharedInstructions.path));
948
944
  if (isStrictDescendant(homeDir, sharedDirectory)) add(roots, {
949
945
  exact: false,
950
946
  path: sharedDirectory
951
947
  });
952
- for (const root of managedHomeRoots === void 0 ? deriveManagedHomeRoots(model, homeDir) : validateManagedHomeRoots(managedHomeRoots, homeDir)) add(roots, root);
948
+ for (const root of deriveManagedHomeRoots(model, homeDir)) add(roots, root);
953
949
  return Object.freeze([...roots.values()]);
954
950
  }
955
951
  /**
@@ -1002,17 +998,6 @@ function rootsForSpec(spec, homeDir) {
1002
998
  path
1003
999
  }];
1004
1000
  }
1005
- function validateManagedHomeRoots(managedHomeRoots, homeDir) {
1006
- return managedHomeRoots.map((candidate) => {
1007
- if (!isAbsolute(candidate)) throw new FixPlanError("invalid-options", `managedHomeRoots must contain absolute paths: ${candidate}`, { path: candidate });
1008
- const path = resolve(candidate);
1009
- if (!isStrictDescendant(homeDir, path)) throw new FixPlanError("invalid-options", `managedHomeRoots must sit strictly inside ${homeDir}: ${candidate}`, { path: candidate });
1010
- return {
1011
- exact: false,
1012
- path
1013
- };
1014
- });
1015
- }
1016
1001
  function add(roots, root) {
1017
1002
  const existing = roots.get(root.path);
1018
1003
  if (existing === void 0 || existing.exact && !root.exact) roots.set(root.path, root);
@@ -1109,12 +1094,12 @@ async function mapWithConcurrency(items, limit, run) {
1109
1094
  //#endregion
1110
1095
  //#region ../core/src/fix-plan/path-policy.ts
1111
1096
  /** Resolves the roots and filesystem traits a plan is validated against. */
1112
- async function createPathPolicy(model, managedHomeRoots) {
1097
+ async function createPathPolicy(model) {
1113
1098
  const sensitivity = await detectCaseSensitivity(resolve(model.projectRoot ?? model.cwd));
1114
1099
  return {
1115
1100
  caseInsensitive: sensitivity !== "sensitive",
1116
1101
  reservedRoots: Object.freeze([backupRoot(model.homeDir)]),
1117
- roots: resolveAllowedRoots(model, managedHomeRoots),
1102
+ roots: resolveAllowedRoots(model),
1118
1103
  rootsCaseInsensitive: sensitivity === "insensitive"
1119
1104
  };
1120
1105
  }
@@ -2878,7 +2863,7 @@ function decodeUtf8(content) {
2878
2863
  //#endregion
2879
2864
  //#region ../core/src/fix-plan/prepare.ts
2880
2865
  async function prepareOperations(options) {
2881
- const policy = await createPathPolicy(options.model, options.managedHomeRoots);
2866
+ const policy = await createPathPolicy(options.model);
2882
2867
  const validated = await validatePlanPaths(options.plan, policy);
2883
2868
  const budget = { remaining: MAX_RETAINED_PLAN_BYTES };
2884
2869
  const operations = [];
@@ -3397,7 +3382,7 @@ async function undoFixPlan(options) {
3397
3382
  if (options.backupId !== void 0) throw new FixPlanError("backup-error", `no undo journal entry named ${options.backupId}`);
3398
3383
  return Object.freeze({ status: "nothing-to-undo" });
3399
3384
  }
3400
- const policy = await createPathPolicy(options.model, options.managedHomeRoots);
3385
+ const policy = await createPathPolicy(options.model);
3401
3386
  return withJournalLock(root, options.now, async () => {
3402
3387
  const selection = await select(root, options);
3403
3388
  if (selection === void 0) return Object.freeze({ status: "nothing-to-undo" });
@@ -3748,50 +3733,32 @@ function portableSkillFilePathKey(path) {
3748
3733
  //#endregion
3749
3734
  //#region ../core/src/workspace/shared-links.ts
3750
3735
  const GLOBAL_PREFIX = "~/";
3751
- const PROJECT_PREFIX = "./";
3752
3736
  const SHARED_INSTRUCTIONS_SEGMENTS = Object.freeze(["agents", "AGENTS.md"]);
3753
3737
  const SHARED_INSTRUCTIONS_HOME_REFERENCE = "~/agents/AGENTS.md";
3754
- const PROJECT_SHARED_INSTRUCTIONS_FILE = "AGENTS.md";
3755
3738
  const SHARED_LINK_KINDS = /* @__PURE__ */ new Set([
3756
3739
  "import-line",
3757
3740
  "native-copy",
3758
3741
  "symlink"
3759
3742
  ]);
3760
- const GLOBAL_SLOT = Object.freeze({
3761
- name: "sharedLink",
3762
- scope: "global"
3763
- });
3764
- const PROJECT_SLOT = Object.freeze({
3765
- name: "projectSharedLink",
3766
- scope: "project"
3767
- });
3743
+ /** The one adapter field a shared-link declaration can come from. */
3744
+ const SLOT_NAME = "sharedLink";
3768
3745
  /** Canonical shared-instruction path for one captured environment. */
3769
3746
  function sharedInstructionsPath(environment) {
3770
3747
  return join(environment.homeDir, ...SHARED_INSTRUCTIONS_SEGMENTS);
3771
3748
  }
3772
3749
  /**
3773
- * Canonical project shared-instruction path for one checkout.
3774
- *
3775
- * Exported because the setup wizard has to name the same file core links adapters to. Two packages
3776
- * each spelling out `AGENTS.md` is how a link ends up pointing at a file setup never wrote.
3777
- */
3778
- function projectSharedInstructionsPath(projectRoot, cwd) {
3779
- return join(projectRoot ?? cwd, PROJECT_SHARED_INSTRUCTIONS_FILE);
3780
- }
3781
- /**
3782
- * Returns every declarative problem in a shared-link contribution to the given scope.
3750
+ * Returns every declarative problem in a shared-link contribution.
3783
3751
  *
3784
- * The prefix is checked against the slot rather than merely being one of the two: a global
3785
- * declaration naming `./something` is what let a home-scoped check demand a file inside whichever
3786
- * repository the user was standing in, written with their home directory spelled out absolutely.
3787
- * Which slot an adapter fills is the whole statement of where the file lives.
3752
+ * The `~/` prefix is required rather than merely accepted. A declaration naming `./something` is
3753
+ * what let a home-scoped check demand a file inside whichever repository the user was standing in,
3754
+ * written with their home directory spelled out absolutely; Aura links only the home entry now, so
3755
+ * the prefix is the whole statement of where the file lives.
3788
3756
  */
3789
- function sharedLinkViolations(link, scope) {
3757
+ function sharedLinkViolations(link) {
3790
3758
  const violations = [];
3791
3759
  if (!SHARED_LINK_KINDS.has(link.kind)) violations.push(`kind "${String(link.kind)}" is not supported`);
3792
- const prefix = entryPrefix(scope);
3793
- const relative = typeof link.entryPath === "string" && link.entryPath.startsWith(prefix) ? link.entryPath.slice(prefix.length) : void 0;
3794
- if (relative === void 0) violations.push(`entryPath must begin with "${prefix}" at ${scope} scope`);
3760
+ const relative = typeof link.entryPath === "string" && link.entryPath.startsWith(GLOBAL_PREFIX) ? link.entryPath.slice(2) : void 0;
3761
+ if (relative === void 0) violations.push(`entryPath must begin with "${GLOBAL_PREFIX}"`);
3795
3762
  else if (relative.length === 0 || relative.includes("\\") || relative.includes("\0") || relative.split("/").some((segment) => segment.length === 0 || segment === "." || segment === "..")) violations.push("entryPath must be a normalized portable path without traversal");
3796
3763
  if (link.kind === "symlink") {
3797
3764
  if (link.lineTemplate !== void 0) violations.push("symlink declarations must not provide lineTemplate");
@@ -3802,51 +3769,36 @@ function sharedLinkViolations(link, scope) {
3802
3769
  }
3803
3770
  /** Resolves and verifies one adapter's shared-link declaration against its read-side files. */
3804
3771
  function resolveAdapterSharedLink(adapter, environment, files) {
3805
- return resolveSharedLink(adapter.sharedLink, GLOBAL_SLOT, environment, files, () => SHARED_INSTRUCTIONS_HOME_REFERENCE);
3806
- }
3807
- function resolveAdapterProjectSharedLink(adapter, environment, files, projectRoot) {
3808
- const target = projectSharedInstructionsPath(projectRoot, environment.cwd);
3809
- return resolveSharedLink(adapter.projectSharedLink, PROJECT_SLOT, environment, files, (entryPath) => portableRelativePath(entryPath, target));
3772
+ return resolveSharedLink(adapter.sharedLink, environment, files);
3810
3773
  }
3811
3774
  /**
3812
- * Resolves one declaration, naming the shared source the way its own scope can survive.
3775
+ * Resolves one declaration, naming the shared source the way a home entry can survive.
3813
3776
  *
3814
- * A home entry names the source through `~`, which is the same string on every machine the user
3815
- * owns. A project entry names it relative to itself, so the file it produces is one the whole team
3816
- * can commit. Nothing here can write a machine-specific absolute path into a repository, because
3817
- * the slot decides both halves at once.
3777
+ * The entry names the source through `~`, which is the same string on every machine the user owns.
3818
3778
  */
3819
- function resolveSharedLink(declaration, slot, environment, files, targetReference) {
3779
+ function resolveSharedLink(declaration, environment, files) {
3820
3780
  if (declaration === void 0) return;
3821
- const entryPath = resolveSharedEntry(declaration, slot, environment, files);
3781
+ const entryPath = resolveSharedEntry(declaration, environment, files);
3822
3782
  if (declaration.kind === "symlink") return {
3823
3783
  entryPath,
3824
3784
  kind: declaration.kind,
3825
- scope: slot.scope
3785
+ scope: "global"
3826
3786
  };
3827
3787
  return {
3828
- content: declaration.lineTemplate?.replace(SHARED_INSTRUCTIONS_TEMPLATE_TOKEN, targetReference(entryPath)),
3788
+ content: declaration.lineTemplate?.replace(SHARED_INSTRUCTIONS_TEMPLATE_TOKEN, SHARED_INSTRUCTIONS_HOME_REFERENCE),
3829
3789
  entryPath,
3830
3790
  kind: declaration.kind,
3831
- scope: slot.scope
3791
+ scope: "global"
3832
3792
  };
3833
3793
  }
3834
- function resolveSharedEntry(declaration, slot, environment, files) {
3835
- const violations = sharedLinkViolations(declaration, slot.scope);
3836
- if (violations.length > 0) throw new Error(`declares an invalid ${slot.name}: ${violations.join("; ")}`);
3837
- const global = slot.scope === "global";
3838
- const relative = declaration.entryPath.slice(entryPrefix(slot.scope).length);
3839
- const entryPath = join(global ? environment.homeDir : environment.cwd, ...relative.split("/"));
3840
- if (![...files.values()].some((file) => resolve(file.spec.path) === resolve(entryPath) && file.spec.scope === slot.scope)) throw new Error(`declares ${slot.name} entry ${entryPath}, but its files() result did not declare that path at ${slot.scope} scope`);
3794
+ function resolveSharedEntry(declaration, environment, files) {
3795
+ const violations = sharedLinkViolations(declaration);
3796
+ if (violations.length > 0) throw new Error(`declares an invalid ${SLOT_NAME}: ${violations.join("; ")}`);
3797
+ const relative = declaration.entryPath.slice(2);
3798
+ const entryPath = join(environment.homeDir, ...relative.split("/"));
3799
+ if (![...files.values()].some((file) => resolve(file.spec.path) === resolve(entryPath) && file.spec.scope === "global")) throw new Error(`declares ${SLOT_NAME} entry ${entryPath}, but its files() result did not declare that path at global scope`);
3841
3800
  return entryPath;
3842
3801
  }
3843
- function portableRelativePath(entryPath, targetPath) {
3844
- const path = relative(dirname(entryPath), targetPath).replaceAll("\\", "/");
3845
- return path.startsWith(".") ? path : `./${path}`;
3846
- }
3847
- function entryPrefix(scope) {
3848
- return scope === "global" ? GLOBAL_PREFIX : PROJECT_PREFIX;
3849
- }
3850
3802
  function countToken(template) {
3851
3803
  return template.split(SHARED_INSTRUCTIONS_TEMPLATE_TOKEN).length - 1;
3852
3804
  }
@@ -3866,6 +3818,41 @@ function toSharedInstructions(path, contents) {
3866
3818
  problem
3867
3819
  };
3868
3820
  }
3821
+ //#endregion
3822
+ //#region ../core/src/plugin-validation-links.ts
3823
+ /**
3824
+ * Validates an adapter's shared-instruction link declaration.
3825
+ *
3826
+ * Two rules, because the contract removed one field and constrained the other. A plugin built
3827
+ * against the previous contract still carries `projectSharedLink`; `apiVersion` already refuses
3828
+ * that plugin, and naming the property is what tells its author which one to delete.
3829
+ */
3830
+ function collectSharedLinkDeclarationViolations(state, adapter, pluginLabel) {
3831
+ if ("projectSharedLink" in adapter) state.violations.push(`${pluginLabel} adapter "${adapter.id}" declares removed projectSharedLink; Aura manages only global shared-instruction links. Remove this property.`);
3832
+ if (adapter.sharedLink === void 0) return;
3833
+ for (const violation of sharedLinkViolations(adapter.sharedLink)) state.violations.push(`${pluginLabel} adapter "${adapter.id}" declares invalid sharedLink: ${violation}.`);
3834
+ }
3835
+ //#endregion
3836
+ //#region ../core/src/plugin-validation-skills.ts
3837
+ /** Rejects skill destinations that are empty, duplicated, or outside the user's home directory. */
3838
+ function collectSkillDirectoryViolations(state, adapter, pluginLabel) {
3839
+ const directories = adapter.capabilities?.skills?.directories;
3840
+ if (directories === void 0) return;
3841
+ if (directories.length === 0) {
3842
+ state.violations.push(`${pluginLabel} adapter "${adapter.id}" declares Agent Skills support without a global skills directory.`);
3843
+ return;
3844
+ }
3845
+ const ids = /* @__PURE__ */ new Set();
3846
+ for (const directory of directories) {
3847
+ if (ids.has(directory.id)) state.violations.push(`${pluginLabel} adapter "${adapter.id}" declares duplicate skills directory ID "${directory.id}".`);
3848
+ ids.add(directory.id);
3849
+ if (!isGlobalSkillPath(directory.entryPath)) state.violations.push(`${pluginLabel} adapter "${adapter.id}" declares invalid global skills directory ${directory.entryPath}.`);
3850
+ }
3851
+ }
3852
+ function isGlobalSkillPath(entryPath) {
3853
+ const relative = entryPath.startsWith("~/") ? entryPath.slice(2) : void 0;
3854
+ return relative !== void 0 && relative.length > 0 && !relative.includes("\\") && !relative.includes("\0") && relative.split("/").every((segment) => segment.length > 0 && segment !== "." && segment !== "..");
3855
+ }
3869
3856
  /** Plugin ids become id namespaces, so `/` and anything shell- or path-surprising is refused. */
3870
3857
  const PLUGIN_ID_PATTERN = /^[a-z0-9][a-z0-9._-]*$/;
3871
3858
  function createRegistryState() {
@@ -3875,7 +3862,7 @@ function createRegistryState() {
3875
3862
  };
3876
3863
  }
3877
3864
  function isSupportedPlugin(candidate) {
3878
- return candidate.apiVersion === 1;
3865
+ return candidate.apiVersion === 2;
3879
3866
  }
3880
3867
  /** Validates each contribution's namespace and collects the ones that claimed their id. */
3881
3868
  function collectNamespaced(state, kind, contributions, plugin, collected) {
@@ -3903,6 +3890,7 @@ function collectUnknownBareCheckIdPlugins(state, bareCheckIdPlugins, candidates)
3903
3890
  function collectIdentityViolations(state, plugin) {
3904
3891
  const violationCount = state.violations.length;
3905
3892
  if (!PLUGIN_ID_PATTERN.test(plugin.id)) state.violations.push(`Plugin "${plugin.name}" declares ID "${plugin.id}", which cannot be used as an ID namespace; expected lowercase letters, digits, ".", "-", or "_", starting with a letter or digit.`);
3893
+ if (plugin.id === "repo") state.violations.push(`Plugin "${plugin.name}" declares ID "repo", which is reserved.`);
3906
3894
  if (plugin.name.trim().length === 0) state.violations.push(`Plugin "${plugin.id}" declares an empty name; reports identify plugins by name.`);
3907
3895
  if (canonicalSemver(plugin.version) !== plugin.version) state.violations.push(`${formatPlugin(plugin)} declares version "${plugin.version}"; expected a semver version such as "1.0.0".`);
3908
3896
  return state.violations.length === violationCount;
@@ -3912,15 +3900,8 @@ function collectAdapterViolations(state, adapters, plugin) {
3912
3900
  for (const adapter of adapters ?? []) {
3913
3901
  if (!PLUGIN_ID_PATTERN.test(adapter.id)) state.violations.push(`${formatPlugin(plugin)} contributes adapter ID "${adapter.id}", which is not a usable adapter ID; expected lowercase letters, digits, ".", "-", or "_", starting with a letter or digit.`);
3914
3902
  collectSharedLinkCapabilityViolations(state, adapter, plugin);
3915
- for (const [name, scope, link] of [[
3916
- "sharedLink",
3917
- "global",
3918
- adapter.sharedLink
3919
- ], [
3920
- "projectSharedLink",
3921
- "project",
3922
- adapter.projectSharedLink
3923
- ]]) if (link !== void 0) for (const violation of sharedLinkViolations(link, scope)) state.violations.push(`${formatPlugin(plugin)} adapter "${adapter.id}" declares invalid ${name}: ${violation}.`);
3903
+ collectSkillDirectoryViolations(state, adapter, formatPlugin(plugin));
3904
+ collectSharedLinkDeclarationViolations(state, adapter, formatPlugin(plugin));
3924
3905
  }
3925
3906
  }
3926
3907
  function collectSharedLinkCapabilityViolations(state, adapter, plugin) {
@@ -3953,8 +3934,8 @@ function claimId(state, kind, id, plugin) {
3953
3934
  return true;
3954
3935
  }
3955
3936
  function formatApiVersionViolation(candidate) {
3956
- const remedy = candidate.apiVersion > 1 ? "Upgrade Aura, or install a plugin release built against" : "Upgrade the plugin to a release built against";
3957
- return `${formatPlugin(candidate)} uses unsupported apiVersion ${candidate.apiVersion}; this Aura build supports apiVersion 1. ${remedy} apiVersion 1.`;
3937
+ const remedy = candidate.apiVersion > 2 ? "Upgrade Aura, or install a plugin release built against" : "Upgrade the plugin to a release built against";
3938
+ return `${formatPlugin(candidate)} uses unsupported apiVersion ${candidate.apiVersion}; this Aura build supports apiVersion 2. ${remedy} apiVersion 2.`;
3958
3939
  }
3959
3940
  function formatViolations(violations) {
3960
3941
  return [violations.length === 1 ? "Aura cannot build the plugin registry:" : `Aura cannot build the plugin registry (${violations.length} problems):`, ...violations.map((violation) => ` - ${violation}`)].join("\n");
@@ -5163,9 +5144,8 @@ function normalizeVersion(version) {
5163
5144
  /**
5164
5145
  * Splits desired servers into the ones Aura may write and the collisions a person has to settle.
5165
5146
  *
5166
- * Scope is part of identity here. A server named `docs` in a project `.mcp.json` is not the `docs`
5167
- * the manifest wants in user-level configuration, and treating them as one either blocks a write
5168
- * that would not have collided or skips one that never happened.
5147
+ * Desired servers are global-only. A server named `docs` in a project `.mcp.json` remains a
5148
+ * read-only observation and cannot collide with the global entry Aura manages.
5169
5149
  */
5170
5150
  function classifyDesired(desired, ledgerNames, state) {
5171
5151
  const ledger = new Set(ledgerNames);
@@ -5184,11 +5164,11 @@ function classifyDesired(desired, ledgerNames, state) {
5184
5164
  /** `owned` to write it, a blocker to refuse, `undefined` when the config already satisfies it. */
5185
5165
  function collisionBlocker(entry, ledger, state) {
5186
5166
  if (ledger.has(entry.name)) return "owned";
5187
- const sameName = (candidate) => candidate.name === entry.name && candidate.scope === entry.scope;
5167
+ const sameName = (candidate) => candidate.name === entry.name && candidate.scope === "global";
5188
5168
  const unusable = state.unusable.find(sameName);
5189
5169
  if (unusable !== void 0) return {
5190
- message: unusable.reason === "disabled" ? `MCP server ${entry.name} is already declared in this application's ${entry.scope} configuration but is turned off there. Remove or enable it, then run the fix again.` : `MCP server ${entry.name} is already declared in this application's ${entry.scope} configuration in a form Aura does not recognize, so Aura left it unchanged.`,
5191
- scope: entry.scope,
5170
+ message: unusable.reason === "disabled" ? `MCP server ${entry.name} is already declared in this application's global configuration but is turned off there. Remove or enable it, then run the fix again.` : `MCP server ${entry.name} is already declared in this application's global configuration in a form Aura does not recognize, so Aura left it unchanged.`,
5171
+ scope: "global",
5192
5172
  sourceId: unusable.sourceId
5193
5173
  };
5194
5174
  const existing = state.servers.filter(sameName);
@@ -5197,7 +5177,7 @@ function collisionBlocker(entry, ledger, state) {
5197
5177
  if ("message" in normalized) return normalized;
5198
5178
  return existing.every((server) => isDeepStrictEqual(server.transport, normalized.transport)) ? void 0 : {
5199
5179
  message: `MCP server ${entry.name} already exists outside Aura's ownership ledger and differs from the manifest.`,
5200
- scope: entry.scope
5180
+ scope: "global"
5201
5181
  };
5202
5182
  }
5203
5183
  /**
@@ -5212,7 +5192,7 @@ function normalizeDesired(entry) {
5212
5192
  } catch (error) {
5213
5193
  return {
5214
5194
  message: error instanceof McpWriteError ? `MCP server ${entry.name} cannot be written as the manifest defines it: ${error.message}` : `MCP server ${entry.name} has a manifest definition Aura cannot represent.`,
5215
- scope: entry.scope
5195
+ scope: "global"
5216
5196
  };
5217
5197
  }
5218
5198
  }
@@ -5226,28 +5206,25 @@ function createAppMcpConvergence(adapter, files, state) {
5226
5206
  const targets = [...files.values()].filter((file) => file.spec.kind === "mcp");
5227
5207
  const classified = classifyDesired(desired, ledgerNames, state);
5228
5208
  if (classified.blockers.length > 0) return blocked(classified.blockers);
5229
- const scopes = ["global", "project"].map((scope) => planScope$1(adapter, writer, targets, classified.owned, ledgerNames, scope, state.secretSightings));
5230
- const blockers = scopes.flatMap((result) => result.blockers);
5231
- const operations = scopes.flatMap((result) => result.operations);
5232
- return blockers.length > 0 ? blocked(blockers) : {
5209
+ const planned = planGlobalTarget(adapter, writer, targets, classified.owned, ledgerNames, state.secretSightings);
5210
+ return planned.blockers.length > 0 ? blocked(planned.blockers) : {
5233
5211
  blockers: [],
5234
- operations,
5212
+ operations: planned.operations,
5235
5213
  ownedNames: [...new Set(classified.owned.map((entry) => entry.name))].sort()
5236
5214
  };
5237
5215
  };
5238
5216
  }
5239
- function planScope$1(adapter, writer, targets, desired, ledgerNames, scope, sightings) {
5240
- const scopedDesired = desired.filter((entry) => entry.scope === scope);
5241
- const scopedTargets = targets.filter((target) => target.spec.scope === scope);
5242
- if (scopedDesired.length > 0 && scopedTargets.length === 0) return blockedScope(`${adapter.displayName} has no ${scope}-scope MCP configuration target.`, scope);
5243
- if (scopedTargets.length > 1) return blockedScope(`${adapter.displayName} declares more than one ${scope}-scope MCP write target.`, scope);
5244
- const target = scopedTargets[0];
5217
+ function planGlobalTarget(adapter, writer, targets, desired, ledgerNames, sightings) {
5218
+ const globalTargets = targets.filter((target) => target.spec.scope === "global");
5219
+ if (desired.length > 0 && globalTargets.length === 0) return blockedScope(`${adapter.displayName} has no global MCP configuration target.`, "global");
5220
+ if (globalTargets.length > 1) return blockedScope(`${adapter.displayName} declares more than one global MCP write target.`, "global");
5221
+ const target = globalTargets[0];
5245
5222
  return target === void 0 ? {
5246
5223
  blockers: [],
5247
5224
  operations: []
5248
- } : writeScopeTarget(adapter, writer, target, scopedDesired, ledgerNames, scope, sightings);
5225
+ } : writeGlobalTarget(adapter, writer, target, desired, ledgerNames, sightings);
5249
5226
  }
5250
- function writeScopeTarget(adapter, writer, target, desired, ledgerNames, scope, sightings) {
5227
+ function writeGlobalTarget(adapter, writer, target, desired, ledgerNames, sightings) {
5251
5228
  if (!needsTargetWrite(target, desired, ledgerNames)) return {
5252
5229
  blockers: [],
5253
5230
  operations: []
@@ -5256,7 +5233,7 @@ function writeScopeTarget(adapter, writer, target, desired, ledgerNames, scope,
5256
5233
  if (unwritable !== void 0) return {
5257
5234
  blockers: [{
5258
5235
  ...unwritable,
5259
- scope,
5236
+ scope: "global",
5260
5237
  sourceId: target.spec.id
5261
5238
  }],
5262
5239
  operations: []
@@ -5266,7 +5243,7 @@ function writeScopeTarget(adapter, writer, target, desired, ledgerNames, scope,
5266
5243
  blockers: [{
5267
5244
  message: written.refusal,
5268
5245
  path: target.spec.path,
5269
- scope,
5246
+ scope: "global",
5270
5247
  sourceId: target.spec.id
5271
5248
  }],
5272
5249
  operations: []
@@ -5275,14 +5252,14 @@ function writeScopeTarget(adapter, writer, target, desired, ledgerNames, scope,
5275
5252
  blockers: [{
5276
5253
  message: `${adapter.displayName}'s MCP configuration at ${target.spec.path} is larger than Aura will rewrite in one operation, so Aura left it unchanged.`,
5277
5254
  path: target.spec.path,
5278
- scope,
5255
+ scope: "global",
5279
5256
  sourceId: target.spec.id
5280
5257
  }],
5281
5258
  operations: []
5282
5259
  };
5283
5260
  const operation = {
5284
5261
  content: written.content,
5285
- mode: scope === "global" ? 384 : 420,
5262
+ mode: 384,
5286
5263
  path: target.spec.path,
5287
5264
  precondition: targetPrecondition(target),
5288
5265
  type: "write"
@@ -5525,11 +5502,9 @@ async function scanAdapter(adapter, context) {
5525
5502
  return { diagnostics: [failure$2(adapter, "files", error)] };
5526
5503
  }
5527
5504
  diagnostics.push(...discovery.diagnostics);
5528
- let projectSharedLink;
5529
5505
  let sharedLink;
5530
5506
  try {
5531
5507
  sharedLink = resolveAdapterSharedLink(adapter, context.environment, discovery.files);
5532
- projectSharedLink = resolveAdapterProjectSharedLink(adapter, context.environment, discovery.files, await context.projectRoot);
5533
5508
  } catch (error) {
5534
5509
  diagnostics.push(failure$2(adapter, "files", error));
5535
5510
  }
@@ -5553,9 +5528,8 @@ async function scanAdapter(adapter, context) {
5553
5528
  mcpServers,
5554
5529
  metadata: snapshot.metadata,
5555
5530
  skills: snapshot.skills,
5556
- skillDirectories: (adapter.capabilities?.skills?.directories ?? []).map((directory) => resolveSkillDirectory(directory, context.environment.homeDir, context.environment.cwd)),
5531
+ skillDirectories: (adapter.capabilities?.skills?.directories ?? []).map((directory) => resolveSkillDirectory(directory, context.environment.homeDir)),
5557
5532
  sourceFiles: [...discovery.files.values()].filter((file) => file.spec.kind !== "probe").map(toStatus),
5558
- ...projectSharedLink === void 0 ? {} : { projectSharedLink },
5559
5533
  ...sharedLink === void 0 ? {} : { sharedLink },
5560
5534
  support: evaluateSupport(adapter.supportedRange, detection.version),
5561
5535
  ...adapter.synthetic === void 0 ? {} : { synthetic: adapter.synthetic },
@@ -6315,12 +6289,13 @@ const MAX_SKILL_DRIVER_CALL_MS = 3e4;
6315
6289
  *
6316
6290
  * Bundled and already-installed trees are local state Aura itself wrote, so they walk unbounded.
6317
6291
  * A driver-materialized tree is whatever a plugin's driver put on disk — the reviewed origin is a
6318
- * claim, not a guarantee — so it walks under {@link DRIVER_WALK_POLICY}, which caps file size,
6319
- * file count, and total bytes, refuses paths that will not survive a portable filesystem, and
6320
- * refuses two paths that would alias on a case-insensitive one.
6292
+ * claim, not a guarantee — so it walks under {@link DRIVER_WALK_POLICY}, which caps directory
6293
+ * count, file size, file count, and total bytes, refuses paths that will not survive a portable
6294
+ * filesystem, and refuses two paths that would alias on a case-insensitive one.
6321
6295
  */
6322
6296
  /** The bounds an untrusted, driver-supplied tree is read under. */
6323
6297
  const DRIVER_WALK_POLICY = Object.freeze({
6298
+ maxDirectories: 200,
6324
6299
  maxFileBytes: MAX_SKILL_FILE_BYTES,
6325
6300
  maxFiles: 200,
6326
6301
  maxTotalBytes: MAX_SKILL_RESPONSE_BYTES
@@ -6334,19 +6309,22 @@ function treeHash(files) {
6334
6309
  return createHash("sha256").update(`${signature}\n`, "utf8").digest("hex");
6335
6310
  }
6336
6311
  /** Reads one skill directory, stopping at the first entry it cannot safely resolve. */
6337
- async function walkTree(root, reader, policy) {
6312
+ async function walkTree(root, reader, policy, outerBoundary) {
6338
6313
  const files = [];
6339
6314
  const entries = [];
6340
6315
  const state = {
6316
+ bytesRead: 0,
6317
+ directoryCount: 0,
6341
6318
  portablePaths: /* @__PURE__ */ new Set(),
6342
6319
  totalBytes: 0
6343
6320
  };
6344
6321
  const path = resolve(root);
6345
6322
  const problem = await walkPath({
6346
- boundary: await reader.realPath(path) ?? path,
6323
+ boundary: outerBoundary ?? await reader.realPath(path) ?? path,
6347
6324
  path
6348
6325
  }, path, reader, files, entries, state, policy);
6349
6326
  return {
6327
+ bytesRead: state.bytesRead,
6350
6328
  entries: Object.freeze(entries),
6351
6329
  files: Object.freeze(files.sort((left, right) => comparePortablePaths(left.path, right.path))),
6352
6330
  ...problem === void 0 ? {} : { problem }
@@ -6368,6 +6346,11 @@ async function walkPath(root, path, reader, files, entries, state, policy) {
6368
6346
  message: `${relativePath || "."} could not be read (${contents.problem})`
6369
6347
  };
6370
6348
  if (contents.entries !== void 0) {
6349
+ if (policy !== void 0 && state.directoryCount >= policy.maxDirectories) return {
6350
+ kind: "too-large",
6351
+ message: `${relativePath || "."} exceeds the skill directory limit`
6352
+ };
6353
+ state.directoryCount += 1;
6371
6354
  entries.push({
6372
6355
  kind: "directory",
6373
6356
  path
@@ -6385,6 +6368,8 @@ function addFile(path, relativePath, contents, files, entries, state, policy) {
6385
6368
  kind: "unsupported",
6386
6369
  message: `${relativePath} is not a readable regular file`
6387
6370
  };
6371
+ const bytes = Buffer$1.byteLength(contents.content, "utf8");
6372
+ state.bytesRead += bytes;
6388
6373
  if (contents.utf8Valid === false) return {
6389
6374
  kind: "unsupported",
6390
6375
  message: `${relativePath} is not valid UTF-8`
@@ -6399,7 +6384,6 @@ function addFile(path, relativePath, contents, files, entries, state, policy) {
6399
6384
  kind: "unsupported",
6400
6385
  message: `${relativePath} aliases another skill file`
6401
6386
  };
6402
- const bytes = Buffer$1.byteLength(contents.content, "utf8");
6403
6387
  if (state.totalBytes + bytes > policy.maxTotalBytes) return {
6404
6388
  kind: "too-large",
6405
6389
  message: `${relativePath} exceeds the skill size limit`
@@ -6425,7 +6409,7 @@ function comparePortablePaths(left, right) {
6425
6409
  //#endregion
6426
6410
  //#region ../core/src/workspace/skills.ts
6427
6411
  const SKILLS_DIAGNOSTIC_ID = "core/skills";
6428
- const SKILL_FILE$1 = "SKILL.md";
6412
+ const SKILL_FILE$2 = "SKILL.md";
6429
6413
  /** Canonical shared skill root for one captured environment. */
6430
6414
  function sharedSkillsPath(environment) {
6431
6415
  return sharedSkillsRoot(environment.homeDir);
@@ -6448,8 +6432,8 @@ async function resolveBundledSkills(registrations, reader) {
6448
6432
  path,
6449
6433
  skill
6450
6434
  };
6451
- if (!tree.files.some((file) => file.path === SKILL_FILE$1)) return {
6452
- message: `does not contain ${SKILL_FILE$1}`,
6435
+ if (!tree.files.some((file) => file.path === SKILL_FILE$2)) return {
6436
+ message: `does not contain ${SKILL_FILE$2}`,
6453
6437
  path,
6454
6438
  skill
6455
6439
  };
@@ -6485,7 +6469,7 @@ async function resolveDriverSkillPack(skill, source, reader) {
6485
6469
  return { kind: "invalid" };
6486
6470
  }
6487
6471
  const tree = await walkTree(path, reader, DRIVER_WALK_POLICY);
6488
- if (tree.problem !== void 0 || !tree.files.some((file) => file.path === SKILL_FILE$1)) return { kind: "invalid" };
6472
+ if (tree.problem !== void 0 || !tree.files.some((file) => file.path === SKILL_FILE$2)) return { kind: "invalid" };
6489
6473
  return {
6490
6474
  kind: "resolved",
6491
6475
  value: Object.freeze({
@@ -6529,8 +6513,8 @@ async function scanSharedSkills(environment, reader) {
6529
6513
  * reports `unreadable` and leaves the real cause to travel in `problemDetail`.
6530
6514
  */
6531
6515
  function sharedSkillDefinition(path, tree, homeDir) {
6532
- const skillFilePath = join(path, SKILL_FILE$1);
6533
- const file = tree.files.find((entry) => entry.path === SKILL_FILE$1);
6516
+ const skillFilePath = join(path, SKILL_FILE$2);
6517
+ const file = tree.files.find((entry) => entry.path === SKILL_FILE$2);
6534
6518
  if (file === void 0) return {
6535
6519
  definitionStatus: tree.problem === void 0 ? "missing-file" : "unreadable",
6536
6520
  skillFilePath
@@ -7264,7 +7248,7 @@ function parseContents(body) {
7264
7248
  }
7265
7249
  //#endregion
7266
7250
  //#region ../core/src/skills/pack-schema.ts
7267
- const SKILL_FILE = "SKILL.md";
7251
+ const SKILL_FILE$1 = "SKILL.md";
7268
7252
  /**
7269
7253
  * Parses a directory's `skills/<id>` body.
7270
7254
  *
@@ -7307,9 +7291,9 @@ function parseDirectorySkillPack(body, expectedId) {
7307
7291
  kind: "invalid",
7308
7292
  problem: files
7309
7293
  };
7310
- if (!files.some((file) => file.path === SKILL_FILE)) return {
7294
+ if (!files.some((file) => file.path === SKILL_FILE$1)) return {
7311
7295
  kind: "invalid",
7312
- problem: `response does not contain a root ${SKILL_FILE}`
7296
+ problem: `response does not contain a root ${SKILL_FILE$1}`
7313
7297
  };
7314
7298
  return {
7315
7299
  files,
@@ -7483,7 +7467,7 @@ async function downloadFiles(environment, location, remoteFiles) {
7483
7467
  async function listAgenticSkills(environment, source, options = {}) {
7484
7468
  const catalog = await loadAgenticCatalog(environment, source, options.noCache === true);
7485
7469
  if (catalog.kind === "failure") return {
7486
- diagnostics: [diagnostic(`Skill source "${source.id}" ${catalog.reason}, so it is unavailable.`)],
7470
+ diagnostics: [diagnostic$1(`Skill source "${source.id}" ${catalog.reason}, so it is unavailable.`)],
7487
7471
  listings: [],
7488
7472
  status: {
7489
7473
  hint: "unreachable",
@@ -7492,7 +7476,7 @@ async function listAgenticSkills(environment, source, options = {}) {
7492
7476
  };
7493
7477
  return {
7494
7478
  collections: catalog.collections,
7495
- diagnostics: [...cacheDiagnostics(source, catalog), ...catalog.problems.map((problem) => diagnostic(`Skill source "${source.id}" catalog ${problem}, so some of it is unavailable.`))],
7479
+ diagnostics: [...cacheDiagnostics(source, catalog), ...catalog.problems.map((problem) => diagnostic$1(`Skill source "${source.id}" catalog ${problem}, so some of it is unavailable.`))],
7496
7480
  listings: Object.freeze(catalog.entries.map(({ listing }) => Object.freeze({
7497
7481
  ...listing,
7498
7482
  source
@@ -7506,7 +7490,7 @@ async function listAgenticSkills(environment, source, options = {}) {
7506
7490
  function cacheDiagnostics(source, catalog) {
7507
7491
  if (catalog.cacheAgeMs === void 0) return [];
7508
7492
  const age = describeCacheAge(catalog.cacheAgeMs);
7509
- return [diagnostic(catalog.staleAfterFailure === true ? `Skill source "${source.id}" could not be reached, so its listing is served from the local cache (${age}).` : `Skill source "${source.id}" listing served from the local cache (${age}); pass --no-cache to refetch it now.`)];
7493
+ return [diagnostic$1(catalog.staleAfterFailure === true ? `Skill source "${source.id}" could not be reached, so its listing is served from the local cache (${age}).` : `Skill source "${source.id}" listing served from the local cache (${age}); pass --no-cache to refetch it now.`)];
7510
7494
  }
7511
7495
  /** Resolves AgenticSkills entries from their exact GitHub directories into reviewed Aura packs. */
7512
7496
  async function resolveAgenticSkills(environment, source, skillIds) {
@@ -7529,7 +7513,7 @@ async function resolveAgenticSkills(environment, source, skillIds) {
7529
7513
  function catalogEntries(catalog) {
7530
7514
  return catalog.kind === "catalog" ? new Map(catalog.entries.map((entry) => [entry.listing.id, entry])) : /* @__PURE__ */ new Map();
7531
7515
  }
7532
- function diagnostic(message) {
7516
+ function diagnostic$1(message) {
7533
7517
  return {
7534
7518
  adapterId: AGENTICSKILLS_DIAGNOSTIC_ID,
7535
7519
  message,
@@ -7537,7 +7521,7 @@ function diagnostic(message) {
7537
7521
  };
7538
7522
  }
7539
7523
  function skillDiagnostic(source, id, reason) {
7540
- return diagnostic(`Skill "${id}" from "${source.id}" ${reason}, so it is unavailable.`);
7524
+ return diagnostic$1(`Skill "${id}" from "${source.id}" ${reason}, so it is unavailable.`);
7541
7525
  }
7542
7526
  function isDiagnostic(value) {
7543
7527
  return "adapterId" in value;
@@ -7851,7 +7835,7 @@ const DIRECTORY_PREFIX = "directory:";
7851
7835
  /** Namespaced MCP catalog id, such as `official/github`. */
7852
7836
  const MCP_CATALOG_ID_PATTERN = /^[a-z0-9][a-z0-9._-]*\/[a-zA-Z0-9][a-zA-Z0-9._-]*$/u;
7853
7837
  const SKILL_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
7854
- const SKILL_SOURCE_ID_PATTERN = /^(?:directory|driver|plugin):[^\s:]+$/u;
7838
+ const SKILL_SOURCE_ID_PATTERN = /^(?:directory|driver|plugin|repo):[^\s:]+$/u;
7855
7839
  /**
7856
7840
  * Picks the first collected problem out of a batch of independently validated fields.
7857
7841
  *
@@ -7984,6 +7968,52 @@ function collectJsonValue(value, path, depth) {
7984
7968
  return collectJsonObject(value, path, depth);
7985
7969
  }
7986
7970
  //#endregion
7971
+ //#region ../core/src/preset/repo-content-limits.ts
7972
+ /**
7973
+ * The largest combined byte size of a repository's content set.
7974
+ *
7975
+ * The snapshot lives in memory for the whole run so planners apply consented bytes rather than a
7976
+ * re-read; a repository must not be able to balloon that residency.
7977
+ */
7978
+ const MAX_REPO_CONTENT_TOTAL_BYTES = 16e6;
7979
+ //#endregion
7980
+ //#region ../core/src/preset/schema-provides.ts
7981
+ const REPO_MCP_ID_PATTERN = /^repo\/[a-zA-Z0-9][a-zA-Z0-9._-]*$/u;
7982
+ /**
7983
+ * Collects the `provides` envelope of a repository preset.
7984
+ *
7985
+ * Content a preset authors reaches command lines and app configuration, so it may only arrive
7986
+ * from the file the user hash-trusted for this repository. Callers validating a downloaded or
7987
+ * bundled preset reject the field wholesale before this collector runs.
7988
+ */
7989
+ function collectProvides(value) {
7990
+ if (value === void 0) return;
7991
+ if (!isRecord$4(value)) return "$.provides: must be an object";
7992
+ const servers = collectProvidedMcpServers(value["mcpServers"]);
7993
+ if (typeof servers === "string") return servers;
7994
+ return servers === void 0 ? Object.freeze({}) : Object.freeze({ mcpServers: servers });
7995
+ }
7996
+ function collectProvidedMcpServers(value) {
7997
+ if (value === void 0) return;
7998
+ if (!Array.isArray(value)) return "$.provides.mcpServers: must be an array of MCP server definitions";
7999
+ if (value.length > 16) return `$.provides.mcpServers: must contain at most ${String(16)} definitions`;
8000
+ const result = [];
8001
+ const ids = /* @__PURE__ */ new Set();
8002
+ const serverNames = /* @__PURE__ */ new Set();
8003
+ for (const [index, candidate] of value.entries()) {
8004
+ const path = `$.provides.mcpServers[${String(index)}]`;
8005
+ const parsed = parseMcpServerManifestValue(candidate);
8006
+ if ("error" in parsed) return `${path}${parsed.error.path.slice(1)}: ${parsed.error.message}`;
8007
+ if (!REPO_MCP_ID_PATTERN.test(parsed.value.id)) return `${path}.id: must be namespaced "repo/<name>"`;
8008
+ if (ids.has(parsed.value.id)) return `${path}.id: must not duplicate another provided server id`;
8009
+ if (serverNames.has(parsed.value.serverName)) return `${path}.serverName: must not duplicate another provided server name`;
8010
+ ids.add(parsed.value.id);
8011
+ serverNames.add(parsed.value.serverName);
8012
+ result.push(parsed.value);
8013
+ }
8014
+ return Object.freeze(result);
8015
+ }
8016
+ //#endregion
7987
8017
  //#region ../core/src/preset/schema-sources.ts
7988
8018
  function collectSkills(value) {
7989
8019
  if (value === void 0) return;
@@ -7997,7 +8027,7 @@ function collectSkills(value) {
7997
8027
  const id = candidate["id"];
7998
8028
  const source = candidate["source"];
7999
8029
  if (typeof id !== "string" || !SKILL_ID_PATTERN.test(id)) return `${path}.id: must be a kebab-case skill id`;
8000
- if (typeof source !== "string" || !SKILL_SOURCE_ID_PATTERN.test(source)) return `${path}.source: must be a plugin:, directory:, or driver: source id`;
8030
+ if (typeof source !== "string" || !SKILL_SOURCE_ID_PATTERN.test(source)) return `${path}.source: must be a plugin:, directory:, driver:, or repo: source id`;
8001
8031
  const identity = `${source}/${id}`;
8002
8032
  if (seen.has(identity)) return `${path}: must not duplicate another skill selection`;
8003
8033
  seen.add(identity);
@@ -8071,31 +8101,32 @@ function toDirectoryId(id) {
8071
8101
  function toSourceId(id) {
8072
8102
  if (id.startsWith("directory:")) return toDirectoryId(id);
8073
8103
  if (id.startsWith("driver:")) return `driver:${id.slice(7)}`;
8104
+ if (id.startsWith("repo:")) return `repo:${id.slice(5)}`;
8074
8105
  return `plugin:${id.slice(id.indexOf(":") + 1)}`;
8075
8106
  }
8076
8107
  //#endregion
8077
8108
  //#region ../core/src/preset/schema.ts
8078
8109
  /** Validates and freezes one parsed JSON document as a data-only team preset. */
8079
- function validateTeamPreset(value) {
8110
+ function validateTeamPreset(value, options = {}) {
8080
8111
  if (!isRecord$4(value)) return invalid$2("$: must be an object");
8081
- const depthProblem = documentDepthProblem(value, "$", 0);
8082
- if (depthProblem !== void 0) return invalid$2(depthProblem);
8083
- if (value["schemaVersion"] !== 1) return invalid$2("$.schemaVersion: must be 1");
8112
+ const problemBeforeFields = envelopeProblem(value, options);
8113
+ if (problemBeforeFields !== void 0) return invalid$2(problemBeforeFields);
8084
8114
  const name = value["name"];
8085
- if (name !== void 0 && (typeof name !== "string" || name.trim().length === 0 || name.length > 128)) return invalid$2("$.name: must be a non-empty string of at most 128 characters");
8086
8115
  const checks = collectChecks(value["checks"]);
8087
8116
  const allowed = collectAllowedSources(value["allowedSkillSources"]);
8088
8117
  const directories = collectDirectories(value["skillDirectories"]);
8089
8118
  const requiredMcpServers = collectIds(value["requiredMcpServers"], "$.requiredMcpServers", MCP_CATALOG_ID_PATTERN);
8090
8119
  const snippets = collectIds(value["snippets"], "$.snippets", CONTENT_ID_PATTERN);
8091
8120
  const skills = collectSkills(value["skills"]);
8121
+ const provides = collectProvides(value["provides"]);
8092
8122
  const problem = firstProblem([
8093
8123
  checks,
8094
8124
  allowed,
8095
8125
  directories,
8096
8126
  requiredMcpServers,
8097
8127
  snippets,
8098
- skills
8128
+ skills,
8129
+ provides
8099
8130
  ]);
8100
8131
  if (problem !== void 0) return invalid$2(problem);
8101
8132
  return {
@@ -8103,6 +8134,7 @@ function validateTeamPreset(value) {
8103
8134
  preset: Object.freeze({
8104
8135
  ...Array.isArray(allowed) ? { allowedSkillSources: allowed } : {},
8105
8136
  ...isChecks(checks) ? { checks } : {},
8137
+ ...typeof provides === "object" ? { provides } : {},
8106
8138
  ...Array.isArray(requiredMcpServers) ? { requiredMcpServers } : {},
8107
8139
  ...typeof name === "string" ? { name } : {},
8108
8140
  schemaVersion: 1,
@@ -8118,6 +8150,15 @@ function invalid$2(problem) {
8118
8150
  problem
8119
8151
  };
8120
8152
  }
8153
+ /** Checks the document envelope — depth, version, origin gate, name — before any field walks. */
8154
+ function envelopeProblem(value, options) {
8155
+ const depthProblem = documentDepthProblem(value, "$", 0);
8156
+ if (depthProblem !== void 0) return depthProblem;
8157
+ if (value["schemaVersion"] !== 1) return "$.schemaVersion: must be 1";
8158
+ if (options.allowProvides !== true && value["provides"] !== void 0) return "$.provides: only the repository preset may provide content";
8159
+ const name = value["name"];
8160
+ if (name !== void 0 && (typeof name !== "string" || name.trim().length === 0 || name.length > 128)) return "$.name: must be a non-empty string of at most 128 characters";
8161
+ }
8121
8162
  /**
8122
8163
  * Rejects a document nested past the limit before any field is read.
8123
8164
  *
@@ -8967,7 +9008,7 @@ async function readTeamPreset(cwd, reader) {
8967
9008
  } catch {
8968
9009
  return failure(path, "is not valid JSON");
8969
9010
  }
8970
- const result = validateTeamPreset(parsed);
9011
+ const result = validateTeamPreset(parsed, { allowProvides: true });
8971
9012
  if (result.kind === "invalid") return failure(path, `is not a valid team preset (${result.problem})`);
8972
9013
  return {
8973
9014
  content: contents.content,
@@ -8989,18 +9030,275 @@ function failure(path, reason) {
8989
9030
  };
8990
9031
  }
8991
9032
  //#endregion
8992
- //#region ../core/src/preset/repo-trust.ts
9033
+ //#region ../core/src/skills/repo-source.ts
9034
+ const REPO_SKILLS_DIAGNOSTIC_ID = "core/repo-skills";
9035
+ const SKILL_FILE = "SKILL.md";
9036
+ /** The one source id repository skills are offered under; one repository, one source. */
9037
+ const REPO_SKILL_SOURCE_ID = "repo:workspace";
9038
+ /** The repository skills directory as a picker-facing source. */
9039
+ function repoSkillSource(path) {
9040
+ return Object.freeze({
9041
+ id: REPO_SKILL_SOURCE_ID,
9042
+ kind: "repo",
9043
+ name: "This repository",
9044
+ path
9045
+ });
9046
+ }
9047
+ /**
9048
+ * Resolves every skill tree below `.aura/skills` into an installable pack.
9049
+ *
9050
+ * A broken entry earns a diagnostic and drops out rather than failing the run: unlike snippets,
9051
+ * these trees are outside the trust hash — they are offers the per-skill review gates, so an
9052
+ * unreadable one is a smaller catalog, not a broken consent record. Each tree walks under the
9053
+ * driver policy: a cloned tree deserves exactly the suspicion a driver-materialized one gets.
9054
+ */
9055
+ async function resolveRepoSkills(root, boundary, reader, maxTotalBytes) {
9056
+ const rootRead = await reader.readWithin(root, [boundary]);
9057
+ const { contents } = rootRead;
9058
+ if (!contents.exists) return {
9059
+ diagnostics: [],
9060
+ skills: []
9061
+ };
9062
+ if (rootRead.kind !== "read" || contents.pathKind === "symlink" || contents.problem !== void 0 || contents.entries === void 0) return {
9063
+ diagnostics: [{
9064
+ adapterId: REPO_SKILLS_DIAGNOSTIC_ID,
9065
+ message: "Repository skills path .aura/skills is not a directory, so none are offered.",
9066
+ path: root,
9067
+ phase: "read"
9068
+ }],
9069
+ skills: []
9070
+ };
9071
+ const diagnostics = [];
9072
+ const named = contents.entries.filter((entry) => !entry.startsWith("."));
9073
+ if (named.length > 32) diagnostics.push({
9074
+ adapterId: REPO_SKILLS_DIAGNOSTIC_ID,
9075
+ message: `Repository skills directory holds more than the ${String(32)} entries Aura offers; the rest are ignored.`,
9076
+ path: root,
9077
+ phase: "read"
9078
+ });
9079
+ const skills = [];
9080
+ const source = repoSkillSource(root);
9081
+ let bytesRead = 0;
9082
+ for (const id of named.slice(0, 32)) {
9083
+ const remainingBytes = maxTotalBytes - bytesRead;
9084
+ if (remainingBytes <= 0) {
9085
+ diagnostics.push(diagnostic(join(root, id), "exceeds the repository content size budget"));
9086
+ continue;
9087
+ }
9088
+ const outcome = await resolveOneSkill(root, id, source, reader, boundary, remainingWalkPolicy(remainingBytes));
9089
+ bytesRead += outcome.bytesRead;
9090
+ if (outcome.problem !== void 0) {
9091
+ diagnostics.push(diagnostic(join(root, id), outcome.bytesRead >= remainingBytes ? "exceeds the repository content size budget" : outcome.problem));
9092
+ continue;
9093
+ }
9094
+ skills.push(outcome.skill);
9095
+ }
9096
+ return {
9097
+ diagnostics: Object.freeze(diagnostics),
9098
+ skills: Object.freeze(skills)
9099
+ };
9100
+ }
9101
+ async function resolveOneSkill(root, id, source, reader, boundary, policy) {
9102
+ const idProblem = skillIdProblem(id);
9103
+ if (idProblem !== void 0) return {
9104
+ bytesRead: 0,
9105
+ problem: idProblem
9106
+ };
9107
+ const tree = await walkTree(join(root, id), reader, policy, boundary);
9108
+ if (tree.problem !== void 0) return {
9109
+ bytesRead: tree.bytesRead,
9110
+ problem: tree.problem.message
9111
+ };
9112
+ const definition = tree.files.find((file) => file.path === SKILL_FILE);
9113
+ if (definition === void 0) return {
9114
+ bytesRead: tree.bytesRead,
9115
+ problem: `does not contain ${SKILL_FILE}`
9116
+ };
9117
+ const frontmatter = parseSkillFrontmatter(definition.content);
9118
+ return {
9119
+ bytesRead: tree.bytesRead,
9120
+ skill: Object.freeze({
9121
+ description: frontmatter.description ?? "Repository skill.",
9122
+ files: tree.files,
9123
+ id,
9124
+ name: frontmatter.name ?? id,
9125
+ source,
9126
+ treeHash: treeHash(tree.files),
9127
+ version: frontmatter.version ?? "0.0.0"
9128
+ })
9129
+ };
9130
+ }
9131
+ /** Caps each attempt by what remains of the repository-wide read budget. */
9132
+ function remainingWalkPolicy(remainingBytes) {
9133
+ return Object.freeze({
9134
+ ...DRIVER_WALK_POLICY,
9135
+ maxFileBytes: Math.min(DRIVER_WALK_POLICY.maxFileBytes, remainingBytes),
9136
+ maxTotalBytes: Math.min(DRIVER_WALK_POLICY.maxTotalBytes, remainingBytes)
9137
+ });
9138
+ }
9139
+ function diagnostic(path, message) {
9140
+ return {
9141
+ adapterId: REPO_SKILLS_DIAGNOSTIC_ID,
9142
+ message: `Repository skill ${message}, so it is not offered.`,
9143
+ path,
9144
+ phase: "read"
9145
+ };
9146
+ }
9147
+ //#endregion
9148
+ //#region ../core/src/preset/repo-content-files.ts
9149
+ /** Matches the optional frontmatter block a snippet file may open with. */
9150
+ const FRONTMATTER_PATTERN = /^---\r?\n[\s\S]*?\r?\n---(?:\r?\n|$)/u;
9151
+ /** Reads every Markdown snippet below `.aura/snippets` under the trust-set rules. */
9152
+ async function readRepoSnippets(root, boundary, reader) {
9153
+ const contents = await reader.read(root);
9154
+ if (!contents.exists) return {
9155
+ files: [],
9156
+ status: "ready"
9157
+ };
9158
+ if (contents.entries === void 0) return {
9159
+ problem: "the snippets path is not a directory",
9160
+ status: "invalid"
9161
+ };
9162
+ const names = contents.entries.filter((entry) => !entry.startsWith(".") && entry.endsWith(".md"));
9163
+ if (names.length > 64) return {
9164
+ problem: `holds more than the ${String(64)} snippet limit`,
9165
+ status: "invalid"
9166
+ };
9167
+ const files = [];
9168
+ for (const name of names) {
9169
+ const result = await readSnippetFile(root, name, boundary, reader);
9170
+ if (typeof result === "string") return {
9171
+ problem: result,
9172
+ status: "invalid"
9173
+ };
9174
+ files.push(result);
9175
+ }
9176
+ files.sort((left, right) => left.entry.id < right.entry.id ? -1 : 1);
9177
+ return {
9178
+ files: Object.freeze(files),
9179
+ status: "ready"
9180
+ };
9181
+ }
9182
+ async function readSnippetFile(root, name, boundary, reader) {
9183
+ const stem = name.slice(0, -3);
9184
+ if (skillIdProblem(stem) !== void 0) return "contains an entry that is not a kebab-case Markdown file name";
9185
+ const read = await reader.readWithin(join(root, name), [boundary], { maxBytes: MAX_SNIPPET_BYTES });
9186
+ const problem = snippetReadProblem(read);
9187
+ if (problem !== void 0) return problem;
9188
+ const text = read.contents.content ?? "";
9189
+ const frontmatter = parseSkillFrontmatter(text);
9190
+ const body = text.replace(FRONTMATTER_PATTERN, "");
9191
+ return {
9192
+ entry: Object.freeze({
9193
+ body,
9194
+ ...frontmatter.description === void 0 ? {} : { description: frontmatter.description },
9195
+ id: `repo/${stem}`,
9196
+ name: frontmatter.name ?? stem
9197
+ }),
9198
+ text
9199
+ };
9200
+ }
9201
+ /** Why a bounded, boundary-checked read did not yield a plain snippet file. */
9202
+ function snippetReadProblem(read) {
9203
+ if (read.kind === "outside") return "contains an entry that leads outside the repository's .aura directory";
9204
+ if (read.kind === "unverified" || read.contents.pathKind === "symlink") return "contains an entry that is not a regular file";
9205
+ const { contents } = read;
9206
+ if (contents.problem !== void 0 || contents.isDirectory || contents.content === void 0 || contents.utf8Valid === false) return "contains an entry that is not a readable UTF-8 file";
9207
+ if ((contents.size ?? 0) > 256e3) return `contains an entry larger than the ${String(MAX_SNIPPET_BYTES)} byte snippet limit`;
9208
+ }
9209
+ //#endregion
9210
+ //#region ../core/src/preset/repo-content.ts
9211
+ const REPO_CONTENT_DIAGNOSTIC_ID = "core/repo-content";
9212
+ /**
9213
+ * Reads the repository's trust-hashed content and any requested installable content in one snapshot.
9214
+ *
9215
+ * The result is what catalogs and planners consume for the whole run: applying from this snapshot
9216
+ * rather than from a re-read is what closes the window between the bytes the user consented to
9217
+ * and the bytes a later write could put on disk. A broken snippet set fails the run — those bytes
9218
+ * are inside the trust hash — while a broken skill tree only shrinks the offers, because skills
9219
+ * stay behind the per-skill review.
9220
+ */
9221
+ async function readRepoContent(auraDir, preset, presetText, reader, options = {}) {
9222
+ const boundary = await reader.realPath(auraDir) ?? auraDir;
9223
+ const snippets = await readRepoSnippets(join(auraDir, "snippets"), boundary, reader);
9224
+ if (snippets.status === "invalid") return {
9225
+ diagnostics: [{
9226
+ adapterId: REPO_CONTENT_DIAGNOSTIC_ID,
9227
+ message: `Repository snippets directory ".aura/snippets" ${snippets.problem}.`,
9228
+ path: join(auraDir, "snippets"),
9229
+ phase: "read"
9230
+ }],
9231
+ status: "invalid"
9232
+ };
9233
+ const snippetBytes = snippets.files.reduce((sum, file) => sum + Buffer$1.byteLength(file.text, "utf8"), 0);
9234
+ if (snippetBytes > 16e6) return {
9235
+ diagnostics: [{
9236
+ adapterId: REPO_CONTENT_DIAGNOSTIC_ID,
9237
+ message: `Repository snippets exceed the ${String(MAX_REPO_CONTENT_TOTAL_BYTES)} byte repository content budget.`,
9238
+ path: join(auraDir, "snippets"),
9239
+ phase: "read"
9240
+ }],
9241
+ status: "invalid"
9242
+ };
9243
+ const skills = options.includeSkills === false ? {
9244
+ diagnostics: [],
9245
+ skills: []
9246
+ } : await resolveRepoSkills(join(auraDir, "skills"), boundary, reader, MAX_REPO_CONTENT_TOTAL_BYTES - snippetBytes);
9247
+ return {
9248
+ contentSet: Object.freeze({
9249
+ mcpServers: preset.provides?.mcpServers ?? [],
9250
+ skills: skills.skills,
9251
+ snippets: Object.freeze(snippets.files.map((file) => file.entry))
9252
+ }),
9253
+ diagnostics: skills.diagnostics,
9254
+ hash: hashRepoContentSet(presetText, snippets.files.map((file) => ({
9255
+ id: file.entry.id,
9256
+ text: file.text
9257
+ }))),
9258
+ status: "ready"
9259
+ };
9260
+ }
9261
+ /**
9262
+ * Lifts the preset's inline MCP definitions into catalog entries.
9263
+ *
9264
+ * The `repo/` namespace is reserved against plugins at registry build, so these can never collide
9265
+ * with — or be shadowed by — a plugin catalog entry. The source URL names the preset file itself:
9266
+ * that is the file whose bytes the user trusted.
9267
+ */
9268
+ function repoMcpServerDefs(servers, presetPath) {
9269
+ return Object.freeze(servers.map((manifest) => Object.freeze({
9270
+ description: manifest.description,
9271
+ id: manifest.id,
9272
+ kind: "mcp-server",
9273
+ manifest,
9274
+ name: manifest.name,
9275
+ source: Object.freeze({
9276
+ type: "file",
9277
+ url: pathToFileURL(presetPath).href
9278
+ }),
9279
+ version: "0.0.0"
9280
+ })));
9281
+ }
8993
9282
  /**
8994
- * Hashes repository preset contents for the trust record.
9283
+ * Hashes the repository content set for the trust record.
8995
9284
  *
8996
- * Normalizes line endings so a checkout that rewrites them does not invalidate a trust the user
8997
- * already granted to the same bytes-as-authored.
9285
+ * With no snippet files the hash is exactly the preset file's own content hash, so a repository
9286
+ * that never adds `.aura/snippets` keeps every trust its users already recorded. The first
9287
+ * snippet changes the descriptor shape — and consent is re-asked, which is the point.
8998
9288
  */
8999
- function hashRepoPreset(content) {
9000
- return hashContent(content);
9289
+ function hashRepoContentSet(presetText, snippets) {
9290
+ if (snippets.length === 0) return hashContent(presetText);
9291
+ const lines = [
9292
+ "aura-repo-content-v1",
9293
+ `preset ${hashContent(presetText)}`,
9294
+ ...[...snippets].sort((left, right) => left.id < right.id ? -1 : 1).map((file) => `snippet ${file.id} ${hashContent(file.text)}`)
9295
+ ];
9296
+ return hashContent(lines.join("\n"));
9001
9297
  }
9298
+ //#endregion
9299
+ //#region ../core/src/preset/repo-trust.ts
9002
9300
  /** Reads and validates the repository preset below the invoking directory. */
9003
- async function readRepoPreset(environment, reader = createFileReader()) {
9301
+ async function readRepoPreset(environment, reader = createFileReader(), options = {}) {
9004
9302
  const cwd = await reader.realPath(environment.cwd) ?? environment.cwd;
9005
9303
  const path = resolveTeamPresetPath(cwd);
9006
9304
  const state = await readTeamPreset(cwd, reader);
@@ -9009,10 +9307,17 @@ async function readRepoPreset(environment, reader = createFileReader()) {
9009
9307
  path,
9010
9308
  status: state.status
9011
9309
  };
9310
+ const content = await readRepoContent(dirname(path), state.preset, state.content, reader, options);
9311
+ if (content.status === "invalid") return {
9312
+ diagnostics: [...state.diagnostics, ...content.diagnostics],
9313
+ path,
9314
+ status: "invalid"
9315
+ };
9012
9316
  const mainWorktreePath = await resolveMainWorktreePresetPath(cwd, path, reader);
9013
9317
  return {
9014
- diagnostics: state.diagnostics,
9015
- hash: hashRepoPreset(state.content),
9318
+ contentSet: content.contentSet,
9319
+ diagnostics: [...state.diagnostics, ...content.diagnostics],
9320
+ hash: content.hash,
9016
9321
  ...mainWorktreePath === void 0 ? {} : { mainWorktreePath },
9017
9322
  path,
9018
9323
  preset: state.preset,
@@ -9057,7 +9362,7 @@ function applyRequiredMcpServers(model, config) {
9057
9362
  model
9058
9363
  };
9059
9364
  const manifest = model.manifest.value;
9060
- const configured = new Set(manifest.mcpServers.map((server) => `${server.scope}\0${server.name}`));
9365
+ const configured = new Set(manifest.mcpServers.map((server) => server.name));
9061
9366
  const configuredCatalogIds = new Set(manifest.mcpServers.flatMap((server) => server.catalogId === void 0 ? [] : [server.catalogId]));
9062
9367
  const diagnostics = [];
9063
9368
  const required = [];
@@ -9078,7 +9383,7 @@ function applyRequiredMcpServers(model, config) {
9078
9383
  continue;
9079
9384
  }
9080
9385
  const name = catalog.manifest.serverName;
9081
- if (configured.has(`global\0${name}`)) {
9386
+ if (configured.has(name)) {
9082
9387
  diagnostics.push(note(`is already configured as "${name}", so your own settings were kept`, catalog.id, selection));
9083
9388
  continue;
9084
9389
  }
@@ -9092,7 +9397,6 @@ function applyRequiredMcpServers(model, config) {
9092
9397
  catalogId: catalog.id,
9093
9398
  name,
9094
9399
  requiredBy: selection.provenance.label,
9095
- scope: "global",
9096
9400
  transport: catalog.manifest.transportTemplate
9097
9401
  }));
9098
9402
  }
@@ -9117,6 +9421,91 @@ function note(reason, id, selection) {
9117
9421
  };
9118
9422
  }
9119
9423
  //#endregion
9424
+ //#region src/runtime-config.ts
9425
+ /** Loads the selected preset and resolves all runtime layers once for a command. */
9426
+ async function resolveRuntimeConfig(input) {
9427
+ if (input.manifest.status === "read-only") return {
9428
+ message: input.manifest.problem.message,
9429
+ status: "invalid"
9430
+ };
9431
+ const manifest = input.manifest.status === "ready" ? input.manifest.value : void 0;
9432
+ const loaded = await loadTeamPreset({
9433
+ cliReference: input.cliReference,
9434
+ defaultReference: input.defaultPreset,
9435
+ environment: input.environment,
9436
+ manifestReference: manifest?.preset,
9437
+ noCache: input.noCache,
9438
+ offline: input.online !== true,
9439
+ presets: input.registry.presets
9440
+ });
9441
+ if (loaded.status === "invalid") return loaded;
9442
+ const repo = input.repoPresetState ?? await readRepoPreset(input.environment, void 0, { includeSkills: false });
9443
+ if (repo.status === "invalid") return {
9444
+ message: `${repo.diagnostics[0]?.message ?? `Repository preset ${repo.path} cannot be read.`} Fix or remove the file to continue.`,
9445
+ status: "invalid"
9446
+ };
9447
+ const repoTrusted = repo.status === "ready" && repo.hash !== void 0 && (repo.hash === input.acceptedRepoPresetHash || isRepoPresetTrusted(manifest, repo, repo.hash));
9448
+ const resolved = resolveEffectiveConfig({
9449
+ checks: input.registry.checks,
9450
+ cli: input.cliLayer,
9451
+ distro: input.defaults,
9452
+ knownMcpServers: /* @__PURE__ */ new Set([...input.registry.mcpServers.map((server) => server.id), ...repoTrusted ? (repo.preset?.provides?.mcpServers ?? []).map((server) => server.id) : []]),
9453
+ ...manifest === void 0 ? {} : { manifest: {
9454
+ ...manifest.checks === void 0 ? {} : { checks: manifest.checks },
9455
+ skills: manifest.skills.map(({ id, source }) => ({
9456
+ id,
9457
+ source
9458
+ })),
9459
+ snippets: manifest.snippets.map(({ id }) => id)
9460
+ } },
9461
+ ...loaded.status === "ready" ? {
9462
+ preset: loaded.preset,
9463
+ selectedPreset: loaded.selected
9464
+ } : {},
9465
+ ...repoTrusted ? { repo: repo.preset } : {}
9466
+ });
9467
+ if (resolved.status === "invalid") return {
9468
+ message: resolved.problems.join("\n"),
9469
+ status: "invalid"
9470
+ };
9471
+ const policyPreset = loaded.status === "ready" || resolved.config.allowedSkillSources !== void 0 || resolved.config.skillDirectories.length > 0 ? Object.freeze({
9472
+ ...loaded.status === "ready" ? loaded.preset : {},
9473
+ ...resolved.config.allowedSkillSources === void 0 ? {} : { allowedSkillSources: resolved.config.allowedSkillSources.value },
9474
+ name: resolved.config.preset?.name ?? resolved.config.allowedSkillSources?.provenance.label ?? resolved.config.skillDirectories[0]?.provenance.label ?? "runtime policy",
9475
+ schemaVersion: 1,
9476
+ skillDirectories: resolved.config.skillDirectories.map(({ value }) => value)
9477
+ }) : void 0;
9478
+ return {
9479
+ config: resolved.config,
9480
+ notes: [...loaded.status === "ready" ? loaded.notes : [], ...repoTrusted ? repo.diagnostics.map((diagnostic) => diagnostic.message) : []],
9481
+ ...policyPreset === void 0 ? {} : { preset: policyPreset },
9482
+ presetOrigin: loaded.status === "ready" ? loaded.origin : policyPreset?.name ?? ".aura/preset.json",
9483
+ ...repo.status === "ready" && repo.hash !== void 0 ? { repoPreset: {
9484
+ ...repoTrusted && repo.contentSet !== void 0 ? { contentSet: repo.contentSet } : {},
9485
+ hash: repo.hash,
9486
+ ...repo.mainWorktreePath === void 0 ? {} : { mainWorktreePath: repo.mainWorktreePath },
9487
+ path: repo.path,
9488
+ ...repoTrusted && repo.preset !== void 0 ? { preset: repo.preset } : {},
9489
+ status: repoTrusted ? "applied" : "held"
9490
+ } } : {},
9491
+ status: "ready"
9492
+ };
9493
+ }
9494
+ /**
9495
+ * Adds a trusted repository's provided MCP definitions to the model's catalog.
9496
+ *
9497
+ * Run before {@link applyRequiredMcpServers} projection wherever a model is projected, so a
9498
+ * repository can require a server it defines itself. A held repository changes nothing.
9499
+ */
9500
+ function withRepoMcpCatalog(model, repoPreset) {
9501
+ const servers = repoPreset?.status === "applied" ? repoPreset.contentSet?.mcpServers ?? [] : [];
9502
+ if (servers.length === 0 || repoPreset === void 0) return model;
9503
+ return Object.freeze({
9504
+ ...model,
9505
+ availableMcpServers: Object.freeze([...model.availableMcpServers, ...repoMcpServerDefs(servers, repoPreset.path)])
9506
+ });
9507
+ }
9508
+ //#endregion
9120
9509
  //#region src/safe-text.ts
9121
9510
  const MAX_FINDING_TEXT_CHARACTERS = 500;
9122
9511
  const UNICODE_FORMAT_CHARACTER = /\p{Cf}/u;
@@ -9287,10 +9676,7 @@ function isBundledSkillSelection(skill) {
9287
9676
  /** Final instruction handling only; paths, sources, and duplicate choices stop here. */
9288
9677
  function instructionActions(selections) {
9289
9678
  if (selections === void 0) return [];
9290
- return [selections.global, ...selections.project === void 0 ? [] : [selections.project]].map(({ action, scope }) => ({
9291
- action,
9292
- scope
9293
- }));
9679
+ return [{ action: selections.global.action }];
9294
9680
  }
9295
9681
  /**
9296
9682
  * Per-check state and severity counts for every executed check, ordered by check ID.
@@ -9339,6 +9725,15 @@ function countsByCheck(findings) {
9339
9725
  }
9340
9726
  //#endregion
9341
9727
  //#region src/command-support.ts
9728
+ /**
9729
+ * Projects preset requirements onto a check scan's model.
9730
+ *
9731
+ * A trusted repository's provided MCP definitions join the catalog first, so a repository can
9732
+ * require a server it defines itself and `check` reports that requirement the way setup would.
9733
+ */
9734
+ function projectConfiguredModel(model, configured) {
9735
+ return applyRequiredMcpServers(withRepoMcpCatalog(model, configured.repoPreset), configured.config);
9736
+ }
9342
9737
  /** The `--home` override every scanning command shares. */
9343
9738
  function homeOption() {
9344
9739
  return Option.String("--home", { description: "Override the home directory." });
@@ -9404,75 +9799,6 @@ function reportUnexpectedFailure(error, subject, branding, withDetail, stderr, t
9404
9799
  return 3;
9405
9800
  }
9406
9801
  //#endregion
9407
- //#region src/runtime-config.ts
9408
- /** Loads the selected preset and resolves all runtime layers once for a command. */
9409
- async function resolveRuntimeConfig(input) {
9410
- if (input.manifest.status === "read-only") return {
9411
- message: input.manifest.problem.message,
9412
- status: "invalid"
9413
- };
9414
- const manifest = input.manifest.status === "ready" ? input.manifest.value : void 0;
9415
- const loaded = await loadTeamPreset({
9416
- cliReference: input.cliReference,
9417
- defaultReference: input.defaultPreset,
9418
- environment: input.environment,
9419
- manifestReference: manifest?.preset,
9420
- noCache: input.noCache,
9421
- offline: input.online !== true,
9422
- presets: input.registry.presets
9423
- });
9424
- if (loaded.status === "invalid") return loaded;
9425
- const repo = await readRepoPreset(input.environment);
9426
- if (repo.status === "invalid") return {
9427
- message: `${repo.diagnostics[0]?.message ?? `Repository preset ${repo.path} cannot be read.`} Fix or remove the file to continue.`,
9428
- status: "invalid"
9429
- };
9430
- const repoTrusted = repo.status === "ready" && repo.hash !== void 0 && (repo.hash === input.acceptedRepoPresetHash || isRepoPresetTrusted(manifest, repo, repo.hash));
9431
- const resolved = resolveEffectiveConfig({
9432
- checks: input.registry.checks,
9433
- cli: input.cliLayer,
9434
- distro: input.defaults,
9435
- knownMcpServers: new Set(input.registry.mcpServers.map((server) => server.id)),
9436
- ...manifest === void 0 ? {} : { manifest: {
9437
- ...manifest.checks === void 0 ? {} : { checks: manifest.checks },
9438
- skills: manifest.skills.map(({ id, source }) => ({
9439
- id,
9440
- source
9441
- })),
9442
- snippets: manifest.snippets.map(({ id }) => id)
9443
- } },
9444
- ...loaded.status === "ready" ? {
9445
- preset: loaded.preset,
9446
- selectedPreset: loaded.selected
9447
- } : {},
9448
- ...repoTrusted ? { repo: repo.preset } : {}
9449
- });
9450
- if (resolved.status === "invalid") return {
9451
- message: resolved.problems.join("\n"),
9452
- status: "invalid"
9453
- };
9454
- const policyPreset = loaded.status === "ready" || resolved.config.allowedSkillSources !== void 0 || resolved.config.skillDirectories.length > 0 ? Object.freeze({
9455
- ...loaded.status === "ready" ? loaded.preset : {},
9456
- ...resolved.config.allowedSkillSources === void 0 ? {} : { allowedSkillSources: resolved.config.allowedSkillSources.value },
9457
- name: resolved.config.preset?.name ?? resolved.config.allowedSkillSources?.provenance.label ?? resolved.config.skillDirectories[0]?.provenance.label ?? "runtime policy",
9458
- schemaVersion: 1,
9459
- skillDirectories: resolved.config.skillDirectories.map(({ value }) => value)
9460
- }) : void 0;
9461
- return {
9462
- config: resolved.config,
9463
- notes: loaded.status === "ready" ? loaded.notes : [],
9464
- ...policyPreset === void 0 ? {} : { preset: policyPreset },
9465
- presetOrigin: loaded.status === "ready" ? loaded.origin : policyPreset?.name ?? ".aura/preset.json",
9466
- ...repo.status === "ready" && repo.hash !== void 0 ? { repoPreset: {
9467
- hash: repo.hash,
9468
- ...repo.mainWorktreePath === void 0 ? {} : { mainWorktreePath: repo.mainWorktreePath },
9469
- path: repo.path,
9470
- status: repoTrusted ? "applied" : "held"
9471
- } } : {},
9472
- status: "ready"
9473
- };
9474
- }
9475
- //#endregion
9476
9802
  //#region src/check-config.ts
9477
9803
  /** Resolves the effective configuration for one `check` run. */
9478
9804
  function resolveCheckConfiguration(options) {
@@ -12917,7 +13243,7 @@ var CheckCommand = class extends Command {
12917
13243
  try {
12918
13244
  const startedAt = environment.now();
12919
13245
  let scan = await scanner.run();
12920
- let projected = applyRequiredMcpServers(scan.model, configured.config);
13246
+ let projected = projectConfiguredModel(scan.model, configured);
12921
13247
  let model = projected.model;
12922
13248
  let run = runChecks(activeChecks, model, configured.config);
12923
13249
  let fixRunDiagnostics = [];
@@ -12945,7 +13271,7 @@ var CheckCommand = class extends Command {
12945
13271
  fixes = outcome.fixes;
12946
13272
  if (outcome.applied) {
12947
13273
  scan = await scanner.run();
12948
- projected = applyRequiredMcpServers(scan.model, configured.config);
13274
+ projected = projectConfiguredModel(scan.model, configured);
12949
13275
  model = projected.model;
12950
13276
  run = runChecks(activeChecks, model, configured.config);
12951
13277
  }
@@ -13029,7 +13355,7 @@ function renderRootHelp(branding) {
13029
13355
  rows: [NO_COLOR_ROW],
13030
13356
  title: "Advanced"
13031
13357
  }
13032
- ], branding.docsUrl === void 0 ? [] : [`Docs: ${branding.docsUrl}`]);
13358
+ ], branding.docsUrl === void 0 ? [] : [`Docs: ${branding.docsUrl}`], ROOT_INTRO);
13033
13359
  }
13034
13360
  function renderCheckHelp(branding) {
13035
13361
  const bin = branding.command;
@@ -13216,6 +13542,17 @@ const NO_COLOR_ROW = {
13216
13542
  term: "--no-color",
13217
13543
  text: "Disable terminal colors"
13218
13544
  };
13545
+ /**
13546
+ * The root screen's one paragraph: what Aura is for, in the words of someone who has not read the
13547
+ * docs yet. It sits above "Get started" because a first-time reader needs the problem before the
13548
+ * command. Hand-wrapped at 80 columns — the renderer aligns columns, it never reflows prose — and
13549
+ * carried only by the root screen, since every other screen is reached by someone who already knows.
13550
+ */
13551
+ const ROOT_INTRO = [
13552
+ "Every AI coding agent keeps its own rules and its own tool settings, in its own",
13553
+ "files. Aura reads all of them, tells you where they disagree or are broken, and",
13554
+ "fixes them after you say yes - so every agent works from the same instructions."
13555
+ ];
13219
13556
  /** Test and CI plumbing, present on every command and interesting to almost nobody. */
13220
13557
  function advancedRows() {
13221
13558
  return [
@@ -13263,9 +13600,10 @@ function headline(branding) {
13263
13600
  return branding.description === void 0 ? name : `${name} — ${branding.description}`;
13264
13601
  }
13265
13602
  /** Terms align to one shared column so the eye scans a single list, not per-section islands. */
13266
- function renderHelpScreen(header, sections, footers) {
13603
+ function renderHelpScreen(header, sections, footers, intro = []) {
13267
13604
  const width = Math.max(...sections.flatMap((section) => section.rows.map((row) => row.term.length)));
13268
13605
  const lines = [header];
13606
+ if (intro.length > 0) lines.push("", ...intro.map((line) => ` ${line}`));
13269
13607
  for (const section of sections) {
13270
13608
  lines.push("", ` ${section.title}`);
13271
13609
  for (const row of section.rows) lines.push(` ${row.term.padEnd(width)} ${row.text}`);
@@ -13507,7 +13845,7 @@ function planLinks$1(context, scopeSelections, archived, ownership, manualSteps)
13507
13845
  }
13508
13846
  function planAppLinks(context, app, scopeSelections, archived, ownership, manualSteps) {
13509
13847
  return scopeSelections.flatMap((selection) => {
13510
- const link = selection.scope === "global" ? app.sharedLink : app.projectSharedLink;
13848
+ const link = app.sharedLink;
13511
13849
  if (link === void 0) return [];
13512
13850
  const outcome = planSharedInstructionLink(app, context.model, {
13513
13851
  link,
@@ -13534,12 +13872,12 @@ function planAppLinks(context, app, scopeSelections, archived, ownership, manual
13534
13872
  * only once that entry is merged here and archived. The wizard does not offer an entry already
13535
13873
  * pointing at the target, so leaving this to the selection strands the app on every run.
13536
13874
  */
13537
- function mandatoryEntryPaths(context, scope) {
13875
+ function mandatoryEntryPaths(context) {
13538
13876
  const managedIds = new Set(managedAppIdList(context));
13539
13877
  const paths = /* @__PURE__ */ new Set();
13540
13878
  for (const app of context.model.apps) {
13541
13879
  if (app.synthetic === true || !managedIds.has(app.adapterId)) continue;
13542
- const link = scope === "global" ? app.sharedLink : app.projectSharedLink;
13880
+ const link = app.sharedLink;
13543
13881
  if (link?.kind === "symlink") paths.add(resolve(link.entryPath));
13544
13882
  }
13545
13883
  return paths;
@@ -13551,7 +13889,7 @@ function mandatoryEntryPaths(context, scope) {
13551
13889
  * archives them.
13552
13890
  */
13553
13891
  function requiredEntries(context, selection, inventory) {
13554
- const required = mandatoryEntryPaths(context, selection.scope);
13892
+ const required = mandatoryEntryPaths(context);
13555
13893
  return inventory.filter((source) => source.scope === selection.scope && required.has(resolve(source.path)));
13556
13894
  }
13557
13895
  /** Folds the required entries into the selection, so the merge and the archive both see them. */
@@ -13586,7 +13924,7 @@ function canonicalSourcePath(document) {
13586
13924
  */
13587
13925
  function isAuraArtifact(document, model) {
13588
13926
  const path = resolve(document.path);
13589
- return model.apps.some((app) => [app.sharedLink, app.projectSharedLink].some((link) => link !== void 0 && resolve(link.entryPath) === path && matchesLink(document, link)));
13927
+ return model.apps.some((app) => app.sharedLink !== void 0 && resolve(app.sharedLink.entryPath) === path && matchesLink(document, app.sharedLink));
13590
13928
  }
13591
13929
  function matchesLink(document, link) {
13592
13930
  if (link.kind === "native-copy") return document.content === link.content;
@@ -13600,7 +13938,7 @@ function matchesLink(document, link) {
13600
13938
  * would otherwise re-walk every application to rediscover the same handful of links.
13601
13939
  */
13602
13940
  function importLineLinks(model) {
13603
- return model.apps.flatMap((app) => [app.sharedLink, app.projectSharedLink]).filter((link) => link?.kind === "import-line" && link.content !== void 0);
13941
+ return model.apps.map((app) => app.sharedLink).filter((link) => link?.kind === "import-line" && link.content !== void 0);
13604
13942
  }
13605
13943
  /** Removes legacy marked links and a plain import Aura appended at the declared entry path. */
13606
13944
  function stripAuraInstructionArtifacts(content, path, links) {
@@ -13736,17 +14074,14 @@ function removeRanges(content, ranges) {
13736
14074
  //#endregion
13737
14075
  //#region src/setup/instructions.ts
13738
14076
  function instructionTargets(model) {
13739
- return {
13740
- global: resolve(model.sharedInstructions.path),
13741
- project: resolve(projectSharedInstructionsPath(model.projectRoot, model.cwd))
13742
- };
14077
+ return { global: resolve(model.sharedInstructions.path) };
13743
14078
  }
13744
14079
  function instructionInventory(model) {
13745
14080
  const targets = instructionTargets(model);
13746
14081
  const owned = new Set(model.manifest.status === "ready" ? Object.values(model.manifest.value.ownership).flatMap((entry) => entry.files.map((path) => resolve(path))) : []);
13747
14082
  const documents = /* @__PURE__ */ new Map();
13748
14083
  for (const document of model.instructionFiles) {
13749
- if ([resolve(document.path), canonicalSourcePath(document)].some((candidate) => candidate === targets.global || candidate === targets.project || owned.has(candidate)) || isAuraArtifact(document, model)) continue;
14084
+ if ([resolve(document.path), canonicalSourcePath(document)].some((candidate) => candidate === targets.global || owned.has(candidate)) || isAuraArtifact(document, model)) continue;
13750
14085
  const key = canonicalSourcePath(document);
13751
14086
  const current = documents.get(key);
13752
14087
  if (current === void 0 || document.content.length > current.content.length) documents.set(key, document);
@@ -13847,10 +14182,8 @@ function planInstructions(context) {
13847
14182
  const ownership = /* @__PURE__ */ new Map();
13848
14183
  const inventory = instructionInventory(context.model);
13849
14184
  const clusters = duplicateClusters(context.findings ?? []);
13850
- const scopeSelections = [selections.global, selections.project].filter((selection) => selection !== void 0 && selection.action !== "blocked" && selection.action !== "skip");
13851
- const linked = [];
13852
- for (const selection of scopeSelections) if (planScope(context, selection, inventory, clusters, state)) linked.push(selection);
13853
- const linkOperations = planLinks$1(context, linked, state.archived, ownership, state.manualSteps);
14185
+ const selection = selections.global;
14186
+ const linkOperations = planLinks$1(context, selection.action !== "blocked" && planScope(context, selection, inventory, clusters, state) ? [selection] : [], state.archived, ownership, state.manualSteps);
13854
14187
  for (const [path, archive] of state.archived) {
13855
14188
  const linkIndex = linkOperations.findIndex((operation) => resolve(primaryPath(operation)) === path);
13856
14189
  const link = linkIndex === -1 ? void 0 : linkOperations.splice(linkIndex, 1)[0];
@@ -13892,14 +14225,10 @@ function planScope(context, requested, inventory, clusters, state) {
13892
14225
  if (selection.action !== "consolidate") return true;
13893
14226
  const content = composeConsolidatedInstructions(chosen, selection, clusters, context.model, existing);
13894
14227
  if (content.trim().length === 0) {
13895
- if (selection.scope === "global") {
13896
- state.blockers.push({
13897
- path: selection.targetPath,
13898
- reason: "No selected instruction content is available to consolidate. Select a source, or choose the starter template."
13899
- });
13900
- return false;
13901
- }
13902
- state.manualSteps.push(`Configure ${selection.targetPath} on the next run: select at least one project source to consolidate, or choose the starter template. Aura wrote nothing there.`);
14228
+ state.blockers.push({
14229
+ path: selection.targetPath,
14230
+ reason: "No selected instruction content is available to consolidate. Select a source, or choose the starter template."
14231
+ });
13903
14232
  return false;
13904
14233
  }
13905
14234
  planConsolidatedTarget(context, selection, existing, content, state);
@@ -14083,7 +14412,7 @@ function withRequiredOverrides(manifest, requiredIds, overriddenIds) {
14083
14412
  * was available to receive it.
14084
14413
  */
14085
14414
  function unconfiguredRequirement(context, id) {
14086
- return context.interactive ? `Required MCP catalog entry ${id} could not be selected for a managed compatible application.` : `Required MCP catalog entry ${id} is not configured yet. Run setup interactively to choose its name, scope, and applications; later non-interactive runs re-apply what the manifest records.`;
14415
+ return context.interactive ? `Required MCP catalog entry ${id} could not be selected for a managed compatible application.` : `Required MCP catalog entry ${id} is not configured yet. Run setup interactively to choose its name and applications; later non-interactive runs re-apply what the manifest records globally.`;
14087
14416
  }
14088
14417
  function withMcpOwnership(manifest, appId, names) {
14089
14418
  const previous = manifest.ownership[appId];
@@ -14160,6 +14489,131 @@ function formatThresholds(thresholds) {
14160
14489
  return pairs.length === 0 ? "no thresholds" : pairs.join(", ");
14161
14490
  }
14162
14491
  //#endregion
14492
+ //#region src/setup/repo-trust-preview.ts
14493
+ /** Printed instead of the row block when the preset carries nothing worth reviewing. */
14494
+ const NO_SETTINGS = "No check, MCP, skill, or snippet settings.";
14495
+ /**
14496
+ * Security-relevant capabilities shown before repository-controlled settings are accepted.
14497
+ *
14498
+ * One row per thing, never one row per field: a snippet the preset both provides and selects is
14499
+ * one snippet, and naming it twice made the screen long enough to skim past. Everything
14500
+ * executable-adjacent is spelled out verbatim (escaped) — the MCP command line or endpoint is
14501
+ * exactly what a later selection would configure, and trust time is the one moment the user is
14502
+ * looking. Snippet bodies are deliberately not echoed — pasting untrusted Markdown into a consent
14503
+ * prompt is its own injection surface — the picker's preview is where they are read, and the
14504
+ * prompt itself carries the explicit-selection boundary.
14505
+ */
14506
+ function repoPresetTrustPreview(preset, contentSet) {
14507
+ const rows = [
14508
+ ...mcpRows(preset, contentSet),
14509
+ ...skillRows(preset, contentSet),
14510
+ ...snippetRows(preset, contentSet),
14511
+ ...(preset.skillDirectories ?? []).map(directoryRow),
14512
+ ...listRow("sources", preset.allowedSkillSources),
14513
+ ...checksRow(preset.checks)
14514
+ ];
14515
+ return [
14516
+ "",
14517
+ ...rows.length === 0 ? [` ${NO_SETTINGS}`] : alignRows(rows),
14518
+ ""
14519
+ ].join("\n");
14520
+ }
14521
+ /** Whether the prompt should promise that admitted content still needs a deliberate tick. */
14522
+ function hasRepoContent(contentSet) {
14523
+ return contentSet !== void 0 && contentSet.mcpServers.length + contentSet.skills.length + contentSet.snippets.length > 0;
14524
+ }
14525
+ function alignRows(rows) {
14526
+ const width = Math.max(...rows.map((row) => row.term.length));
14527
+ return rows.map((row) => ` ${row.term.padEnd(width)} ${row.detail}`);
14528
+ }
14529
+ /**
14530
+ * Every MCP server the repository would configure, provided ones with their full command line.
14531
+ *
14532
+ * A required id the repository does not define itself resolves from the catalog, so it gets a row
14533
+ * saying that rather than an executable surface this file cannot promise.
14534
+ */
14535
+ function mcpRows(preset, contentSet) {
14536
+ const provided = contentSet?.mcpServers ?? [];
14537
+ const providedIds = new Set(provided.map((server) => server.id));
14538
+ const required = new Set(preset.requiredMcpServers ?? []);
14539
+ return [...provided.map((server) => ({
14540
+ detail: `${mcpServerPreview(server)}${required.has(server.id) ? " · required" : ""}`,
14541
+ term: "mcp"
14542
+ })), ...[...required].filter((id) => !providedIds.has(id)).map((id) => ({
14543
+ detail: `${safe(id)} · required, from the catalog`,
14544
+ term: "mcp"
14545
+ }))];
14546
+ }
14547
+ /** Skill trees the repository ships, then selections that point somewhere else. */
14548
+ function skillRows(preset, contentSet) {
14549
+ const provided = contentSet?.skills ?? [];
14550
+ const providedKeys = new Set(provided.map((skill) => `${skill.source.id}/${skill.id}`));
14551
+ return [...provided.map((skill) => ({
14552
+ detail: `${safe(skill.id)} (${count(skill.files.length, "file")})`,
14553
+ term: "skill"
14554
+ })), ...(preset.skills ?? []).map((selection) => `${selection.source}/${selection.id}`).filter((key) => !providedKeys.has(key)).map((key) => ({
14555
+ detail: `${safe(key)} · selected`,
14556
+ term: "skill"
14557
+ }))];
14558
+ }
14559
+ /** Snippets the repository ships, then selections from the catalog. */
14560
+ function snippetRows(preset, contentSet) {
14561
+ const provided = contentSet?.snippets ?? [];
14562
+ const providedIds = new Set(provided.map((snippet) => snippet.id));
14563
+ return [...provided.map((snippet) => ({
14564
+ detail: `${safe(snippet.id)} (${String(Buffer$1.byteLength(snippet.body, "utf8"))} B)`,
14565
+ term: "snippet"
14566
+ })), ...(preset.snippets ?? []).filter((id) => !providedIds.has(id)).map((id) => ({
14567
+ detail: `${safe(id)} · selected`,
14568
+ term: "snippet"
14569
+ }))];
14570
+ }
14571
+ /** The full executable surface of one provided MCP server, escaped but verbatim. */
14572
+ function mcpServerPreview(server) {
14573
+ return `${safe(server.id)} "${safe(server.serverName)}" → ${transportPreview(server.transportTemplate)}`;
14574
+ }
14575
+ function transportPreview(transport) {
14576
+ if (transport.type === "http") return `http ${safe(transport.url)}`;
14577
+ const args = (transport.args ?? []).map((argument) => `"${safe(argument)}"`).join(", ");
14578
+ const env = (transport.env ?? []).map(safe).join(", ");
14579
+ return [
14580
+ `stdio "${safe(transport.command)}"`,
14581
+ ...args === "" ? [] : [`args [${args}]`],
14582
+ ...env === "" ? [] : [`env ${env}`]
14583
+ ].join(", ");
14584
+ }
14585
+ /** The exact check settings the repository preset asks the user to trust. */
14586
+ function checksRow(checks) {
14587
+ if (checks === void 0) return [];
14588
+ const settings = [
14589
+ ...(checks.disabled ?? []).map((id) => `${safe(id)}: disabled`),
14590
+ ...(checks.enabled ?? []).map((id) => `${safe(id)}: enabled`),
14591
+ ...Object.entries(checks.severity ?? {}).map(([id, severity]) => `${safe(id)}: severity ${safe(severity)}`),
14592
+ ...Object.entries(checks.thresholds ?? {}).map(([id, thresholds]) => `${safe(id)}: thresholds ${safe(JSON.stringify(thresholds))}`)
14593
+ ].sort();
14594
+ return [{
14595
+ detail: settings.length === 0 ? "(none)" : settings.join("; "),
14596
+ term: "checks"
14597
+ }];
14598
+ }
14599
+ function directoryRow(source) {
14600
+ const token = source.kind === "private-directory" ? ` · token ${safe(source.tokenEnv)}` : "";
14601
+ return {
14602
+ detail: `${safe(source.name)} → ${safe(source.url)}${token}`,
14603
+ term: "directory"
14604
+ };
14605
+ }
14606
+ function listRow(term, values) {
14607
+ if (values === void 0) return [];
14608
+ return [{
14609
+ detail: values.length === 0 ? "(none)" : values.map(safe).join(", "),
14610
+ term
14611
+ }];
14612
+ }
14613
+ function count(total, noun) {
14614
+ return `${String(total)} ${noun}${total === 1 ? "" : "s"}`;
14615
+ }
14616
+ //#endregion
14163
14617
  //#region src/setup/repo-trust.ts
14164
14618
  /**
14165
14619
  * Asks whether an untrusted repository preset may be applied, once per repo and contents.
@@ -14169,61 +14623,39 @@ function formatThresholds(thresholds) {
14169
14623
  * answer. A non-interactive run resolves without the layer, and the plan summary says so.
14170
14624
  */
14171
14625
  async function establishRepoPresetTrust(options) {
14172
- const repo = await readRepoPreset(options.environment);
14626
+ const repo = options.repoPresetState ?? await readRepoPreset(options.environment);
14173
14627
  if (repo.status !== "ready" || repo.hash === void 0) return { kind: "resolved" };
14174
14628
  const manifest = options.manifest.status === "ready" ? options.manifest.value : void 0;
14175
14629
  if (isRepoPresetTrusted(manifest, repo, repo.hash) || !options.interactive) return { kind: "resolved" };
14176
14630
  const name = repo.preset?.name;
14177
- options.io.note(name === void 0 ? `This repository provides a preset at ${AURA_TEAM_PRESET_PATH}.` : `This repository provides the preset "${safe(name)}" at ${AURA_TEAM_PRESET_PATH}.`);
14178
- if (repo.preset !== void 0) options.io.note(repoPresetTrustPreview(repo.preset));
14631
+ options.io.note(name === void 0 ? `Repository preset ${AURA_TEAM_PRESET_PATH}` : `Repository preset "${safe(name)}" ${AURA_TEAM_PRESET_PATH}`);
14632
+ if (repo.preset !== void 0) options.io.note(repoPresetTrustPreview(repo.preset, repo.contentSet));
14179
14633
  if (repo.mainWorktreePath !== void 0) options.io.note("This directory is a linked worktree, so trusting these contents also applies them in every other worktree of the same checkout.");
14180
14634
  const changed = (manifest?.trustedRepoPresets ?? []).some((entry) => entry.path === repo.path);
14181
- const confirmation = await options.io.confirm(changed ? `The repository preset at ${AURA_TEAM_PRESET_PATH} changed since you trusted it. Trust the new contents?` : `Trust the repository preset at ${AURA_TEAM_PRESET_PATH}? Its settings apply to every Aura run in this repository until the file changes.`);
14635
+ const confirmation = await options.io.confirm(trustPrompt(changed, repo.contentSet));
14182
14636
  if (confirmation === "aborted") return { kind: "aborted" };
14183
14637
  return confirmation === "accepted" ? {
14184
14638
  acceptedHash: repo.hash,
14185
14639
  kind: "resolved"
14186
14640
  } : { kind: "resolved" };
14187
14641
  }
14188
- /** Security-relevant capabilities shown before repository-controlled settings are accepted. */
14189
- function repoPresetTrustPreview(preset) {
14190
- const settings = [
14191
- checksPreview(preset.checks),
14192
- listPreview("Required MCP servers", preset.requiredMcpServers),
14193
- listPreview("Allowed skill sources", preset.allowedSkillSources),
14194
- ...(preset.skillDirectories ?? []).map(directoryPreview),
14195
- listPreview("Selected skills", preset.skills?.map((skill) => `${skill.source}/${skill.id}`)),
14196
- listPreview("Selected snippets", preset.snippets)
14197
- ].filter((line) => line !== void 0);
14198
- return ["Review these repository-controlled settings before trusting:", ...settings.length === 0 ? ["No check, MCP, skill, or snippet settings."] : settings].join("\n");
14199
- }
14200
- /** The exact check settings the repository preset asks the user to trust. */
14201
- function checksPreview(checks) {
14202
- if (checks === void 0) return;
14203
- const settings = [
14204
- ...(checks.disabled ?? []).map((id) => `${safe(id)}: disabled`),
14205
- ...(checks.enabled ?? []).map((id) => `${safe(id)}: enabled`),
14206
- ...Object.entries(checks.severity ?? {}).map(([id, severity]) => `${safe(id)}: severity ${safe(severity)}`),
14207
- ...Object.entries(checks.thresholds ?? {}).map(([id, thresholds]) => `${safe(id)}: thresholds ${safe(JSON.stringify(thresholds))}`)
14208
- ].sort();
14209
- return `Checks: ${settings.length === 0 ? "(none)" : settings.join("; ")}`;
14210
- }
14211
- function directoryPreview(source) {
14212
- const token = source.kind === "private-directory" ? `; token ${safe(source.tokenEnv)}` : "";
14213
- return `Skill directory: ${safe(source.name)} — ${safe(source.url)}${token}`;
14214
- }
14215
- function listPreview(label, values) {
14216
- return values === void 0 ? void 0 : `${label}: ${listValues(values)}`;
14217
- }
14218
- function listValues(values) {
14219
- return values.length === 0 ? "(none)" : values.map(safe).join(", ");
14642
+ /**
14643
+ * The question itself, short because the note above it already named the file and its contents.
14644
+ *
14645
+ * The two-tier contract rides here rather than in the summary: a preset that only carries policy
14646
+ * installs nothing either way, so promising a later tick would be noise in the one line the user
14647
+ * is certain to read.
14648
+ */
14649
+ function trustPrompt(changed, contentSet) {
14650
+ if (changed) return "The preset changed since you trusted it. Trust the new contents?";
14651
+ return hasRepoContent(contentSet) ? "Trust it? Nothing installs until you pick it; applies to every run here until the file changes." : "Trust it? Applies to every run here until the file changes.";
14220
14652
  }
14221
14653
  /**
14222
14654
  * Whether this run's prompt accepted exactly the contents the resolved layer applied.
14223
14655
  *
14224
- * The two reads of the file the prompt's and configuration resolution's — are independent, so a
14225
- * write that lands between them leaves the layer held. Recording trust for a layer that did not
14226
- * apply would claim consent for settings this run never used.
14656
+ * Boot passes one immutable snapshot through the prompt and configuration resolution. Comparing
14657
+ * hashes here keeps this helper correct for direct callers too: trust is recorded only for the
14658
+ * layer this run actually applied.
14227
14659
  */
14228
14660
  function acceptedRepoPreset(configured, acceptedHash) {
14229
14661
  const repo = configured.repoPreset;
@@ -14236,9 +14668,11 @@ function setupRepoPresetContext(configured, acceptedHash, recorded) {
14236
14668
  return Object.freeze({
14237
14669
  accepted: acceptedRepoPreset(configured, acceptedHash),
14238
14670
  checkSummary: repo.status === "applied" ? presetCheckSummary(configured.config, "repo") : Object.freeze([]),
14671
+ ...repo.contentSet === void 0 ? {} : { contentSet: repo.contentSet },
14239
14672
  hash: repo.hash,
14240
14673
  ...repo.mainWorktreePath === void 0 ? {} : { mainWorktreePath: repo.mainWorktreePath },
14241
14674
  path: repo.path,
14675
+ ...repo.preset === void 0 ? {} : { preset: repo.preset },
14242
14676
  recorded,
14243
14677
  status: repo.status
14244
14678
  });
@@ -14856,11 +15290,15 @@ async function bootSetup(request, environment) {
14856
15290
  message: manifest.problem.message,
14857
15291
  status: "invalid"
14858
15292
  };
15293
+ const initialRepoPreset = await readRepoPreset(environment, void 0, { includeSkills: request.interactive });
15294
+ const manifestValue = manifest.status === "ready" ? manifest.value : void 0;
15295
+ const repoPresetState = !request.interactive && initialRepoPreset.status === "ready" && initialRepoPreset.hash !== void 0 && isRepoPresetTrusted(manifestValue, initialRepoPreset, initialRepoPreset.hash) ? await readRepoPreset(environment) : initialRepoPreset;
14859
15296
  const trust = await establishRepoPresetTrust({
14860
15297
  environment,
14861
15298
  interactive: request.interactive,
14862
15299
  io: request.io,
14863
- manifest
15300
+ manifest,
15301
+ repoPresetState
14864
15302
  });
14865
15303
  if (trust.kind === "aborted") return { status: "aborted" };
14866
15304
  const scanCancellation = new AbortController();
@@ -14882,7 +15320,8 @@ async function bootSetup(request, environment) {
14882
15320
  manifest,
14883
15321
  noCache: request.noCache,
14884
15322
  online: true,
14885
- registry: request.registry
15323
+ registry: request.registry,
15324
+ repoPresetState
14886
15325
  });
14887
15326
  if (configured.status === "invalid") {
14888
15327
  scanCancellation.abort();
@@ -14890,7 +15329,7 @@ async function bootSetup(request, environment) {
14890
15329
  }
14891
15330
  const scan = await settleScan(request.io);
14892
15331
  const trusted = await bootRepoPresetTrust(request, configured, trust.acceptedHash, scan);
14893
- const projected = applyRequiredMcpServers(trusted.scan.model, configured.config);
15332
+ const projected = applyRequiredMcpServers(withRepoMcpCatalog(trusted.scan.model, configured.repoPreset), configured.config);
14894
15333
  return {
14895
15334
  activeChecks: enabledChecks(request.registry.checks, configured.config),
14896
15335
  configured,
@@ -14907,8 +15346,8 @@ async function bootSetup(request, environment) {
14907
15346
  };
14908
15347
  }
14909
15348
  /** Re-projects a rescan so the closing checklist reads the same virtual state the run planned. */
14910
- function projectRescan(rescanned, config) {
14911
- const projection = applyRequiredMcpServers(rescanned.model, config);
15349
+ function projectRescan(rescanned, config, repoPreset) {
15350
+ const projection = applyRequiredMcpServers(withRepoMcpCatalog(rescanned.model, repoPreset), config);
14912
15351
  return {
14913
15352
  ...rescanned,
14914
15353
  diagnostics: [...rescanned.diagnostics, ...projection.diagnostics],
@@ -15242,6 +15681,7 @@ function createMcpSetupCatalog(inputs) {
15242
15681
  entries.push({
15243
15682
  catalog,
15244
15683
  key: `catalog:${catalog.id}`,
15684
+ ...isRepoCatalogId(catalog.id) ? { repo: true } : {},
15245
15685
  required: requiredIds.has(catalog.id),
15246
15686
  sourceName: owner?.name
15247
15687
  });
@@ -15253,6 +15693,7 @@ function createMcpSetupCatalog(inputs) {
15253
15693
  catalog,
15254
15694
  existing: match.server,
15255
15695
  key: `manifest:${String(match.index)}`,
15696
+ ...isRepoCatalogId(catalog.id) ? { repo: true } : {},
15256
15697
  required: requiredIds.has(catalog.id),
15257
15698
  sourceName: owner?.name
15258
15699
  });
@@ -15265,6 +15706,7 @@ function createMcpSetupCatalog(inputs) {
15265
15706
  ...catalog === void 0 ? {} : { catalog },
15266
15707
  existing: server,
15267
15708
  key: `manifest:${String(index)}`,
15709
+ ...server.catalogId !== void 0 && isRepoCatalogId(server.catalogId) ? { repo: true } : {},
15268
15710
  required: server.catalogId !== void 0 && requiredIds.has(server.catalogId),
15269
15711
  sourceName: server.catalogId === void 0 ? void 0 : inputs.registry.ownerOf("mcp-server", server.catalogId)?.name
15270
15712
  });
@@ -15277,12 +15719,24 @@ function createMcpSetupCatalog(inputs) {
15277
15719
  requiredIds
15278
15720
  });
15279
15721
  }
15722
+ /**
15723
+ * Required rows first (they carry blocker semantics whoever asked for them), then this
15724
+ * repository's own definitions, then configured rows, then everything else by name. Repo rows
15725
+ * lead the optional block — they are what a person running setup inside the repository came for —
15726
+ * without outranking a requirement they did not make.
15727
+ */
15280
15728
  function compareEntries(left, right) {
15281
15729
  if (left.required !== right.required) return left.required ? -1 : 1;
15730
+ const leftRepo = left.repo === true;
15731
+ if (leftRepo !== (right.repo === true)) return leftRepo ? -1 : 1;
15282
15732
  const leftConfigured = left.existing !== void 0;
15283
15733
  if (leftConfigured !== (right.existing !== void 0)) return leftConfigured ? -1 : 1;
15284
15734
  return entryName(left).localeCompare(entryName(right));
15285
15735
  }
15736
+ /** The `repo/` namespace is reserved against plugins, so the prefix alone is provenance. */
15737
+ function isRepoCatalogId(id) {
15738
+ return id.startsWith("repo/");
15739
+ }
15286
15740
  /** User-facing name used by the picker and deterministic sorting. */
15287
15741
  function mcpCatalogEntryName(entry) {
15288
15742
  return entryName(entry);
@@ -15365,7 +15819,7 @@ const MAX_CONCURRENT_SOURCE_LISTINGS = 4;
15365
15819
  * of both. Only the draining is ordered, which is what keeps the picker's rows stable.
15366
15820
  */
15367
15821
  async function loadListing(request) {
15368
- const entries = [...bundledEntries(request.inputs)];
15822
+ const entries = [...repoEntries(request.inputs), ...bundledEntries(request.inputs)];
15369
15823
  const notes = [...request.notes];
15370
15824
  const unavailableSources = request.unapproved.map((source) => ({
15371
15825
  hint: `connection not approved; token ${source.tokenEnv}`,
@@ -15485,6 +15939,30 @@ function memoizedDriverListing(request, source) {
15485
15939
  request.driverListings.set(source.id, listing);
15486
15940
  return listing;
15487
15941
  }
15942
+ /** Repository skills the active allowlist admits; the source id is `repo:workspace`. */
15943
+ function allowedRepoSkills(inputs) {
15944
+ return (inputs.repoSkills ?? []).filter((skill) => isSkillSourceAllowed(inputs.preset, skill.source.id));
15945
+ }
15946
+ /**
15947
+ * This repository's own skills, resolved from the trusted snapshot rather than any fetch.
15948
+ *
15949
+ * `remote` stays false — no fetch is needed — but the review stage still gates their first
15950
+ * install by identity: trees that arrive by cloning get the same on-screen reading a directory
15951
+ * skill does.
15952
+ */
15953
+ function repoEntries(inputs) {
15954
+ return allowedRepoSkills(inputs).map((skill) => ({
15955
+ description: skill.description,
15956
+ id: skill.id,
15957
+ identity: skillIdentity(skill.source.id, skill.id),
15958
+ name: skill.name,
15959
+ preview: skill.files.find((file) => file.path === "SKILL.md")?.content,
15960
+ remote: false,
15961
+ sourceId: skill.source.id,
15962
+ sourceName: skill.source.name,
15963
+ version: skill.version
15964
+ }));
15965
+ }
15488
15966
  function bundledEntries(inputs) {
15489
15967
  return (inputs.model.availableSkills ?? []).filter((skill) => isSkillSourceAllowed(inputs.preset, skill.source.id)).map((skill) => ({
15490
15968
  description: skill.description,
@@ -15525,6 +16003,7 @@ function remoteEntries(listings, source) {
15525
16003
  function createSkillCatalog(inputs) {
15526
16004
  const collected = collectSkillDirectorySources(inputs.registryDirectories, inputs.preset);
15527
16005
  const packs = /* @__PURE__ */ new Map();
16006
+ for (const skill of allowedRepoSkills(inputs)) packs.set(skillIdentity(skill.source.id, skill.id), skill);
15528
16007
  const failures = /* @__PURE__ */ new Map();
15529
16008
  const pending = /* @__PURE__ */ new Map();
15530
16009
  const driverListings = /* @__PURE__ */ new Map();
@@ -15601,7 +16080,8 @@ function createSetupCatalogs(inputs) {
15601
16080
  presetOrigin: inputs.presetOrigin,
15602
16081
  registryDirectories: inputs.registry.skillDirectories,
15603
16082
  registryDrivers: inputs.registry.skillSources,
15604
- registryPresets: inputs.registry.presets
16083
+ registryPresets: inputs.registry.presets,
16084
+ repoSkills: inputs.repoSkills
15605
16085
  })
15606
16086
  };
15607
16087
  }
@@ -15632,15 +16112,17 @@ function requiringPreset(preset, config) {
15632
16112
  }
15633
16113
  //#endregion
15634
16114
  //#region src/setup/snippets.ts
16115
+ /** The picker group every repository-defined snippet renders under. */
16116
+ const REPO_SNIPPET_CATEGORY = "From this repository";
15635
16117
  /** Bounds simultaneous snippet handles independently of how many contributions a distro ships. */
15636
16118
  const MAX_CONCURRENT_SNIPPET_READS = 24;
15637
- function createSnippetCatalog(snippets, manifest, presetIds = []) {
16119
+ function createSnippetCatalog(snippets, manifest, presetIds = [], repoSnippets = []) {
15638
16120
  let resolved = [];
15639
16121
  let pending;
15640
16122
  return {
15641
16123
  entries: () => resolved,
15642
16124
  load: () => {
15643
- pending ??= resolveSnippetCatalog(snippets, manifest, presetIds).then((entries) => {
16125
+ pending ??= resolveSnippetCatalog(snippets, manifest, presetIds, repoSnippets).then((entries) => {
15644
16126
  resolved = entries;
15645
16127
  return entries;
15646
16128
  });
@@ -15649,13 +16131,23 @@ function createSnippetCatalog(snippets, manifest, presetIds = []) {
15649
16131
  };
15650
16132
  }
15651
16133
  /** Resolves every registry source once and retains manifest-only selections as unavailable rows. */
15652
- async function resolveSnippetCatalog(snippets, manifest, presetIds = []) {
15653
- const registered = new Set(snippets.map((snippet) => snippet.id));
16134
+ async function resolveSnippetCatalog(snippets, manifest, presetIds = [], repoSnippets = []) {
16135
+ const repoEntries = repoSnippets.map((snippet) => Object.freeze({
16136
+ category: REPO_SNIPPET_CATEGORY,
16137
+ content: snippet.body,
16138
+ description: snippet.description ?? "Defined by this repository.",
16139
+ id: snippet.id,
16140
+ name: snippet.name,
16141
+ origin: "repo",
16142
+ status: "available"
16143
+ }));
16144
+ const registered = /* @__PURE__ */ new Set([...snippets.map((snippet) => snippet.id), ...repoEntries.map((entry) => entry.id)]);
15654
16145
  const limit = createLimiter(MAX_CONCURRENT_SNIPPET_READS);
15655
16146
  const resolved = await Promise.all(snippets.map((snippet) => limit(() => resolveSnippet(snippet))));
15656
16147
  const previous = manifest.status === "ready" ? manifest.value.snippets : [];
15657
16148
  const previousIds = new Set(previous.map((entry) => entry.id));
15658
16149
  return Object.freeze([
16150
+ ...repoEntries,
15659
16151
  ...resolved,
15660
16152
  ...previous.map((entry) => entry.id).filter((id) => !registered.has(id)).map((id) => Object.freeze({
15661
16153
  category: "general",
@@ -16071,7 +16563,6 @@ function excerpt(sources, path, startLine, endLine) {
16071
16563
  //#region src/setup/steps/instruction-stages.ts
16072
16564
  const TEMPLATE_VALUE = "template";
16073
16565
  const CONSOLIDATE_VALUE = "consolidate";
16074
- const SKIP_VALUE = "skip";
16075
16566
  /**
16076
16567
  * The target's current text when this scope has nothing left to decide, otherwise undefined.
16077
16568
  *
@@ -16102,13 +16593,13 @@ function scopeStages(input) {
16102
16593
  return [
16103
16594
  {
16104
16595
  isApplicable: () => !input.blocked,
16105
- label: input.scope === "global" ? "Global" : "Project",
16596
+ label: "Personal",
16106
16597
  apply: (state, answers) => update(state, { action: settled(answers[actionId], offered, fallback) }),
16107
16598
  questions: (state) => input.blocked ? void 0 : [{
16108
16599
  id: actionId,
16109
16600
  initial: [draft(state).action ?? fallback],
16110
16601
  kind: "select",
16111
- label: input.scope === "global" ? "Global" : "Project",
16602
+ label: "Personal",
16112
16603
  options,
16113
16604
  prompt: `How should Aura configure ${input.targetPath}?`
16114
16605
  }]
@@ -16127,7 +16618,7 @@ function scopeStages(input) {
16127
16618
  label: source.path,
16128
16619
  value: source.path
16129
16620
  })),
16130
- prompt: `Which ${input.scope} instruction files should Aura consolidate?`
16621
+ prompt: "Which personal instruction files should Aura consolidate?"
16131
16622
  }] : void 0
16132
16623
  },
16133
16624
  {
@@ -16164,10 +16655,8 @@ function settled(answer, offered, fallback) {
16164
16655
  * advice, the mark and the cursor on row `1.` together, and leaves the rest of the menu in the
16165
16656
  * order it was built. A scope with nothing to combine recommends nothing and keeps that order.
16166
16657
  *
16167
- * The opt-out is offered on the project scope only. Declining the global scope would leave INS-001
16168
- * and INS-002 firing at error severity, so setup could not end on green — an answer the wizard
16169
- * offers should not be one the closing checklist then fails the run over. The project tier has no
16170
- * error-severity counterpart, so a declined project scope still ends green.
16658
+ * Personal instructions cannot be skipped because setup must leave applications connected to the
16659
+ * global shared source.
16171
16660
  */
16172
16661
  function actionOptions(input) {
16173
16662
  const options = [];
@@ -16187,11 +16676,6 @@ function actionOptions(input) {
16187
16676
  label: "Use starter template",
16188
16677
  value: TEMPLATE_VALUE
16189
16678
  });
16190
- if (input.scope === "project") options.push({
16191
- description: hasTargetContent(input) ? `Aura writes nothing here and adds no project-level link; ${basename(input.targetPath)} and anything already linking to it stay as they are.` : "Aura writes nothing here and adds no project-level link.",
16192
- label: "Skip project instructions",
16193
- value: SKIP_VALUE
16194
- });
16195
16679
  return recommendedFirst(options);
16196
16680
  }
16197
16681
  function hasTargetContent(input) {
@@ -16200,7 +16684,7 @@ function hasTargetContent(input) {
16200
16684
  //#endregion
16201
16685
  //#region src/setup/steps/instructions.ts
16202
16686
  /**
16203
- * The instructions step: one back-navigable chain of forms across both scopes.
16687
+ * The instructions step: one back-navigable chain of forms for personal instructions.
16204
16688
  *
16205
16689
  * Each scope contributes action → sources → duplicate review stages; a stage whose precondition no
16206
16690
  * longer holds (a non-consolidate action, no duplicated paragraphs left) simply disappears from
@@ -16210,9 +16694,8 @@ function hasTargetContent(input) {
16210
16694
  const instructionsStep = {
16211
16695
  gather: async (context, io) => {
16212
16696
  const inputs = scopeInputs(context);
16213
- const scopes = scopeList(inputs);
16214
- if (context.revisited !== true) for (const input of scopes) emitScopeNotes(input, io);
16215
- const stages = scopes.flatMap(scopeStages);
16697
+ if (context.revisited !== true) emitScopeNotes(inputs.global, io);
16698
+ const stages = scopeStages(inputs.global);
16216
16699
  const result = await runFormChain(stages, initialState(context), io, {
16217
16700
  entry: context.enteredBackward === true ? "end" : "start",
16218
16701
  flow: context.flow
@@ -16226,55 +16709,34 @@ const instructionsStep = {
16226
16709
  telemetryCategory: "instructions",
16227
16710
  title: "Instructions"
16228
16711
  };
16229
- /** Both scopes in flow order, which is the order notes are emitted and stages are chained in. */
16230
- function scopeList(inputs) {
16231
- return inputs.project === void 0 ? [inputs.global] : [inputs.global, inputs.project];
16232
- }
16233
- /** Folds both scopes' drafts into the step's selections, unoffered when it asked nothing to do it. */
16712
+ /** Folds the personal draft into the step's selections, unoffered when it asked nothing to do it. */
16234
16713
  function settle(context, inputs, result, unoffered) {
16235
- const project = inputs.project === void 0 ? void 0 : scopeSelection(inputs.project, result.project);
16236
16714
  const selections = {
16237
16715
  ...context.selections,
16238
- instructions: {
16239
- global: scopeSelection(inputs.global, result.global),
16240
- ...project === void 0 ? {} : { project }
16241
- }
16716
+ instructions: { global: scopeSelection(inputs.global, result.global) }
16242
16717
  };
16243
16718
  return unoffered ? {
16244
16719
  selections,
16245
16720
  unoffered: true
16246
16721
  } : selections;
16247
16722
  }
16248
- /** Everything both scopes need from the workspace; project is absent when it has nothing to do. */
16723
+ /** Everything personal instruction setup needs from the workspace. */
16249
16724
  function scopeInputs(context) {
16250
16725
  const inventory = instructionInventory(context.model);
16251
16726
  const targets = instructionTargets(context.model);
16252
16727
  const clusters = duplicateClusters(context.findings ?? []);
16253
- const global = {
16728
+ return { global: {
16254
16729
  blocked: context.model.sharedInstructions.problem !== void 0,
16255
16730
  clusters,
16256
16731
  scope: "global",
16257
16732
  sources: inventory.filter((source) => source.scope === "global"),
16258
16733
  targetContentValue: instructionTargetContent(context.model, "global", targets.global),
16259
16734
  targetPath: targets.global
16260
- };
16261
- const projectSources = inventory.filter((source) => source.scope === "project");
16262
- const projectContent = instructionTargetContent(context.model, "project", targets.project);
16263
- return {
16264
- global,
16265
- project: projectSources.length === 0 && projectContent === void 0 ? void 0 : {
16266
- blocked: false,
16267
- clusters,
16268
- scope: "project",
16269
- sources: projectSources,
16270
- targetContentValue: projectContent,
16271
- targetPath: targets.project
16272
- }
16273
- };
16735
+ } };
16274
16736
  }
16275
16737
  function scopeSelection(input, draft) {
16276
16738
  if (input.blocked) return inactiveSelection(input, "blocked");
16277
- if (draft.action !== "consolidate") return inactiveSelection(input, inactiveAction(draft.action, input.scope));
16739
+ if (draft.action !== "consolidate") return inactiveSelection(input, inactiveAction(draft.action));
16278
16740
  const selectedSources = draft.selectedSources ?? [];
16279
16741
  const relevant = relevantDuplicateClusters(selectedSources, input.clusters);
16280
16742
  return {
@@ -16292,9 +16754,9 @@ function scopeSelection(input, draft) {
16292
16754
  * INS-002 firing at error severity, so the action menu never offers it there; this mapping is the
16293
16755
  * second half of that rule, keeping an action off the menu from becoming one the planner obeys.
16294
16756
  */
16295
- function inactiveAction(action, scope) {
16757
+ function inactiveAction(action) {
16296
16758
  if (action === "template") return "template";
16297
- return action === "skip" && scope === "project" ? "skip" : "keep";
16759
+ return "keep";
16298
16760
  }
16299
16761
  function inactiveSelection(input, action) {
16300
16762
  return {
@@ -16308,10 +16770,7 @@ function inactiveSelection(input, action) {
16308
16770
  /** A re-entered step resumes from what this run already decided, not from cold-start defaults. */
16309
16771
  function initialState(context) {
16310
16772
  const existing = context.selections.instructions;
16311
- return {
16312
- global: toDraft(existing?.global),
16313
- project: toDraft(existing?.project)
16314
- };
16773
+ return { global: toDraft(existing?.global) };
16315
16774
  }
16316
16775
  function toDraft(selection) {
16317
16776
  if (selection === void 0 || selection.action === "blocked") return {};
@@ -16333,10 +16792,10 @@ function emitScopeNotes(input, io) {
16333
16792
  path: input.targetPath,
16334
16793
  scope: input.scope
16335
16794
  });
16336
- io.note(`${input.scope === "global" ? "Global" : "Project"} instructions already live in ${input.targetPath} (${size}); Aura found nothing else to consolidate and leaves the file as it is.`);
16795
+ io.note(`Personal instructions already live in ${input.targetPath} (${size}); Aura found nothing else to consolidate and leaves the file as it is.`);
16337
16796
  return;
16338
16797
  }
16339
- if (input.sources.length > 0) io.note(`Found ${describeSources(input.sources)} for ${input.scope} consolidation.`);
16798
+ if (input.sources.length > 0) io.note(`Found ${describeSources(input.sources)} for personal consolidation.`);
16340
16799
  }
16341
16800
  function describeSources(sources) {
16342
16801
  return sources.map((source) => `${basename(source.path)} (${describeInstructionSource(source)})`).join(" and ");
@@ -16525,47 +16984,27 @@ async function configureEntry(context, entry, taken, io) {
16525
16984
  return SETUP_BACK;
16526
16985
  }
16527
16986
  const defaultName = seed?.name ?? catalog?.serverName ?? "custom";
16528
- const defaultScope = seed?.scope ?? "global";
16529
16987
  const apps = appChoices(context, catalog?.supportedApps, seed?.apps ?? []);
16530
16988
  if (apps.eligible.length === 0 && (seed?.apps.length ?? 0) === 0) {
16531
16989
  io.note("No detected, managed, compatible application can receive this MCP server.");
16532
16990
  return;
16533
16991
  }
16534
16992
  let name = defaultName;
16535
- let scope = defaultScope;
16536
16993
  let selectedApps = seed === void 0 || seed.apps.length === 0 ? apps.eligible : seed.apps;
16537
16994
  for (;;) {
16538
- const result = await io.ask([
16539
- textQuestion("mcp-name", "Name", "Configuration name", name),
16540
- {
16541
- id: "mcp-scope",
16542
- initial: [scope],
16543
- kind: "select",
16544
- label: "Scope",
16545
- options: [{
16546
- label: "Global",
16547
- value: "global"
16548
- }, {
16549
- label: "Project",
16550
- value: "project"
16551
- }],
16552
- prompt: "Where should applications configure this server?"
16553
- },
16554
- {
16555
- id: "mcp-apps",
16556
- initial: selectedApps,
16557
- kind: "multiselect",
16558
- label: "Applications",
16559
- options: apps.options,
16560
- prompt: "Which applications should enable this server?"
16561
- }
16562
- ]);
16995
+ const result = await io.ask([textQuestion("mcp-name", "Name", "Configuration name", name), {
16996
+ id: "mcp-apps",
16997
+ initial: selectedApps,
16998
+ kind: "multiselect",
16999
+ label: "Applications",
17000
+ options: apps.options,
17001
+ prompt: "Which applications should enable this server?"
17002
+ }]);
16563
17003
  if (result === "aborted") return SETUP_ABORTED;
16564
17004
  if (result === "back") return SETUP_BACK;
16565
17005
  name = answerText(result["mcp-name"]);
16566
- scope = selectedValues(result["mcp-scope"])[0] === "project" ? "project" : "global";
16567
17006
  selectedApps = selectedValues(result["mcp-apps"]);
16568
- const problem = entryProblem(name, scope, selectedApps, taken);
17007
+ const problem = entryProblem(name, selectedApps, taken);
16569
17008
  if (problem !== void 0) {
16570
17009
  io.note(problem);
16571
17010
  if (!context.interactive) return SETUP_ABORTED;
@@ -16575,7 +17014,6 @@ async function configureEntry(context, entry, taken, io) {
16575
17014
  apps: Object.freeze([...new Set(selectedApps)]),
16576
17015
  ...catalog === void 0 ? {} : { catalogId: catalog.id },
16577
17016
  name,
16578
- scope,
16579
17017
  transport
16580
17018
  });
16581
17019
  }
@@ -16588,12 +17026,12 @@ async function configureEntry(context, entry, taken, io) {
16588
17026
  * the form that is one line to re-type; caught at serialization it would end the run after every
16589
17027
  * question had been answered.
16590
17028
  */
16591
- function entryProblem(name, scope, apps, taken) {
17029
+ function entryProblem(name, apps, taken) {
16592
17030
  const nameProblem = mcpServerNameProblem(name);
16593
17031
  if (nameProblem !== void 0) return `MCP server $.name ${nameProblem}.`;
16594
17032
  if (apps.length === 0) return "MCP server $.apps must select at least one application.";
16595
17033
  const selected = new Set(apps);
16596
- return taken.some((server) => server.name === name && server.scope === scope && server.apps.some((app) => selected.has(app))) ? `MCP server $.name ${name} is already configured at ${scope} scope for an application selected here. Choose another name.` : void 0;
17034
+ return taken.some((server) => server.name === name && server.apps.some((app) => selected.has(app))) ? `MCP server $.name ${name} is already configured globally for an application selected here. Choose another name.` : void 0;
16597
17035
  }
16598
17036
  function appChoices(context, supportedApps, existingApps) {
16599
17037
  const managed = new Set(managedAppIdList(context));
@@ -16654,7 +17092,7 @@ function workingEntries(context) {
16654
17092
  }
16655
17093
  function matchesEntry(server, entry) {
16656
17094
  const existing = entry.existing;
16657
- if (existing !== void 0) return existing.catalogId === server.catalogId && existing.name === server.name && existing.scope === server.scope;
17095
+ if (existing !== void 0) return existing.catalogId === server.catalogId && existing.name === server.name;
16658
17096
  return entry.catalog?.id === server.catalogId;
16659
17097
  }
16660
17098
  function nextCustomKey(entries) {
@@ -16665,7 +17103,7 @@ function nextCustomKey(entries) {
16665
17103
  /**
16666
17104
  * A default name no other row has taken.
16667
17105
  *
16668
- * Two servers cannot share one name in one application and scope — the manifest refuses to record
17106
+ * Two servers cannot share one name in one application — the manifest refuses to record
16669
17107
  * it — so seeding every custom server with `custom` would turn "add two, accept the defaults" into
16670
17108
  * a plan the manifest declines to hold.
16671
17109
  */
@@ -16727,7 +17165,6 @@ async function gatherMcp(context, io) {
16727
17165
  const server = {
16728
17166
  apps: [],
16729
17167
  name: nextCustomName(entries),
16730
- scope: "global",
16731
17168
  transport
16732
17169
  };
16733
17170
  entries.push({
@@ -16802,15 +17239,24 @@ function requiredCatalogIds(entries) {
16802
17239
  return [...new Set(entries.flatMap((entry) => entry.required && entry.catalog !== void 0 ? [entry.catalog.id] : []))];
16803
17240
  }
16804
17241
  function serverOption(entry) {
16805
- const configured = entry.selectedServer !== void 0 || entry.existing !== void 0;
16806
- const tags = [...entry.required ? ["from preset", "required"] : [], ...configured ? [entry.catalog === void 0 ? "custom" : "configured"] : []];
17242
+ const tags = serverTags(entry);
16807
17243
  const suffix = tags.length === 0 ? "" : ` (${tags.join(", ")})`;
16808
17244
  return {
16809
- description: entry.sourceName === void 0 ? "Custom server" : `Plugin: ${entry.sourceName}`,
17245
+ description: serverProvenance(entry),
16810
17246
  label: `${safe(mcpCatalogEntryName(entry))}${suffix}`,
16811
17247
  value: entry.key
16812
17248
  };
16813
17249
  }
17250
+ /** What the run knows about a row: who selected it, whether it is required or configured. */
17251
+ function serverTags(entry) {
17252
+ const configured = entry.selectedServer !== void 0 || entry.existing !== void 0;
17253
+ const origin = entry.repo === true ? "from repo" : "from preset";
17254
+ return [...entry.required ? [origin, "required"] : entry.repo === true ? [origin] : [], ...configured ? [entry.catalog === void 0 ? "custom" : "configured"] : []];
17255
+ }
17256
+ function serverProvenance(entry) {
17257
+ if (entry.repo === true) return "Repository: .aura/preset.json";
17258
+ return entry.sourceName === void 0 ? "Custom server" : `Plugin: ${entry.sourceName}`;
17259
+ }
16814
17260
  function hasManagedMcpApp(context) {
16815
17261
  const managed = new Set(managedAppIdList(context));
16816
17262
  return context.appCatalog.some((entry) => entry.kind === "detected" && entry.supportsMcp && managed.has(catalogEntryId(entry)));
@@ -16927,11 +17373,7 @@ function pickerOptions(inputs) {
16927
17373
  const policy = inputs.catalog.policy;
16928
17374
  const unsupported = !hasSkillsHome(inputs.managedApps);
16929
17375
  const preset = new Set(inputs.presetSkills.map((skill) => skillIdentity(skill.source, skill.id)));
16930
- const entryRows = sortForDisplay(inputs.listing.entries, (entry) => [
16931
- entry.sourceName,
16932
- entry.name,
16933
- entry.id
16934
- ]).map((entry) => ({
17376
+ const entryRow = (entry) => ({
16935
17377
  description: `${entry.description} · v${entry.version}`,
16936
17378
  get disabled() {
16937
17379
  return unsupported || inputs.listing.verification?.isMissing(entry.identity) === true;
@@ -16941,10 +17383,18 @@ function pickerOptions(inputs) {
16941
17383
  return inputs.listing.verification?.isMissing(entry.identity) === true ? "source no longer publishes this skill" : void 0;
16942
17384
  },
16943
17385
  group: entry.sourceName,
16944
- label: `${entry.name}${preset.has(entry.identity) ? " (from preset)" : ""}`,
17386
+ label: `${entry.name}${selectedBySuffix(entry.identity)}`,
16945
17387
  ...previewField(inputs, entry),
16946
17388
  value: entry.identity
16947
- }));
17389
+ });
17390
+ const selectedBySuffix = (identity) => inputs.repoSelectedIdentities.has(identity) ? " (from repo)" : preset.has(identity) ? " (from preset)" : "";
17391
+ const sortedEntries = sortForDisplay(inputs.listing.entries, (entry) => [
17392
+ entry.sourceName,
17393
+ entry.name,
17394
+ entry.id
17395
+ ]);
17396
+ const repoEntryRows = sortedEntries.filter((entry) => entry.sourceId.startsWith("repo:")).map(entryRow);
17397
+ const entryRows = sortedEntries.filter((entry) => !entry.sourceId.startsWith("repo:")).map(entryRow);
16948
17398
  const manifestRows = inputs.manifestSkills.flatMap((skill) => {
16949
17399
  const identity = skillIdentity(skill.source, skill.id);
16950
17400
  if (covered.has(identity)) return [];
@@ -16999,18 +17449,23 @@ function pickerOptions(inputs) {
16999
17449
  value: `truncated:${source.id}`
17000
17450
  }));
17001
17451
  const represented = /* @__PURE__ */ new Set([...inputs.listing.entries.map((entry) => entry.identity), ...inputs.manifestSkills.map((skill) => skillIdentity(skill.source, skill.id))]);
17002
- const presetRows = inputs.presetSkills.filter((skill) => !represented.has(skillIdentity(skill.source, skill.id))).map((skill) => ({
17003
- description: "Selected by the active team preset, but unavailable in this run.",
17004
- disabled: true,
17005
- disabledNote: "preset selection unavailable",
17006
- group: skill.source,
17007
- label: `${skill.id} (from preset)`,
17008
- value: skillIdentity(skill.source, skill.id)
17009
- }));
17452
+ const presetRows = inputs.presetSkills.filter((skill) => !represented.has(skillIdentity(skill.source, skill.id))).map((skill) => {
17453
+ const identity = skillIdentity(skill.source, skill.id);
17454
+ const fromRepo = inputs.repoSelectedIdentities.has(identity);
17455
+ return {
17456
+ description: fromRepo ? "Selected by the repository preset, but unavailable in this run." : "Selected by the active team preset, but unavailable in this run.",
17457
+ disabled: true,
17458
+ disabledNote: fromRepo ? "repo selection unavailable" : "preset selection unavailable",
17459
+ group: skill.source,
17460
+ label: `${skill.id} ${fromRepo ? "(from repo)" : "(from preset)"}`,
17461
+ value: identity
17462
+ };
17463
+ });
17010
17464
  return [
17011
17465
  ...sourceRows,
17012
17466
  ...truncatedRows,
17013
17467
  ...packRows,
17468
+ ...repoEntryRows,
17014
17469
  ...entryRows,
17015
17470
  ...manifestRows,
17016
17471
  ...presetRows
@@ -17093,6 +17548,16 @@ function skillStages(inputs) {
17093
17548
  function isRemoteIdentity(identity) {
17094
17549
  return identity.startsWith("directory:") || identity.startsWith("driver:");
17095
17550
  }
17551
+ /**
17552
+ * A repository identity's install needs no fetch, but the review boundary all the same.
17553
+ *
17554
+ * The trees arrive by cloning, not by anything the user selected — "local files" is the bundled
17555
+ * exemption's mechanism, but distribution vouching was its justification, and a checkout has
17556
+ * neither.
17557
+ */
17558
+ function isRepoIdentity(identity) {
17559
+ return identity.startsWith("repo:");
17560
+ }
17096
17561
  /** The typed selection behind each offered identity: catalog entries plus manifest rows. */
17097
17562
  function selectionsByIdentity(inputs) {
17098
17563
  const selections = /* @__PURE__ */ new Map();
@@ -17152,7 +17617,8 @@ function reviewStage(inputs) {
17152
17617
  const offered = selectionsByIdentity(inputs);
17153
17618
  const entries = new Map(inputs.listing.entries.map((entry) => [entry.identity, entry]));
17154
17619
  const bundled = bundledByIdentity(inputs.availableSkills);
17155
- const reviewable = (identity) => needsReview(identity, recorded, bundled);
17620
+ const repo = bundledByIdentity(inputs.repoSkills);
17621
+ const reviewable = (identity) => needsReview(identity, recorded, bundled, repo);
17156
17622
  return {
17157
17623
  apply: (state, answers) => ({
17158
17624
  ...state,
@@ -17169,7 +17635,11 @@ function reviewStage(inputs) {
17169
17635
  }), inputs.approvedPrivateSourceIds);
17170
17636
  const resolutionWithBundled = {
17171
17637
  problems: resolution.problems,
17172
- resolved: new Map([...resolution.resolved, ...bundled])
17638
+ resolved: new Map([
17639
+ ...resolution.resolved,
17640
+ ...bundled,
17641
+ ...repo
17642
+ ])
17173
17643
  };
17174
17644
  const questions = state.selected.filter(reviewable).flatMap((identity) => reviewQuestion(identity, state.decisions, resolutionWithBundled, recorded, entries.get(identity), offered.get(identity)));
17175
17645
  return questions.length === 0 ? void 0 : questions;
@@ -17179,8 +17649,13 @@ function reviewStage(inputs) {
17179
17649
  function bundledByIdentity(packs) {
17180
17650
  return new Map(packs.map((pack) => [skillIdentity(pack.source.id, pack.id), pack]));
17181
17651
  }
17182
- function needsReview(identity, recorded, bundled) {
17652
+ function needsReview(identity, recorded, bundled, repo) {
17183
17653
  if (isRemoteIdentity(identity)) return true;
17654
+ if (isRepoIdentity(identity)) {
17655
+ const previous = recorded.get(identity);
17656
+ const pack = repo.get(identity);
17657
+ return previous === void 0 || pack !== void 0 && movedFrom(previous, pack);
17658
+ }
17184
17659
  const previous = recorded.get(identity);
17185
17660
  const available = bundled.get(identity);
17186
17661
  return previous !== void 0 && available !== void 0 && movedFrom(previous, available);
@@ -17206,11 +17681,19 @@ async function finalizeSkills(state, inputs, manifestSkills) {
17206
17681
  const selection = offered.get(identity);
17207
17682
  return selection === void 0 ? [] : [selection];
17208
17683
  }), inputs.approvedPrivateSourceIds);
17684
+ const repoPacks = new Map(inputs.repoSkills.map((pack) => [skillIdentity(pack.source.id, pack.id), pack]));
17209
17685
  const selected = [];
17210
17686
  const resolved = [];
17211
17687
  for (const identity of state.selected) {
17212
17688
  const selection = offered.get(identity);
17213
17689
  if (selection === void 0) continue;
17690
+ if (isRepoIdentity(identity)) {
17691
+ finalizeRemote(identity, selection, state, repoPacks.get(identity), recorded.get(identity), {
17692
+ resolved,
17693
+ selected
17694
+ });
17695
+ continue;
17696
+ }
17214
17697
  if (!isRemoteIdentity(identity)) {
17215
17698
  selected.push(selection);
17216
17699
  continue;
@@ -17283,15 +17766,7 @@ async function gatherApprovedSkills(context, io, approval) {
17283
17766
  const manifestSkills = recordedSkills(context);
17284
17767
  emitNotes(context, io, listing, manifestSkills);
17285
17768
  if (isEmptyCatalog(listing, manifestSkills)) return emptyCatalogOutcome$1(context);
17286
- const inputs = {
17287
- approvedPrivateSourceIds,
17288
- availableSkills: context.model.availableSkills ?? [],
17289
- catalog: context.skillCatalog,
17290
- listing,
17291
- managedApps: managedSkillApps(context),
17292
- manifestSkills,
17293
- presetSkills: context.preset?.skills ?? []
17294
- };
17769
+ const inputs = stageInputs(context, approvedPrivateSourceIds, listing, manifestSkills);
17295
17770
  const opening = openingSelection(context, inputs, manifestSkills);
17296
17771
  noteHeldBack(inputs, io, opening.heldBack);
17297
17772
  const result = await runFormChain(skillStages(inputs), opening.state, io, {
@@ -17316,6 +17791,21 @@ function noteHeldBack(inputs, io, heldBack) {
17316
17791
  function recordedSkills(context) {
17317
17792
  return context.manifest.status === "ready" ? context.manifest.value.skills : [];
17318
17793
  }
17794
+ /** Everything the picker/review chain reads, with the repository selections folded in. */
17795
+ function stageInputs(context, approvedPrivateSourceIds, listing, manifestSkills) {
17796
+ const repoSelections = context.repoPreset?.preset?.skills ?? [];
17797
+ return {
17798
+ approvedPrivateSourceIds,
17799
+ availableSkills: context.model.availableSkills ?? [],
17800
+ catalog: context.skillCatalog,
17801
+ listing,
17802
+ managedApps: managedSkillApps(context),
17803
+ manifestSkills,
17804
+ presetSkills: [...context.preset?.skills ?? [], ...repoSelections],
17805
+ repoSelectedIdentities: new Set(repoSelections.map((skill) => skillIdentity(skill.source, skill.id))),
17806
+ repoSkills: context.repoPreset?.contentSet?.skills ?? []
17807
+ };
17808
+ }
17319
17809
  function emptyCatalogOutcome$1(context) {
17320
17810
  return context.enteredBackward === true ? SETUP_BACK : {
17321
17811
  selections: { ...context.selections },
@@ -17439,7 +17929,7 @@ const snippetsStep = {
17439
17929
  const installed = installedSnippetIds(context);
17440
17930
  if (context.revisited !== true) emitCatalogNotes(context, catalog, installed, io.note);
17441
17931
  if (catalog.length === 0) return emptyCatalogOutcome(context, io);
17442
- const options = snippetOptions(catalog, installed, new Set(context.preset?.snippets ?? []));
17932
+ const options = snippetOptions(catalog, installed, new Set(context.preset?.snippets ?? []), new Set(context.repoPreset?.preset?.snippets ?? []));
17443
17933
  const selectable = new Set(options.filter((option) => option.disabled !== true).map((option) => option.value));
17444
17934
  const question = {
17445
17935
  id: "snippets",
@@ -17479,14 +17969,23 @@ const RECORD_NOTE = "Aura keeps the record; the text stays where it is.";
17479
17969
  * checkbox has anything to do — the row is there to report the record, not to be answered with.
17480
17970
  * Gathering those rows under one heading is what lets the numbers below start at 1 and count only
17481
17971
  * what a tick would change, and it empties out any category whose every snippet is installed.
17972
+ *
17973
+ * Repository rows lead the offered rows: they are this repository's own guidance, which is what a
17974
+ * person running setup inside it most likely came for. Plugin categories keep their sorted order
17975
+ * underneath.
17482
17976
  */
17483
- function snippetOptions(catalog, installed, preset) {
17977
+ function snippetOptions(catalog, installed, preset, repoSelected) {
17484
17978
  const sorted = sortForDisplay(catalog, (entry) => [
17485
17979
  entry.category,
17486
17980
  entry.name,
17487
17981
  entry.id
17488
17982
  ]);
17489
- return [...recordBlock(sorted.filter((entry) => installed.has(entry.id))), ...sorted.filter((entry) => !installed.has(entry.id)).map((entry) => offeredOption(entry, preset))];
17983
+ const offered = sorted.filter((entry) => !installed.has(entry.id));
17984
+ return [
17985
+ ...recordBlock(sorted.filter((entry) => installed.has(entry.id))),
17986
+ ...offered.filter((entry) => entry.origin === "repo").map((entry) => offeredOption(entry, preset, repoSelected)),
17987
+ ...offered.filter((entry) => entry.origin !== "repo").map((entry) => offeredOption(entry, preset, repoSelected))
17988
+ ];
17490
17989
  }
17491
17990
  /**
17492
17991
  * The installed rows, with the shared note appended to the last of them.
@@ -17506,10 +18005,11 @@ function recordBlock(entries) {
17506
18005
  }));
17507
18006
  }
17508
18007
  /** A row a tick would act on, or one disabled because no installed plugin publishes its text. */
17509
- function offeredOption(entry, preset) {
18008
+ function offeredOption(entry, preset, repoSelected) {
18009
+ const selectedBy = repoSelected.has(entry.id) ? " (from repo)" : preset.has(entry.id) ? " (from preset)" : "";
17510
18010
  const base = {
17511
18011
  group: entry.category,
17512
- label: `${entry.name}${preset.has(entry.id) ? " (from preset)" : ""}`,
18012
+ label: `${entry.name}${selectedBy}`,
17513
18013
  value: entry.id,
17514
18014
  ...entry.status === "available" ? { preview: entry.content } : {}
17515
18015
  };
@@ -17535,19 +18035,20 @@ function snippetsPrompt(options) {
17535
18035
  return options.every((option) => option.locked === true) ? "Every snippet the installed plugins provide is already in your instructions." : "Nothing here can be added: every snippet is either installed already or unavailable.";
17536
18036
  }
17537
18037
  /**
17538
- * What the picker opens with: every installed id, plus this run's answer or the preset.
18038
+ * What the picker opens with: every installed id, plus this run's answer or team-preset defaults.
17539
18039
  *
17540
18040
  * Installed rows are locked, so they are seeded on every visit — there is no earlier answer that
17541
18041
  * could have dropped one. Their `✔` comes from the lock rather than from this seeding, so what it
17542
- * settles is the answer the form reports back, not the checkbox the picker draws. The preset adds
17543
- * only what it can actually contribute: a disabled row opening ticked is an answer `--yes` has no
17544
- * way to take back.
18042
+ * settles is the answer the form reports back, not the checkbox the picker draws. Team-preset
18043
+ * defaults add only what their installed plugins can contribute. Repository rows deliberately
18044
+ * open unticked even when the repository preset selects them: their bodies are arbitrary agent
18045
+ * instructions, so a first install requires a person to select the previewable row explicitly.
17545
18046
  */
17546
18047
  function initialSelection(context, options, installed) {
17547
- const preset = new Set(context.preset?.snippets ?? []);
18048
+ const selected = new Set(context.preset?.snippets ?? []);
17548
18049
  const chosen = context.selections.snippets;
17549
18050
  const kept = [...installed];
17550
- const added = chosen?.selected ?? options.filter((option) => option.disabled !== true && !installed.has(option.value) && preset.has(option.value)).map((option) => option.value);
18051
+ const added = chosen?.selected ?? options.filter((option) => option.disabled !== true && !installed.has(option.value) && selected.has(option.value)).map((option) => option.value);
17551
18052
  return [...kept, ...added];
17552
18053
  }
17553
18054
  function emptyCatalogOutcome(context, io) {
@@ -17642,36 +18143,9 @@ async function runSetup(request) {
17642
18143
  stdout.write("\nLeft everything as it was.\n");
17643
18144
  return finish(1, "aborted");
17644
18145
  }
17645
- const { activeChecks, configured, effectiveModel, effectiveScan, projected, scan } = booted;
17646
- const model = scan.model;
18146
+ const { activeChecks, configured, effectiveModel, effectiveScan } = booted;
17647
18147
  const steps = request.steps ?? SETUP_STEPS;
17648
- const initialFindings = steps.some((step) => step.needsFindings === true) ? gatherFindings(activeChecks, effectiveModel, io, configured.config) : void 0;
17649
- const catalogs = createSetupCatalogs({
17650
- config: configured.config,
17651
- environment,
17652
- interactive: request.interactive,
17653
- model: effectiveModel,
17654
- noCache: request.noCache,
17655
- preset: configured.preset,
17656
- presetNotes: [...configured.notes, ...projected.diagnostics.map((diagnostic) => diagnostic.message)],
17657
- presetOrigin: configured.presetOrigin,
17658
- registry: request.registry
17659
- });
17660
- const preset = setupPresetContext(request, configured.config, configured.preset);
17661
- const repoPreset = booted.repoPreset;
17662
- const stepContext = {
17663
- appCatalog: buildAppCatalog(request.registry.adapters, model, scan.skipped),
17664
- ...initialFindings === void 0 ? {} : { findings: initialFindings },
17665
- interactive: request.interactive,
17666
- isEnvironmentVariableSet: (name) => environment.readVariable(name) !== void 0,
17667
- manifest: model.manifest,
17668
- mcpCatalog: catalogs.mcpCatalog,
17669
- model: effectiveModel,
17670
- skillCatalog: catalogs.skillCatalog,
17671
- snippetCatalog: createSnippetCatalog(request.registry.snippets, model.manifest, preset?.snippets ?? []),
17672
- ...preset === void 0 ? {} : { preset },
17673
- ...repoPreset === void 0 ? {} : { repoPreset }
17674
- };
18148
+ const stepContext = buildStepContext(request, booted, environment, steps.some((step) => step.needsFindings === true) ? gatherFindings(activeChecks, effectiveModel, io, configured.config) : void 0);
17675
18149
  let start = {
17676
18150
  index: 0,
17677
18151
  offered: /* @__PURE__ */ new Set(),
@@ -17701,7 +18175,7 @@ async function runSetup(request) {
17701
18175
  environment,
17702
18176
  mcpCatalog: request.registry.mcpServers,
17703
18177
  skills: request.registry.skills
17704
- }), configured.config), activeChecks, configured.config), "applied", {
18178
+ }), configured.config, configured.repoPreset), activeChecks, configured.config), "applied", {
17705
18179
  actions: setupActions(ready.gathered),
17706
18180
  appliedOperationCount: result.appliedOperationCount
17707
18181
  });
@@ -17710,6 +18184,46 @@ async function runSetup(request) {
17710
18184
  function plannedActions(gathered) {
17711
18185
  return gathered === void 0 ? void 0 : setupActions(gathered);
17712
18186
  }
18187
+ /** Assembles the catalogs and the immutable context every wizard step reads. */
18188
+ function buildStepContext(request, booted, environment, initialFindings) {
18189
+ const { configured, effectiveModel, repoPreset, scan } = booted;
18190
+ const model = scan.model;
18191
+ const catalogs = buildCatalogs(request, booted, environment);
18192
+ const preset = setupPresetContext(request, configured.config, configured.preset);
18193
+ return {
18194
+ appCatalog: buildAppCatalog(request.registry.adapters, model, scan.skipped),
18195
+ ...initialFindings === void 0 ? {} : { findings: initialFindings },
18196
+ interactive: request.interactive,
18197
+ isEnvironmentVariableSet: (name) => environment.readVariable(name) !== void 0,
18198
+ manifest: model.manifest,
18199
+ mcpCatalog: catalogs.mcpCatalog,
18200
+ model: effectiveModel,
18201
+ skillCatalog: catalogs.skillCatalog,
18202
+ snippetCatalog: buildSnippetCatalog(request, booted, preset),
18203
+ ...preset === void 0 ? {} : { preset },
18204
+ ...repoPreset === void 0 ? {} : { repoPreset }
18205
+ };
18206
+ }
18207
+ function buildCatalogs(request, booted, environment) {
18208
+ const { configured, effectiveModel, projected, repoPreset } = booted;
18209
+ return createSetupCatalogs({
18210
+ config: configured.config,
18211
+ environment,
18212
+ interactive: request.interactive,
18213
+ model: effectiveModel,
18214
+ noCache: request.noCache,
18215
+ preset: configured.preset,
18216
+ presetNotes: [...configured.notes, ...projected.diagnostics.map((diagnostic) => diagnostic.message)],
18217
+ presetOrigin: configured.presetOrigin,
18218
+ registry: request.registry,
18219
+ repoSkills: repoPreset?.contentSet?.skills ?? []
18220
+ });
18221
+ }
18222
+ /** The snippet catalog, with the trusted repository's bodies and default selections folded in. */
18223
+ function buildSnippetCatalog(request, booted, preset) {
18224
+ const repoPreset = booted.repoPreset;
18225
+ return createSnippetCatalog(request.registry.snippets, booted.scan.model.manifest, [...preset?.snippets ?? [], ...repoPreset?.preset?.snippets ?? []], repoPreset?.contentSet?.snippets ?? []);
18226
+ }
17713
18227
  function setupPresetContext(request, config, preset) {
17714
18228
  const selected = config.preset;
17715
18229
  if (selected === void 0 || preset === void 0) return;