@tryaura/aura-cli 0.3.0 → 0.3.1

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-AcSkw9bO.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";
@@ -3903,6 +3903,7 @@ function collectUnknownBareCheckIdPlugins(state, bareCheckIdPlugins, candidates)
3903
3903
  function collectIdentityViolations(state, plugin) {
3904
3904
  const violationCount = state.violations.length;
3905
3905
  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.`);
3906
+ if (plugin.id === "repo") state.violations.push(`Plugin "${plugin.name}" declares ID "repo", which is reserved.`);
3906
3907
  if (plugin.name.trim().length === 0) state.violations.push(`Plugin "${plugin.id}" declares an empty name; reports identify plugins by name.`);
3907
3908
  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
3909
  return state.violations.length === violationCount;
@@ -6315,12 +6316,13 @@ const MAX_SKILL_DRIVER_CALL_MS = 3e4;
6315
6316
  *
6316
6317
  * Bundled and already-installed trees are local state Aura itself wrote, so they walk unbounded.
6317
6318
  * 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.
6319
+ * claim, not a guarantee — so it walks under {@link DRIVER_WALK_POLICY}, which caps directory
6320
+ * count, file size, file count, and total bytes, refuses paths that will not survive a portable
6321
+ * filesystem, and refuses two paths that would alias on a case-insensitive one.
6321
6322
  */
6322
6323
  /** The bounds an untrusted, driver-supplied tree is read under. */
6323
6324
  const DRIVER_WALK_POLICY = Object.freeze({
6325
+ maxDirectories: 200,
6324
6326
  maxFileBytes: MAX_SKILL_FILE_BYTES,
6325
6327
  maxFiles: 200,
6326
6328
  maxTotalBytes: MAX_SKILL_RESPONSE_BYTES
@@ -6334,19 +6336,22 @@ function treeHash(files) {
6334
6336
  return createHash("sha256").update(`${signature}\n`, "utf8").digest("hex");
6335
6337
  }
6336
6338
  /** Reads one skill directory, stopping at the first entry it cannot safely resolve. */
6337
- async function walkTree(root, reader, policy) {
6339
+ async function walkTree(root, reader, policy, outerBoundary) {
6338
6340
  const files = [];
6339
6341
  const entries = [];
6340
6342
  const state = {
6343
+ bytesRead: 0,
6344
+ directoryCount: 0,
6341
6345
  portablePaths: /* @__PURE__ */ new Set(),
6342
6346
  totalBytes: 0
6343
6347
  };
6344
6348
  const path = resolve(root);
6345
6349
  const problem = await walkPath({
6346
- boundary: await reader.realPath(path) ?? path,
6350
+ boundary: outerBoundary ?? await reader.realPath(path) ?? path,
6347
6351
  path
6348
6352
  }, path, reader, files, entries, state, policy);
6349
6353
  return {
6354
+ bytesRead: state.bytesRead,
6350
6355
  entries: Object.freeze(entries),
6351
6356
  files: Object.freeze(files.sort((left, right) => comparePortablePaths(left.path, right.path))),
6352
6357
  ...problem === void 0 ? {} : { problem }
@@ -6368,6 +6373,11 @@ async function walkPath(root, path, reader, files, entries, state, policy) {
6368
6373
  message: `${relativePath || "."} could not be read (${contents.problem})`
6369
6374
  };
6370
6375
  if (contents.entries !== void 0) {
6376
+ if (policy !== void 0 && state.directoryCount >= policy.maxDirectories) return {
6377
+ kind: "too-large",
6378
+ message: `${relativePath || "."} exceeds the skill directory limit`
6379
+ };
6380
+ state.directoryCount += 1;
6371
6381
  entries.push({
6372
6382
  kind: "directory",
6373
6383
  path
@@ -6385,6 +6395,8 @@ function addFile(path, relativePath, contents, files, entries, state, policy) {
6385
6395
  kind: "unsupported",
6386
6396
  message: `${relativePath} is not a readable regular file`
6387
6397
  };
6398
+ const bytes = Buffer$1.byteLength(contents.content, "utf8");
6399
+ state.bytesRead += bytes;
6388
6400
  if (contents.utf8Valid === false) return {
6389
6401
  kind: "unsupported",
6390
6402
  message: `${relativePath} is not valid UTF-8`
@@ -6399,7 +6411,6 @@ function addFile(path, relativePath, contents, files, entries, state, policy) {
6399
6411
  kind: "unsupported",
6400
6412
  message: `${relativePath} aliases another skill file`
6401
6413
  };
6402
- const bytes = Buffer$1.byteLength(contents.content, "utf8");
6403
6414
  if (state.totalBytes + bytes > policy.maxTotalBytes) return {
6404
6415
  kind: "too-large",
6405
6416
  message: `${relativePath} exceeds the skill size limit`
@@ -6425,7 +6436,7 @@ function comparePortablePaths(left, right) {
6425
6436
  //#endregion
6426
6437
  //#region ../core/src/workspace/skills.ts
6427
6438
  const SKILLS_DIAGNOSTIC_ID = "core/skills";
6428
- const SKILL_FILE$1 = "SKILL.md";
6439
+ const SKILL_FILE$2 = "SKILL.md";
6429
6440
  /** Canonical shared skill root for one captured environment. */
6430
6441
  function sharedSkillsPath(environment) {
6431
6442
  return sharedSkillsRoot(environment.homeDir);
@@ -6448,8 +6459,8 @@ async function resolveBundledSkills(registrations, reader) {
6448
6459
  path,
6449
6460
  skill
6450
6461
  };
6451
- if (!tree.files.some((file) => file.path === SKILL_FILE$1)) return {
6452
- message: `does not contain ${SKILL_FILE$1}`,
6462
+ if (!tree.files.some((file) => file.path === SKILL_FILE$2)) return {
6463
+ message: `does not contain ${SKILL_FILE$2}`,
6453
6464
  path,
6454
6465
  skill
6455
6466
  };
@@ -6485,7 +6496,7 @@ async function resolveDriverSkillPack(skill, source, reader) {
6485
6496
  return { kind: "invalid" };
6486
6497
  }
6487
6498
  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" };
6499
+ if (tree.problem !== void 0 || !tree.files.some((file) => file.path === SKILL_FILE$2)) return { kind: "invalid" };
6489
6500
  return {
6490
6501
  kind: "resolved",
6491
6502
  value: Object.freeze({
@@ -6529,8 +6540,8 @@ async function scanSharedSkills(environment, reader) {
6529
6540
  * reports `unreadable` and leaves the real cause to travel in `problemDetail`.
6530
6541
  */
6531
6542
  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);
6543
+ const skillFilePath = join(path, SKILL_FILE$2);
6544
+ const file = tree.files.find((entry) => entry.path === SKILL_FILE$2);
6534
6545
  if (file === void 0) return {
6535
6546
  definitionStatus: tree.problem === void 0 ? "missing-file" : "unreadable",
6536
6547
  skillFilePath
@@ -7264,7 +7275,7 @@ function parseContents(body) {
7264
7275
  }
7265
7276
  //#endregion
7266
7277
  //#region ../core/src/skills/pack-schema.ts
7267
- const SKILL_FILE = "SKILL.md";
7278
+ const SKILL_FILE$1 = "SKILL.md";
7268
7279
  /**
7269
7280
  * Parses a directory's `skills/<id>` body.
7270
7281
  *
@@ -7307,9 +7318,9 @@ function parseDirectorySkillPack(body, expectedId) {
7307
7318
  kind: "invalid",
7308
7319
  problem: files
7309
7320
  };
7310
- if (!files.some((file) => file.path === SKILL_FILE)) return {
7321
+ if (!files.some((file) => file.path === SKILL_FILE$1)) return {
7311
7322
  kind: "invalid",
7312
- problem: `response does not contain a root ${SKILL_FILE}`
7323
+ problem: `response does not contain a root ${SKILL_FILE$1}`
7313
7324
  };
7314
7325
  return {
7315
7326
  files,
@@ -7483,7 +7494,7 @@ async function downloadFiles(environment, location, remoteFiles) {
7483
7494
  async function listAgenticSkills(environment, source, options = {}) {
7484
7495
  const catalog = await loadAgenticCatalog(environment, source, options.noCache === true);
7485
7496
  if (catalog.kind === "failure") return {
7486
- diagnostics: [diagnostic(`Skill source "${source.id}" ${catalog.reason}, so it is unavailable.`)],
7497
+ diagnostics: [diagnostic$1(`Skill source "${source.id}" ${catalog.reason}, so it is unavailable.`)],
7487
7498
  listings: [],
7488
7499
  status: {
7489
7500
  hint: "unreachable",
@@ -7492,7 +7503,7 @@ async function listAgenticSkills(environment, source, options = {}) {
7492
7503
  };
7493
7504
  return {
7494
7505
  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.`))],
7506
+ diagnostics: [...cacheDiagnostics(source, catalog), ...catalog.problems.map((problem) => diagnostic$1(`Skill source "${source.id}" catalog ${problem}, so some of it is unavailable.`))],
7496
7507
  listings: Object.freeze(catalog.entries.map(({ listing }) => Object.freeze({
7497
7508
  ...listing,
7498
7509
  source
@@ -7506,7 +7517,7 @@ async function listAgenticSkills(environment, source, options = {}) {
7506
7517
  function cacheDiagnostics(source, catalog) {
7507
7518
  if (catalog.cacheAgeMs === void 0) return [];
7508
7519
  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.`)];
7520
+ 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
7521
  }
7511
7522
  /** Resolves AgenticSkills entries from their exact GitHub directories into reviewed Aura packs. */
7512
7523
  async function resolveAgenticSkills(environment, source, skillIds) {
@@ -7529,7 +7540,7 @@ async function resolveAgenticSkills(environment, source, skillIds) {
7529
7540
  function catalogEntries(catalog) {
7530
7541
  return catalog.kind === "catalog" ? new Map(catalog.entries.map((entry) => [entry.listing.id, entry])) : /* @__PURE__ */ new Map();
7531
7542
  }
7532
- function diagnostic(message) {
7543
+ function diagnostic$1(message) {
7533
7544
  return {
7534
7545
  adapterId: AGENTICSKILLS_DIAGNOSTIC_ID,
7535
7546
  message,
@@ -7537,7 +7548,7 @@ function diagnostic(message) {
7537
7548
  };
7538
7549
  }
7539
7550
  function skillDiagnostic(source, id, reason) {
7540
- return diagnostic(`Skill "${id}" from "${source.id}" ${reason}, so it is unavailable.`);
7551
+ return diagnostic$1(`Skill "${id}" from "${source.id}" ${reason}, so it is unavailable.`);
7541
7552
  }
7542
7553
  function isDiagnostic(value) {
7543
7554
  return "adapterId" in value;
@@ -7851,7 +7862,7 @@ const DIRECTORY_PREFIX = "directory:";
7851
7862
  /** Namespaced MCP catalog id, such as `official/github`. */
7852
7863
  const MCP_CATALOG_ID_PATTERN = /^[a-z0-9][a-z0-9._-]*\/[a-zA-Z0-9][a-zA-Z0-9._-]*$/u;
7853
7864
  const SKILL_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
7854
- const SKILL_SOURCE_ID_PATTERN = /^(?:directory|driver|plugin):[^\s:]+$/u;
7865
+ const SKILL_SOURCE_ID_PATTERN = /^(?:directory|driver|plugin|repo):[^\s:]+$/u;
7855
7866
  /**
7856
7867
  * Picks the first collected problem out of a batch of independently validated fields.
7857
7868
  *
@@ -7984,6 +7995,52 @@ function collectJsonValue(value, path, depth) {
7984
7995
  return collectJsonObject(value, path, depth);
7985
7996
  }
7986
7997
  //#endregion
7998
+ //#region ../core/src/preset/repo-content-limits.ts
7999
+ /**
8000
+ * The largest combined byte size of a repository's content set.
8001
+ *
8002
+ * The snapshot lives in memory for the whole run so planners apply consented bytes rather than a
8003
+ * re-read; a repository must not be able to balloon that residency.
8004
+ */
8005
+ const MAX_REPO_CONTENT_TOTAL_BYTES = 16e6;
8006
+ //#endregion
8007
+ //#region ../core/src/preset/schema-provides.ts
8008
+ const REPO_MCP_ID_PATTERN = /^repo\/[a-zA-Z0-9][a-zA-Z0-9._-]*$/u;
8009
+ /**
8010
+ * Collects the `provides` envelope of a repository preset.
8011
+ *
8012
+ * Content a preset authors reaches command lines and app configuration, so it may only arrive
8013
+ * from the file the user hash-trusted for this repository. Callers validating a downloaded or
8014
+ * bundled preset reject the field wholesale before this collector runs.
8015
+ */
8016
+ function collectProvides(value) {
8017
+ if (value === void 0) return;
8018
+ if (!isRecord$4(value)) return "$.provides: must be an object";
8019
+ const servers = collectProvidedMcpServers(value["mcpServers"]);
8020
+ if (typeof servers === "string") return servers;
8021
+ return servers === void 0 ? Object.freeze({}) : Object.freeze({ mcpServers: servers });
8022
+ }
8023
+ function collectProvidedMcpServers(value) {
8024
+ if (value === void 0) return;
8025
+ if (!Array.isArray(value)) return "$.provides.mcpServers: must be an array of MCP server definitions";
8026
+ if (value.length > 16) return `$.provides.mcpServers: must contain at most ${String(16)} definitions`;
8027
+ const result = [];
8028
+ const ids = /* @__PURE__ */ new Set();
8029
+ const serverNames = /* @__PURE__ */ new Set();
8030
+ for (const [index, candidate] of value.entries()) {
8031
+ const path = `$.provides.mcpServers[${String(index)}]`;
8032
+ const parsed = parseMcpServerManifestValue(candidate);
8033
+ if ("error" in parsed) return `${path}${parsed.error.path.slice(1)}: ${parsed.error.message}`;
8034
+ if (!REPO_MCP_ID_PATTERN.test(parsed.value.id)) return `${path}.id: must be namespaced "repo/<name>"`;
8035
+ if (ids.has(parsed.value.id)) return `${path}.id: must not duplicate another provided server id`;
8036
+ if (serverNames.has(parsed.value.serverName)) return `${path}.serverName: must not duplicate another provided server name`;
8037
+ ids.add(parsed.value.id);
8038
+ serverNames.add(parsed.value.serverName);
8039
+ result.push(parsed.value);
8040
+ }
8041
+ return Object.freeze(result);
8042
+ }
8043
+ //#endregion
7987
8044
  //#region ../core/src/preset/schema-sources.ts
7988
8045
  function collectSkills(value) {
7989
8046
  if (value === void 0) return;
@@ -7997,7 +8054,7 @@ function collectSkills(value) {
7997
8054
  const id = candidate["id"];
7998
8055
  const source = candidate["source"];
7999
8056
  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`;
8057
+ if (typeof source !== "string" || !SKILL_SOURCE_ID_PATTERN.test(source)) return `${path}.source: must be a plugin:, directory:, driver:, or repo: source id`;
8001
8058
  const identity = `${source}/${id}`;
8002
8059
  if (seen.has(identity)) return `${path}: must not duplicate another skill selection`;
8003
8060
  seen.add(identity);
@@ -8071,31 +8128,32 @@ function toDirectoryId(id) {
8071
8128
  function toSourceId(id) {
8072
8129
  if (id.startsWith("directory:")) return toDirectoryId(id);
8073
8130
  if (id.startsWith("driver:")) return `driver:${id.slice(7)}`;
8131
+ if (id.startsWith("repo:")) return `repo:${id.slice(5)}`;
8074
8132
  return `plugin:${id.slice(id.indexOf(":") + 1)}`;
8075
8133
  }
8076
8134
  //#endregion
8077
8135
  //#region ../core/src/preset/schema.ts
8078
8136
  /** Validates and freezes one parsed JSON document as a data-only team preset. */
8079
- function validateTeamPreset(value) {
8137
+ function validateTeamPreset(value, options = {}) {
8080
8138
  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");
8139
+ const problemBeforeFields = envelopeProblem(value, options);
8140
+ if (problemBeforeFields !== void 0) return invalid$2(problemBeforeFields);
8084
8141
  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
8142
  const checks = collectChecks(value["checks"]);
8087
8143
  const allowed = collectAllowedSources(value["allowedSkillSources"]);
8088
8144
  const directories = collectDirectories(value["skillDirectories"]);
8089
8145
  const requiredMcpServers = collectIds(value["requiredMcpServers"], "$.requiredMcpServers", MCP_CATALOG_ID_PATTERN);
8090
8146
  const snippets = collectIds(value["snippets"], "$.snippets", CONTENT_ID_PATTERN);
8091
8147
  const skills = collectSkills(value["skills"]);
8148
+ const provides = collectProvides(value["provides"]);
8092
8149
  const problem = firstProblem([
8093
8150
  checks,
8094
8151
  allowed,
8095
8152
  directories,
8096
8153
  requiredMcpServers,
8097
8154
  snippets,
8098
- skills
8155
+ skills,
8156
+ provides
8099
8157
  ]);
8100
8158
  if (problem !== void 0) return invalid$2(problem);
8101
8159
  return {
@@ -8103,6 +8161,7 @@ function validateTeamPreset(value) {
8103
8161
  preset: Object.freeze({
8104
8162
  ...Array.isArray(allowed) ? { allowedSkillSources: allowed } : {},
8105
8163
  ...isChecks(checks) ? { checks } : {},
8164
+ ...typeof provides === "object" ? { provides } : {},
8106
8165
  ...Array.isArray(requiredMcpServers) ? { requiredMcpServers } : {},
8107
8166
  ...typeof name === "string" ? { name } : {},
8108
8167
  schemaVersion: 1,
@@ -8118,6 +8177,15 @@ function invalid$2(problem) {
8118
8177
  problem
8119
8178
  };
8120
8179
  }
8180
+ /** Checks the document envelope — depth, version, origin gate, name — before any field walks. */
8181
+ function envelopeProblem(value, options) {
8182
+ const depthProblem = documentDepthProblem(value, "$", 0);
8183
+ if (depthProblem !== void 0) return depthProblem;
8184
+ if (value["schemaVersion"] !== 1) return "$.schemaVersion: must be 1";
8185
+ if (options.allowProvides !== true && value["provides"] !== void 0) return "$.provides: only the repository preset may provide content";
8186
+ const name = value["name"];
8187
+ 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";
8188
+ }
8121
8189
  /**
8122
8190
  * Rejects a document nested past the limit before any field is read.
8123
8191
  *
@@ -8967,7 +9035,7 @@ async function readTeamPreset(cwd, reader) {
8967
9035
  } catch {
8968
9036
  return failure(path, "is not valid JSON");
8969
9037
  }
8970
- const result = validateTeamPreset(parsed);
9038
+ const result = validateTeamPreset(parsed, { allowProvides: true });
8971
9039
  if (result.kind === "invalid") return failure(path, `is not a valid team preset (${result.problem})`);
8972
9040
  return {
8973
9041
  content: contents.content,
@@ -8989,18 +9057,275 @@ function failure(path, reason) {
8989
9057
  };
8990
9058
  }
8991
9059
  //#endregion
8992
- //#region ../core/src/preset/repo-trust.ts
9060
+ //#region ../core/src/skills/repo-source.ts
9061
+ const REPO_SKILLS_DIAGNOSTIC_ID = "core/repo-skills";
9062
+ const SKILL_FILE = "SKILL.md";
9063
+ /** The one source id repository skills are offered under; one repository, one source. */
9064
+ const REPO_SKILL_SOURCE_ID = "repo:workspace";
9065
+ /** The repository skills directory as a picker-facing source. */
9066
+ function repoSkillSource(path) {
9067
+ return Object.freeze({
9068
+ id: REPO_SKILL_SOURCE_ID,
9069
+ kind: "repo",
9070
+ name: "This repository",
9071
+ path
9072
+ });
9073
+ }
9074
+ /**
9075
+ * Resolves every skill tree below `.aura/skills` into an installable pack.
9076
+ *
9077
+ * A broken entry earns a diagnostic and drops out rather than failing the run: unlike snippets,
9078
+ * these trees are outside the trust hash — they are offers the per-skill review gates, so an
9079
+ * unreadable one is a smaller catalog, not a broken consent record. Each tree walks under the
9080
+ * driver policy: a cloned tree deserves exactly the suspicion a driver-materialized one gets.
9081
+ */
9082
+ async function resolveRepoSkills(root, boundary, reader, maxTotalBytes) {
9083
+ const rootRead = await reader.readWithin(root, [boundary]);
9084
+ const { contents } = rootRead;
9085
+ if (!contents.exists) return {
9086
+ diagnostics: [],
9087
+ skills: []
9088
+ };
9089
+ if (rootRead.kind !== "read" || contents.pathKind === "symlink" || contents.problem !== void 0 || contents.entries === void 0) return {
9090
+ diagnostics: [{
9091
+ adapterId: REPO_SKILLS_DIAGNOSTIC_ID,
9092
+ message: "Repository skills path .aura/skills is not a directory, so none are offered.",
9093
+ path: root,
9094
+ phase: "read"
9095
+ }],
9096
+ skills: []
9097
+ };
9098
+ const diagnostics = [];
9099
+ const named = contents.entries.filter((entry) => !entry.startsWith("."));
9100
+ if (named.length > 32) diagnostics.push({
9101
+ adapterId: REPO_SKILLS_DIAGNOSTIC_ID,
9102
+ message: `Repository skills directory holds more than the ${String(32)} entries Aura offers; the rest are ignored.`,
9103
+ path: root,
9104
+ phase: "read"
9105
+ });
9106
+ const skills = [];
9107
+ const source = repoSkillSource(root);
9108
+ let bytesRead = 0;
9109
+ for (const id of named.slice(0, 32)) {
9110
+ const remainingBytes = maxTotalBytes - bytesRead;
9111
+ if (remainingBytes <= 0) {
9112
+ diagnostics.push(diagnostic(join(root, id), "exceeds the repository content size budget"));
9113
+ continue;
9114
+ }
9115
+ const outcome = await resolveOneSkill(root, id, source, reader, boundary, remainingWalkPolicy(remainingBytes));
9116
+ bytesRead += outcome.bytesRead;
9117
+ if (outcome.problem !== void 0) {
9118
+ diagnostics.push(diagnostic(join(root, id), outcome.bytesRead >= remainingBytes ? "exceeds the repository content size budget" : outcome.problem));
9119
+ continue;
9120
+ }
9121
+ skills.push(outcome.skill);
9122
+ }
9123
+ return {
9124
+ diagnostics: Object.freeze(diagnostics),
9125
+ skills: Object.freeze(skills)
9126
+ };
9127
+ }
9128
+ async function resolveOneSkill(root, id, source, reader, boundary, policy) {
9129
+ const idProblem = skillIdProblem(id);
9130
+ if (idProblem !== void 0) return {
9131
+ bytesRead: 0,
9132
+ problem: idProblem
9133
+ };
9134
+ const tree = await walkTree(join(root, id), reader, policy, boundary);
9135
+ if (tree.problem !== void 0) return {
9136
+ bytesRead: tree.bytesRead,
9137
+ problem: tree.problem.message
9138
+ };
9139
+ const definition = tree.files.find((file) => file.path === SKILL_FILE);
9140
+ if (definition === void 0) return {
9141
+ bytesRead: tree.bytesRead,
9142
+ problem: `does not contain ${SKILL_FILE}`
9143
+ };
9144
+ const frontmatter = parseSkillFrontmatter(definition.content);
9145
+ return {
9146
+ bytesRead: tree.bytesRead,
9147
+ skill: Object.freeze({
9148
+ description: frontmatter.description ?? "Repository skill.",
9149
+ files: tree.files,
9150
+ id,
9151
+ name: frontmatter.name ?? id,
9152
+ source,
9153
+ treeHash: treeHash(tree.files),
9154
+ version: frontmatter.version ?? "0.0.0"
9155
+ })
9156
+ };
9157
+ }
9158
+ /** Caps each attempt by what remains of the repository-wide read budget. */
9159
+ function remainingWalkPolicy(remainingBytes) {
9160
+ return Object.freeze({
9161
+ ...DRIVER_WALK_POLICY,
9162
+ maxFileBytes: Math.min(DRIVER_WALK_POLICY.maxFileBytes, remainingBytes),
9163
+ maxTotalBytes: Math.min(DRIVER_WALK_POLICY.maxTotalBytes, remainingBytes)
9164
+ });
9165
+ }
9166
+ function diagnostic(path, message) {
9167
+ return {
9168
+ adapterId: REPO_SKILLS_DIAGNOSTIC_ID,
9169
+ message: `Repository skill ${message}, so it is not offered.`,
9170
+ path,
9171
+ phase: "read"
9172
+ };
9173
+ }
9174
+ //#endregion
9175
+ //#region ../core/src/preset/repo-content-files.ts
9176
+ /** Matches the optional frontmatter block a snippet file may open with. */
9177
+ const FRONTMATTER_PATTERN = /^---\r?\n[\s\S]*?\r?\n---(?:\r?\n|$)/u;
9178
+ /** Reads every Markdown snippet below `.aura/snippets` under the trust-set rules. */
9179
+ async function readRepoSnippets(root, boundary, reader) {
9180
+ const contents = await reader.read(root);
9181
+ if (!contents.exists) return {
9182
+ files: [],
9183
+ status: "ready"
9184
+ };
9185
+ if (contents.entries === void 0) return {
9186
+ problem: "the snippets path is not a directory",
9187
+ status: "invalid"
9188
+ };
9189
+ const names = contents.entries.filter((entry) => !entry.startsWith(".") && entry.endsWith(".md"));
9190
+ if (names.length > 64) return {
9191
+ problem: `holds more than the ${String(64)} snippet limit`,
9192
+ status: "invalid"
9193
+ };
9194
+ const files = [];
9195
+ for (const name of names) {
9196
+ const result = await readSnippetFile(root, name, boundary, reader);
9197
+ if (typeof result === "string") return {
9198
+ problem: result,
9199
+ status: "invalid"
9200
+ };
9201
+ files.push(result);
9202
+ }
9203
+ files.sort((left, right) => left.entry.id < right.entry.id ? -1 : 1);
9204
+ return {
9205
+ files: Object.freeze(files),
9206
+ status: "ready"
9207
+ };
9208
+ }
9209
+ async function readSnippetFile(root, name, boundary, reader) {
9210
+ const stem = name.slice(0, -3);
9211
+ if (skillIdProblem(stem) !== void 0) return "contains an entry that is not a kebab-case Markdown file name";
9212
+ const read = await reader.readWithin(join(root, name), [boundary], { maxBytes: MAX_SNIPPET_BYTES });
9213
+ const problem = snippetReadProblem(read);
9214
+ if (problem !== void 0) return problem;
9215
+ const text = read.contents.content ?? "";
9216
+ const frontmatter = parseSkillFrontmatter(text);
9217
+ const body = text.replace(FRONTMATTER_PATTERN, "");
9218
+ return {
9219
+ entry: Object.freeze({
9220
+ body,
9221
+ ...frontmatter.description === void 0 ? {} : { description: frontmatter.description },
9222
+ id: `repo/${stem}`,
9223
+ name: frontmatter.name ?? stem
9224
+ }),
9225
+ text
9226
+ };
9227
+ }
9228
+ /** Why a bounded, boundary-checked read did not yield a plain snippet file. */
9229
+ function snippetReadProblem(read) {
9230
+ if (read.kind === "outside") return "contains an entry that leads outside the repository's .aura directory";
9231
+ if (read.kind === "unverified" || read.contents.pathKind === "symlink") return "contains an entry that is not a regular file";
9232
+ const { contents } = read;
9233
+ 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";
9234
+ if ((contents.size ?? 0) > 256e3) return `contains an entry larger than the ${String(MAX_SNIPPET_BYTES)} byte snippet limit`;
9235
+ }
9236
+ //#endregion
9237
+ //#region ../core/src/preset/repo-content.ts
9238
+ const REPO_CONTENT_DIAGNOSTIC_ID = "core/repo-content";
9239
+ /**
9240
+ * Reads the repository's trust-hashed content and any requested installable content in one snapshot.
9241
+ *
9242
+ * The result is what catalogs and planners consume for the whole run: applying from this snapshot
9243
+ * rather than from a re-read is what closes the window between the bytes the user consented to
9244
+ * and the bytes a later write could put on disk. A broken snippet set fails the run — those bytes
9245
+ * are inside the trust hash — while a broken skill tree only shrinks the offers, because skills
9246
+ * stay behind the per-skill review.
9247
+ */
9248
+ async function readRepoContent(auraDir, preset, presetText, reader, options = {}) {
9249
+ const boundary = await reader.realPath(auraDir) ?? auraDir;
9250
+ const snippets = await readRepoSnippets(join(auraDir, "snippets"), boundary, reader);
9251
+ if (snippets.status === "invalid") return {
9252
+ diagnostics: [{
9253
+ adapterId: REPO_CONTENT_DIAGNOSTIC_ID,
9254
+ message: `Repository snippets directory ".aura/snippets" ${snippets.problem}.`,
9255
+ path: join(auraDir, "snippets"),
9256
+ phase: "read"
9257
+ }],
9258
+ status: "invalid"
9259
+ };
9260
+ const snippetBytes = snippets.files.reduce((sum, file) => sum + Buffer$1.byteLength(file.text, "utf8"), 0);
9261
+ if (snippetBytes > 16e6) return {
9262
+ diagnostics: [{
9263
+ adapterId: REPO_CONTENT_DIAGNOSTIC_ID,
9264
+ message: `Repository snippets exceed the ${String(MAX_REPO_CONTENT_TOTAL_BYTES)} byte repository content budget.`,
9265
+ path: join(auraDir, "snippets"),
9266
+ phase: "read"
9267
+ }],
9268
+ status: "invalid"
9269
+ };
9270
+ const skills = options.includeSkills === false ? {
9271
+ diagnostics: [],
9272
+ skills: []
9273
+ } : await resolveRepoSkills(join(auraDir, "skills"), boundary, reader, MAX_REPO_CONTENT_TOTAL_BYTES - snippetBytes);
9274
+ return {
9275
+ contentSet: Object.freeze({
9276
+ mcpServers: preset.provides?.mcpServers ?? [],
9277
+ skills: skills.skills,
9278
+ snippets: Object.freeze(snippets.files.map((file) => file.entry))
9279
+ }),
9280
+ diagnostics: skills.diagnostics,
9281
+ hash: hashRepoContentSet(presetText, snippets.files.map((file) => ({
9282
+ id: file.entry.id,
9283
+ text: file.text
9284
+ }))),
9285
+ status: "ready"
9286
+ };
9287
+ }
8993
9288
  /**
8994
- * Hashes repository preset contents for the trust record.
9289
+ * Lifts the preset's inline MCP definitions into catalog entries.
8995
9290
  *
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.
9291
+ * The `repo/` namespace is reserved against plugins at registry build, so these can never collide
9292
+ * with or be shadowed by — a plugin catalog entry. The source URL names the preset file itself:
9293
+ * that is the file whose bytes the user trusted.
8998
9294
  */
8999
- function hashRepoPreset(content) {
9000
- return hashContent(content);
9295
+ function repoMcpServerDefs(servers, presetPath) {
9296
+ return Object.freeze(servers.map((manifest) => Object.freeze({
9297
+ description: manifest.description,
9298
+ id: manifest.id,
9299
+ kind: "mcp-server",
9300
+ manifest,
9301
+ name: manifest.name,
9302
+ source: Object.freeze({
9303
+ type: "file",
9304
+ url: pathToFileURL(presetPath).href
9305
+ }),
9306
+ version: "0.0.0"
9307
+ })));
9001
9308
  }
9309
+ /**
9310
+ * Hashes the repository content set for the trust record.
9311
+ *
9312
+ * With no snippet files the hash is exactly the preset file's own content hash, so a repository
9313
+ * that never adds `.aura/snippets` keeps every trust its users already recorded. The first
9314
+ * snippet changes the descriptor shape — and consent is re-asked, which is the point.
9315
+ */
9316
+ function hashRepoContentSet(presetText, snippets) {
9317
+ if (snippets.length === 0) return hashContent(presetText);
9318
+ const lines = [
9319
+ "aura-repo-content-v1",
9320
+ `preset ${hashContent(presetText)}`,
9321
+ ...[...snippets].sort((left, right) => left.id < right.id ? -1 : 1).map((file) => `snippet ${file.id} ${hashContent(file.text)}`)
9322
+ ];
9323
+ return hashContent(lines.join("\n"));
9324
+ }
9325
+ //#endregion
9326
+ //#region ../core/src/preset/repo-trust.ts
9002
9327
  /** Reads and validates the repository preset below the invoking directory. */
9003
- async function readRepoPreset(environment, reader = createFileReader()) {
9328
+ async function readRepoPreset(environment, reader = createFileReader(), options = {}) {
9004
9329
  const cwd = await reader.realPath(environment.cwd) ?? environment.cwd;
9005
9330
  const path = resolveTeamPresetPath(cwd);
9006
9331
  const state = await readTeamPreset(cwd, reader);
@@ -9009,10 +9334,17 @@ async function readRepoPreset(environment, reader = createFileReader()) {
9009
9334
  path,
9010
9335
  status: state.status
9011
9336
  };
9337
+ const content = await readRepoContent(dirname(path), state.preset, state.content, reader, options);
9338
+ if (content.status === "invalid") return {
9339
+ diagnostics: [...state.diagnostics, ...content.diagnostics],
9340
+ path,
9341
+ status: "invalid"
9342
+ };
9012
9343
  const mainWorktreePath = await resolveMainWorktreePresetPath(cwd, path, reader);
9013
9344
  return {
9014
- diagnostics: state.diagnostics,
9015
- hash: hashRepoPreset(state.content),
9345
+ contentSet: content.contentSet,
9346
+ diagnostics: [...state.diagnostics, ...content.diagnostics],
9347
+ hash: content.hash,
9016
9348
  ...mainWorktreePath === void 0 ? {} : { mainWorktreePath },
9017
9349
  path,
9018
9350
  preset: state.preset,
@@ -9117,6 +9449,91 @@ function note(reason, id, selection) {
9117
9449
  };
9118
9450
  }
9119
9451
  //#endregion
9452
+ //#region src/runtime-config.ts
9453
+ /** Loads the selected preset and resolves all runtime layers once for a command. */
9454
+ async function resolveRuntimeConfig(input) {
9455
+ if (input.manifest.status === "read-only") return {
9456
+ message: input.manifest.problem.message,
9457
+ status: "invalid"
9458
+ };
9459
+ const manifest = input.manifest.status === "ready" ? input.manifest.value : void 0;
9460
+ const loaded = await loadTeamPreset({
9461
+ cliReference: input.cliReference,
9462
+ defaultReference: input.defaultPreset,
9463
+ environment: input.environment,
9464
+ manifestReference: manifest?.preset,
9465
+ noCache: input.noCache,
9466
+ offline: input.online !== true,
9467
+ presets: input.registry.presets
9468
+ });
9469
+ if (loaded.status === "invalid") return loaded;
9470
+ const repo = input.repoPresetState ?? await readRepoPreset(input.environment, void 0, { includeSkills: false });
9471
+ if (repo.status === "invalid") return {
9472
+ message: `${repo.diagnostics[0]?.message ?? `Repository preset ${repo.path} cannot be read.`} Fix or remove the file to continue.`,
9473
+ status: "invalid"
9474
+ };
9475
+ const repoTrusted = repo.status === "ready" && repo.hash !== void 0 && (repo.hash === input.acceptedRepoPresetHash || isRepoPresetTrusted(manifest, repo, repo.hash));
9476
+ const resolved = resolveEffectiveConfig({
9477
+ checks: input.registry.checks,
9478
+ cli: input.cliLayer,
9479
+ distro: input.defaults,
9480
+ knownMcpServers: /* @__PURE__ */ new Set([...input.registry.mcpServers.map((server) => server.id), ...repoTrusted ? (repo.preset?.provides?.mcpServers ?? []).map((server) => server.id) : []]),
9481
+ ...manifest === void 0 ? {} : { manifest: {
9482
+ ...manifest.checks === void 0 ? {} : { checks: manifest.checks },
9483
+ skills: manifest.skills.map(({ id, source }) => ({
9484
+ id,
9485
+ source
9486
+ })),
9487
+ snippets: manifest.snippets.map(({ id }) => id)
9488
+ } },
9489
+ ...loaded.status === "ready" ? {
9490
+ preset: loaded.preset,
9491
+ selectedPreset: loaded.selected
9492
+ } : {},
9493
+ ...repoTrusted ? { repo: repo.preset } : {}
9494
+ });
9495
+ if (resolved.status === "invalid") return {
9496
+ message: resolved.problems.join("\n"),
9497
+ status: "invalid"
9498
+ };
9499
+ const policyPreset = loaded.status === "ready" || resolved.config.allowedSkillSources !== void 0 || resolved.config.skillDirectories.length > 0 ? Object.freeze({
9500
+ ...loaded.status === "ready" ? loaded.preset : {},
9501
+ ...resolved.config.allowedSkillSources === void 0 ? {} : { allowedSkillSources: resolved.config.allowedSkillSources.value },
9502
+ name: resolved.config.preset?.name ?? resolved.config.allowedSkillSources?.provenance.label ?? resolved.config.skillDirectories[0]?.provenance.label ?? "runtime policy",
9503
+ schemaVersion: 1,
9504
+ skillDirectories: resolved.config.skillDirectories.map(({ value }) => value)
9505
+ }) : void 0;
9506
+ return {
9507
+ config: resolved.config,
9508
+ notes: [...loaded.status === "ready" ? loaded.notes : [], ...repoTrusted ? repo.diagnostics.map((diagnostic) => diagnostic.message) : []],
9509
+ ...policyPreset === void 0 ? {} : { preset: policyPreset },
9510
+ presetOrigin: loaded.status === "ready" ? loaded.origin : policyPreset?.name ?? ".aura/preset.json",
9511
+ ...repo.status === "ready" && repo.hash !== void 0 ? { repoPreset: {
9512
+ ...repoTrusted && repo.contentSet !== void 0 ? { contentSet: repo.contentSet } : {},
9513
+ hash: repo.hash,
9514
+ ...repo.mainWorktreePath === void 0 ? {} : { mainWorktreePath: repo.mainWorktreePath },
9515
+ path: repo.path,
9516
+ ...repoTrusted && repo.preset !== void 0 ? { preset: repo.preset } : {},
9517
+ status: repoTrusted ? "applied" : "held"
9518
+ } } : {},
9519
+ status: "ready"
9520
+ };
9521
+ }
9522
+ /**
9523
+ * Adds a trusted repository's provided MCP definitions to the model's catalog.
9524
+ *
9525
+ * Run before {@link applyRequiredMcpServers} projection wherever a model is projected, so a
9526
+ * repository can require a server it defines itself. A held repository changes nothing.
9527
+ */
9528
+ function withRepoMcpCatalog(model, repoPreset) {
9529
+ const servers = repoPreset?.status === "applied" ? repoPreset.contentSet?.mcpServers ?? [] : [];
9530
+ if (servers.length === 0 || repoPreset === void 0) return model;
9531
+ return Object.freeze({
9532
+ ...model,
9533
+ availableMcpServers: Object.freeze([...model.availableMcpServers, ...repoMcpServerDefs(servers, repoPreset.path)])
9534
+ });
9535
+ }
9536
+ //#endregion
9120
9537
  //#region src/safe-text.ts
9121
9538
  const MAX_FINDING_TEXT_CHARACTERS = 500;
9122
9539
  const UNICODE_FORMAT_CHARACTER = /\p{Cf}/u;
@@ -9339,6 +9756,15 @@ function countsByCheck(findings) {
9339
9756
  }
9340
9757
  //#endregion
9341
9758
  //#region src/command-support.ts
9759
+ /**
9760
+ * Projects preset requirements onto a check scan's model.
9761
+ *
9762
+ * A trusted repository's provided MCP definitions join the catalog first, so a repository can
9763
+ * require a server it defines itself and `check` reports that requirement the way setup would.
9764
+ */
9765
+ function projectConfiguredModel(model, configured) {
9766
+ return applyRequiredMcpServers(withRepoMcpCatalog(model, configured.repoPreset), configured.config);
9767
+ }
9342
9768
  /** The `--home` override every scanning command shares. */
9343
9769
  function homeOption() {
9344
9770
  return Option.String("--home", { description: "Override the home directory." });
@@ -9404,75 +9830,6 @@ function reportUnexpectedFailure(error, subject, branding, withDetail, stderr, t
9404
9830
  return 3;
9405
9831
  }
9406
9832
  //#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
9833
  //#region src/check-config.ts
9477
9834
  /** Resolves the effective configuration for one `check` run. */
9478
9835
  function resolveCheckConfiguration(options) {
@@ -12917,7 +13274,7 @@ var CheckCommand = class extends Command {
12917
13274
  try {
12918
13275
  const startedAt = environment.now();
12919
13276
  let scan = await scanner.run();
12920
- let projected = applyRequiredMcpServers(scan.model, configured.config);
13277
+ let projected = projectConfiguredModel(scan.model, configured);
12921
13278
  let model = projected.model;
12922
13279
  let run = runChecks(activeChecks, model, configured.config);
12923
13280
  let fixRunDiagnostics = [];
@@ -12945,7 +13302,7 @@ var CheckCommand = class extends Command {
12945
13302
  fixes = outcome.fixes;
12946
13303
  if (outcome.applied) {
12947
13304
  scan = await scanner.run();
12948
- projected = applyRequiredMcpServers(scan.model, configured.config);
13305
+ projected = projectConfiguredModel(scan.model, configured);
12949
13306
  model = projected.model;
12950
13307
  run = runChecks(activeChecks, model, configured.config);
12951
13308
  }
@@ -13029,7 +13386,7 @@ function renderRootHelp(branding) {
13029
13386
  rows: [NO_COLOR_ROW],
13030
13387
  title: "Advanced"
13031
13388
  }
13032
- ], branding.docsUrl === void 0 ? [] : [`Docs: ${branding.docsUrl}`]);
13389
+ ], branding.docsUrl === void 0 ? [] : [`Docs: ${branding.docsUrl}`], ROOT_INTRO);
13033
13390
  }
13034
13391
  function renderCheckHelp(branding) {
13035
13392
  const bin = branding.command;
@@ -13216,6 +13573,17 @@ const NO_COLOR_ROW = {
13216
13573
  term: "--no-color",
13217
13574
  text: "Disable terminal colors"
13218
13575
  };
13576
+ /**
13577
+ * The root screen's one paragraph: what Aura is for, in the words of someone who has not read the
13578
+ * docs yet. It sits above "Get started" because a first-time reader needs the problem before the
13579
+ * command. Hand-wrapped at 80 columns — the renderer aligns columns, it never reflows prose — and
13580
+ * carried only by the root screen, since every other screen is reached by someone who already knows.
13581
+ */
13582
+ const ROOT_INTRO = [
13583
+ "Every AI coding agent keeps its own rules and its own tool settings, in its own",
13584
+ "files. Aura reads all of them, tells you where they disagree or are broken, and",
13585
+ "fixes them after you say yes - so every agent works from the same instructions."
13586
+ ];
13219
13587
  /** Test and CI plumbing, present on every command and interesting to almost nobody. */
13220
13588
  function advancedRows() {
13221
13589
  return [
@@ -13263,9 +13631,10 @@ function headline(branding) {
13263
13631
  return branding.description === void 0 ? name : `${name} — ${branding.description}`;
13264
13632
  }
13265
13633
  /** Terms align to one shared column so the eye scans a single list, not per-section islands. */
13266
- function renderHelpScreen(header, sections, footers) {
13634
+ function renderHelpScreen(header, sections, footers, intro = []) {
13267
13635
  const width = Math.max(...sections.flatMap((section) => section.rows.map((row) => row.term.length)));
13268
13636
  const lines = [header];
13637
+ if (intro.length > 0) lines.push("", ...intro.map((line) => ` ${line}`));
13269
13638
  for (const section of sections) {
13270
13639
  lines.push("", ` ${section.title}`);
13271
13640
  for (const row of section.rows) lines.push(` ${row.term.padEnd(width)} ${row.text}`);
@@ -14160,6 +14529,131 @@ function formatThresholds(thresholds) {
14160
14529
  return pairs.length === 0 ? "no thresholds" : pairs.join(", ");
14161
14530
  }
14162
14531
  //#endregion
14532
+ //#region src/setup/repo-trust-preview.ts
14533
+ /** Printed instead of the row block when the preset carries nothing worth reviewing. */
14534
+ const NO_SETTINGS = "No check, MCP, skill, or snippet settings.";
14535
+ /**
14536
+ * Security-relevant capabilities shown before repository-controlled settings are accepted.
14537
+ *
14538
+ * One row per thing, never one row per field: a snippet the preset both provides and selects is
14539
+ * one snippet, and naming it twice made the screen long enough to skim past. Everything
14540
+ * executable-adjacent is spelled out verbatim (escaped) — the MCP command line or endpoint is
14541
+ * exactly what a later selection would configure, and trust time is the one moment the user is
14542
+ * looking. Snippet bodies are deliberately not echoed — pasting untrusted Markdown into a consent
14543
+ * prompt is its own injection surface — the picker's preview is where they are read, and the
14544
+ * prompt itself carries the explicit-selection boundary.
14545
+ */
14546
+ function repoPresetTrustPreview(preset, contentSet) {
14547
+ const rows = [
14548
+ ...mcpRows(preset, contentSet),
14549
+ ...skillRows(preset, contentSet),
14550
+ ...snippetRows(preset, contentSet),
14551
+ ...(preset.skillDirectories ?? []).map(directoryRow),
14552
+ ...listRow("sources", preset.allowedSkillSources),
14553
+ ...checksRow(preset.checks)
14554
+ ];
14555
+ return [
14556
+ "",
14557
+ ...rows.length === 0 ? [` ${NO_SETTINGS}`] : alignRows(rows),
14558
+ ""
14559
+ ].join("\n");
14560
+ }
14561
+ /** Whether the prompt should promise that admitted content still needs a deliberate tick. */
14562
+ function hasRepoContent(contentSet) {
14563
+ return contentSet !== void 0 && contentSet.mcpServers.length + contentSet.skills.length + contentSet.snippets.length > 0;
14564
+ }
14565
+ function alignRows(rows) {
14566
+ const width = Math.max(...rows.map((row) => row.term.length));
14567
+ return rows.map((row) => ` ${row.term.padEnd(width)} ${row.detail}`);
14568
+ }
14569
+ /**
14570
+ * Every MCP server the repository would configure, provided ones with their full command line.
14571
+ *
14572
+ * A required id the repository does not define itself resolves from the catalog, so it gets a row
14573
+ * saying that rather than an executable surface this file cannot promise.
14574
+ */
14575
+ function mcpRows(preset, contentSet) {
14576
+ const provided = contentSet?.mcpServers ?? [];
14577
+ const providedIds = new Set(provided.map((server) => server.id));
14578
+ const required = new Set(preset.requiredMcpServers ?? []);
14579
+ return [...provided.map((server) => ({
14580
+ detail: `${mcpServerPreview(server)}${required.has(server.id) ? " · required" : ""}`,
14581
+ term: "mcp"
14582
+ })), ...[...required].filter((id) => !providedIds.has(id)).map((id) => ({
14583
+ detail: `${safe(id)} · required, from the catalog`,
14584
+ term: "mcp"
14585
+ }))];
14586
+ }
14587
+ /** Skill trees the repository ships, then selections that point somewhere else. */
14588
+ function skillRows(preset, contentSet) {
14589
+ const provided = contentSet?.skills ?? [];
14590
+ const providedKeys = new Set(provided.map((skill) => `${skill.source.id}/${skill.id}`));
14591
+ return [...provided.map((skill) => ({
14592
+ detail: `${safe(skill.id)} (${count(skill.files.length, "file")})`,
14593
+ term: "skill"
14594
+ })), ...(preset.skills ?? []).map((selection) => `${selection.source}/${selection.id}`).filter((key) => !providedKeys.has(key)).map((key) => ({
14595
+ detail: `${safe(key)} · selected`,
14596
+ term: "skill"
14597
+ }))];
14598
+ }
14599
+ /** Snippets the repository ships, then selections from the catalog. */
14600
+ function snippetRows(preset, contentSet) {
14601
+ const provided = contentSet?.snippets ?? [];
14602
+ const providedIds = new Set(provided.map((snippet) => snippet.id));
14603
+ return [...provided.map((snippet) => ({
14604
+ detail: `${safe(snippet.id)} (${String(Buffer$1.byteLength(snippet.body, "utf8"))} B)`,
14605
+ term: "snippet"
14606
+ })), ...(preset.snippets ?? []).filter((id) => !providedIds.has(id)).map((id) => ({
14607
+ detail: `${safe(id)} · selected`,
14608
+ term: "snippet"
14609
+ }))];
14610
+ }
14611
+ /** The full executable surface of one provided MCP server, escaped but verbatim. */
14612
+ function mcpServerPreview(server) {
14613
+ return `${safe(server.id)} "${safe(server.serverName)}" → ${transportPreview(server.transportTemplate)}`;
14614
+ }
14615
+ function transportPreview(transport) {
14616
+ if (transport.type === "http") return `http ${safe(transport.url)}`;
14617
+ const args = (transport.args ?? []).map((argument) => `"${safe(argument)}"`).join(", ");
14618
+ const env = (transport.env ?? []).map(safe).join(", ");
14619
+ return [
14620
+ `stdio "${safe(transport.command)}"`,
14621
+ ...args === "" ? [] : [`args [${args}]`],
14622
+ ...env === "" ? [] : [`env ${env}`]
14623
+ ].join(", ");
14624
+ }
14625
+ /** The exact check settings the repository preset asks the user to trust. */
14626
+ function checksRow(checks) {
14627
+ if (checks === void 0) return [];
14628
+ const settings = [
14629
+ ...(checks.disabled ?? []).map((id) => `${safe(id)}: disabled`),
14630
+ ...(checks.enabled ?? []).map((id) => `${safe(id)}: enabled`),
14631
+ ...Object.entries(checks.severity ?? {}).map(([id, severity]) => `${safe(id)}: severity ${safe(severity)}`),
14632
+ ...Object.entries(checks.thresholds ?? {}).map(([id, thresholds]) => `${safe(id)}: thresholds ${safe(JSON.stringify(thresholds))}`)
14633
+ ].sort();
14634
+ return [{
14635
+ detail: settings.length === 0 ? "(none)" : settings.join("; "),
14636
+ term: "checks"
14637
+ }];
14638
+ }
14639
+ function directoryRow(source) {
14640
+ const token = source.kind === "private-directory" ? ` · token ${safe(source.tokenEnv)}` : "";
14641
+ return {
14642
+ detail: `${safe(source.name)} → ${safe(source.url)}${token}`,
14643
+ term: "directory"
14644
+ };
14645
+ }
14646
+ function listRow(term, values) {
14647
+ if (values === void 0) return [];
14648
+ return [{
14649
+ detail: values.length === 0 ? "(none)" : values.map(safe).join(", "),
14650
+ term
14651
+ }];
14652
+ }
14653
+ function count(total, noun) {
14654
+ return `${String(total)} ${noun}${total === 1 ? "" : "s"}`;
14655
+ }
14656
+ //#endregion
14163
14657
  //#region src/setup/repo-trust.ts
14164
14658
  /**
14165
14659
  * Asks whether an untrusted repository preset may be applied, once per repo and contents.
@@ -14169,61 +14663,39 @@ function formatThresholds(thresholds) {
14169
14663
  * answer. A non-interactive run resolves without the layer, and the plan summary says so.
14170
14664
  */
14171
14665
  async function establishRepoPresetTrust(options) {
14172
- const repo = await readRepoPreset(options.environment);
14666
+ const repo = options.repoPresetState ?? await readRepoPreset(options.environment);
14173
14667
  if (repo.status !== "ready" || repo.hash === void 0) return { kind: "resolved" };
14174
14668
  const manifest = options.manifest.status === "ready" ? options.manifest.value : void 0;
14175
14669
  if (isRepoPresetTrusted(manifest, repo, repo.hash) || !options.interactive) return { kind: "resolved" };
14176
14670
  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));
14671
+ options.io.note(name === void 0 ? `Repository preset ${AURA_TEAM_PRESET_PATH}` : `Repository preset "${safe(name)}" ${AURA_TEAM_PRESET_PATH}`);
14672
+ if (repo.preset !== void 0) options.io.note(repoPresetTrustPreview(repo.preset, repo.contentSet));
14179
14673
  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
14674
  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.`);
14675
+ const confirmation = await options.io.confirm(trustPrompt(changed, repo.contentSet));
14182
14676
  if (confirmation === "aborted") return { kind: "aborted" };
14183
14677
  return confirmation === "accepted" ? {
14184
14678
  acceptedHash: repo.hash,
14185
14679
  kind: "resolved"
14186
14680
  } : { kind: "resolved" };
14187
14681
  }
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(", ");
14682
+ /**
14683
+ * The question itself, short because the note above it already named the file and its contents.
14684
+ *
14685
+ * The two-tier contract rides here rather than in the summary: a preset that only carries policy
14686
+ * installs nothing either way, so promising a later tick would be noise in the one line the user
14687
+ * is certain to read.
14688
+ */
14689
+ function trustPrompt(changed, contentSet) {
14690
+ if (changed) return "The preset changed since you trusted it. Trust the new contents?";
14691
+ 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
14692
  }
14221
14693
  /**
14222
14694
  * Whether this run's prompt accepted exactly the contents the resolved layer applied.
14223
14695
  *
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.
14696
+ * Boot passes one immutable snapshot through the prompt and configuration resolution. Comparing
14697
+ * hashes here keeps this helper correct for direct callers too: trust is recorded only for the
14698
+ * layer this run actually applied.
14227
14699
  */
14228
14700
  function acceptedRepoPreset(configured, acceptedHash) {
14229
14701
  const repo = configured.repoPreset;
@@ -14236,9 +14708,11 @@ function setupRepoPresetContext(configured, acceptedHash, recorded) {
14236
14708
  return Object.freeze({
14237
14709
  accepted: acceptedRepoPreset(configured, acceptedHash),
14238
14710
  checkSummary: repo.status === "applied" ? presetCheckSummary(configured.config, "repo") : Object.freeze([]),
14711
+ ...repo.contentSet === void 0 ? {} : { contentSet: repo.contentSet },
14239
14712
  hash: repo.hash,
14240
14713
  ...repo.mainWorktreePath === void 0 ? {} : { mainWorktreePath: repo.mainWorktreePath },
14241
14714
  path: repo.path,
14715
+ ...repo.preset === void 0 ? {} : { preset: repo.preset },
14242
14716
  recorded,
14243
14717
  status: repo.status
14244
14718
  });
@@ -14856,11 +15330,15 @@ async function bootSetup(request, environment) {
14856
15330
  message: manifest.problem.message,
14857
15331
  status: "invalid"
14858
15332
  };
15333
+ const initialRepoPreset = await readRepoPreset(environment, void 0, { includeSkills: request.interactive });
15334
+ const manifestValue = manifest.status === "ready" ? manifest.value : void 0;
15335
+ const repoPresetState = !request.interactive && initialRepoPreset.status === "ready" && initialRepoPreset.hash !== void 0 && isRepoPresetTrusted(manifestValue, initialRepoPreset, initialRepoPreset.hash) ? await readRepoPreset(environment) : initialRepoPreset;
14859
15336
  const trust = await establishRepoPresetTrust({
14860
15337
  environment,
14861
15338
  interactive: request.interactive,
14862
15339
  io: request.io,
14863
- manifest
15340
+ manifest,
15341
+ repoPresetState
14864
15342
  });
14865
15343
  if (trust.kind === "aborted") return { status: "aborted" };
14866
15344
  const scanCancellation = new AbortController();
@@ -14882,7 +15360,8 @@ async function bootSetup(request, environment) {
14882
15360
  manifest,
14883
15361
  noCache: request.noCache,
14884
15362
  online: true,
14885
- registry: request.registry
15363
+ registry: request.registry,
15364
+ repoPresetState
14886
15365
  });
14887
15366
  if (configured.status === "invalid") {
14888
15367
  scanCancellation.abort();
@@ -14890,7 +15369,7 @@ async function bootSetup(request, environment) {
14890
15369
  }
14891
15370
  const scan = await settleScan(request.io);
14892
15371
  const trusted = await bootRepoPresetTrust(request, configured, trust.acceptedHash, scan);
14893
- const projected = applyRequiredMcpServers(trusted.scan.model, configured.config);
15372
+ const projected = applyRequiredMcpServers(withRepoMcpCatalog(trusted.scan.model, configured.repoPreset), configured.config);
14894
15373
  return {
14895
15374
  activeChecks: enabledChecks(request.registry.checks, configured.config),
14896
15375
  configured,
@@ -14907,8 +15386,8 @@ async function bootSetup(request, environment) {
14907
15386
  };
14908
15387
  }
14909
15388
  /** 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);
15389
+ function projectRescan(rescanned, config, repoPreset) {
15390
+ const projection = applyRequiredMcpServers(withRepoMcpCatalog(rescanned.model, repoPreset), config);
14912
15391
  return {
14913
15392
  ...rescanned,
14914
15393
  diagnostics: [...rescanned.diagnostics, ...projection.diagnostics],
@@ -15242,6 +15721,7 @@ function createMcpSetupCatalog(inputs) {
15242
15721
  entries.push({
15243
15722
  catalog,
15244
15723
  key: `catalog:${catalog.id}`,
15724
+ ...isRepoCatalogId(catalog.id) ? { repo: true } : {},
15245
15725
  required: requiredIds.has(catalog.id),
15246
15726
  sourceName: owner?.name
15247
15727
  });
@@ -15253,6 +15733,7 @@ function createMcpSetupCatalog(inputs) {
15253
15733
  catalog,
15254
15734
  existing: match.server,
15255
15735
  key: `manifest:${String(match.index)}`,
15736
+ ...isRepoCatalogId(catalog.id) ? { repo: true } : {},
15256
15737
  required: requiredIds.has(catalog.id),
15257
15738
  sourceName: owner?.name
15258
15739
  });
@@ -15265,6 +15746,7 @@ function createMcpSetupCatalog(inputs) {
15265
15746
  ...catalog === void 0 ? {} : { catalog },
15266
15747
  existing: server,
15267
15748
  key: `manifest:${String(index)}`,
15749
+ ...server.catalogId !== void 0 && isRepoCatalogId(server.catalogId) ? { repo: true } : {},
15268
15750
  required: server.catalogId !== void 0 && requiredIds.has(server.catalogId),
15269
15751
  sourceName: server.catalogId === void 0 ? void 0 : inputs.registry.ownerOf("mcp-server", server.catalogId)?.name
15270
15752
  });
@@ -15277,12 +15759,24 @@ function createMcpSetupCatalog(inputs) {
15277
15759
  requiredIds
15278
15760
  });
15279
15761
  }
15762
+ /**
15763
+ * Required rows first (they carry blocker semantics whoever asked for them), then this
15764
+ * repository's own definitions, then configured rows, then everything else by name. Repo rows
15765
+ * lead the optional block — they are what a person running setup inside the repository came for —
15766
+ * without outranking a requirement they did not make.
15767
+ */
15280
15768
  function compareEntries(left, right) {
15281
15769
  if (left.required !== right.required) return left.required ? -1 : 1;
15770
+ const leftRepo = left.repo === true;
15771
+ if (leftRepo !== (right.repo === true)) return leftRepo ? -1 : 1;
15282
15772
  const leftConfigured = left.existing !== void 0;
15283
15773
  if (leftConfigured !== (right.existing !== void 0)) return leftConfigured ? -1 : 1;
15284
15774
  return entryName(left).localeCompare(entryName(right));
15285
15775
  }
15776
+ /** The `repo/` namespace is reserved against plugins, so the prefix alone is provenance. */
15777
+ function isRepoCatalogId(id) {
15778
+ return id.startsWith("repo/");
15779
+ }
15286
15780
  /** User-facing name used by the picker and deterministic sorting. */
15287
15781
  function mcpCatalogEntryName(entry) {
15288
15782
  return entryName(entry);
@@ -15365,7 +15859,7 @@ const MAX_CONCURRENT_SOURCE_LISTINGS = 4;
15365
15859
  * of both. Only the draining is ordered, which is what keeps the picker's rows stable.
15366
15860
  */
15367
15861
  async function loadListing(request) {
15368
- const entries = [...bundledEntries(request.inputs)];
15862
+ const entries = [...repoEntries(request.inputs), ...bundledEntries(request.inputs)];
15369
15863
  const notes = [...request.notes];
15370
15864
  const unavailableSources = request.unapproved.map((source) => ({
15371
15865
  hint: `connection not approved; token ${source.tokenEnv}`,
@@ -15485,6 +15979,30 @@ function memoizedDriverListing(request, source) {
15485
15979
  request.driverListings.set(source.id, listing);
15486
15980
  return listing;
15487
15981
  }
15982
+ /** Repository skills the active allowlist admits; the source id is `repo:workspace`. */
15983
+ function allowedRepoSkills(inputs) {
15984
+ return (inputs.repoSkills ?? []).filter((skill) => isSkillSourceAllowed(inputs.preset, skill.source.id));
15985
+ }
15986
+ /**
15987
+ * This repository's own skills, resolved from the trusted snapshot rather than any fetch.
15988
+ *
15989
+ * `remote` stays false — no fetch is needed — but the review stage still gates their first
15990
+ * install by identity: trees that arrive by cloning get the same on-screen reading a directory
15991
+ * skill does.
15992
+ */
15993
+ function repoEntries(inputs) {
15994
+ return allowedRepoSkills(inputs).map((skill) => ({
15995
+ description: skill.description,
15996
+ id: skill.id,
15997
+ identity: skillIdentity(skill.source.id, skill.id),
15998
+ name: skill.name,
15999
+ preview: skill.files.find((file) => file.path === "SKILL.md")?.content,
16000
+ remote: false,
16001
+ sourceId: skill.source.id,
16002
+ sourceName: skill.source.name,
16003
+ version: skill.version
16004
+ }));
16005
+ }
15488
16006
  function bundledEntries(inputs) {
15489
16007
  return (inputs.model.availableSkills ?? []).filter((skill) => isSkillSourceAllowed(inputs.preset, skill.source.id)).map((skill) => ({
15490
16008
  description: skill.description,
@@ -15525,6 +16043,7 @@ function remoteEntries(listings, source) {
15525
16043
  function createSkillCatalog(inputs) {
15526
16044
  const collected = collectSkillDirectorySources(inputs.registryDirectories, inputs.preset);
15527
16045
  const packs = /* @__PURE__ */ new Map();
16046
+ for (const skill of allowedRepoSkills(inputs)) packs.set(skillIdentity(skill.source.id, skill.id), skill);
15528
16047
  const failures = /* @__PURE__ */ new Map();
15529
16048
  const pending = /* @__PURE__ */ new Map();
15530
16049
  const driverListings = /* @__PURE__ */ new Map();
@@ -15601,7 +16120,8 @@ function createSetupCatalogs(inputs) {
15601
16120
  presetOrigin: inputs.presetOrigin,
15602
16121
  registryDirectories: inputs.registry.skillDirectories,
15603
16122
  registryDrivers: inputs.registry.skillSources,
15604
- registryPresets: inputs.registry.presets
16123
+ registryPresets: inputs.registry.presets,
16124
+ repoSkills: inputs.repoSkills
15605
16125
  })
15606
16126
  };
15607
16127
  }
@@ -15632,15 +16152,17 @@ function requiringPreset(preset, config) {
15632
16152
  }
15633
16153
  //#endregion
15634
16154
  //#region src/setup/snippets.ts
16155
+ /** The picker group every repository-defined snippet renders under. */
16156
+ const REPO_SNIPPET_CATEGORY = "From this repository";
15635
16157
  /** Bounds simultaneous snippet handles independently of how many contributions a distro ships. */
15636
16158
  const MAX_CONCURRENT_SNIPPET_READS = 24;
15637
- function createSnippetCatalog(snippets, manifest, presetIds = []) {
16159
+ function createSnippetCatalog(snippets, manifest, presetIds = [], repoSnippets = []) {
15638
16160
  let resolved = [];
15639
16161
  let pending;
15640
16162
  return {
15641
16163
  entries: () => resolved,
15642
16164
  load: () => {
15643
- pending ??= resolveSnippetCatalog(snippets, manifest, presetIds).then((entries) => {
16165
+ pending ??= resolveSnippetCatalog(snippets, manifest, presetIds, repoSnippets).then((entries) => {
15644
16166
  resolved = entries;
15645
16167
  return entries;
15646
16168
  });
@@ -15649,13 +16171,23 @@ function createSnippetCatalog(snippets, manifest, presetIds = []) {
15649
16171
  };
15650
16172
  }
15651
16173
  /** 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));
16174
+ async function resolveSnippetCatalog(snippets, manifest, presetIds = [], repoSnippets = []) {
16175
+ const repoEntries = repoSnippets.map((snippet) => Object.freeze({
16176
+ category: REPO_SNIPPET_CATEGORY,
16177
+ content: snippet.body,
16178
+ description: snippet.description ?? "Defined by this repository.",
16179
+ id: snippet.id,
16180
+ name: snippet.name,
16181
+ origin: "repo",
16182
+ status: "available"
16183
+ }));
16184
+ const registered = /* @__PURE__ */ new Set([...snippets.map((snippet) => snippet.id), ...repoEntries.map((entry) => entry.id)]);
15654
16185
  const limit = createLimiter(MAX_CONCURRENT_SNIPPET_READS);
15655
16186
  const resolved = await Promise.all(snippets.map((snippet) => limit(() => resolveSnippet(snippet))));
15656
16187
  const previous = manifest.status === "ready" ? manifest.value.snippets : [];
15657
16188
  const previousIds = new Set(previous.map((entry) => entry.id));
15658
16189
  return Object.freeze([
16190
+ ...repoEntries,
15659
16191
  ...resolved,
15660
16192
  ...previous.map((entry) => entry.id).filter((id) => !registered.has(id)).map((id) => Object.freeze({
15661
16193
  category: "general",
@@ -16525,7 +17057,7 @@ async function configureEntry(context, entry, taken, io) {
16525
17057
  return SETUP_BACK;
16526
17058
  }
16527
17059
  const defaultName = seed?.name ?? catalog?.serverName ?? "custom";
16528
- const defaultScope = seed?.scope ?? "global";
17060
+ const defaultScope = seed?.scope ?? (entry.repo === true ? "project" : "global");
16529
17061
  const apps = appChoices(context, catalog?.supportedApps, seed?.apps ?? []);
16530
17062
  if (apps.eligible.length === 0 && (seed?.apps.length ?? 0) === 0) {
16531
17063
  io.note("No detected, managed, compatible application can receive this MCP server.");
@@ -16802,15 +17334,24 @@ function requiredCatalogIds(entries) {
16802
17334
  return [...new Set(entries.flatMap((entry) => entry.required && entry.catalog !== void 0 ? [entry.catalog.id] : []))];
16803
17335
  }
16804
17336
  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"] : []];
17337
+ const tags = serverTags(entry);
16807
17338
  const suffix = tags.length === 0 ? "" : ` (${tags.join(", ")})`;
16808
17339
  return {
16809
- description: entry.sourceName === void 0 ? "Custom server" : `Plugin: ${entry.sourceName}`,
17340
+ description: serverProvenance(entry),
16810
17341
  label: `${safe(mcpCatalogEntryName(entry))}${suffix}`,
16811
17342
  value: entry.key
16812
17343
  };
16813
17344
  }
17345
+ /** What the run knows about a row: who selected it, whether it is required or configured. */
17346
+ function serverTags(entry) {
17347
+ const configured = entry.selectedServer !== void 0 || entry.existing !== void 0;
17348
+ const origin = entry.repo === true ? "from repo" : "from preset";
17349
+ return [...entry.required ? [origin, "required"] : entry.repo === true ? [origin] : [], ...configured ? [entry.catalog === void 0 ? "custom" : "configured"] : []];
17350
+ }
17351
+ function serverProvenance(entry) {
17352
+ if (entry.repo === true) return "Repository: .aura/preset.json";
17353
+ return entry.sourceName === void 0 ? "Custom server" : `Plugin: ${entry.sourceName}`;
17354
+ }
16814
17355
  function hasManagedMcpApp(context) {
16815
17356
  const managed = new Set(managedAppIdList(context));
16816
17357
  return context.appCatalog.some((entry) => entry.kind === "detected" && entry.supportsMcp && managed.has(catalogEntryId(entry)));
@@ -16927,11 +17468,7 @@ function pickerOptions(inputs) {
16927
17468
  const policy = inputs.catalog.policy;
16928
17469
  const unsupported = !hasSkillsHome(inputs.managedApps);
16929
17470
  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) => ({
17471
+ const entryRow = (entry) => ({
16935
17472
  description: `${entry.description} · v${entry.version}`,
16936
17473
  get disabled() {
16937
17474
  return unsupported || inputs.listing.verification?.isMissing(entry.identity) === true;
@@ -16941,10 +17478,18 @@ function pickerOptions(inputs) {
16941
17478
  return inputs.listing.verification?.isMissing(entry.identity) === true ? "source no longer publishes this skill" : void 0;
16942
17479
  },
16943
17480
  group: entry.sourceName,
16944
- label: `${entry.name}${preset.has(entry.identity) ? " (from preset)" : ""}`,
17481
+ label: `${entry.name}${selectedBySuffix(entry.identity)}`,
16945
17482
  ...previewField(inputs, entry),
16946
17483
  value: entry.identity
16947
- }));
17484
+ });
17485
+ const selectedBySuffix = (identity) => inputs.repoSelectedIdentities.has(identity) ? " (from repo)" : preset.has(identity) ? " (from preset)" : "";
17486
+ const sortedEntries = sortForDisplay(inputs.listing.entries, (entry) => [
17487
+ entry.sourceName,
17488
+ entry.name,
17489
+ entry.id
17490
+ ]);
17491
+ const repoEntryRows = sortedEntries.filter((entry) => entry.sourceId.startsWith("repo:")).map(entryRow);
17492
+ const entryRows = sortedEntries.filter((entry) => !entry.sourceId.startsWith("repo:")).map(entryRow);
16948
17493
  const manifestRows = inputs.manifestSkills.flatMap((skill) => {
16949
17494
  const identity = skillIdentity(skill.source, skill.id);
16950
17495
  if (covered.has(identity)) return [];
@@ -16999,18 +17544,23 @@ function pickerOptions(inputs) {
16999
17544
  value: `truncated:${source.id}`
17000
17545
  }));
17001
17546
  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
- }));
17547
+ const presetRows = inputs.presetSkills.filter((skill) => !represented.has(skillIdentity(skill.source, skill.id))).map((skill) => {
17548
+ const identity = skillIdentity(skill.source, skill.id);
17549
+ const fromRepo = inputs.repoSelectedIdentities.has(identity);
17550
+ return {
17551
+ description: fromRepo ? "Selected by the repository preset, but unavailable in this run." : "Selected by the active team preset, but unavailable in this run.",
17552
+ disabled: true,
17553
+ disabledNote: fromRepo ? "repo selection unavailable" : "preset selection unavailable",
17554
+ group: skill.source,
17555
+ label: `${skill.id} ${fromRepo ? "(from repo)" : "(from preset)"}`,
17556
+ value: identity
17557
+ };
17558
+ });
17010
17559
  return [
17011
17560
  ...sourceRows,
17012
17561
  ...truncatedRows,
17013
17562
  ...packRows,
17563
+ ...repoEntryRows,
17014
17564
  ...entryRows,
17015
17565
  ...manifestRows,
17016
17566
  ...presetRows
@@ -17093,6 +17643,16 @@ function skillStages(inputs) {
17093
17643
  function isRemoteIdentity(identity) {
17094
17644
  return identity.startsWith("directory:") || identity.startsWith("driver:");
17095
17645
  }
17646
+ /**
17647
+ * A repository identity's install needs no fetch, but the review boundary all the same.
17648
+ *
17649
+ * The trees arrive by cloning, not by anything the user selected — "local files" is the bundled
17650
+ * exemption's mechanism, but distribution vouching was its justification, and a checkout has
17651
+ * neither.
17652
+ */
17653
+ function isRepoIdentity(identity) {
17654
+ return identity.startsWith("repo:");
17655
+ }
17096
17656
  /** The typed selection behind each offered identity: catalog entries plus manifest rows. */
17097
17657
  function selectionsByIdentity(inputs) {
17098
17658
  const selections = /* @__PURE__ */ new Map();
@@ -17152,7 +17712,8 @@ function reviewStage(inputs) {
17152
17712
  const offered = selectionsByIdentity(inputs);
17153
17713
  const entries = new Map(inputs.listing.entries.map((entry) => [entry.identity, entry]));
17154
17714
  const bundled = bundledByIdentity(inputs.availableSkills);
17155
- const reviewable = (identity) => needsReview(identity, recorded, bundled);
17715
+ const repo = bundledByIdentity(inputs.repoSkills);
17716
+ const reviewable = (identity) => needsReview(identity, recorded, bundled, repo);
17156
17717
  return {
17157
17718
  apply: (state, answers) => ({
17158
17719
  ...state,
@@ -17169,7 +17730,11 @@ function reviewStage(inputs) {
17169
17730
  }), inputs.approvedPrivateSourceIds);
17170
17731
  const resolutionWithBundled = {
17171
17732
  problems: resolution.problems,
17172
- resolved: new Map([...resolution.resolved, ...bundled])
17733
+ resolved: new Map([
17734
+ ...resolution.resolved,
17735
+ ...bundled,
17736
+ ...repo
17737
+ ])
17173
17738
  };
17174
17739
  const questions = state.selected.filter(reviewable).flatMap((identity) => reviewQuestion(identity, state.decisions, resolutionWithBundled, recorded, entries.get(identity), offered.get(identity)));
17175
17740
  return questions.length === 0 ? void 0 : questions;
@@ -17179,8 +17744,13 @@ function reviewStage(inputs) {
17179
17744
  function bundledByIdentity(packs) {
17180
17745
  return new Map(packs.map((pack) => [skillIdentity(pack.source.id, pack.id), pack]));
17181
17746
  }
17182
- function needsReview(identity, recorded, bundled) {
17747
+ function needsReview(identity, recorded, bundled, repo) {
17183
17748
  if (isRemoteIdentity(identity)) return true;
17749
+ if (isRepoIdentity(identity)) {
17750
+ const previous = recorded.get(identity);
17751
+ const pack = repo.get(identity);
17752
+ return previous === void 0 || pack !== void 0 && movedFrom(previous, pack);
17753
+ }
17184
17754
  const previous = recorded.get(identity);
17185
17755
  const available = bundled.get(identity);
17186
17756
  return previous !== void 0 && available !== void 0 && movedFrom(previous, available);
@@ -17206,11 +17776,19 @@ async function finalizeSkills(state, inputs, manifestSkills) {
17206
17776
  const selection = offered.get(identity);
17207
17777
  return selection === void 0 ? [] : [selection];
17208
17778
  }), inputs.approvedPrivateSourceIds);
17779
+ const repoPacks = new Map(inputs.repoSkills.map((pack) => [skillIdentity(pack.source.id, pack.id), pack]));
17209
17780
  const selected = [];
17210
17781
  const resolved = [];
17211
17782
  for (const identity of state.selected) {
17212
17783
  const selection = offered.get(identity);
17213
17784
  if (selection === void 0) continue;
17785
+ if (isRepoIdentity(identity)) {
17786
+ finalizeRemote(identity, selection, state, repoPacks.get(identity), recorded.get(identity), {
17787
+ resolved,
17788
+ selected
17789
+ });
17790
+ continue;
17791
+ }
17214
17792
  if (!isRemoteIdentity(identity)) {
17215
17793
  selected.push(selection);
17216
17794
  continue;
@@ -17283,15 +17861,7 @@ async function gatherApprovedSkills(context, io, approval) {
17283
17861
  const manifestSkills = recordedSkills(context);
17284
17862
  emitNotes(context, io, listing, manifestSkills);
17285
17863
  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
- };
17864
+ const inputs = stageInputs(context, approvedPrivateSourceIds, listing, manifestSkills);
17295
17865
  const opening = openingSelection(context, inputs, manifestSkills);
17296
17866
  noteHeldBack(inputs, io, opening.heldBack);
17297
17867
  const result = await runFormChain(skillStages(inputs), opening.state, io, {
@@ -17316,6 +17886,21 @@ function noteHeldBack(inputs, io, heldBack) {
17316
17886
  function recordedSkills(context) {
17317
17887
  return context.manifest.status === "ready" ? context.manifest.value.skills : [];
17318
17888
  }
17889
+ /** Everything the picker/review chain reads, with the repository selections folded in. */
17890
+ function stageInputs(context, approvedPrivateSourceIds, listing, manifestSkills) {
17891
+ const repoSelections = context.repoPreset?.preset?.skills ?? [];
17892
+ return {
17893
+ approvedPrivateSourceIds,
17894
+ availableSkills: context.model.availableSkills ?? [],
17895
+ catalog: context.skillCatalog,
17896
+ listing,
17897
+ managedApps: managedSkillApps(context),
17898
+ manifestSkills,
17899
+ presetSkills: [...context.preset?.skills ?? [], ...repoSelections],
17900
+ repoSelectedIdentities: new Set(repoSelections.map((skill) => skillIdentity(skill.source, skill.id))),
17901
+ repoSkills: context.repoPreset?.contentSet?.skills ?? []
17902
+ };
17903
+ }
17319
17904
  function emptyCatalogOutcome$1(context) {
17320
17905
  return context.enteredBackward === true ? SETUP_BACK : {
17321
17906
  selections: { ...context.selections },
@@ -17439,7 +18024,7 @@ const snippetsStep = {
17439
18024
  const installed = installedSnippetIds(context);
17440
18025
  if (context.revisited !== true) emitCatalogNotes(context, catalog, installed, io.note);
17441
18026
  if (catalog.length === 0) return emptyCatalogOutcome(context, io);
17442
- const options = snippetOptions(catalog, installed, new Set(context.preset?.snippets ?? []));
18027
+ const options = snippetOptions(catalog, installed, new Set(context.preset?.snippets ?? []), new Set(context.repoPreset?.preset?.snippets ?? []));
17443
18028
  const selectable = new Set(options.filter((option) => option.disabled !== true).map((option) => option.value));
17444
18029
  const question = {
17445
18030
  id: "snippets",
@@ -17479,14 +18064,23 @@ const RECORD_NOTE = "Aura keeps the record; the text stays where it is.";
17479
18064
  * checkbox has anything to do — the row is there to report the record, not to be answered with.
17480
18065
  * Gathering those rows under one heading is what lets the numbers below start at 1 and count only
17481
18066
  * what a tick would change, and it empties out any category whose every snippet is installed.
18067
+ *
18068
+ * Repository rows lead the offered rows: they are this repository's own guidance, which is what a
18069
+ * person running setup inside it most likely came for. Plugin categories keep their sorted order
18070
+ * underneath.
17482
18071
  */
17483
- function snippetOptions(catalog, installed, preset) {
18072
+ function snippetOptions(catalog, installed, preset, repoSelected) {
17484
18073
  const sorted = sortForDisplay(catalog, (entry) => [
17485
18074
  entry.category,
17486
18075
  entry.name,
17487
18076
  entry.id
17488
18077
  ]);
17489
- return [...recordBlock(sorted.filter((entry) => installed.has(entry.id))), ...sorted.filter((entry) => !installed.has(entry.id)).map((entry) => offeredOption(entry, preset))];
18078
+ const offered = sorted.filter((entry) => !installed.has(entry.id));
18079
+ return [
18080
+ ...recordBlock(sorted.filter((entry) => installed.has(entry.id))),
18081
+ ...offered.filter((entry) => entry.origin === "repo").map((entry) => offeredOption(entry, preset, repoSelected)),
18082
+ ...offered.filter((entry) => entry.origin !== "repo").map((entry) => offeredOption(entry, preset, repoSelected))
18083
+ ];
17490
18084
  }
17491
18085
  /**
17492
18086
  * The installed rows, with the shared note appended to the last of them.
@@ -17506,10 +18100,11 @@ function recordBlock(entries) {
17506
18100
  }));
17507
18101
  }
17508
18102
  /** A row a tick would act on, or one disabled because no installed plugin publishes its text. */
17509
- function offeredOption(entry, preset) {
18103
+ function offeredOption(entry, preset, repoSelected) {
18104
+ const selectedBy = repoSelected.has(entry.id) ? " (from repo)" : preset.has(entry.id) ? " (from preset)" : "";
17510
18105
  const base = {
17511
18106
  group: entry.category,
17512
- label: `${entry.name}${preset.has(entry.id) ? " (from preset)" : ""}`,
18107
+ label: `${entry.name}${selectedBy}`,
17513
18108
  value: entry.id,
17514
18109
  ...entry.status === "available" ? { preview: entry.content } : {}
17515
18110
  };
@@ -17535,19 +18130,20 @@ function snippetsPrompt(options) {
17535
18130
  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
18131
  }
17537
18132
  /**
17538
- * What the picker opens with: every installed id, plus this run's answer or the preset.
18133
+ * What the picker opens with: every installed id, plus this run's answer or team-preset defaults.
17539
18134
  *
17540
18135
  * Installed rows are locked, so they are seeded on every visit — there is no earlier answer that
17541
18136
  * 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.
18137
+ * settles is the answer the form reports back, not the checkbox the picker draws. Team-preset
18138
+ * defaults add only what their installed plugins can contribute. Repository rows deliberately
18139
+ * open unticked even when the repository preset selects them: their bodies are arbitrary agent
18140
+ * instructions, so a first install requires a person to select the previewable row explicitly.
17545
18141
  */
17546
18142
  function initialSelection(context, options, installed) {
17547
- const preset = new Set(context.preset?.snippets ?? []);
18143
+ const selected = new Set(context.preset?.snippets ?? []);
17548
18144
  const chosen = context.selections.snippets;
17549
18145
  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);
18146
+ const added = chosen?.selected ?? options.filter((option) => option.disabled !== true && !installed.has(option.value) && selected.has(option.value)).map((option) => option.value);
17551
18147
  return [...kept, ...added];
17552
18148
  }
17553
18149
  function emptyCatalogOutcome(context, io) {
@@ -17642,36 +18238,9 @@ async function runSetup(request) {
17642
18238
  stdout.write("\nLeft everything as it was.\n");
17643
18239
  return finish(1, "aborted");
17644
18240
  }
17645
- const { activeChecks, configured, effectiveModel, effectiveScan, projected, scan } = booted;
17646
- const model = scan.model;
18241
+ const { activeChecks, configured, effectiveModel, effectiveScan } = booted;
17647
18242
  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
- };
18243
+ const stepContext = buildStepContext(request, booted, environment, steps.some((step) => step.needsFindings === true) ? gatherFindings(activeChecks, effectiveModel, io, configured.config) : void 0);
17675
18244
  let start = {
17676
18245
  index: 0,
17677
18246
  offered: /* @__PURE__ */ new Set(),
@@ -17701,7 +18270,7 @@ async function runSetup(request) {
17701
18270
  environment,
17702
18271
  mcpCatalog: request.registry.mcpServers,
17703
18272
  skills: request.registry.skills
17704
- }), configured.config), activeChecks, configured.config), "applied", {
18273
+ }), configured.config, configured.repoPreset), activeChecks, configured.config), "applied", {
17705
18274
  actions: setupActions(ready.gathered),
17706
18275
  appliedOperationCount: result.appliedOperationCount
17707
18276
  });
@@ -17710,6 +18279,46 @@ async function runSetup(request) {
17710
18279
  function plannedActions(gathered) {
17711
18280
  return gathered === void 0 ? void 0 : setupActions(gathered);
17712
18281
  }
18282
+ /** Assembles the catalogs and the immutable context every wizard step reads. */
18283
+ function buildStepContext(request, booted, environment, initialFindings) {
18284
+ const { configured, effectiveModel, repoPreset, scan } = booted;
18285
+ const model = scan.model;
18286
+ const catalogs = buildCatalogs(request, booted, environment);
18287
+ const preset = setupPresetContext(request, configured.config, configured.preset);
18288
+ return {
18289
+ appCatalog: buildAppCatalog(request.registry.adapters, model, scan.skipped),
18290
+ ...initialFindings === void 0 ? {} : { findings: initialFindings },
18291
+ interactive: request.interactive,
18292
+ isEnvironmentVariableSet: (name) => environment.readVariable(name) !== void 0,
18293
+ manifest: model.manifest,
18294
+ mcpCatalog: catalogs.mcpCatalog,
18295
+ model: effectiveModel,
18296
+ skillCatalog: catalogs.skillCatalog,
18297
+ snippetCatalog: buildSnippetCatalog(request, booted, preset),
18298
+ ...preset === void 0 ? {} : { preset },
18299
+ ...repoPreset === void 0 ? {} : { repoPreset }
18300
+ };
18301
+ }
18302
+ function buildCatalogs(request, booted, environment) {
18303
+ const { configured, effectiveModel, projected, repoPreset } = booted;
18304
+ return createSetupCatalogs({
18305
+ config: configured.config,
18306
+ environment,
18307
+ interactive: request.interactive,
18308
+ model: effectiveModel,
18309
+ noCache: request.noCache,
18310
+ preset: configured.preset,
18311
+ presetNotes: [...configured.notes, ...projected.diagnostics.map((diagnostic) => diagnostic.message)],
18312
+ presetOrigin: configured.presetOrigin,
18313
+ registry: request.registry,
18314
+ repoSkills: repoPreset?.contentSet?.skills ?? []
18315
+ });
18316
+ }
18317
+ /** The snippet catalog, with the trusted repository's bodies and default selections folded in. */
18318
+ function buildSnippetCatalog(request, booted, preset) {
18319
+ const repoPreset = booted.repoPreset;
18320
+ return createSnippetCatalog(request.registry.snippets, booted.scan.model.manifest, [...preset?.snippets ?? [], ...repoPreset?.preset?.snippets ?? []], repoPreset?.contentSet?.snippets ?? []);
18321
+ }
17713
18322
  function setupPresetContext(request, config, preset) {
17714
18323
  const selected = config.preset;
17715
18324
  if (selected === void 0 || preset === void 0) return;