rulesync 16.21.0 → 16.22.0

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.
@@ -221,6 +221,35 @@ function hasDeceptiveHiddenCharacters(text) {
221
221
  return !joinsCharacter(characters[index - 1]) || !joinsCharacter(characters[index + 1]);
222
222
  });
223
223
  }
224
+ /** A mark that draws around the character before it — a circle, a square, a keycap box. */
225
+ const ENCLOSING_MARK_PATTERN = /\p{Me}/u;
226
+ /**
227
+ * Whether `text` carries an enclosing mark that is not the keycap of an emoji
228
+ * keycap sequence.
229
+ *
230
+ * An enclosing mark (`\p{Me}`: U+20DD COMBINING ENCLOSING CIRCLE, U+20E3
231
+ * COMBINING ENCLOSING KEYCAP, the Cyrillic and Vedic ones) is drawn over the
232
+ * character before it and takes no column of its own, so `pdf` with one after
233
+ * it occupies the three columns of `pdf` and is a fourth directory underneath.
234
+ * Unlike a joiner or a variation selector it is not invisible — the box is
235
+ * drawn — which is why `hasDeceptiveHiddenCharacters` does not refuse it and
236
+ * why it is a question for the confusable-name note instead: the row is drawn,
237
+ * only not the way its name reads. The one place an enclosing mark belongs in
238
+ * a name is the keycap sequence of UTS #51, which `isKeycapSequence` matches
239
+ * whole; every other one is left over.
240
+ *
241
+ * Restricted to `\p{Me}` on purpose: a non-spacing mark (`\p{Mn}`) is how
242
+ * Devanagari, Arabic and Vietnamese write, and folding those would mark
243
+ * ordinary names in every one of them.
244
+ */
245
+ function hasEnclosingMarkOutsideKeycap(text) {
246
+ const characters = [...text];
247
+ return characters.some((character, index) => ENCLOSING_MARK_PATTERN.test(character) && !isKeycapSequence({
248
+ base: characters[index - 2],
249
+ selector: characters[index - 1] ?? "",
250
+ following: character
251
+ }));
252
+ }
224
253
  //#endregion
225
254
  //#region src/utils/truncate.ts
226
255
  /**
@@ -444,9 +473,11 @@ const rulesProcessorToolTargetTuple = [
444
473
  "claudecode",
445
474
  "claudecode-legacy",
446
475
  "cline",
476
+ "codebuddy",
447
477
  "codexcli",
448
478
  "copilot",
449
479
  "copilotcli",
480
+ "crush",
450
481
  "cursor",
451
482
  "deepagents",
452
483
  "factorydroid",
@@ -482,6 +513,7 @@ const ignoreProcessorToolTargetTuple = [
482
513
  "claudecode",
483
514
  "claudecode-legacy",
484
515
  "cline",
516
+ "crush",
485
517
  "cursor",
486
518
  "hermesagent",
487
519
  "junie",
@@ -623,6 +655,7 @@ const skillsProcessorToolTargetTuple = [
623
655
  "codexcli",
624
656
  "copilot",
625
657
  "copilotcli",
658
+ "crush",
626
659
  "cursor",
627
660
  "deepagents",
628
661
  "factorydroid",
@@ -3327,6 +3360,42 @@ var RulesyncFile = class extends AiFile {
3327
3360
  }
3328
3361
  };
3329
3362
  //#endregion
3363
+ //#region src/utils/prototype-pollution.ts
3364
+ /**
3365
+ * Keys that, if walked into when constructing or merging objects from
3366
+ * untrusted input, can mutate `Object.prototype` (or otherwise the prototype
3367
+ * chain) and propagate state to every other object in the runtime. Any code
3368
+ * that copies arbitrary user-supplied keys into a fresh object — frontmatter
3369
+ * parsing, MCP config conversion, settings round-trip — should skip these.
3370
+ */
3371
+ const PROTOTYPE_POLLUTION_KEYS = /* @__PURE__ */ new Set([
3372
+ "__proto__",
3373
+ "constructor",
3374
+ "prototype"
3375
+ ]);
3376
+ function isPrototypePollutionKey(key) {
3377
+ return PROTOTYPE_POLLUTION_KEYS.has(key);
3378
+ }
3379
+ /**
3380
+ * Returns a shallow copy of a record's own entries with every
3381
+ * prototype-pollution key (`__proto__`, `constructor`, `prototype`) dropped.
3382
+ *
3383
+ * Use when copying a nested, user-supplied string map — an MCP server's `env`
3384
+ * or `headers` table — into freshly generated config. Carrying such a map by
3385
+ * reference, or re-assigning its keys via bracket notation, would let a literal
3386
+ * `__proto__` key ride along (and re-assigning it would mutate the target's
3387
+ * prototype). Walking the entries through this helper severs that path while
3388
+ * preserving every legitimate key.
3389
+ */
3390
+ function omitPrototypePollutionKeys(record) {
3391
+ const sanitized = {};
3392
+ for (const [key, value] of Object.entries(record)) {
3393
+ if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
3394
+ sanitized[key] = value;
3395
+ }
3396
+ return sanitized;
3397
+ }
3398
+ //#endregion
3330
3399
  //#region src/utils/type-guards.ts
3331
3400
  /**
3332
3401
  * Type guard to check if a value is a plain object (Record<string, unknown>).
@@ -3391,67 +3460,192 @@ function loadYaml(content) {
3391
3460
  }
3392
3461
  //#endregion
3393
3462
  //#region src/utils/frontmatter.ts
3394
- function deepRemoveNullishValue(value) {
3463
+ /**
3464
+ * Upper bound on the number of values a frontmatter document may expand to
3465
+ * once every YAML alias is written out.
3466
+ *
3467
+ * A YAML alias makes one parsed container reachable from many keys, and the
3468
+ * cleaners below copy each reachable value, so a small file with a few levels
3469
+ * of nested aliases (an "alias bomb") can expand into megabytes of output or
3470
+ * exhaust the heap. Counting every visited value against this budget turns
3471
+ * that into an error instead. Real frontmatter is a handful of keys; even a
3472
+ * generous skill manifest stays orders of magnitude below the limit.
3473
+ */
3474
+ const MAX_FRONTMATTER_VALUES = 1e5;
3475
+ /**
3476
+ * Upper bound on the total character count of string leaves a frontmatter
3477
+ * document may expand to.
3478
+ *
3479
+ * {@link MAX_FRONTMATTER_VALUES} bounds how many values are visited, but a
3480
+ * single long string aliased thousands of times still fits that budget while
3481
+ * the duplicated output balloons: one scalar of a few KB, chained through a
3482
+ * handful of aliases within the value budget, can multiply into a document
3483
+ * many megabytes larger than it started. Charging every visited string's
3484
+ * length against this separate budget bounds that output regardless of how
3485
+ * many aliases point at it.
3486
+ */
3487
+ const MAX_FRONTMATTER_STRING_CHARS = 4e6;
3488
+ /**
3489
+ * Upper bound on the raw character length of the `---`-delimited frontmatter
3490
+ * block itself, checked before it is ever handed to the YAML parser.
3491
+ *
3492
+ * The budgets above only bound the *parsed* document — the walk over
3493
+ * `matter()`'s output — but a complex YAML key (an array or mapping used as a
3494
+ * mapping key) is joined into a string by js-yaml while it parses, and a
3495
+ * mapping with many such keys can cost real memory before that walk ever
3496
+ * starts, or even before `matter()` returns. Capping the raw block size keeps
3497
+ * that parse-time cost bounded regardless of what the block contains. Real
3498
+ * frontmatter blocks are a few hundred bytes at most; even a large project
3499
+ * manifest stays well under this.
3500
+ */
3501
+ const MAX_FRONTMATTER_RAW_CHARS = 65536;
3502
+ /** Charge string content (a string leaf or an object key) against the character budget. */
3503
+ function chargeStringChars({ options, chars }) {
3504
+ options.budget.stringCharsRemaining -= chars;
3505
+ if (options.budget.stringCharsRemaining < 0) throw new Error(`Frontmatter's string values expand to more than ${MAX_FRONTMATTER_STRING_CHARS} characters; refusing to process it (a chain of YAML aliases may be amplifying the document)`);
3506
+ }
3507
+ function consumeBudget({ options, stringChars = 0 }) {
3508
+ options.budget.remaining -= 1;
3509
+ if (options.budget.remaining < 0) throw new Error(`Frontmatter expands to more than ${MAX_FRONTMATTER_VALUES} values; refusing to process it (a chain of YAML aliases may be amplifying the document)`);
3510
+ chargeStringChars({
3511
+ options,
3512
+ chars: stringChars
3513
+ });
3514
+ }
3515
+ /** Enter one more container level, throwing if the depth cap is exceeded. */
3516
+ function enterContainer({ options, container }) {
3517
+ options.depth += 1;
3518
+ if (options.depth > 64) throw new Error(`Frontmatter nests more than 64 levels deep; refusing to process it (a chain of YAML aliases may be amplifying the document)`);
3519
+ options.ancestors.add(container);
3520
+ }
3521
+ /** Leave a container level entered via {@link enterContainer}. */
3522
+ function leaveContainer({ options, container }) {
3523
+ options.ancestors.delete(container);
3524
+ options.depth -= 1;
3525
+ }
3526
+ /**
3527
+ * Estimate the serialized character cost of a leaf that is not a string (a
3528
+ * string leaf is charged by its own length instead).
3529
+ *
3530
+ * js-yaml's default schema resolves `!!binary` scalars to a `Uint8Array`, and
3531
+ * its dumper writes one back out as base64 — roughly 4 output characters per
3532
+ * 3 input bytes. Without this, an aliased binary blob would walk the budget
3533
+ * for free even though it can dominate the emitted document's size.
3534
+ */
3535
+ function estimateLeafChars(value) {
3536
+ if (value instanceof Uint8Array) return Math.ceil(value.byteLength / 3) * 4;
3537
+ return 0;
3538
+ }
3539
+ /**
3540
+ * Copy one parsed value, dropping nullish leaves and cyclic references.
3541
+ *
3542
+ * Every alias is still written out as an independent copy, as gray-matter's
3543
+ * default YAML engine would otherwise serialize shared references as `&ref_0`
3544
+ * anchors that simplified frontmatter parsers cannot read; the expansion is
3545
+ * bounded by {@link MAX_FRONTMATTER_VALUES} instead.
3546
+ */
3547
+ function deepCleanValue(value, options) {
3548
+ consumeBudget({
3549
+ options,
3550
+ stringChars: typeof value === "string" ? value.length : estimateLeafChars(value)
3551
+ });
3395
3552
  if (value === null || value === void 0) return;
3396
- if (Array.isArray(value)) return value.map((item) => deepRemoveNullishValue(item)).filter((item) => item !== void 0);
3397
- if (isPlainObject$1(value)) {
3398
- const result = {};
3399
- for (const [key, val] of Object.entries(value)) {
3400
- const cleaned = deepRemoveNullishValue(val);
3401
- if (cleaned !== void 0) result[key] = cleaned;
3553
+ if (typeof value === "string") return options.transformString ? options.transformString(value) : value;
3554
+ if (Array.isArray(value)) {
3555
+ if (options.ancestors.has(value)) return;
3556
+ enterContainer({
3557
+ options,
3558
+ container: value
3559
+ });
3560
+ const cleanedArray = [];
3561
+ for (const item of value) {
3562
+ const cleaned = deepCleanValue(item, options);
3563
+ if (cleaned !== void 0) cleanedArray.push(cleaned);
3402
3564
  }
3565
+ leaveContainer({
3566
+ options,
3567
+ container: value
3568
+ });
3569
+ return cleanedArray;
3570
+ }
3571
+ if (isPlainObject$1(value)) {
3572
+ if (options.ancestors.has(value)) return;
3573
+ enterContainer({
3574
+ options,
3575
+ container: value
3576
+ });
3577
+ const result = cleanOwnEntries(value, options);
3578
+ leaveContainer({
3579
+ options,
3580
+ container: value
3581
+ });
3403
3582
  return result;
3404
3583
  }
3405
3584
  return value;
3406
3585
  }
3407
- function deepRemoveNullishObject(obj) {
3408
- if (!obj || typeof obj !== "object") return {};
3586
+ /**
3587
+ * Copy the cleaned own entries of a parsed object into a fresh record.
3588
+ *
3589
+ * A YAML parser defines a `__proto__:` key as an own property, and assigning
3590
+ * it back with bracket notation would instead replace the new record's
3591
+ * prototype, whose members zod's loose object schemas then promote to real
3592
+ * keys. So a fetched skill could hide `allowed-tools` under an innocuous
3593
+ * looking `__proto__:` block. That key, `constructor` and `prototype` are
3594
+ * therefore dropped rather than copied, and cannot be used as frontmatter
3595
+ * keys.
3596
+ */
3597
+ function cleanOwnEntries(obj, options) {
3409
3598
  const result = {};
3410
3599
  for (const [key, val] of Object.entries(obj)) {
3411
- const cleaned = deepRemoveNullishValue(val);
3600
+ chargeStringChars({
3601
+ options,
3602
+ chars: key.length
3603
+ });
3604
+ const cleaned = deepCleanValue(val, options);
3605
+ if (isPrototypePollutionKey(key)) continue;
3412
3606
  if (cleaned !== void 0) result[key] = cleaned;
3413
3607
  }
3414
3608
  return result;
3415
3609
  }
3416
- function deepFlattenStringsValue(value) {
3417
- if (value === null || value === void 0) return;
3418
- if (typeof value === "string") return value.replace(/\n+/g, " ").trim();
3419
- if (Array.isArray(value)) return value.map((item) => deepFlattenStringsValue(item)).filter((item) => item !== void 0);
3420
- if (isPlainObject$1(value)) {
3421
- const result = {};
3422
- for (const [key, val] of Object.entries(value)) {
3423
- const cleaned = deepFlattenStringsValue(val);
3424
- if (cleaned !== void 0) result[key] = cleaned;
3425
- }
3426
- return result;
3427
- }
3428
- return value;
3610
+ function deepCleanObject(obj, options) {
3611
+ if (!obj || typeof obj !== "object") return {};
3612
+ return cleanOwnEntries(obj, {
3613
+ ...options,
3614
+ ancestors: new WeakSet([obj]),
3615
+ budget: {
3616
+ remaining: MAX_FRONTMATTER_VALUES,
3617
+ stringCharsRemaining: MAX_FRONTMATTER_STRING_CHARS
3618
+ },
3619
+ depth: 1
3620
+ });
3621
+ }
3622
+ /** Drop null and undefined values, recursively. */
3623
+ function deepRemoveNullishObject(obj) {
3624
+ return deepCleanObject(obj, {});
3429
3625
  }
3626
+ /** Drop nullish values and collapse every string onto a single line. */
3430
3627
  function deepFlattenStringsObject(obj) {
3431
- if (!obj || typeof obj !== "object") return {};
3432
- const result = {};
3433
- for (const [key, val] of Object.entries(obj)) {
3434
- const cleaned = deepFlattenStringsValue(val);
3435
- if (cleaned !== void 0) result[key] = cleaned;
3436
- }
3437
- return result;
3628
+ return deepCleanObject(obj, { transformString: (value) => value.replace(/\n+/g, " ").trim() });
3438
3629
  }
3439
3630
  function stringifyFrontmatter(body, frontmatter, options) {
3440
3631
  const { avoidBlockScalars = false } = options ?? {};
3441
3632
  const cleanFrontmatter = avoidBlockScalars ? deepFlattenStringsObject(frontmatter) : deepRemoveNullishObject(frontmatter);
3442
- if (avoidBlockScalars) return matter.stringify(body, cleanFrontmatter, { engines: { yaml: {
3633
+ const file = { content: body };
3634
+ if (avoidBlockScalars) return matter.stringify(file, cleanFrontmatter, { engines: { yaml: {
3443
3635
  parse: (input) => loadYaml(input) ?? {},
3444
3636
  stringify: (data) => dump(data, { lineWidth: -1 })
3445
3637
  } } });
3446
- return matter.stringify(body, cleanFrontmatter);
3638
+ return matter.stringify(file, cleanFrontmatter);
3447
3639
  }
3448
3640
  function parseFrontmatter(content, filePath) {
3449
3641
  let frontmatter;
3450
3642
  let body;
3451
3643
  let hasFrontmatter;
3452
3644
  try {
3645
+ const bounds = findFrontmatterBlockBounds(content);
3646
+ if (bounds && bounds.blockEnd - bounds.blockStart > 65536) throw new Error(`Frontmatter block is larger than ${MAX_FRONTMATTER_RAW_CHARS} characters; refusing to parse it (a complex YAML key can cost memory while parsing, before any post-parse budget applies)`);
3453
3647
  const result = matter(content, {});
3454
- frontmatter = result.data;
3648
+ frontmatter = deepRemoveNullishObject(result.data);
3455
3649
  body = result.content;
3456
3650
  hasFrontmatter = result.matter !== "" || content.trimStart().startsWith("---");
3457
3651
  } catch (error) {
@@ -3459,7 +3653,7 @@ function parseFrontmatter(content, filePath) {
3459
3653
  throw error;
3460
3654
  }
3461
3655
  return {
3462
- frontmatter: deepRemoveNullishObject(frontmatter),
3656
+ frontmatter,
3463
3657
  body,
3464
3658
  hasFrontmatter
3465
3659
  };
@@ -3508,17 +3702,34 @@ function repairFrontmatterLine(line) {
3508
3702
  };
3509
3703
  }
3510
3704
  /**
3511
- * Quote the unquoted scalars that make a frontmatter block unparseable, or
3512
- * return `undefined` when there is nothing to repair. Only the frontmatter
3513
- * block is rewritten; the body is passed through untouched.
3705
+ * Locate a raw `---`-delimited frontmatter block's bounds within `content`,
3706
+ * without parsing it. Shared by the size guard in {@link parseFrontmatter} and
3707
+ * the YAML repair pass below, so both agree on exactly what gray-matter would
3708
+ * treat as the block: gray-matter ends it at the first `\n---`, with no
3709
+ * requirement that the delimiter be alone on its line, so a stricter pattern
3710
+ * here would run past gray-matter's delimiter and act on text that is really
3711
+ * the body.
3514
3712
  */
3515
- function repairMalformedFrontmatterYaml(content) {
3713
+ function findFrontmatterBlockBounds(content) {
3516
3714
  const opening = /^\uFEFF?---[^\S\r\n]*\r?\n/.exec(content);
3517
3715
  if (!opening) return;
3518
3716
  const blockStart = opening[0].length;
3519
3717
  const closing = /\r?\n---/.exec(content.slice(blockStart));
3520
3718
  if (!closing) return;
3521
- const blockEnd = blockStart + closing.index;
3719
+ return {
3720
+ blockStart,
3721
+ blockEnd: blockStart + closing.index
3722
+ };
3723
+ }
3724
+ /**
3725
+ * Quote the unquoted scalars that make a frontmatter block unparseable, or
3726
+ * return `undefined` when there is nothing to repair. Only the frontmatter
3727
+ * block is rewritten; the body is passed through untouched.
3728
+ */
3729
+ function repairMalformedFrontmatterYaml(content) {
3730
+ const bounds = findFrontmatterBlockBounds(content);
3731
+ if (!bounds) return;
3732
+ const { blockStart, blockEnd } = bounds;
3522
3733
  const block = content.slice(blockStart, blockEnd);
3523
3734
  const repairedLines = block.split("\n").map(repairFrontmatterLine);
3524
3735
  const repairedBlock = repairedLines.map(({ line }) => line).join("\n");
@@ -5197,42 +5408,6 @@ const CANONICAL_TO_GROKCLI_EVENT_NAMES = {
5197
5408
  */
5198
5409
  const GROKCLI_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_GROKCLI_EVENT_NAMES).map(([k, v]) => [v, k]));
5199
5410
  //#endregion
5200
- //#region src/utils/prototype-pollution.ts
5201
- /**
5202
- * Keys that, if walked into when constructing or merging objects from
5203
- * untrusted input, can mutate `Object.prototype` (or otherwise the prototype
5204
- * chain) and propagate state to every other object in the runtime. Any code
5205
- * that copies arbitrary user-supplied keys into a fresh object — frontmatter
5206
- * parsing, MCP config conversion, settings round-trip — should skip these.
5207
- */
5208
- const PROTOTYPE_POLLUTION_KEYS = /* @__PURE__ */ new Set([
5209
- "__proto__",
5210
- "constructor",
5211
- "prototype"
5212
- ]);
5213
- function isPrototypePollutionKey(key) {
5214
- return PROTOTYPE_POLLUTION_KEYS.has(key);
5215
- }
5216
- /**
5217
- * Returns a shallow copy of a record's own entries with every
5218
- * prototype-pollution key (`__proto__`, `constructor`, `prototype`) dropped.
5219
- *
5220
- * Use when copying a nested, user-supplied string map — an MCP server's `env`
5221
- * or `headers` table — into freshly generated config. Carrying such a map by
5222
- * reference, or re-assigning its keys via bracket notation, would let a literal
5223
- * `__proto__` key ride along (and re-assigning it would mutate the target's
5224
- * prototype). Walking the entries through this helper severs that path while
5225
- * preserving every legitimate key.
5226
- */
5227
- function omitPrototypePollutionKeys(record) {
5228
- const sanitized = {};
5229
- for (const [key, value] of Object.entries(record)) {
5230
- if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
5231
- sanitized[key] = value;
5232
- }
5233
- return sanitized;
5234
- }
5235
- //#endregion
5236
5411
  //#region src/utils/jsonc.ts
5237
5412
  /**
5238
5413
  * Rebuild the parsed value from its own enumerable entries, dropping
@@ -5983,6 +6158,17 @@ var RulesyncMcp = class RulesyncMcp extends RulesyncFile {
5983
6158
  const fallbackDirPath = overrideDirPath ?? paths.recommended.relativeDirPath;
5984
6159
  throw new RulesyncSourceNotFoundError(`No ${join(outputRoot, fallbackDirPath, paths.recommended.relativeFilePath)} found.`);
5985
6160
  }
6161
+ /**
6162
+ * Return one server exactly as authored, before `getMcpServers()` strips
6163
+ * rulesync- and tool-specific fields. Keep this lookup here so every target
6164
+ * that re-merges one of those fields shares the same own-property and
6165
+ * prototype-pollution guards.
6166
+ */
6167
+ getRawMcpServer(name) {
6168
+ if (isPrototypePollutionKey(name)) return void 0;
6169
+ const mcpServers = isRecord$1(this.json) ? this.json.mcpServers : void 0;
6170
+ return isRecord$1(mcpServers) && Object.hasOwn(mcpServers, name) ? mcpServers[name] : void 0;
6171
+ }
5986
6172
  getMcpServers() {
5987
6173
  const mcpServers = this.json.mcpServers ?? {};
5988
6174
  const entries = Object.entries(mcpServers);
@@ -7609,6 +7795,11 @@ const RulesyncRuleFrontmatterSchema = z.object({
7609
7795
  globs: z.optional(z.array(z.string())),
7610
7796
  agentsmd: z.optional(z.looseObject({ subprojectPath: z.optional(z.string()) })),
7611
7797
  claudecode: z.optional(z.looseObject({ paths: z.optional(z.array(z.string())) })),
7798
+ codebuddy: z.optional(z.looseObject({
7799
+ paths: z.optional(z.array(z.string())),
7800
+ alwaysApply: z.optional(z.boolean()),
7801
+ description: z.optional(z.string())
7802
+ })),
7612
7803
  cursor: z.optional(z.looseObject({
7613
7804
  alwaysApply: z.optional(z.boolean()),
7614
7805
  description: z.optional(z.string()),
@@ -7650,7 +7841,8 @@ const RulesyncRuleFrontmatterSchema = z.object({
7650
7841
  name: z.optional(z.string()),
7651
7842
  extends: z.optional(z.string()),
7652
7843
  facet: z.optional(z.enum(["policies", "output-contracts"]))
7653
- }))
7844
+ })),
7845
+ factorydroid: z.optional(z.looseObject({ channel: z.optional(z.enum(["design"])) }))
7654
7846
  });
7655
7847
  /**
7656
7848
  * The `agentsmd.subprojectPath` every consumer should act on, resolved once so
@@ -8765,15 +8957,15 @@ const RulesyncSkillFrontmatterSchema = z.looseObject({
8765
8957
  })),
8766
8958
  opencode: z.optional(z.looseObject({
8767
8959
  "allowed-tools": z.optional(z.array(z.string())),
8768
- license: z.optional(z.string()),
8769
- compatibility: z.optional(z.union([z.string(), z.looseObject({})])),
8770
- metadata: z.optional(z.looseObject({}))
8960
+ license: z.optional(z.unknown()),
8961
+ compatibility: z.optional(z.unknown()),
8962
+ metadata: z.optional(z.unknown())
8771
8963
  })),
8772
8964
  kilo: z.optional(z.looseObject({
8773
8965
  "allowed-tools": z.optional(z.array(z.string())),
8774
- license: z.optional(z.string()),
8775
- compatibility: z.optional(z.union([z.string(), z.looseObject({})])),
8776
- metadata: z.optional(z.looseObject({}))
8966
+ license: z.optional(z.unknown()),
8967
+ compatibility: z.optional(z.unknown()),
8968
+ metadata: z.optional(z.unknown())
8777
8969
  })),
8778
8970
  kiro: z.optional(z.looseObject({
8779
8971
  license: z.optional(z.string()),
@@ -8782,9 +8974,9 @@ const RulesyncSkillFrontmatterSchema = z.looseObject({
8782
8974
  })),
8783
8975
  deepagents: z.optional(z.looseObject({
8784
8976
  "allowed-tools": z.optional(z.array(z.string())),
8785
- license: z.optional(z.string()),
8786
- compatibility: z.optional(z.union([z.string(), z.looseObject({})])),
8787
- metadata: z.optional(z.looseObject({}))
8977
+ license: z.optional(z.unknown()),
8978
+ compatibility: z.optional(z.unknown()),
8979
+ metadata: z.optional(z.unknown())
8788
8980
  })),
8789
8981
  copilot: z.optional(z.looseObject({
8790
8982
  license: z.optional(z.string()),
@@ -8891,6 +9083,13 @@ const RulesyncSkillFrontmatterSchema = z.looseObject({
8891
9083
  takt: z.optional(z.looseObject({
8892
9084
  name: z.optional(z.string()),
8893
9085
  extends: z.optional(z.string())
9086
+ })),
9087
+ crush: z.optional(z.looseObject({
9088
+ "disable-model-invocation": z.optional(z.boolean()),
9089
+ "user-invocable": z.optional(z.boolean()),
9090
+ license: z.optional(z.string()),
9091
+ compatibility: z.optional(z.union([z.string(), z.looseObject({})])),
9092
+ metadata: z.optional(z.looseObject({}))
8894
9093
  }))
8895
9094
  });
8896
9095
  /**
@@ -9116,7 +9315,7 @@ async function getLocalSkillDirNames(sourceTree) {
9116
9315
  *
9117
9316
  * The rulesync skill frontmatter exposes a root-level `disable-model-invocation`
9118
9317
  * default that applies to every tool supporting the flag (claudecode, copilot,
9119
- * copilotcli, cursor, zed, pi, qwencode, grokcli, factorydroid). Each tool's own section may override that
9318
+ * copilotcli, crush, cursor, zed, pi, qwencode, grokcli, factorydroid). Each tool's own section may override that
9120
9319
  * default with a per-target value. A defined section value (including `false`)
9121
9320
  * always wins over the root default.
9122
9321
  *
@@ -9135,7 +9334,7 @@ function resolveDisableModelInvocation({ rootFrontmatter, section }) {
9135
9334
  *
9136
9335
  * The rulesync skill frontmatter exposes a root-level `user-invocable` default
9137
9336
  * that applies to every tool supporting the flag (claudecode, copilot,
9138
- * copilotcli, cursor, qwencode, vibe, grokcli, factorydroid). Each tool's own section may override that default with a
9337
+ * copilotcli, crush, cursor, qwencode, vibe, grokcli, factorydroid). Each tool's own section may override that default with a
9139
9338
  * per-target value. A defined section value (including `false`) always wins
9140
9339
  * over the root default.
9141
9340
  *
@@ -9600,8 +9799,8 @@ var FeatureProcessor = class extends RulesyncSourceConsumer {
9600
9799
  * This only deletes files that are no longer in the rulesync source, not files that will be overwritten.
9601
9800
  */
9602
9801
  async removeOrphanAiFiles(existingFiles, generatedFiles) {
9603
- const generatedPaths = new Set(generatedFiles.map((f) => f.getFilePath()));
9604
- const orphanFiles = existingFiles.filter((f) => !generatedPaths.has(f.getFilePath()));
9802
+ const generatedPaths = new Set(generatedFiles.map((f) => caseFoldIdentity(f.getFilePath())));
9803
+ const orphanFiles = existingFiles.filter((f) => !generatedPaths.has(caseFoldIdentity(f.getFilePath())));
9605
9804
  for (const aiFile of orphanFiles) {
9606
9805
  const filePath = aiFile.getFilePath();
9607
9806
  const loggedPath = stripControlCharacters(filePath);
@@ -10728,6 +10927,15 @@ const FACTORYDROID_COMMANDS_DIR_PATH = join(FACTORYDROID_DIR, "commands");
10728
10927
  const FACTORYDROID_SKILLS_DIR_PATH = join(FACTORYDROID_DIR, "skills");
10729
10928
  const FACTORYDROID_DROIDS_DIR_PATH = join(FACTORYDROID_DIR, "droids");
10730
10929
  const FACTORYDROID_RULE_FILE_NAME = "AGENTS.md";
10930
+ /**
10931
+ * Factory Droid's design-guidelines instruction file: "Always-on design-system,
10932
+ * UX, visual, and interaction guidance", loaded separately from `AGENTS.md`'s
10933
+ * coding guidelines. Project scope only — Factory's docs describe root and
10934
+ * nested `DESIGN.md` files like `AGENTS.md`, but document no personal/global
10935
+ * home-directory equivalent.
10936
+ * @see https://docs.factory.ai/cli/configuration/agents-md
10937
+ */
10938
+ const FACTORYDROID_DESIGN_FILE_NAME = "DESIGN.md";
10731
10939
  const FACTORYDROID_MCP_FILE_NAME = "mcp.json";
10732
10940
  const FACTORYDROID_SETTINGS_FILE_NAME = "settings.json";
10733
10941
  const FACTORYDROID_HOOKS_FILE_NAME = "hooks.json";
@@ -15627,6 +15835,10 @@ function toAllowedToolsArray(value) {
15627
15835
  * The spec types `compatibility` as a free-form string. An object from a legacy
15628
15836
  * rulesync input is flattened to `key: value` pairs instead of being emitted as
15629
15837
  * a YAML mapping, which conformant clients reject.
15838
+ *
15839
+ * Exported for `CrushSkill`, which requires the same bare-string shape (Crush's
15840
+ * Go struct types `Compatibility` as a plain `string`) and reuses this
15841
+ * implementation rather than maintaining a second, divergent copy.
15630
15842
  */
15631
15843
  function toCompatibilityString(value) {
15632
15844
  if (typeof value === "string") return value;
@@ -15635,6 +15847,10 @@ function toCompatibilityString(value) {
15635
15847
  /**
15636
15848
  * The spec types `metadata` as "a map from string keys to string values", so
15637
15849
  * non-string values (e.g. a YAML number `version: 1`) are stringified.
15850
+ *
15851
+ * Exported for `CrushSkill`, which requires the same `map[string]string`
15852
+ * shape (Crush's Go struct types `Metadata` that way) and reuses this
15853
+ * implementation rather than maintaining a second, divergent copy.
15638
15854
  */
15639
15855
  function toStringMetadata(metadata) {
15640
15856
  return Object.fromEntries(Object.entries(metadata).map(([key, value]) => [key, stringifyValue(value)]));
@@ -18645,7 +18861,13 @@ var CommandsProcessor = class extends FeatureProcessor {
18645
18861
  if (!matchByBasename || flatOnly && dirname(key) !== ".") return [key];
18646
18862
  return [key, basename(key)];
18647
18863
  };
18648
- const seen = new Set(toolCommands.flatMap((command) => keysOf(command)));
18864
+ const claimedKeys = new ClaimedIdentities();
18865
+ const primarySource = paths.relativeDirPath;
18866
+ const secondarySource = "a secondary source";
18867
+ for (const command of toolCommands) for (const candidate of keysOf(command)) claimedKeys.claim({
18868
+ identity: candidate,
18869
+ source: primarySource
18870
+ });
18649
18871
  const additionalCommands = await factory.class.loadAdditionalImportFiles({
18650
18872
  outputRoot: this.outputRoot,
18651
18873
  global: this.global,
@@ -18653,11 +18875,26 @@ var CommandsProcessor = class extends FeatureProcessor {
18653
18875
  });
18654
18876
  for (const command of additionalCommands) {
18655
18877
  const key = command.getRelativeFilePath();
18656
- if (keysOf(command, true).some((candidate) => seen.has(candidate))) {
18657
- this.logger.warn(`Duplicate ${this.toolTarget} command "${key}" from a secondary source; keeping the one already loaded.`);
18878
+ const collision = [...new Set(keysOf(command, true))].map((candidate) => {
18879
+ const claimed = claimedKeys.claim({
18880
+ identity: candidate,
18881
+ source: secondarySource
18882
+ });
18883
+ return claimed === null ? void 0 : {
18884
+ candidate,
18885
+ claimed
18886
+ };
18887
+ }).find((hit) => hit !== void 0);
18888
+ if (collision) {
18889
+ const { candidate, claimed } = collision;
18890
+ if (claimed.spelling === candidate) this.logger.warn(`Duplicate ${this.toolTarget} command "${stripControlCharacters(key)}" from ${secondarySource}; keeping the one already loaded.`);
18891
+ else this.logger.warn(`Case-insensitive ${this.toolTarget} command collision: "${stripControlCharacters(claimed.spelling)}" and "${stripControlCharacters(candidate)}" resolve to the same command file. Keeping "${stripControlCharacters(claimed.spelling)}" from ${claimed.source === secondarySource ? "earlier in the same source" : `the higher-precedence ${claimed.source}`} and ignoring "${stripControlCharacters(key)}" from ${secondarySource}, which is not imported.`);
18658
18892
  continue;
18659
18893
  }
18660
- for (const candidate of keysOf(command)) seen.add(candidate);
18894
+ for (const candidate of keysOf(command)) claimedKeys.claim({
18895
+ identity: candidate,
18896
+ source: secondarySource
18897
+ });
18661
18898
  toolCommands.push(command);
18662
18899
  }
18663
18900
  }
@@ -18947,6 +19184,21 @@ var AmpHooks = class AmpHooks extends ToolHooks {
18947
19184
  }
18948
19185
  };
18949
19186
  //#endregion
19187
+ //#region src/utils/own-lookup.ts
19188
+ /**
19189
+ * Read a key from a plain string map without walking its prototype chain.
19190
+ *
19191
+ * A bracket read on an object literal resolves inherited members too, so a
19192
+ * user-supplied key such as `toString` or `constructor` "succeeds" with an
19193
+ * `Object.prototype` function instead of falling through to the caller's
19194
+ * `?? fallback`. Hook adapters translate native event names this way from
19195
+ * `Object.entries()` over a config file, so route the read through here to keep
19196
+ * the fallback honest: only a key the map itself defines yields a value.
19197
+ */
19198
+ function lookupOwn({ record, key }) {
19199
+ return Object.hasOwn(record, key) ? record[key] : void 0;
19200
+ }
19201
+ //#endregion
18950
19202
  //#region src/utils/object.ts
18951
19203
  /**
18952
19204
  * Return a shallow copy of `obj` keeping only the entries whose value is
@@ -19331,7 +19583,10 @@ function canonicalToToolHooks({ config, toolOverrideHooks, converterConfig, logg
19331
19583
  const warn = warnOnce(logger);
19332
19584
  const result = {};
19333
19585
  for (const [eventName, definitions] of Object.entries(effectiveHooks)) {
19334
- const toolEventName = converterConfig.canonicalToToolEventNames[eventName] ?? eventName;
19586
+ const toolEventName = lookupOwn({
19587
+ record: converterConfig.canonicalToToolEventNames,
19588
+ key: eventName
19589
+ }) ?? eventName;
19335
19590
  const byMatcher = groupDefinitionsByMatcher({
19336
19591
  definitions,
19337
19592
  converterConfig
@@ -19759,7 +20014,10 @@ function toolHooksToCanonical({ hooks, converterConfig, logger }) {
19759
20014
  const warn = warnOnce(logger);
19760
20015
  const canonical = {};
19761
20016
  for (const [toolEventName, matcherEntries] of Object.entries(hooks)) {
19762
- const eventName = converterConfig.toolToCanonicalEventNames[toolEventName] ?? toolEventName;
20017
+ const eventName = lookupOwn({
20018
+ record: converterConfig.toolToCanonicalEventNames,
20019
+ key: toolEventName
20020
+ }) ?? toolEventName;
19763
20021
  if (!Array.isArray(matcherEntries)) continue;
19764
20022
  const defs = [];
19765
20023
  for (const rawEntry of matcherEntries) {
@@ -19815,7 +20073,10 @@ function flattenAntigravityHooks(parsed) {
19815
20073
  const flat = {};
19816
20074
  const addEvent = (event, entries) => {
19817
20075
  if (isPrototypePollutionKey(event) || !Array.isArray(entries)) return;
19818
- const existing = Object.hasOwn(flat, event) ? flat[event] : void 0;
20076
+ const existing = lookupOwn({
20077
+ record: flat,
20078
+ key: event
20079
+ });
19819
20080
  flat[event] = existing ? [...existing, ...entries] : [...entries];
19820
20081
  };
19821
20082
  for (const [key, value] of Object.entries(parsed)) if (Array.isArray(value)) addEvent(key, value);
@@ -20202,6 +20463,16 @@ var AugmentcodeHooks = class AugmentcodeHooks extends ToolHooks {
20202
20463
  const paths = AugmentcodeHooks.getSettablePaths({ global });
20203
20464
  const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
20204
20465
  const existingContent = await readFileContentOrNull(filePath) ?? JSON.stringify({}, null, 2);
20466
+ let existingHooks = {};
20467
+ try {
20468
+ const parsed = JSON.parse(existingContent);
20469
+ const candidate = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed.hooks : void 0;
20470
+ if (candidate && typeof candidate === "object" && !Array.isArray(candidate)) existingHooks = candidate;
20471
+ } catch {
20472
+ existingHooks = {};
20473
+ }
20474
+ const nativeEventKeys = new Set(Object.values(CANONICAL_TO_AUGMENTCODE_EVENT_NAMES));
20475
+ const preservedHooks = Object.fromEntries(Object.entries(existingHooks).filter(([key]) => !nativeEventKeys.has(key)));
20205
20476
  const config = rulesyncHooks.getJson();
20206
20477
  const augmentHooks = canonicalToToolHooks({
20207
20478
  config,
@@ -20213,7 +20484,10 @@ var AugmentcodeHooks = class AugmentcodeHooks extends ToolHooks {
20213
20484
  fileKey: sharedConfigFileKey(paths),
20214
20485
  feature: "hooks",
20215
20486
  existingContent,
20216
- patch: { hooks: augmentHooks },
20487
+ patch: { hooks: {
20488
+ ...preservedHooks,
20489
+ ...augmentHooks
20490
+ } },
20217
20491
  filePath
20218
20492
  });
20219
20493
  return new AugmentcodeHooks({
@@ -20971,7 +21245,10 @@ function canonicalToCopilotHooks(config) {
20971
21245
  };
20972
21246
  const copilot = {};
20973
21247
  for (const [eventName, definitions] of Object.entries(effectiveHooks)) {
20974
- const copilotEventName = CANONICAL_TO_COPILOT_EVENT_NAMES[eventName] ?? eventName;
21248
+ const copilotEventName = lookupOwn({
21249
+ record: CANONICAL_TO_COPILOT_EVENT_NAMES,
21250
+ key: eventName
21251
+ }) ?? eventName;
20975
21252
  const entries = [];
20976
21253
  for (const def of definitions) {
20977
21254
  const hookType = def.type ?? "command";
@@ -21048,7 +21325,10 @@ function copilotHooksToCanonical(copilotHooks, logger) {
21048
21325
  if (copilotHooks === null || copilotHooks === void 0 || typeof copilotHooks !== "object") return {};
21049
21326
  const canonical = {};
21050
21327
  for (const [copilotEventName, hookEntries] of Object.entries(copilotHooks)) {
21051
- const eventName = COPILOT_TO_CANONICAL_EVENT_NAMES[copilotEventName] ?? copilotEventName;
21328
+ const eventName = lookupOwn({
21329
+ record: COPILOT_TO_CANONICAL_EVENT_NAMES,
21330
+ key: copilotEventName
21331
+ }) ?? copilotEventName;
21052
21332
  if (!Array.isArray(hookEntries)) continue;
21053
21333
  const defs = [];
21054
21334
  for (const rawEntry of hookEntries) {
@@ -21318,7 +21598,10 @@ function canonicalToCopilotCliHooks(config, logger) {
21318
21598
  };
21319
21599
  const out = {};
21320
21600
  for (const [eventName, definitions] of Object.entries(effectiveHooks)) {
21321
- const copilotEventName = CANONICAL_TO_COPILOTCLI_EVENT_NAMES[eventName] ?? eventName;
21601
+ const copilotEventName = lookupOwn({
21602
+ record: CANONICAL_TO_COPILOTCLI_EVENT_NAMES,
21603
+ key: eventName
21604
+ }) ?? eventName;
21322
21605
  const entries = buildCopilotCliEntriesForEvent({
21323
21606
  eventName,
21324
21607
  definitions,
@@ -21375,7 +21658,10 @@ function copilotCliHooksToCanonical(rawHooks, logger) {
21375
21658
  if (rawHooks === null || rawHooks === void 0 || typeof rawHooks !== "object") return {};
21376
21659
  const canonical = {};
21377
21660
  for (const [copilotEventName, hookEntries] of Object.entries(rawHooks)) {
21378
- const eventName = COPILOTCLI_TO_CANONICAL_EVENT_NAMES[copilotEventName] ?? copilotEventName;
21661
+ const eventName = lookupOwn({
21662
+ record: COPILOTCLI_TO_CANONICAL_EVENT_NAMES,
21663
+ key: copilotEventName
21664
+ }) ?? copilotEventName;
21379
21665
  if (!Array.isArray(hookEntries)) continue;
21380
21666
  const defs = [];
21381
21667
  for (const rawEntry of hookEntries) {
@@ -21527,7 +21813,10 @@ var CursorHooks = class CursorHooks extends ToolHooks {
21527
21813
  const mappedHooks = {};
21528
21814
  const cursorSupportedTypes = /* @__PURE__ */ new Set(["command", "prompt"]);
21529
21815
  for (const [eventName, defs] of Object.entries(mergedHooks)) {
21530
- const cursorEventName = CANONICAL_TO_CURSOR_EVENT_NAMES[eventName] ?? eventName;
21816
+ const cursorEventName = lookupOwn({
21817
+ record: CANONICAL_TO_CURSOR_EVENT_NAMES,
21818
+ key: eventName
21819
+ }) ?? eventName;
21531
21820
  const mappedDefs = defs.filter((def) => cursorSupportedTypes.has(def.type ?? "command")).map((def) => ({
21532
21821
  ...def.type !== void 0 && def.type !== null && { type: def.type },
21533
21822
  ...def.command !== void 0 && def.command !== null && { command: def.command },
@@ -21560,7 +21849,10 @@ var CursorHooks = class CursorHooks extends ToolHooks {
21560
21849
  const cursorHooks = parsed.hooks ?? {};
21561
21850
  const canonicalHooks = {};
21562
21851
  for (const [cursorEventName, defs] of Object.entries(cursorHooks)) {
21563
- const eventName = CURSOR_TO_CANONICAL_EVENT_NAMES[cursorEventName] ?? cursorEventName;
21852
+ const eventName = lookupOwn({
21853
+ record: CURSOR_TO_CANONICAL_EVENT_NAMES,
21854
+ key: cursorEventName
21855
+ }) ?? cursorEventName;
21564
21856
  canonicalHooks[eventName] = defs;
21565
21857
  }
21566
21858
  const version = parsed.version ?? 1;
@@ -21630,7 +21922,10 @@ function canonicalToDeepagentsHooks(config) {
21630
21922
  const hooks = {};
21631
21923
  for (const [canonicalEvent, definitions] of Object.entries(effectiveHooks)) {
21632
21924
  if (!supported.has(canonicalEvent)) continue;
21633
- const deepagentsEvent = CANONICAL_TO_DEEPAGENTS_EVENT_NAMES[canonicalEvent];
21925
+ const deepagentsEvent = lookupOwn({
21926
+ record: CANONICAL_TO_DEEPAGENTS_EVENT_NAMES,
21927
+ key: canonicalEvent
21928
+ });
21634
21929
  if (!deepagentsEvent) continue;
21635
21930
  for (const def of definitions) {
21636
21931
  if ((def.type ?? "command") !== "command") continue;
@@ -21659,7 +21954,10 @@ function canonicalToDeepagentsHooks(config) {
21659
21954
  function deepagentsToCanonicalHooks(hooks) {
21660
21955
  const canonical = {};
21661
21956
  for (const [deepagentsEvent, groups] of Object.entries(hooks)) {
21662
- const canonicalEvent = DEEPAGENTS_TO_CANONICAL_EVENT_NAMES[deepagentsEvent];
21957
+ const canonicalEvent = lookupOwn({
21958
+ record: DEEPAGENTS_TO_CANONICAL_EVENT_NAMES,
21959
+ key: deepagentsEvent
21960
+ });
21663
21961
  if (!canonicalEvent || !Array.isArray(groups)) continue;
21664
21962
  for (const group of groups) {
21665
21963
  if (!isRecord(group) || !Array.isArray(group.hooks)) continue;
@@ -21692,7 +21990,10 @@ function deepagentsLegacyToCanonicalHooks(entries) {
21692
21990
  const command = argv.length === 3 && argv[0] === "bash" && argv[1] === "-c" ? String(argv[2] ?? "") : argv.join(" ");
21693
21991
  const events = Array.isArray(entry.events) ? entry.events : [];
21694
21992
  for (const legacyEvent of events) {
21695
- const canonicalEvent = typeof legacyEvent === "string" ? DEEPAGENTS_LEGACY_TO_CANONICAL_EVENT_NAMES[legacyEvent] : void 0;
21993
+ const canonicalEvent = typeof legacyEvent === "string" ? lookupOwn({
21994
+ record: DEEPAGENTS_LEGACY_TO_CANONICAL_EVENT_NAMES,
21995
+ key: legacyEvent
21996
+ }) : void 0;
21696
21997
  if (!canonicalEvent) continue;
21697
21998
  (canonical[canonicalEvent] ??= []).push({
21698
21999
  type: "command",
@@ -22413,7 +22714,10 @@ function canonicalToHermesHooks({ config, toolOverrideHooks, logger }) {
22413
22714
  const result = {};
22414
22715
  for (const [canonicalEvent, definitions] of Object.entries(config.hooks)) {
22415
22716
  if (!HERMESAGENT_CANONICAL_EVENTS.has(canonicalEvent)) continue;
22416
- const nativeEvent = CANONICAL_TO_HERMESAGENT_EVENT_NAMES[canonicalEvent];
22717
+ const nativeEvent = lookupOwn({
22718
+ record: CANONICAL_TO_HERMESAGENT_EVENT_NAMES,
22719
+ key: canonicalEvent
22720
+ });
22417
22721
  if (nativeEvent) setHermesHookEntries({
22418
22722
  result,
22419
22723
  event: nativeEvent,
@@ -22424,7 +22728,10 @@ function canonicalToHermesHooks({ config, toolOverrideHooks, logger }) {
22424
22728
  }
22425
22729
  for (const [canonicalEvent, definitions] of Object.entries(toolOverrideHooks ?? {})) {
22426
22730
  if (!HERMESAGENT_CANONICAL_EVENTS.has(canonicalEvent)) continue;
22427
- const nativeEvent = CANONICAL_TO_HERMESAGENT_EVENT_NAMES[canonicalEvent];
22731
+ const nativeEvent = lookupOwn({
22732
+ record: CANONICAL_TO_HERMESAGENT_EVENT_NAMES,
22733
+ key: canonicalEvent
22734
+ });
22428
22735
  if (nativeEvent) setHermesHookEntries({
22429
22736
  result,
22430
22737
  event: nativeEvent,
@@ -22490,7 +22797,10 @@ function hermesHooksToCanonical(hooks) {
22490
22797
  for (const [nativeEvent, entries] of Object.entries(hooks)) {
22491
22798
  if (PROTOTYPE_POLLUTION_KEYS.has(nativeEvent) || !Array.isArray(entries)) continue;
22492
22799
  if (!isHermesHookEventEntry(nativeEvent, entries)) continue;
22493
- const rulesyncEvent = HERMESAGENT_TO_CANONICAL_EVENT_NAMES[nativeEvent] ?? nativeEvent;
22800
+ const rulesyncEvent = lookupOwn({
22801
+ record: HERMESAGENT_TO_CANONICAL_EVENT_NAMES,
22802
+ key: nativeEvent
22803
+ }) ?? nativeEvent;
22494
22804
  const defs = entries.map((raw) => hermesEntryToDefinition({
22495
22805
  nativeEvent,
22496
22806
  raw
@@ -23127,7 +23437,10 @@ function canonicalToKimiCodeHooks({ config, toolOverrideHooks, trustedDirectory,
23127
23437
  const result = [];
23128
23438
  const nativeEvents = new Set(KIMI_CODE_NATIVE_HOOK_EVENTS);
23129
23439
  for (const [event, definitions] of Object.entries(buildEffectiveHooks(config, toolOverrideHooks))) {
23130
- const nativeEvent = CANONICAL_TO_KIMI_CODE_EVENT_NAMES[event] ?? event;
23440
+ const nativeEvent = lookupOwn({
23441
+ record: CANONICAL_TO_KIMI_CODE_EVENT_NAMES,
23442
+ key: event
23443
+ }) ?? event;
23131
23444
  if (!nativeEvents.has(nativeEvent)) {
23132
23445
  logger?.warn(`Kimi Code hooks: skipping unsupported event "${event}".`);
23133
23446
  continue;
@@ -23161,14 +23474,22 @@ function kimiCodeHooksToCanonical(hooks) {
23161
23474
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) continue;
23162
23475
  const entry = raw;
23163
23476
  if (typeof entry.event !== "string" || typeof entry.command !== "string") continue;
23164
- const event = KIMI_CODE_TO_CANONICAL_EVENT_NAMES[entry.event] ?? entry.event;
23477
+ const event = lookupOwn({
23478
+ record: KIMI_CODE_TO_CANONICAL_EVENT_NAMES,
23479
+ key: entry.event
23480
+ }) ?? entry.event;
23165
23481
  const definition = {
23166
23482
  type: "command",
23167
23483
  command: stripTrustedDirectoryWrapper(entry.command),
23168
23484
  ...typeof entry.matcher === "string" && { matcher: entry.matcher },
23169
23485
  ...typeof entry.timeout === "number" && { timeout: entry.timeout }
23170
23486
  };
23171
- (result[event] ??= []).push(definition);
23487
+ const list = lookupOwn({
23488
+ record: result,
23489
+ key: event
23490
+ }) ?? [];
23491
+ list.push(definition);
23492
+ result[event] = list;
23172
23493
  }
23173
23494
  return result;
23174
23495
  }
@@ -23371,7 +23692,13 @@ function canonicalToKiroIdeHooks(config) {
23371
23692
  };
23372
23693
  const entries = [];
23373
23694
  for (const [eventName, definitions] of Object.entries(effectiveHooks)) {
23374
- const trigger = CANONICAL_TO_KIRO_IDE_EVENT_NAMES[eventName] ?? KIRO_LEGACY_TO_KIRO_IDE_TRIGGER_NAMES[eventName] ?? eventName;
23695
+ const trigger = lookupOwn({
23696
+ record: CANONICAL_TO_KIRO_IDE_EVENT_NAMES,
23697
+ key: eventName
23698
+ }) ?? lookupOwn({
23699
+ record: KIRO_LEGACY_TO_KIRO_IDE_TRIGGER_NAMES,
23700
+ key: eventName
23701
+ }) ?? eventName;
23375
23702
  entries.push(...buildKiroIdeEntriesForEvent(trigger, definitions));
23376
23703
  }
23377
23704
  return entries;
@@ -23380,7 +23707,10 @@ function kiroIdeHooksToCanonical(entries) {
23380
23707
  const canonical = {};
23381
23708
  for (const entry of entries) {
23382
23709
  if (entry.trigger === void 0 || entry.action === void 0) continue;
23383
- const eventName = KIRO_IDE_TO_CANONICAL_EVENT_NAMES[entry.trigger] ?? entry.trigger;
23710
+ const eventName = lookupOwn({
23711
+ record: KIRO_IDE_TO_CANONICAL_EVENT_NAMES,
23712
+ key: entry.trigger
23713
+ }) ?? entry.trigger;
23384
23714
  if (isPrototypePollutionKey(eventName)) continue;
23385
23715
  const def = {};
23386
23716
  if (entry.action.type === "command") {
@@ -23397,7 +23727,12 @@ function kiroIdeHooksToCanonical(entries) {
23397
23727
  if (entry.matcher !== void 0 && entry.matcher !== null && entry.matcher !== "") def.matcher = entry.matcher;
23398
23728
  if (entry.timeout !== void 0 && entry.timeout !== null) def.timeout = entry.timeout;
23399
23729
  if (entry.enabled === false) def.enabled = false;
23400
- (canonical[eventName] ??= []).push(def);
23730
+ const list = lookupOwn({
23731
+ record: canonical,
23732
+ key: eventName
23733
+ }) ?? [];
23734
+ list.push(def);
23735
+ canonical[eventName] = list;
23401
23736
  }
23402
23737
  return canonical;
23403
23738
  }
@@ -23578,7 +23913,10 @@ function canonicalToKiroHooks({ config, logger }) {
23578
23913
  };
23579
23914
  const kiro = {};
23580
23915
  for (const [eventName, definitions] of Object.entries(effectiveHooks)) {
23581
- const kiroEventName = CANONICAL_TO_KIRO_EVENT_NAMES[eventName] ?? eventName;
23916
+ const kiroEventName = lookupOwn({
23917
+ record: CANONICAL_TO_KIRO_EVENT_NAMES,
23918
+ key: eventName
23919
+ }) ?? eventName;
23582
23920
  const entries = buildKiroEntriesForEvent(definitions);
23583
23921
  if (entries.length > 0) if (kiro[kiroEventName]) kiro[kiroEventName].push(...entries);
23584
23922
  else kiro[kiroEventName] = entries;
@@ -23609,7 +23947,10 @@ function kiroHooksToCanonical(kiroHooks) {
23609
23947
  if (kiroHooks === null || kiroHooks === void 0 || typeof kiroHooks !== "object") return {};
23610
23948
  const canonical = {};
23611
23949
  for (const [kiroEventName, entries] of Object.entries(kiroHooks)) {
23612
- const eventName = KIRO_TO_CANONICAL_EVENT_NAMES[kiroEventName] ?? kiroEventName;
23950
+ const eventName = lookupOwn({
23951
+ record: KIRO_TO_CANONICAL_EVENT_NAMES,
23952
+ key: kiroEventName
23953
+ }) ?? kiroEventName;
23613
23954
  if (!Array.isArray(entries)) continue;
23614
23955
  const defs = [];
23615
23956
  for (const rawEntry of entries) {
@@ -24168,7 +24509,10 @@ function canonicalToQwencodeHooks(config, logger) {
24168
24509
  ]);
24169
24510
  const qwencode = {};
24170
24511
  for (const [eventName, definitions] of Object.entries(effectiveHooks)) {
24171
- const qwencodeEventName = CANONICAL_TO_QWENCODE_EVENT_NAMES[eventName] ?? eventName;
24512
+ const qwencodeEventName = lookupOwn({
24513
+ record: CANONICAL_TO_QWENCODE_EVENT_NAMES,
24514
+ key: eventName
24515
+ }) ?? eventName;
24172
24516
  const byMatcher = /* @__PURE__ */ new Map();
24173
24517
  for (const def of definitions) {
24174
24518
  if (!qwencodeSupportedTypes.has(def.type ?? "command")) continue;
@@ -24273,7 +24617,10 @@ function qwencodeHooksToCanonical(qwencodeHooks) {
24273
24617
  if (qwencodeHooks === null || qwencodeHooks === void 0 || typeof qwencodeHooks !== "object") return {};
24274
24618
  const canonical = {};
24275
24619
  for (const [qwencodeEventName, matcherEntries] of Object.entries(qwencodeHooks)) {
24276
- const eventName = QWENCODE_TO_CANONICAL_EVENT_NAMES[qwencodeEventName] ?? qwencodeEventName;
24620
+ const eventName = lookupOwn({
24621
+ record: QWENCODE_TO_CANONICAL_EVENT_NAMES,
24622
+ key: qwencodeEventName
24623
+ }) ?? qwencodeEventName;
24277
24624
  if (!Array.isArray(matcherEntries)) continue;
24278
24625
  const defs = [];
24279
24626
  for (const rawEntry of matcherEntries) {
@@ -24388,7 +24735,10 @@ function canonicalToReasonixHooks({ config, toolOverrideHooks, logger }) {
24388
24735
  const result = {};
24389
24736
  for (const [event, defs] of Object.entries(effectiveHooks)) {
24390
24737
  if (!SUPPORTED_REASONIX_EVENTS.has(event)) continue;
24391
- const reasonixEvent = CANONICAL_TO_REASONIX_EVENT_NAMES[event] ?? event;
24738
+ const reasonixEvent = lookupOwn({
24739
+ record: CANONICAL_TO_REASONIX_EVENT_NAMES,
24740
+ key: event
24741
+ }) ?? event;
24392
24742
  const isMatcherEvent = REASONIX_MATCHER_EVENTS.has(reasonixEvent);
24393
24743
  const entries = [];
24394
24744
  for (const def of defs) {
@@ -24401,7 +24751,10 @@ function canonicalToReasonixHooks({ config, toolOverrideHooks, logger }) {
24401
24751
  if (typeof def.timeout === "number") entry.timeout = Math.round(def.timeout * 1e3);
24402
24752
  entries.push(entry);
24403
24753
  }
24404
- if (entries.length > 0) result[reasonixEvent] = [...result[reasonixEvent] ?? [], ...entries];
24754
+ if (entries.length > 0) result[reasonixEvent] = [...lookupOwn({
24755
+ record: result,
24756
+ key: reasonixEvent
24757
+ }) ?? [], ...entries];
24405
24758
  }
24406
24759
  return result;
24407
24760
  }
@@ -24414,7 +24767,10 @@ function reasonixHooksToCanonical(hooks) {
24414
24767
  if (hooks === null || hooks === void 0 || typeof hooks !== "object" || Array.isArray(hooks)) return canonical;
24415
24768
  for (const [reasonixEvent, rawEntries] of Object.entries(hooks)) {
24416
24769
  if (!Array.isArray(rawEntries)) continue;
24417
- const canonicalEvent = REASONIX_TO_CANONICAL_EVENT_NAMES[reasonixEvent] ?? reasonixEvent;
24770
+ const canonicalEvent = lookupOwn({
24771
+ record: REASONIX_TO_CANONICAL_EVENT_NAMES,
24772
+ key: reasonixEvent
24773
+ }) ?? reasonixEvent;
24418
24774
  const defs = [];
24419
24775
  for (const rawEntry of rawEntries) {
24420
24776
  if (rawEntry === null || typeof rawEntry !== "object" || Array.isArray(rawEntry)) continue;
@@ -24429,7 +24785,10 @@ function reasonixHooksToCanonical(hooks) {
24429
24785
  if (typeof entry.timeout === "number") def.timeout = entry.timeout / 1e3;
24430
24786
  defs.push(def);
24431
24787
  }
24432
- if (defs.length > 0) canonical[canonicalEvent] = [...canonical[canonicalEvent] ?? [], ...defs];
24788
+ if (defs.length > 0) canonical[canonicalEvent] = [...lookupOwn({
24789
+ record: canonical,
24790
+ key: canonicalEvent
24791
+ }) ?? [], ...defs];
24433
24792
  }
24434
24793
  return canonical;
24435
24794
  }
@@ -24557,7 +24916,10 @@ function canonicalToVibeHooks(config, toolOverride) {
24557
24916
  const hooks = [];
24558
24917
  for (const [event, defs] of Object.entries(effective)) {
24559
24918
  if (!SUPPORTED_VIBE_EVENTS.has(event)) continue;
24560
- const vibeEvent = CANONICAL_TO_VIBE_EVENT_NAMES[event] ?? event;
24919
+ const vibeEvent = lookupOwn({
24920
+ record: CANONICAL_TO_VIBE_EVENT_NAMES,
24921
+ key: event
24922
+ }) ?? event;
24561
24923
  let index = 0;
24562
24924
  for (const def of defs) {
24563
24925
  if ((def.type ?? "command") !== "command") continue;
@@ -24587,7 +24949,10 @@ function vibeEntryToCanonicalDef(raw) {
24587
24949
  const vibeEvent = typeof entry.type === "string" ? entry.type : void 0;
24588
24950
  if (vibeEvent === void 0) return null;
24589
24951
  if (isPrototypePollutionKey(vibeEvent)) return null;
24590
- const canonicalEvent = VIBE_TO_CANONICAL_EVENT_NAMES[vibeEvent] ?? vibeEvent;
24952
+ const canonicalEvent = lookupOwn({
24953
+ record: VIBE_TO_CANONICAL_EVENT_NAMES,
24954
+ key: vibeEvent
24955
+ }) ?? vibeEvent;
24591
24956
  const def = { type: "command" };
24592
24957
  if (typeof entry.command === "string") def.command = entry.command;
24593
24958
  if (typeof entry.match === "string" && entry.match !== "" && entry.match !== "*") def.matcher = entry.match;
@@ -24612,7 +24977,10 @@ function vibeHooksToCanonical(parsed) {
24612
24977
  for (const raw of rawHooks) {
24613
24978
  const result = vibeEntryToCanonicalDef(raw);
24614
24979
  if (result === null) continue;
24615
- const list = canonical[result.canonicalEvent] ?? [];
24980
+ const list = lookupOwn({
24981
+ record: canonical,
24982
+ key: result.canonicalEvent
24983
+ }) ?? [];
24616
24984
  list.push(result.def);
24617
24985
  canonical[result.canonicalEvent] = list;
24618
24986
  }
@@ -25691,6 +26059,68 @@ var ClineIgnore = class ClineIgnore extends ToolIgnore {
25691
26059
  }
25692
26060
  };
25693
26061
  //#endregion
26062
+ //#region src/constants/crush-paths.ts
26063
+ const CRUSH_RULE_FILE_NAME = "CRUSH.md";
26064
+ const CRUSH_GLOBAL_DIR = join(".config", "crush");
26065
+ const CRUSH_IGNORE_FILE_NAME = ".crushignore";
26066
+ const CRUSH_SKILLS_PROJECT_DIR = join(".crush", "skills");
26067
+ const CRUSH_SKILLS_GLOBAL_DIR = join(CRUSH_GLOBAL_DIR, "skills");
26068
+ //#endregion
26069
+ //#region src/features/ignore/crush-ignore.ts
26070
+ /**
26071
+ * Ignore generator for Crush.
26072
+ *
26073
+ * Crush excludes files from tool access via a `.crushignore` file, read
26074
+ * hierarchically (root and any subdirectory, the same way it walks
26075
+ * `.gitignore`) using gitignore syntax. Crush documents no global/user-scope
26076
+ * ignore file, so this is project-only.
26077
+ * @see https://github.com/charmbracelet/crush/blob/main/internal/fsext/fileutil.go
26078
+ */
26079
+ var CrushIgnore = class CrushIgnore extends ToolIgnore {
26080
+ static getSettablePaths() {
26081
+ return {
26082
+ relativeDirPath: ".",
26083
+ relativeFilePath: CRUSH_IGNORE_FILE_NAME
26084
+ };
26085
+ }
26086
+ toRulesyncIgnore() {
26087
+ return new RulesyncIgnore({
26088
+ outputRoot: ".",
26089
+ relativeDirPath: ".",
26090
+ relativeFilePath: RULESYNC_AIIGNORE_RELATIVE_FILE_PATH,
26091
+ fileContent: this.fileContent
26092
+ });
26093
+ }
26094
+ static fromRulesyncIgnore({ outputRoot = process.cwd(), rulesyncIgnore }) {
26095
+ const body = rulesyncIgnore.getFileContent();
26096
+ return new CrushIgnore({
26097
+ outputRoot,
26098
+ relativeDirPath: this.getSettablePaths().relativeDirPath,
26099
+ relativeFilePath: this.getSettablePaths().relativeFilePath,
26100
+ fileContent: body
26101
+ });
26102
+ }
26103
+ static async fromFile({ outputRoot = process.cwd(), validate = true }) {
26104
+ const fileContent = await readFileContent(join(outputRoot, this.getSettablePaths().relativeDirPath, this.getSettablePaths().relativeFilePath));
26105
+ return new CrushIgnore({
26106
+ outputRoot,
26107
+ relativeDirPath: this.getSettablePaths().relativeDirPath,
26108
+ relativeFilePath: this.getSettablePaths().relativeFilePath,
26109
+ fileContent,
26110
+ validate
26111
+ });
26112
+ }
26113
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
26114
+ return new CrushIgnore({
26115
+ outputRoot,
26116
+ relativeDirPath,
26117
+ relativeFilePath,
26118
+ fileContent: "",
26119
+ validate: false
26120
+ });
26121
+ }
26122
+ };
26123
+ //#endregion
25694
26124
  //#region src/features/ignore/cursor-ignore.ts
25695
26125
  /**
25696
26126
  * Cursor ignore adapter.
@@ -26687,6 +27117,7 @@ const toolIgnoreFactories = /* @__PURE__ */ new Map([
26687
27117
  ["claudecode", { class: ClaudecodeIgnore }],
26688
27118
  ["claudecode-legacy", { class: ClaudecodeIgnore }],
26689
27119
  ["cline", { class: ClineIgnore }],
27120
+ ["crush", { class: CrushIgnore }],
26690
27121
  ["cursor", { class: CursorIgnore }],
26691
27122
  ["hermesagent", { class: HermesagentIgnore }],
26692
27123
  ["junie", { class: JunieIgnore }],
@@ -27991,9 +28422,8 @@ var CodexcliMcp = class CodexcliMcp extends ToolMcp {
27991
28422
  throw new Error(`Failed to parse existing Codex CLI config at ${configTomlFilePath}: ${formatError(error)}`, { cause: error });
27992
28423
  }
27993
28424
  const strippedMcpServers = rulesyncMcp.getMcpServers();
27994
- const rawMcpServers = rulesyncMcp.getJson().mcpServers;
27995
28425
  const converted = convertToCodexFormat(Object.fromEntries(Object.entries(strippedMcpServers).map(([serverName, serverConfig]) => {
27996
- const rawServer = isRecord$1(rawMcpServers) ? rawMcpServers[serverName] : void 0;
28426
+ const rawServer = rulesyncMcp.getRawMcpServer(serverName);
27997
28427
  return [serverName, {
27998
28428
  ...serverConfig,
27999
28429
  ...isRecord$1(rawServer) && isEnvVarEntryArray(rawServer.envVars) ? { envVars: rawServer.envVars } : {},
@@ -30956,9 +31386,8 @@ var MusecodeMcp = class MusecodeMcp extends ToolMcp {
30956
31386
  const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
30957
31387
  const existingContent = await readFileContentOrNull(filePath) ?? "";
30958
31388
  const existing = parseMusecodeSettings(existingContent, filePath);
30959
- const rawMcpServers = rulesyncMcp.getJson().mcpServers;
30960
31389
  const converted = convertToMusecodeFormat(Object.fromEntries(Object.entries(rulesyncMcp.getMcpServers()).map(([serverName, serverConfig]) => {
30961
- const rawServer = isRecord$1(rawMcpServers) ? rawMcpServers[serverName] : void 0;
31390
+ const rawServer = rulesyncMcp.getRawMcpServer(serverName);
30962
31391
  const mode = asMusecodeMode(isRecord$1(rawServer) ? rawServer.musecodeMode : void 0);
30963
31392
  return [serverName, {
30964
31393
  ...serverConfig,
@@ -31907,6 +32336,66 @@ async function readRovodevConfigYaml({ outputRoot }) {
31907
32336
  filePath: join(ROVODEV_DIR, ROVODEV_CONFIG_FILE_NAME)
31908
32337
  });
31909
32338
  }
32339
+ /**
32340
+ * Decide the absolute path a configured `mcpConfigPath` would name in the
32341
+ * given scope, without yet checking whether that path stays inside it. Split
32342
+ * out of resolveRovodevMcpImportPath so each function's branching stays
32343
+ * within the project's complexity budget.
32344
+ */
32345
+ function resolveMcpConfigCandidatePath({ outputRoot, global, normalizedPath, configuredPath }) {
32346
+ if (global && normalizedPath.startsWith("~/")) return { path: resolve(outputRoot, normalizedPath.slice(2)) };
32347
+ if (!global && normalizedPath.startsWith("~/")) return { rejectionMessage: `Rovo Dev MCP: mcp.mcpConfigPath is ${quoteValueForWarning(configuredPath)} in project scope. A home-anchored path cannot be imported as part of a project, so importing ${join(ROVODEV_DIR, ROVODEV_MCP_FILE_NAME)} instead.` };
32348
+ if (isAbsolute(normalizedPath)) return { path: resolve(normalizedPath) };
32349
+ if (!global) return { path: resolve(outputRoot, normalizedPath) };
32350
+ return { rejectionMessage: `Rovo Dev MCP: mcp.mcpConfigPath is ${quoteValueForWarning(configuredPath)} in global scope. Only home-anchored or absolute paths can be imported safely, so importing ${join(ROVODEV_DIR, ROVODEV_MCP_FILE_NAME)} instead.` };
32351
+ }
32352
+ /**
32353
+ * Resolve the active Rovo Dev MCP config without following a pointer outside
32354
+ * the import scope. The implementation is deliberately separate from
32355
+ * fromFile: it keeps path-policy decisions testable without changing the
32356
+ * public ToolMcp contract.
32357
+ */
32358
+ async function resolveRovodevMcpImportPath({ outputRoot, global, config, logger }) {
32359
+ const fallback = {
32360
+ filePath: join(outputRoot, ROVODEV_DIR, ROVODEV_MCP_FILE_NAME),
32361
+ relativeDirPath: ROVODEV_DIR,
32362
+ relativeFilePath: ROVODEV_MCP_FILE_NAME
32363
+ };
32364
+ const configuredPath = (config && isRecord$1(config.mcp) ? config.mcp : {}).mcpConfigPath;
32365
+ if (configuredPath === void 0) {
32366
+ logger?.warn(`Rovo Dev MCP: mcp.mcpConfigPath is unset in ${join(ROVODEV_DIR, ROVODEV_CONFIG_FILE_NAME)}. Importing ${join(ROVODEV_DIR, ROVODEV_MCP_FILE_NAME)}, which may not be the file Rovo Dev reads.`);
32367
+ return fallback;
32368
+ }
32369
+ if (typeof configuredPath !== "string" || configuredPath.trim() === "") {
32370
+ logger?.warn(`Rovo Dev MCP: mcp.mcpConfigPath in ${join(ROVODEV_DIR, ROVODEV_CONFIG_FILE_NAME)} must be a non-empty string. Importing ${join(ROVODEV_DIR, ROVODEV_MCP_FILE_NAME)} instead.`);
32371
+ return fallback;
32372
+ }
32373
+ const normalizedPath = normalizeMcpConfigPathValue(configuredPath.trim());
32374
+ const candidateResult = resolveMcpConfigCandidatePath({
32375
+ outputRoot,
32376
+ global,
32377
+ normalizedPath,
32378
+ configuredPath
32379
+ });
32380
+ if ("rejectionMessage" in candidateResult) {
32381
+ logger?.warn(candidateResult.rejectionMessage);
32382
+ return fallback;
32383
+ }
32384
+ const candidatePath = candidateResult.path;
32385
+ const relativePath = relative(resolve(outputRoot), candidatePath);
32386
+ if (relativePath === "" || splitPathSegments(normalizedPath).includes("..") || pathEscapesRoot(relativePath) || await resolvedPathEscapesRoot({
32387
+ rootPath: outputRoot,
32388
+ targetPath: candidatePath
32389
+ })) {
32390
+ logger?.warn(`Rovo Dev MCP: mcp.mcpConfigPath is ${quoteValueForWarning(configuredPath)}, which is outside the import scope or traverses a symbolic link. Importing ${join(ROVODEV_DIR, ROVODEV_MCP_FILE_NAME)} instead.`);
32391
+ return fallback;
32392
+ }
32393
+ return {
32394
+ filePath: candidatePath,
32395
+ relativeDirPath: dirname(relativePath),
32396
+ relativeFilePath: basename(relativePath)
32397
+ };
32398
+ }
31910
32399
  function disabledNamesOf(config) {
31911
32400
  const mcpBlock = config && isRecord$1(config.mcp) ? config.mcp : {};
31912
32401
  return isStringArray$2(mcpBlock.disabledMcpServers) ? mcpBlock.disabledMcpServers : [];
@@ -32012,6 +32501,25 @@ function envVarMcpFileSpellings({ fileName }) {
32012
32501
  return [`$HOME/${tail}`, `\${HOME}/${tail}`];
32013
32502
  }
32014
32503
  /**
32504
+ * Classify the existing `mcpConfigPath` without deciding how to report it.
32505
+ * Keep the known-file checks in this order: the generated file is valid in
32506
+ * either scope, while the documented default and environment-variable
32507
+ * spellings are global-only alternatives that need their own warnings.
32508
+ */
32509
+ function classifyExistingPointer({ existing, global, outputRoot }) {
32510
+ if (existing === void 0) return { kind: "unset" };
32511
+ const normalized = typeof existing === "string" ? normalizeMcpConfigPathValue(existing) : void 0;
32512
+ const namesFile = (fileName) => normalized !== void 0 && mcpFileSpellings({
32513
+ fileName,
32514
+ global,
32515
+ outputRoot
32516
+ }).includes(normalized);
32517
+ if (namesFile("mcp.json")) return { kind: "already-generated" };
32518
+ if (global && namesFile(ROVODEV_ALTERNATE_MCP_FILE_NAME)) return { kind: "documented-default" };
32519
+ if (global && normalized !== void 0 && envVarMcpFileSpellings({ fileName: "mcp.json" }).includes(normalized)) return { kind: "env-var-spelling" };
32520
+ return { kind: "unrelated" };
32521
+ }
32522
+ /**
32015
32523
  * Point `mcp.mcpConfigPath` at the `mcp.json` rulesync writes for this scope,
32016
32524
  * and report whether the block gained a value it did not already carry.
32017
32525
  *
@@ -32122,45 +32630,44 @@ function announcePointer({ global, logger }) {
32122
32630
  async function applyMcpConfigPointer({ existingMcp, global, hasLiveServers, outputRoot, logger }) {
32123
32631
  const { pointer, configLabel, mcpLabel } = pointerLabels(global);
32124
32632
  const existing = existingMcp.mcpConfigPath;
32125
- const normalizedExisting = typeof existing === "string" ? normalizeMcpConfigPathValue(existing) : void 0;
32126
- const namesFile = (fileName) => normalizedExisting !== void 0 && mcpFileSpellings({
32127
- fileName,
32633
+ const classification = classifyExistingPointer({
32634
+ existing,
32128
32635
  global,
32129
32636
  outputRoot
32130
- }).includes(normalizedExisting);
32131
- const pointsAtGeneratedFile = namesFile(ROVODEV_MCP_FILE_NAME);
32637
+ });
32132
32638
  if (!hasLiveServers) {
32133
- if (pointsAtGeneratedFile) logger?.warn(`Rovo Dev MCP: mcp.mcpConfigPath in ${configLabel} points at ${mcpLabel}, which now has no enabled server. Rovo Dev reads MCP servers from that file and nowhere else, so ${global ? "Rovo Dev has" : "this project has"} no MCP servers at all until one targeting rovodev is added back — remove the mcp.mcpConfigPath line to fall back to ${global ? "Rovo Dev's own default" : "the global config"}.`);
32639
+ if (classification.kind === "already-generated") logger?.warn(`Rovo Dev MCP: mcp.mcpConfigPath in ${configLabel} points at ${mcpLabel}, which now has no enabled server. Rovo Dev reads MCP servers from that file and nowhere else, so ${global ? "Rovo Dev has" : "this project has"} no MCP servers at all until one targeting rovodev is added back — remove the mcp.mcpConfigPath line to fall back to ${global ? "Rovo Dev's own default" : "the global config"}.`);
32134
32640
  return false;
32135
32641
  }
32136
- if (existing === void 0) {
32137
- const displaced = global ? await describeDisplacedGlobalServers({ outputRoot }) : null;
32138
- if (displaced !== null) {
32139
- logger?.warn(`Rovo Dev MCP: leaving mcp.mcpConfigPath unset in ${configLabel}, because ${displaced}. Atlassian documents that path and ${mcpLabel} as two different defaults for the setting, and mcpConfigPath names one config rather than merging, so pointing it at ${mcpLabel} would stop those servers being read on every project. Move the ones you want to keep into .rulesync/mcp.jsonc — or none at all, if it turns out to hold nothing you need — then set mcp.mcpConfigPath to "${pointer}" yourself, which rulesync will not overwrite.`);
32140
- return false;
32642
+ switch (classification.kind) {
32643
+ case "unset": {
32644
+ const displaced = global ? await describeDisplacedGlobalServers({ outputRoot }) : null;
32645
+ if (displaced !== null) {
32646
+ logger?.warn(`Rovo Dev MCP: leaving mcp.mcpConfigPath unset in ${configLabel}, because ${displaced}. Atlassian documents that path and ${mcpLabel} as two different defaults for the setting, and mcpConfigPath names one config rather than merging, so pointing it at ${mcpLabel} would stop those servers being read on every project. Move the ones you want to keep into .rulesync/mcp.jsonc — or none at all, if it turns out to hold nothing you need — then set mcp.mcpConfigPath to "${pointer}" yourself, which rulesync will not overwrite.`);
32647
+ return false;
32648
+ }
32649
+ existingMcp.mcpConfigPath = pointer;
32650
+ announcePointer({
32651
+ global,
32652
+ logger
32653
+ });
32654
+ return true;
32141
32655
  }
32142
- existingMcp.mcpConfigPath = pointer;
32143
- announcePointer({
32144
- global,
32145
- logger
32146
- });
32147
- return true;
32148
- }
32149
- if (pointsAtGeneratedFile) return false;
32150
- if (global && namesFile(ROVODEV_ALTERNATE_MCP_FILE_NAME)) {
32151
- await warnAtDocumentedDefault({
32152
- existing,
32153
- outputRoot,
32154
- logger
32155
- });
32156
- return false;
32157
- }
32158
- if (global && normalizedExisting !== void 0 && envVarMcpFileSpellings({ fileName: "mcp.json" }).includes(normalizedExisting)) {
32159
- logger?.warn(`Rovo Dev MCP: mcp.mcpConfigPath in ${configLabel} is ${quoteValueForWarning(existing)}. That names ${mcpLabel} only if Rovo Dev expands environment variables in this setting, which Atlassian does not document — if it does not, the path resolves literally and Rovo Dev reads no MCP servers at all. Write "${pointer}" instead, the form its own documented default uses.`);
32160
- return false;
32656
+ case "already-generated": return false;
32657
+ case "documented-default":
32658
+ await warnAtDocumentedDefault({
32659
+ existing,
32660
+ outputRoot,
32661
+ logger
32662
+ });
32663
+ return false;
32664
+ case "env-var-spelling":
32665
+ logger?.warn(`Rovo Dev MCP: mcp.mcpConfigPath in ${configLabel} is ${quoteValueForWarning(existing)}. That names ${mcpLabel} only if Rovo Dev expands environment variables in this setting, which Atlassian does not document — if it does not, the path resolves literally and Rovo Dev reads no MCP servers at all. Write "${pointer}" instead, the form its own documented default uses.`);
32666
+ return false;
32667
+ case "unrelated":
32668
+ logger?.warn(`Rovo Dev MCP: leaving mcp.mcpConfigPath as ${quoteValueForWarning(existing)} in ${configLabel}. Rovo Dev reads MCP servers from that path, so the generated ${mcpLabel} is unused until it is set to "${pointer}".`);
32669
+ return false;
32161
32670
  }
32162
- logger?.warn(`Rovo Dev MCP: leaving mcp.mcpConfigPath as ${quoteValueForWarning(existing)} in ${configLabel}. Rovo Dev reads MCP servers from that path, so the generated ${mcpLabel} is unused until it is set to "${pointer}".`);
32163
- return false;
32164
32671
  }
32165
32672
  /**
32166
32673
  * Auxiliary writer for the `mcp:` block of `.rovodev/config.yml` (project) /
@@ -32202,14 +32709,20 @@ var RovodevMcp = class RovodevMcp extends ToolMcp {
32202
32709
  relativeFilePath: ROVODEV_MCP_FILE_NAME
32203
32710
  };
32204
32711
  }
32205
- static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
32206
- const paths = this.getSettablePaths({ global });
32207
- const json = parseRovodevMcpJson(await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"mcpServers\":{}}", paths.relativeDirPath, paths.relativeFilePath);
32712
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false, logger }) {
32713
+ const rovodevConfig = await readRovodevConfigYaml({ outputRoot });
32714
+ const paths = await resolveRovodevMcpImportPath({
32715
+ outputRoot,
32716
+ global,
32717
+ config: rovodevConfig,
32718
+ logger
32719
+ });
32720
+ const json = parseRovodevMcpJson(await readFileContentOrNull(paths.filePath) ?? "{\"mcpServers\":{}}", paths.relativeDirPath, paths.relativeFilePath);
32208
32721
  const newJson = {
32209
32722
  ...json,
32210
32723
  mcpServers: json.mcpServers ?? {}
32211
32724
  };
32212
- const disabledNames = disabledNamesOf(await readRovodevConfigYaml({ outputRoot }));
32725
+ const disabledNames = disabledNamesOf(rovodevConfig);
32213
32726
  if (disabledNames.length > 0 && isMcpServers(newJson.mcpServers)) {
32214
32727
  const servers = newJson.mcpServers;
32215
32728
  for (const name of disabledNames) {
@@ -32239,9 +32752,8 @@ var RovodevMcp = class RovodevMcp extends ToolMcp {
32239
32752
  } catch {
32240
32753
  canWriteDisableToggle = false;
32241
32754
  }
32242
- const rawMcpServers = rulesyncMcp.getJson().mcpServers;
32243
32755
  const mcpServers = Object.fromEntries(Object.entries(rulesyncMcp.getMcpServers()).map(([name, server]) => {
32244
- const rawServer = isRecord$1(rawMcpServers) && Object.hasOwn(rawMcpServers, name) ? rawMcpServers[name] : void 0;
32756
+ const rawServer = rulesyncMcp.getRawMcpServer(name);
32245
32757
  const record = {
32246
32758
  ...server,
32247
32759
  ...readEnableInstructions(rawServer) && { rovodevEnableInstructions: true }
@@ -34229,6 +34741,518 @@ function convertAmpToRulesync({ disable, permissions }) {
34229
34741
  return { permission };
34230
34742
  }
34231
34743
  //#endregion
34744
+ //#region src/utils/glob.ts
34745
+ /**
34746
+ * Convert a glob-like pattern into an anchored regex source string.
34747
+ *
34748
+ * Only `*` (any run of characters) and `?` (one character) carry meaning;
34749
+ * every other regex metacharacter is escaped so it matches literally. The
34750
+ * result is anchored at both ends, because the callers ask "is this the whole
34751
+ * name?" rather than "does this appear somewhere in it?".
34752
+ *
34753
+ * Note that `[` and `]` are escaped along with everything else, so a bracket
34754
+ * class is a literal here while `matchesGlob` below reads it as a class. The
34755
+ * one caller wants exactly that: AugmentCode writes this source into its own
34756
+ * config as the tool's own shell-command regex, and never executes it, so it
34757
+ * has to say what the tool would read rather than what a glob means. Use
34758
+ * `matchesGlob` for an actual comparison.
34759
+ */
34760
+ function globToAnchoredRegexSource(glob) {
34761
+ let source = "";
34762
+ for (const char of glob) if (char === "*") source += ".*";
34763
+ else if (char === "?") source += ".";
34764
+ else if (/[\\^$.|+(){}[\]]/.test(char)) source += `\\${char}`;
34765
+ else source += char;
34766
+ return `^${source}$`;
34767
+ }
34768
+ /**
34769
+ * Read a `[...]` class body starting just past the `[`, or `undefined` when the
34770
+ * bracket is never closed — in which case it is an ordinary character.
34771
+ */
34772
+ function parseGlobClass(characters, start) {
34773
+ let index = start;
34774
+ const negated = characters[index] === "!" || characters[index] === "^";
34775
+ if (negated) index += 1;
34776
+ const members = /* @__PURE__ */ new Set();
34777
+ const ranges = [];
34778
+ let first = true;
34779
+ while (index < characters.length) {
34780
+ const character = characters[index] ?? "";
34781
+ if (character === "]" && !first) return {
34782
+ step: {
34783
+ kind: "class",
34784
+ negated,
34785
+ members,
34786
+ ranges
34787
+ },
34788
+ next: index + 1
34789
+ };
34790
+ first = false;
34791
+ const high = characters[index + 2];
34792
+ if (characters[index + 1] === "-" && high !== void 0 && high !== "]") {
34793
+ ranges.push([character.codePointAt(0) ?? 0, high.codePointAt(0) ?? 0]);
34794
+ index += 3;
34795
+ continue;
34796
+ }
34797
+ members.add(character);
34798
+ index += 1;
34799
+ }
34800
+ }
34801
+ /** Split a glob into the steps `matchesGlob` walks. */
34802
+ function parseGlob(glob) {
34803
+ const characters = [...glob];
34804
+ const steps = [];
34805
+ let index = 0;
34806
+ let bracketsAreClosed = true;
34807
+ while (index < characters.length) {
34808
+ const character = characters[index] ?? "";
34809
+ index += 1;
34810
+ if (character === "*") {
34811
+ if (steps.at(-1)?.kind !== "star") steps.push({ kind: "star" });
34812
+ continue;
34813
+ }
34814
+ if (character === "?") {
34815
+ steps.push({ kind: "any" });
34816
+ continue;
34817
+ }
34818
+ if (character === "[" && bracketsAreClosed) {
34819
+ const parsed = parseGlobClass(characters, index);
34820
+ if (parsed === void 0) bracketsAreClosed = false;
34821
+ else {
34822
+ steps.push(parsed.step);
34823
+ index = parsed.next;
34824
+ continue;
34825
+ }
34826
+ }
34827
+ steps.push({
34828
+ kind: "literal",
34829
+ character
34830
+ });
34831
+ }
34832
+ return steps;
34833
+ }
34834
+ function matchesGlobStep(step, character) {
34835
+ if (step.kind === "star") return false;
34836
+ if (step.kind === "any") return true;
34837
+ if (step.kind === "literal") return step.character === character;
34838
+ const code = character.codePointAt(0) ?? 0;
34839
+ const admitted = step.members.has(character) || step.ranges.some(([low, high]) => code >= low && code <= high);
34840
+ return step.negated ? !admitted : admitted;
34841
+ }
34842
+ /** Whether two single-character steps can both match one same character. */
34843
+ function stepsShareACharacter(left, right) {
34844
+ if (left.kind === "any" || right.kind === "any") return true;
34845
+ if (left.kind === "literal" && right.kind === "literal") return left.character === right.character;
34846
+ if (left.kind === "literal") return matchesGlobStep(right, left.character);
34847
+ if (right.kind === "literal") return matchesGlobStep(left, right.character);
34848
+ return true;
34849
+ }
34850
+ /** Whether every step from `index` on can match the empty string. */
34851
+ function isAllStars(steps, index) {
34852
+ for (let step = index; step < steps.length; step++) if (steps[step]?.kind !== "star") return false;
34853
+ return true;
34854
+ }
34855
+ /**
34856
+ * The most work one intersection walk will do, counted in cells times the cost
34857
+ * of one. Past it the two patterns are reported as intersecting without being
34858
+ * walked: the product of two lengths grows quadratically, and a pattern long
34859
+ * enough to reach this is pathological rather than a command anybody typed.
34860
+ * Answering `true` withholds an `allow`, which is the direction that fails
34861
+ * closed.
34862
+ */
34863
+ const MAX_INTERSECTION_CELLS = 1e6;
34864
+ /**
34865
+ * The most work a whole run of comparisons will do. A caller holding R
34866
+ * restrictions and A allow rules asks R x A times, and a per-pair cap alone
34867
+ * bounds none of that: a hundred restrictions against a hundred allow rules,
34868
+ * each pattern just under the per-pair cap, is ten thousand affordable walks
34869
+ * that together take minutes. The shared budget is spent down across the run
34870
+ * and, once it is gone, every remaining pair is reported as intersecting —
34871
+ * again the direction that withholds an `allow` rather than writing one.
34872
+ */
34873
+ const MAX_TOTAL_INTERSECTION_CELLS = 1e7;
34874
+ /**
34875
+ * What a pair costs on top of the cells it walks: the call itself, sizing and
34876
+ * filling the two rows the table is held in, and collecting the answer.
34877
+ * Charging only cells would leave the *number* of pairs unbounded — a pair of
34878
+ * one-step patterns walks a single cell, so n short restrictions against n
34879
+ * short allow rules is n squared comparisons that never spend the budget down
34880
+ * however many of them there are. Charging a floor per pair puts pair count and
34881
+ * walk length on the same exhaustible resource.
34882
+ *
34883
+ * For the short patterns of an ordinary config the floor is the whole charge,
34884
+ * which lowers how many pairs a run compares from around a million to about
34885
+ * 150,000 — roughly 400 restrictions against 400 allow rules. A config past
34886
+ * that line withholds every allow it has not yet compared, the same fail-closed
34887
+ * answer exhaustion gives everywhere else.
34888
+ */
34889
+ const INTERSECTION_PAIR_COST = 64;
34890
+ /**
34891
+ * A budget for one caller's run of comparisons. Hand the same one to every
34892
+ * `parsedGlobsIntersect` call that belongs together — one adapter reading one
34893
+ * config — so the run as a whole stays bounded rather than only each pair in
34894
+ * it.
34895
+ */
34896
+ function createIntersectionBudget(remaining = MAX_TOTAL_INTERSECTION_CELLS) {
34897
+ return { remaining };
34898
+ }
34899
+ /**
34900
+ * Parse `glob` into the form `parsedGlobsIntersect` walks. A caller comparing
34901
+ * the same pattern against a whole list parses it once and reuses the result.
34902
+ */
34903
+ function parseGlobPattern(glob) {
34904
+ const steps = parseGlob(glob);
34905
+ return {
34906
+ steps,
34907
+ maxRanges: maxRangeCount(steps)
34908
+ };
34909
+ }
34910
+ /**
34911
+ * What one cell can cost, as a multiplier on the cell count. A literal met by a
34912
+ * `[a-z...]` class walks that class's ranges, so a single class carrying
34913
+ * thousands of them turns a walk that looks affordable by cell count alone into
34914
+ * a quadratic one — which is why the budget is spent on cells times this rather
34915
+ * than on cells.
34916
+ */
34917
+ function maxRangeCount(steps) {
34918
+ let most = 0;
34919
+ for (const step of steps) if (step.kind === "class" && step.ranges.length > most) most = step.ranges.length;
34920
+ return most;
34921
+ }
34922
+ /**
34923
+ * `globsIntersect` for two globs already parsed, optionally spending a budget
34924
+ * shared with the rest of the caller's run — see `createIntersectionBudget`.
34925
+ * Once that budget is exhausted every further pair answers `true` without being
34926
+ * walked, so a caller reading the answer as a reason to restrict stays on the
34927
+ * safe side.
34928
+ */
34929
+ function parsedGlobsIntersect(left, right, budget) {
34930
+ const [rows, columns] = left.steps.length >= right.steps.length ? [left.steps, right.steps] : [right.steps, left.steps];
34931
+ const cellCost = 1 + left.maxRanges + right.maxRanges;
34932
+ const cost = rows.length * columns.length * cellCost;
34933
+ if (cost > MAX_INTERSECTION_CELLS) return true;
34934
+ if (budget !== void 0) {
34935
+ const charge = cost + INTERSECTION_PAIR_COST;
34936
+ if (charge > budget.remaining) {
34937
+ budget.remaining = 0;
34938
+ return true;
34939
+ }
34940
+ budget.remaining -= charge;
34941
+ }
34942
+ let next = Array.from({ length: columns.length + 1 }, (_, j) => isAllStars(columns, j));
34943
+ for (let i = rows.length - 1; i >= 0; i--) {
34944
+ const row = Array.from({ length: columns.length + 1 }, () => false);
34945
+ row[columns.length] = isAllStars(rows, i);
34946
+ for (let j = columns.length - 1; j >= 0; j--) {
34947
+ const rowStep = rows[i];
34948
+ const columnStep = columns[j];
34949
+ if (rowStep === void 0 || columnStep === void 0) continue;
34950
+ if (rowStep.kind === "star" || columnStep.kind === "star") {
34951
+ row[j] = (next[j] ?? false) || (row[j + 1] ?? false);
34952
+ continue;
34953
+ }
34954
+ row[j] = stepsShareACharacter(rowStep, columnStep) && (next[j + 1] ?? false);
34955
+ }
34956
+ next = row;
34957
+ }
34958
+ return next[0] ?? false;
34959
+ }
34960
+ //#endregion
34961
+ //#region src/features/permissions/shell-command-categories.ts
34962
+ /** The canonical category that names a shell command's permissions. */
34963
+ const SHELL_PERMISSION_CATEGORY = "bash";
34964
+ /**
34965
+ * Collect the canonical rules that govern shell commands, for the adapters
34966
+ * whose tool models commands and nothing else.
34967
+ *
34968
+ * The `bash` category contributes every rule. The all-tools `*` category
34969
+ * contributes its **restricting** rules — `deny` and `ask` — because a rule
34970
+ * written there covers shell commands too, and dropping it inverts the
34971
+ * author's intent: with `{"*": {"rm *": "deny"}, "bash": {"rm *": "allow"}}`,
34972
+ * an adapter that reads only `bash` auto-approves the very command the file
34973
+ * denies.
34974
+ *
34975
+ * Its `allow` rules are deliberately **not** contributed. A pattern under `*`
34976
+ * need not be a command at all — `secrets/**` under `*` denies a path — and
34977
+ * carrying it in the restricting direction only over-restricts, while carrying
34978
+ * it in the permissive direction would grant something the author never said
34979
+ * about commands. Both directions therefore fail closed.
34980
+ */
34981
+ function collectShellCommandRules(permission) {
34982
+ const rules = [];
34983
+ const foreignRestrictingCategories = [];
34984
+ const ignoredAllToolsAllowPatterns = [];
34985
+ for (const [category, categoryRules] of Object.entries(permission)) {
34986
+ if (category === "bash") {
34987
+ for (const [pattern, action] of Object.entries(categoryRules)) rules.push({
34988
+ pattern,
34989
+ action,
34990
+ fromAllToolsCategory: false
34991
+ });
34992
+ continue;
34993
+ }
34994
+ if (category === "*") {
34995
+ for (const [pattern, action] of Object.entries(categoryRules)) {
34996
+ if (action === "allow") {
34997
+ ignoredAllToolsAllowPatterns.push(pattern);
34998
+ continue;
34999
+ }
35000
+ rules.push({
35001
+ pattern,
35002
+ action,
35003
+ fromAllToolsCategory: true
35004
+ });
35005
+ }
35006
+ continue;
35007
+ }
35008
+ if (Object.values(categoryRules).some((action) => action === "deny" || action === "ask")) foreignRestrictingCategories.push(category);
35009
+ }
35010
+ return {
35011
+ rules,
35012
+ foreignRestrictingCategories,
35013
+ ignoredAllToolsAllowPatterns
35014
+ };
35015
+ }
35016
+ /**
35017
+ * Build the test an adapter applies to an `allow` pattern before writing it:
35018
+ * which restrictions it cannot write name some of the same commands? The
35019
+ * answer is the list of those restrictions — empty when the `allow` may be
35020
+ * written — so a caller can report both the allow rules it withheld and the
35021
+ * restrictions that withheld nothing.
35022
+ *
35023
+ * Canonically the stricter rule wins **whatever its width** — rulesync collapses
35024
+ * colliding rules as `deny > ask > allow` — so the two patterns are compared by
35025
+ * asking whether any one command matches both. Width does not enter into it: an
35026
+ * `ask` on `*` overlaps an allowed `git *`, an `ask` on `npm publish` overlaps
35027
+ * an allowed `npm *`, and an `ask` on `* --force` overlaps an allowed `git *`
35028
+ * on every `git ... --force` command even though neither pattern covers the
35029
+ * other's spelling. Comparing only identical spellings would let the most
35030
+ * ordinary catch-all (`{"*": {"*": "ask"}}`) disappear without a word.
35031
+ *
35032
+ * Identical spellings are still compared as strings first, as a shortcut past
35033
+ * the walk for the commonest case.
35034
+ *
35035
+ * `normalizePattern` rewrites a pattern written in the tool's own language into
35036
+ * the widest glob it could stand for, for a tool whose patterns are not globs.
35037
+ * It reaches the `bash` rules and the `allow` rules, which is where such a
35038
+ * pattern is written; an all-tools `*` pattern is canonical — it is read by
35039
+ * every tool, so it is a glob already — and is compared as it stands. The
35040
+ * rewrite must only ever widen what a pattern covers, so an inexact reading
35041
+ * withholds an allow rather than writing one the config restricts — see
35042
+ * `warpCommandPatternToGlob`.
35043
+ */
35044
+ function createShadowingRestrictionsTest(restrictions, { normalizePattern = (pattern) => pattern, budget = createIntersectionBudget() } = {}) {
35045
+ const normalized = restrictions.map(({ pattern, fromAllToolsCategory }) => ({
35046
+ pattern,
35047
+ glob: parseGlobPattern(fromAllToolsCategory ? pattern : normalizePattern(pattern))
35048
+ }));
35049
+ return (allowPattern) => {
35050
+ if (budget.remaining === 0) return normalized.map(({ pattern }) => pattern);
35051
+ const allowGlob = parseGlobPattern(normalizePattern(allowPattern));
35052
+ return normalized.filter(({ pattern, glob }) => pattern === allowPattern || parsedGlobsIntersect(glob, allowGlob, budget)).map(({ pattern }) => pattern);
35053
+ };
35054
+ }
35055
+ /**
35056
+ * Which of the given all-tools `*` restrictions look like they may not name a
35057
+ * command at all — the question a `deny` and an `ask` written there both raise.
35058
+ *
35059
+ * "Withheld no allow rule" alone does not answer it: a config with no `allow`
35060
+ * rules has nothing to withhold, and a pattern the author also wrote under
35061
+ * `bash` is a command on their own word. Both are excluded, so what remains is
35062
+ * a `*` pattern that had allow rules to overlap, overlapped none of them, and
35063
+ * is claimed as a command nowhere else — the shape `secrets/**` has.
35064
+ *
35065
+ * A `bash` restriction never belongs here: it names a command by construction,
35066
+ * so overlapping no allow rule says nothing is wrong with it.
35067
+ */
35068
+ function collectUnenforcedAllToolsPatterns({ rules, allToolsPatterns, withholdingPatterns }) {
35069
+ if (!rules.some(({ action }) => action === "allow")) return [];
35070
+ const shellPatterns = new Set(rules.filter(({ fromAllToolsCategory }) => !fromAllToolsCategory).map(({ pattern }) => pattern));
35071
+ return uniq(allToolsPatterns).filter((pattern) => !withholdingPatterns.has(pattern) && !shellPatterns.has(pattern));
35072
+ }
35073
+ /**
35074
+ * Split shell-command rules into the allow and deny lists of a tool that models
35075
+ * commands with those two tiers and nothing else.
35076
+ *
35077
+ * `ask` has no list of its own — such a tool already prompts for whatever it
35078
+ * does not auto-approve, so an `ask` rule is satisfied by writing nothing. It
35079
+ * still has to *withhold* the `allow` rules it covers, though: the canonical
35080
+ * order is `deny > ask > allow`, so auto-approving a command the file also asks
35081
+ * about would answer the prompt the author wanted.
35082
+ *
35083
+ * `writesAllToolsDeny` says whether the tool's denylist can carry a pattern
35084
+ * from the all-tools `*` category. Warp's cannot: it matches commands with
35085
+ * regular expressions rather than globs, and writing any denylist **replaces**
35086
+ * Warp's built-in default one, so an inert `secrets/**` entry there would trade
35087
+ * the tool's own protection for a rule that matches no command. Where the deny
35088
+ * cannot be written it withholds the allow rules it covers instead, which
35089
+ * restricts in the same direction without touching the denylist.
35090
+ *
35091
+ * A `bash` deny withholds nothing: it names a command by construction, so the
35092
+ * denylist entry enforces it wherever the tool's deny-beats-allow order applies,
35093
+ * and a narrow deny keeps carving an exception out of a wider allow (`git *`
35094
+ * allowed, `git push *` denied). An all-tools `*` deny withholds all the same,
35095
+ * even where it is written: a pattern under `*` need not name a command —
35096
+ * `secrets/**` there denies a path — so as a denylist entry it may match nothing
35097
+ * at all, and leaving an overlapping allow beside it would auto-approve the very
35098
+ * commands the author meant to stop. Over-restricting a `*` deny that *was* a
35099
+ * command pattern is reported; failing open would not be.
35100
+ *
35101
+ * `normalizePattern` is handed to `createShadowingRestrictionsTest` for a tool whose
35102
+ * patterns are not globs.
35103
+ */
35104
+ function partitionCommandRules({ rules, writesAllToolsDeny, normalizePattern }) {
35105
+ const deny = [];
35106
+ const unwrittenDenyPatterns = [];
35107
+ const restrictions = [];
35108
+ const writtenAllToolsDenyPatterns = [];
35109
+ const allToolsAskPatterns = [];
35110
+ for (const rule of rules) {
35111
+ const { pattern, action, fromAllToolsCategory } = rule;
35112
+ if (action === "allow") continue;
35113
+ if (action !== "deny") {
35114
+ restrictions.push(rule);
35115
+ if (fromAllToolsCategory) allToolsAskPatterns.push(pattern);
35116
+ continue;
35117
+ }
35118
+ if (writesAllToolsDeny || !fromAllToolsCategory) {
35119
+ deny.push(pattern);
35120
+ if (fromAllToolsCategory) writtenAllToolsDenyPatterns.push(pattern);
35121
+ } else unwrittenDenyPatterns.push(pattern);
35122
+ if (fromAllToolsCategory) restrictions.push(rule);
35123
+ }
35124
+ const budget = createIntersectionBudget();
35125
+ const shadowingRestrictions = createShadowingRestrictionsTest(restrictions, {
35126
+ normalizePattern,
35127
+ budget
35128
+ });
35129
+ const allow = [];
35130
+ const shadowedAllowPatterns = [];
35131
+ const withholdingPatterns = /* @__PURE__ */ new Set();
35132
+ for (const { pattern, action } of rules) {
35133
+ if (action !== "allow") continue;
35134
+ const shadowing = shadowingRestrictions(pattern);
35135
+ if (shadowing.length > 0) {
35136
+ shadowedAllowPatterns.push(pattern);
35137
+ for (const restriction of shadowing) withholdingPatterns.add(restriction);
35138
+ continue;
35139
+ }
35140
+ allow.push(pattern);
35141
+ }
35142
+ return {
35143
+ allow,
35144
+ deny,
35145
+ shadowedAllowPatterns,
35146
+ unwrittenDenyPatterns,
35147
+ unenforcedAllToolsDenyPatterns: collectUnenforcedAllToolsPatterns({
35148
+ rules,
35149
+ allToolsPatterns: writtenAllToolsDenyPatterns,
35150
+ withholdingPatterns
35151
+ }),
35152
+ unenforcedAllToolsAskPatterns: collectUnenforcedAllToolsPatterns({
35153
+ rules,
35154
+ allToolsPatterns: allToolsAskPatterns,
35155
+ withholdingPatterns
35156
+ }),
35157
+ intersectionBudgetExhausted: budget.remaining === 0
35158
+ };
35159
+ }
35160
+ /**
35161
+ * Report, for one command-only tool, every canonical rule its two lists could
35162
+ * not carry. Every command-only adapter shares this reporting, so a rule
35163
+ * dropped in one is worded the same way in all.
35164
+ */
35165
+ function warnAboutUnwrittenCommandRules({ toolLabel, surfaceLabel, foreignRestrictingCategories, shadowedAllowPatterns, unwrittenDenyPatterns = [], unwrittenDenyReason, unenforcedAllToolsDenyPatterns = [], unenforcedAllToolsAskPatterns = [], ignoredAllToolsAllowPatterns = [], intersectionBudgetExhausted = false, logger }) {
35166
+ if (intersectionBudgetExhausted) warnWithFallback(logger, `${toolLabel} reached the limit on how much work one generation may spend comparing .rulesync/permissions.jsonc's allow rules against its deny and ask rules, so the allow rules left over were withheld rather than compared — the safe answer, but a wider one than the file asks for. Write fewer or shorter command patterns to have them all compared.`);
35167
+ for (const category of foreignRestrictingCategories) warnWithFallback(logger, `${toolLabel} only models shell-command permissions (${surfaceLabel}); '${category}' deny and ask rules cannot be represented and were skipped.`);
35168
+ if (unwrittenDenyPatterns.length > 0) warnWithFallback(logger, `${toolLabel} did not write the all-tools '*' deny rule(s) for ${unwrittenDenyPatterns.join(", ")} into its denylist.${unwrittenDenyReason === void 0 ? "" : ` ${unwrittenDenyReason}`} They restrict only by withholding the allow rules they cover; write them under 'bash' to have them enforced as commands.`);
35169
+ if (unenforcedAllToolsDenyPatterns.length > 0) warnWithFallback(logger, `${toolLabel} wrote the all-tools '*' deny rule(s) for ${unenforcedAllToolsDenyPatterns.join(", ")} into its denylist as they stand, but they withheld none of the allow rules beside them. A pattern written under '*' need not name a command — 'secrets/**' there denies a path — and a denylist entry that names none blocks nothing; write it under 'bash' too if it is a command pattern.`);
35170
+ if (unenforcedAllToolsAskPatterns.length > 0) warnWithFallback(logger, `${toolLabel} has no ask tier (${surfaceLabel}), so the all-tools '*' ask rule(s) for ${unenforcedAllToolsAskPatterns.join(", ")} restrict only by withholding the allow rules they cover — and they covered none. A pattern written under '*' need not name a command, so nothing observed says these ones do; write them under 'bash' if they are command patterns.`);
35171
+ if (ignoredAllToolsAllowPatterns.length > 0) warnWithFallback(logger, `${toolLabel} reads the all-tools '*' category for its deny and ask rules only, so the allow rule(s) for ${ignoredAllToolsAllowPatterns.join(", ")} were skipped — a pattern written under '*' need not be a command. Write them under 'bash' to auto-approve them as commands.`);
35172
+ if (shadowedAllowPatterns.length > 0) warnWithFallback(logger, `${toolLabel} was not given the allow rule(s) for ${shadowedAllowPatterns.join(", ")} because .rulesync/permissions.jsonc restricts the same commands elsewhere, and the stricter rule wins whatever its width.`);
35173
+ }
35174
+ function resolveShellCommandState(permission, writesAllToolsDeny) {
35175
+ const { rules, foreignRestrictingCategories, ignoredAllToolsAllowPatterns } = collectShellCommandRules(permission);
35176
+ const partitioned = partitionCommandRules({
35177
+ rules,
35178
+ writesAllToolsDeny
35179
+ });
35180
+ return {
35181
+ allow: partitioned.allow,
35182
+ deny: partitioned.deny,
35183
+ bash: bashRulesHonoringAllTools(permission),
35184
+ foreignRestrictingCategories,
35185
+ ignoredAllToolsAllowPatterns,
35186
+ shadowedAllowPatterns: partitioned.shadowedAllowPatterns,
35187
+ unwrittenDenyPatterns: partitioned.unwrittenDenyPatterns,
35188
+ unenforcedAllToolsDenyPatterns: partitioned.unenforcedAllToolsDenyPatterns,
35189
+ unenforcedAllToolsAskPatterns: partitioned.unenforcedAllToolsAskPatterns,
35190
+ intersectionBudgetExhausted: partitioned.intersectionBudgetExhausted
35191
+ };
35192
+ }
35193
+ /**
35194
+ * Collect shell-command allow/deny lists the way the command-only adapters do,
35195
+ * and report every restriction the surface cannot carry.
35196
+ */
35197
+ function resolveShellCommandLists({ permission, writesAllToolsDeny, toolLabel, surfaceLabel, logger }) {
35198
+ const resolved = resolveShellCommandState(permission, writesAllToolsDeny);
35199
+ warnAboutUnwrittenCommandRules({
35200
+ toolLabel,
35201
+ surfaceLabel,
35202
+ foreignRestrictingCategories: resolved.foreignRestrictingCategories,
35203
+ shadowedAllowPatterns: resolved.shadowedAllowPatterns,
35204
+ unwrittenDenyPatterns: resolved.unwrittenDenyPatterns,
35205
+ unenforcedAllToolsDenyPatterns: resolved.unenforcedAllToolsDenyPatterns,
35206
+ unenforcedAllToolsAskPatterns: resolved.unenforcedAllToolsAskPatterns,
35207
+ ignoredAllToolsAllowPatterns: resolved.ignoredAllToolsAllowPatterns,
35208
+ intersectionBudgetExhausted: resolved.intersectionBudgetExhausted,
35209
+ logger
35210
+ });
35211
+ return {
35212
+ allow: resolved.allow,
35213
+ deny: resolved.deny,
35214
+ bash: resolved.bash
35215
+ };
35216
+ }
35217
+ /**
35218
+ * The `bash` category after all-tools `*` restrictions have been applied. A
35219
+ * `deny`/`ask` written under `*` covers shell commands too, so a bash `allow`
35220
+ * it overlaps is withheld, a `*` deny is copied in, and a `*` ask is copied in
35221
+ * wherever `bash` says nothing about that exact pattern yet — otherwise it
35222
+ * would vanish from the resolved category entirely rather than falling back to
35223
+ * a tier that still prompts. An existing `bash` entry for the same pattern is
35224
+ * never downgraded by a `*` ask (a bash `allow` was already dropped above, and
35225
+ * a bash `deny`/`ask` there is at least as strict already).
35226
+ */
35227
+ function bashRulesHonoringAllTools(permission) {
35228
+ const { rules } = collectShellCommandRules(permission);
35229
+ const allToolsRestrictions = rules.filter(({ fromAllToolsCategory }) => fromAllToolsCategory);
35230
+ const shadowingRestrictions = createShadowingRestrictionsTest(allToolsRestrictions);
35231
+ const bash = { ...permission.bash };
35232
+ for (const [pattern, action] of Object.entries(bash)) if (action === "allow" && shadowingRestrictions(pattern).length > 0) delete bash[pattern];
35233
+ for (const { pattern, action } of allToolsRestrictions) {
35234
+ if (isPrototypePollutionKey(pattern)) continue;
35235
+ if (action === "deny") {
35236
+ if (bash[pattern] !== "ask") bash[pattern] = "deny";
35237
+ continue;
35238
+ }
35239
+ if (bash[pattern] === void 0) bash[pattern] = "ask";
35240
+ }
35241
+ return bash;
35242
+ }
35243
+ /**
35244
+ * Return a permission block whose `bash` category honors all-tools `*`
35245
+ * restrictions. Other categories are unchanged, so adapters that already model
35246
+ * `*` keep doing so.
35247
+ */
35248
+ function honorAllToolsOnBash(permission) {
35249
+ if (permission.bash === void 0) return permission;
35250
+ return {
35251
+ ...permission,
35252
+ bash: bashRulesHonoringAllTools(permission)
35253
+ };
35254
+ }
35255
+ //#endregion
34232
35256
  //#region src/features/permissions/antigravity-cli-permissions.ts
34233
35257
  /**
34234
35258
  * Top-level `~/.gemini/antigravity-cli/settings.json` keys the `antigravity-cli`
@@ -34476,7 +35500,7 @@ function convertRulesyncToAntigravityCliPermissions(config) {
34476
35500
  const allow = [];
34477
35501
  const ask = [];
34478
35502
  const deny = [];
34479
- for (const [category, rules] of Object.entries(config.permission)) {
35503
+ for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) {
34480
35504
  const cliToolName = toAntigravityCliToolName(category);
34481
35505
  for (const [pattern, action] of Object.entries(rules)) {
34482
35506
  const entry = buildPermissionEntry$1(cliToolName, pattern);
@@ -34687,7 +35711,7 @@ function convertRulesyncToAntigravityIdePermissions(config) {
34687
35711
  const allow = [];
34688
35712
  const ask = [];
34689
35713
  const deny = [];
34690
- for (const [category, rules] of Object.entries(config.permission)) {
35714
+ for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) {
34691
35715
  const action = toIdeAction(category);
34692
35716
  for (const [pattern, permissionAction] of Object.entries(rules)) {
34693
35717
  const entry = buildPermissionEntry(action, pattern);
@@ -34727,223 +35751,6 @@ function convertAntigravityIdeToRulesyncPermissions(params) {
34727
35751
  return { permission };
34728
35752
  }
34729
35753
  //#endregion
34730
- //#region src/utils/glob.ts
34731
- /**
34732
- * Convert a glob-like pattern into an anchored regex source string.
34733
- *
34734
- * Only `*` (any run of characters) and `?` (one character) carry meaning;
34735
- * every other regex metacharacter is escaped so it matches literally. The
34736
- * result is anchored at both ends, because the callers ask "is this the whole
34737
- * name?" rather than "does this appear somewhere in it?".
34738
- *
34739
- * Note that `[` and `]` are escaped along with everything else, so a bracket
34740
- * class is a literal here while `matchesGlob` below reads it as a class. The
34741
- * one caller wants exactly that: AugmentCode writes this source into its own
34742
- * config as the tool's own shell-command regex, and never executes it, so it
34743
- * has to say what the tool would read rather than what a glob means. Use
34744
- * `matchesGlob` for an actual comparison.
34745
- */
34746
- function globToAnchoredRegexSource(glob) {
34747
- let source = "";
34748
- for (const char of glob) if (char === "*") source += ".*";
34749
- else if (char === "?") source += ".";
34750
- else if (/[\\^$.|+(){}[\]]/.test(char)) source += `\\${char}`;
34751
- else source += char;
34752
- return `^${source}$`;
34753
- }
34754
- /**
34755
- * Read a `[...]` class body starting just past the `[`, or `undefined` when the
34756
- * bracket is never closed — in which case it is an ordinary character.
34757
- */
34758
- function parseGlobClass(characters, start) {
34759
- let index = start;
34760
- const negated = characters[index] === "!" || characters[index] === "^";
34761
- if (negated) index += 1;
34762
- const members = /* @__PURE__ */ new Set();
34763
- const ranges = [];
34764
- let first = true;
34765
- while (index < characters.length) {
34766
- const character = characters[index] ?? "";
34767
- if (character === "]" && !first) return {
34768
- step: {
34769
- kind: "class",
34770
- negated,
34771
- members,
34772
- ranges
34773
- },
34774
- next: index + 1
34775
- };
34776
- first = false;
34777
- const high = characters[index + 2];
34778
- if (characters[index + 1] === "-" && high !== void 0 && high !== "]") {
34779
- ranges.push([character.codePointAt(0) ?? 0, high.codePointAt(0) ?? 0]);
34780
- index += 3;
34781
- continue;
34782
- }
34783
- members.add(character);
34784
- index += 1;
34785
- }
34786
- }
34787
- /** Split a glob into the steps `matchesGlob` walks. */
34788
- function parseGlob(glob) {
34789
- const characters = [...glob];
34790
- const steps = [];
34791
- let index = 0;
34792
- let bracketsAreClosed = true;
34793
- while (index < characters.length) {
34794
- const character = characters[index] ?? "";
34795
- index += 1;
34796
- if (character === "*") {
34797
- if (steps.at(-1)?.kind !== "star") steps.push({ kind: "star" });
34798
- continue;
34799
- }
34800
- if (character === "?") {
34801
- steps.push({ kind: "any" });
34802
- continue;
34803
- }
34804
- if (character === "[" && bracketsAreClosed) {
34805
- const parsed = parseGlobClass(characters, index);
34806
- if (parsed === void 0) bracketsAreClosed = false;
34807
- else {
34808
- steps.push(parsed.step);
34809
- index = parsed.next;
34810
- continue;
34811
- }
34812
- }
34813
- steps.push({
34814
- kind: "literal",
34815
- character
34816
- });
34817
- }
34818
- return steps;
34819
- }
34820
- function matchesGlobStep(step, character) {
34821
- if (step.kind === "star") return false;
34822
- if (step.kind === "any") return true;
34823
- if (step.kind === "literal") return step.character === character;
34824
- const code = character.codePointAt(0) ?? 0;
34825
- const admitted = step.members.has(character) || step.ranges.some(([low, high]) => code >= low && code <= high);
34826
- return step.negated ? !admitted : admitted;
34827
- }
34828
- /** Whether two single-character steps can both match one same character. */
34829
- function stepsShareACharacter(left, right) {
34830
- if (left.kind === "any" || right.kind === "any") return true;
34831
- if (left.kind === "literal" && right.kind === "literal") return left.character === right.character;
34832
- if (left.kind === "literal") return matchesGlobStep(right, left.character);
34833
- if (right.kind === "literal") return matchesGlobStep(left, right.character);
34834
- return true;
34835
- }
34836
- /** Whether every step from `index` on can match the empty string. */
34837
- function isAllStars(steps, index) {
34838
- for (let step = index; step < steps.length; step++) if (steps[step]?.kind !== "star") return false;
34839
- return true;
34840
- }
34841
- /**
34842
- * The most work one intersection walk will do, counted in cells times the cost
34843
- * of one. Past it the two patterns are reported as intersecting without being
34844
- * walked: the product of two lengths grows quadratically, and a pattern long
34845
- * enough to reach this is pathological rather than a command anybody typed.
34846
- * Answering `true` withholds an `allow`, which is the direction that fails
34847
- * closed.
34848
- */
34849
- const MAX_INTERSECTION_CELLS = 1e6;
34850
- /**
34851
- * The most work a whole run of comparisons will do. A caller holding R
34852
- * restrictions and A allow rules asks R x A times, and a per-pair cap alone
34853
- * bounds none of that: a hundred restrictions against a hundred allow rules,
34854
- * each pattern just under the per-pair cap, is ten thousand affordable walks
34855
- * that together take minutes. The shared budget is spent down across the run
34856
- * and, once it is gone, every remaining pair is reported as intersecting —
34857
- * again the direction that withholds an `allow` rather than writing one.
34858
- */
34859
- const MAX_TOTAL_INTERSECTION_CELLS = 1e7;
34860
- /**
34861
- * What a pair costs on top of the cells it walks: the call itself, sizing and
34862
- * filling the two rows the table is held in, and collecting the answer.
34863
- * Charging only cells would leave the *number* of pairs unbounded — a pair of
34864
- * one-step patterns walks a single cell, so n short restrictions against n
34865
- * short allow rules is n squared comparisons that never spend the budget down
34866
- * however many of them there are. Charging a floor per pair puts pair count and
34867
- * walk length on the same exhaustible resource.
34868
- *
34869
- * For the short patterns of an ordinary config the floor is the whole charge,
34870
- * which lowers how many pairs a run compares from around a million to about
34871
- * 150,000 — roughly 400 restrictions against 400 allow rules. A config past
34872
- * that line withholds every allow it has not yet compared, the same fail-closed
34873
- * answer exhaustion gives everywhere else.
34874
- */
34875
- const INTERSECTION_PAIR_COST = 64;
34876
- /**
34877
- * A budget for one caller's run of comparisons. Hand the same one to every
34878
- * `parsedGlobsIntersect` call that belongs together — one adapter reading one
34879
- * config — so the run as a whole stays bounded rather than only each pair in
34880
- * it.
34881
- */
34882
- function createIntersectionBudget(remaining = MAX_TOTAL_INTERSECTION_CELLS) {
34883
- return { remaining };
34884
- }
34885
- /**
34886
- * Parse `glob` into the form `parsedGlobsIntersect` walks. A caller comparing
34887
- * the same pattern against a whole list parses it once and reuses the result.
34888
- */
34889
- function parseGlobPattern(glob) {
34890
- const steps = parseGlob(glob);
34891
- return {
34892
- steps,
34893
- maxRanges: maxRangeCount(steps)
34894
- };
34895
- }
34896
- /**
34897
- * What one cell can cost, as a multiplier on the cell count. A literal met by a
34898
- * `[a-z...]` class walks that class's ranges, so a single class carrying
34899
- * thousands of them turns a walk that looks affordable by cell count alone into
34900
- * a quadratic one — which is why the budget is spent on cells times this rather
34901
- * than on cells.
34902
- */
34903
- function maxRangeCount(steps) {
34904
- let most = 0;
34905
- for (const step of steps) if (step.kind === "class" && step.ranges.length > most) most = step.ranges.length;
34906
- return most;
34907
- }
34908
- /**
34909
- * `globsIntersect` for two globs already parsed, optionally spending a budget
34910
- * shared with the rest of the caller's run — see `createIntersectionBudget`.
34911
- * Once that budget is exhausted every further pair answers `true` without being
34912
- * walked, so a caller reading the answer as a reason to restrict stays on the
34913
- * safe side.
34914
- */
34915
- function parsedGlobsIntersect(left, right, budget) {
34916
- const [rows, columns] = left.steps.length >= right.steps.length ? [left.steps, right.steps] : [right.steps, left.steps];
34917
- const cellCost = 1 + left.maxRanges + right.maxRanges;
34918
- const cost = rows.length * columns.length * cellCost;
34919
- if (cost > MAX_INTERSECTION_CELLS) return true;
34920
- if (budget !== void 0) {
34921
- const charge = cost + INTERSECTION_PAIR_COST;
34922
- if (charge > budget.remaining) {
34923
- budget.remaining = 0;
34924
- return true;
34925
- }
34926
- budget.remaining -= charge;
34927
- }
34928
- let next = Array.from({ length: columns.length + 1 }, (_, j) => isAllStars(columns, j));
34929
- for (let i = rows.length - 1; i >= 0; i--) {
34930
- const row = Array.from({ length: columns.length + 1 }, () => false);
34931
- row[columns.length] = isAllStars(rows, i);
34932
- for (let j = columns.length - 1; j >= 0; j--) {
34933
- const rowStep = rows[i];
34934
- const columnStep = columns[j];
34935
- if (rowStep === void 0 || columnStep === void 0) continue;
34936
- if (rowStep.kind === "star" || columnStep.kind === "star") {
34937
- row[j] = (next[j] ?? false) || (row[j + 1] ?? false);
34938
- continue;
34939
- }
34940
- row[j] = stepsShareACharacter(rowStep, columnStep) && (next[j + 1] ?? false);
34941
- }
34942
- next = row;
34943
- }
34944
- return next[0] ?? false;
34945
- }
34946
- //#endregion
34947
35754
  //#region src/features/permissions/augmentcode-permissions.ts
34948
35755
  const moduleLogger$2 = fallbackLogger;
34949
35756
  z.enum([
@@ -35195,6 +36002,7 @@ var AugmentcodePermissions = class AugmentcodePermissions extends ToolPermission
35195
36002
  const basicExistingEntries = existingEntries.filter((entry) => !isSpecialEntry(entry));
35196
36003
  const generatedKeys = new Set(generated.map((e) => `${e.toolName}|${e.shellInputRegex ?? ""}|${e.permission.type}`));
35197
36004
  const preservedBasicEntries = basicExistingEntries.filter((entry) => {
36005
+ if (entry.toolName === "*") return false;
35198
36006
  if (!MANAGED_AUGMENT_TOOL_NAMES.has(entry.toolName)) return true;
35199
36007
  if (entry.permission.type === "deny") {
35200
36008
  const key = `${entry.toolName}|${entry.shellInputRegex ?? ""}|${entry.permission.type}`;
@@ -35272,7 +36080,18 @@ var AugmentcodePermissions = class AugmentcodePermissions extends ToolPermission
35272
36080
  };
35273
36081
  function convertRulesyncToAugmentEntries({ config, logger }) {
35274
36082
  const entries = [];
35275
- for (const [category, rules] of Object.entries(config.permission)) {
36083
+ const resolvedBashRules = bashRulesHonoringAllTools(config.permission);
36084
+ const permission = config.permission.bash !== void 0 || Object.keys(resolvedBashRules).length > 0 ? {
36085
+ ...config.permission,
36086
+ bash: resolvedBashRules
36087
+ } : config.permission;
36088
+ const allToolsFailClosedType = computeAllToolsFailClosedType(config.permission["*"]);
36089
+ const categoriesWithOwnEntries = /* @__PURE__ */ new Set();
36090
+ for (const [category, rules] of Object.entries(permission)) {
36091
+ if (category === "*") {
36092
+ logger?.warn("AugmentCode permissions: the all-tools '*' category cannot be emitted as a single entry because AugmentCode has no wildcard toolName. Deny/ask rules are folded into 'launch-process' and, for any other managed tool that has no explicit rules of its own, applied as a blanket deny/ask; all-tools allow rules and tools that already define their own rules are left untouched.");
36093
+ continue;
36094
+ }
35276
36095
  const augmentToolName = toAugmentToolName(category);
35277
36096
  if (!MANAGED_AUGMENT_TOOL_NAMES.has(augmentToolName) && augmentToolName === category) logger?.warn(`AugmentCode permissions: passing through unknown tool category '${category}' as toolName.`);
35278
36097
  if (augmentToolName === "launch-process") {
@@ -35297,19 +36116,50 @@ function convertRulesyncToAugmentEntries({ config, logger }) {
35297
36116
  toolName: augmentToolName,
35298
36117
  permission: { type: "deny" }
35299
36118
  });
36119
+ categoriesWithOwnEntries.add(category);
35300
36120
  continue;
35301
36121
  }
35302
36122
  const droppedPatterns = [];
35303
- for (const [pattern, action] of Object.entries(rules)) if (pattern === "*") entries.push({
35304
- toolName: augmentToolName,
35305
- permission: { type: actionToAugmentType(action) }
35306
- });
35307
- else droppedPatterns.push(pattern);
36123
+ for (const [pattern, action] of Object.entries(rules)) if (pattern === "*") {
36124
+ entries.push({
36125
+ toolName: augmentToolName,
36126
+ permission: { type: actionToAugmentType(action) }
36127
+ });
36128
+ categoriesWithOwnEntries.add(category);
36129
+ } else droppedPatterns.push(pattern);
35308
36130
  if (droppedPatterns.length > 0) logger?.warn(`AugmentCode permissions: dropping non-wildcard patterns for category '${category}' (${droppedPatterns.join(", ")}); AugmentCode does not document a per-input matcher for this tool. Use a 'deny' rule with pattern '*' if you need to block this tool entirely.`);
35309
36131
  }
36132
+ entries.push(...synthesizeManagedToolFallbackEntries(categoriesWithOwnEntries, allToolsFailClosedType));
35310
36133
  return entries;
35311
36134
  }
35312
36135
  /**
36136
+ * The strictest action the all-tools `*` category imposes, for tools with no per-input matcher to
36137
+ * narrow it onto (see {@link synthesizeManagedToolFallbackEntries}). `deny` wins over `ask`,
36138
+ * and an all-tools `allow` never forces an entry — there is nothing to fail closed on.
36139
+ */
36140
+ function computeAllToolsFailClosedType(allToolsRules) {
36141
+ if (!allToolsRules) return void 0;
36142
+ const actions = Object.values(allToolsRules);
36143
+ if (actions.some((action) => action === "deny")) return "deny";
36144
+ if (actions.some((action) => action === "ask")) return "ask-user";
36145
+ }
36146
+ /**
36147
+ * Extend the same fail-closed treatment `bash` gets (via {@link bashRulesHonoringAllTools}) to the
36148
+ * other managed tools: one that produced no entries of its own above must not fall back to
36149
+ * AugmentCode's own default just because it has no per-input matcher to narrow the all-tools
36150
+ * restriction onto. A category can be stated yet still emit nothing (e.g. only non-`*` allow/ask
36151
+ * patterns, dropped with a warning), so this checks emitted entries rather than whether the
36152
+ * category was merely present in the source config. A tool whose own rules did emit entries is
36153
+ * left untouched here.
36154
+ */
36155
+ function synthesizeManagedToolFallbackEntries(categoriesWithOwnEntries, allToolsFailClosedType) {
36156
+ if (allToolsFailClosedType === void 0) return [];
36157
+ return Object.entries(CANONICAL_TO_AUGMENT_TOOL_NAMES).filter(([canonicalName]) => canonicalName !== "bash" && !categoriesWithOwnEntries.has(canonicalName)).map(([, augmentToolName]) => ({
36158
+ toolName: augmentToolName,
36159
+ permission: { type: allToolsFailClosedType }
36160
+ }));
36161
+ }
36162
+ /**
35313
36163
  * Sort AugmentCode tool-permission entries to make the `first-match-wins` semantics safe and predictable.
35314
36164
  *
35315
36165
  * Augment evaluates `toolPermissions` top-to-bottom and stops at the first match. To prevent a
@@ -35404,216 +36254,6 @@ function convertAugmentToRulesyncPermissions({ entries, logger }) {
35404
36254
  }
35405
36255
  return { permission };
35406
36256
  }
35407
- /**
35408
- * Collect the canonical rules that govern shell commands, for the adapters
35409
- * whose tool models commands and nothing else.
35410
- *
35411
- * The `bash` category contributes every rule. The all-tools `*` category
35412
- * contributes its **restricting** rules — `deny` and `ask` — because a rule
35413
- * written there covers shell commands too, and dropping it inverts the
35414
- * author's intent: with `{"*": {"rm *": "deny"}, "bash": {"rm *": "allow"}}`,
35415
- * an adapter that reads only `bash` auto-approves the very command the file
35416
- * denies.
35417
- *
35418
- * Its `allow` rules are deliberately **not** contributed. A pattern under `*`
35419
- * need not be a command at all — `secrets/**` under `*` denies a path — and
35420
- * carrying it in the restricting direction only over-restricts, while carrying
35421
- * it in the permissive direction would grant something the author never said
35422
- * about commands. Both directions therefore fail closed.
35423
- */
35424
- function collectShellCommandRules(permission) {
35425
- const rules = [];
35426
- const foreignRestrictingCategories = [];
35427
- const ignoredAllToolsAllowPatterns = [];
35428
- for (const [category, categoryRules] of Object.entries(permission)) {
35429
- if (category === "bash") {
35430
- for (const [pattern, action] of Object.entries(categoryRules)) rules.push({
35431
- pattern,
35432
- action,
35433
- fromAllToolsCategory: false
35434
- });
35435
- continue;
35436
- }
35437
- if (category === "*") {
35438
- for (const [pattern, action] of Object.entries(categoryRules)) {
35439
- if (action === "allow") {
35440
- ignoredAllToolsAllowPatterns.push(pattern);
35441
- continue;
35442
- }
35443
- rules.push({
35444
- pattern,
35445
- action,
35446
- fromAllToolsCategory: true
35447
- });
35448
- }
35449
- continue;
35450
- }
35451
- if (Object.values(categoryRules).some((action) => action === "deny" || action === "ask")) foreignRestrictingCategories.push(category);
35452
- }
35453
- return {
35454
- rules,
35455
- foreignRestrictingCategories,
35456
- ignoredAllToolsAllowPatterns
35457
- };
35458
- }
35459
- /**
35460
- * Build the test an adapter applies to an `allow` pattern before writing it:
35461
- * which restrictions it cannot write name some of the same commands? The
35462
- * answer is the list of those restrictions — empty when the `allow` may be
35463
- * written — so a caller can report both the allow rules it withheld and the
35464
- * restrictions that withheld nothing.
35465
- *
35466
- * Canonically the stricter rule wins **whatever its width** — rulesync collapses
35467
- * colliding rules as `deny > ask > allow` — so the two patterns are compared by
35468
- * asking whether any one command matches both. Width does not enter into it: an
35469
- * `ask` on `*` overlaps an allowed `git *`, an `ask` on `npm publish` overlaps
35470
- * an allowed `npm *`, and an `ask` on `* --force` overlaps an allowed `git *`
35471
- * on every `git ... --force` command even though neither pattern covers the
35472
- * other's spelling. Comparing only identical spellings would let the most
35473
- * ordinary catch-all (`{"*": {"*": "ask"}}`) disappear without a word.
35474
- *
35475
- * Identical spellings are still compared as strings first, as a shortcut past
35476
- * the walk for the commonest case.
35477
- *
35478
- * `normalizePattern` rewrites a pattern written in the tool's own language into
35479
- * the widest glob it could stand for, for a tool whose patterns are not globs.
35480
- * It reaches the `bash` rules and the `allow` rules, which is where such a
35481
- * pattern is written; an all-tools `*` pattern is canonical — it is read by
35482
- * every tool, so it is a glob already — and is compared as it stands. The
35483
- * rewrite must only ever widen what a pattern covers, so an inexact reading
35484
- * withholds an allow rather than writing one the config restricts — see
35485
- * `warpCommandPatternToGlob`.
35486
- */
35487
- function createShadowingRestrictionsTest(restrictions, { normalizePattern = (pattern) => pattern, budget = createIntersectionBudget() } = {}) {
35488
- const normalized = restrictions.map(({ pattern, fromAllToolsCategory }) => ({
35489
- pattern,
35490
- glob: parseGlobPattern(fromAllToolsCategory ? pattern : normalizePattern(pattern))
35491
- }));
35492
- return (allowPattern) => {
35493
- if (budget.remaining === 0) return normalized.map(({ pattern }) => pattern);
35494
- const allowGlob = parseGlobPattern(normalizePattern(allowPattern));
35495
- return normalized.filter(({ pattern, glob }) => pattern === allowPattern || parsedGlobsIntersect(glob, allowGlob, budget)).map(({ pattern }) => pattern);
35496
- };
35497
- }
35498
- /**
35499
- * Which of the given all-tools `*` restrictions look like they may not name a
35500
- * command at all — the question a `deny` and an `ask` written there both raise.
35501
- *
35502
- * "Withheld no allow rule" alone does not answer it: a config with no `allow`
35503
- * rules has nothing to withhold, and a pattern the author also wrote under
35504
- * `bash` is a command on their own word. Both are excluded, so what remains is
35505
- * a `*` pattern that had allow rules to overlap, overlapped none of them, and
35506
- * is claimed as a command nowhere else — the shape `secrets/**` has.
35507
- *
35508
- * A `bash` restriction never belongs here: it names a command by construction,
35509
- * so overlapping no allow rule says nothing is wrong with it.
35510
- */
35511
- function collectUnenforcedAllToolsPatterns({ rules, allToolsPatterns, withholdingPatterns }) {
35512
- if (!rules.some(({ action }) => action === "allow")) return [];
35513
- const shellPatterns = new Set(rules.filter(({ fromAllToolsCategory }) => !fromAllToolsCategory).map(({ pattern }) => pattern));
35514
- return uniq(allToolsPatterns).filter((pattern) => !withholdingPatterns.has(pattern) && !shellPatterns.has(pattern));
35515
- }
35516
- /**
35517
- * Split shell-command rules into the allow and deny lists of a tool that models
35518
- * commands with those two tiers and nothing else.
35519
- *
35520
- * `ask` has no list of its own — such a tool already prompts for whatever it
35521
- * does not auto-approve, so an `ask` rule is satisfied by writing nothing. It
35522
- * still has to *withhold* the `allow` rules it covers, though: the canonical
35523
- * order is `deny > ask > allow`, so auto-approving a command the file also asks
35524
- * about would answer the prompt the author wanted.
35525
- *
35526
- * `writesAllToolsDeny` says whether the tool's denylist can carry a pattern
35527
- * from the all-tools `*` category. Warp's cannot: it matches commands with
35528
- * regular expressions rather than globs, and writing any denylist **replaces**
35529
- * Warp's built-in default one, so an inert `secrets/**` entry there would trade
35530
- * the tool's own protection for a rule that matches no command. Where the deny
35531
- * cannot be written it withholds the allow rules it covers instead, which
35532
- * restricts in the same direction without touching the denylist.
35533
- *
35534
- * A `bash` deny withholds nothing: it names a command by construction, so the
35535
- * denylist entry enforces it wherever the tool's deny-beats-allow order applies,
35536
- * and a narrow deny keeps carving an exception out of a wider allow (`git *`
35537
- * allowed, `git push *` denied). An all-tools `*` deny withholds all the same,
35538
- * even where it is written: a pattern under `*` need not name a command —
35539
- * `secrets/**` there denies a path — so as a denylist entry it may match nothing
35540
- * at all, and leaving an overlapping allow beside it would auto-approve the very
35541
- * commands the author meant to stop. Over-restricting a `*` deny that *was* a
35542
- * command pattern is reported; failing open would not be.
35543
- *
35544
- * `normalizePattern` is handed to `createShadowingRestrictionsTest` for a tool whose
35545
- * patterns are not globs.
35546
- */
35547
- function partitionCommandRules({ rules, writesAllToolsDeny, normalizePattern }) {
35548
- const deny = [];
35549
- const unwrittenDenyPatterns = [];
35550
- const restrictions = [];
35551
- const writtenAllToolsDenyPatterns = [];
35552
- const allToolsAskPatterns = [];
35553
- for (const rule of rules) {
35554
- const { pattern, action, fromAllToolsCategory } = rule;
35555
- if (action === "allow") continue;
35556
- if (action !== "deny") {
35557
- restrictions.push(rule);
35558
- if (fromAllToolsCategory) allToolsAskPatterns.push(pattern);
35559
- continue;
35560
- }
35561
- if (writesAllToolsDeny || !fromAllToolsCategory) {
35562
- deny.push(pattern);
35563
- if (fromAllToolsCategory) writtenAllToolsDenyPatterns.push(pattern);
35564
- } else unwrittenDenyPatterns.push(pattern);
35565
- if (fromAllToolsCategory) restrictions.push(rule);
35566
- }
35567
- const budget = createIntersectionBudget();
35568
- const shadowingRestrictions = createShadowingRestrictionsTest(restrictions, {
35569
- normalizePattern,
35570
- budget
35571
- });
35572
- const allow = [];
35573
- const shadowedAllowPatterns = [];
35574
- const withholdingPatterns = /* @__PURE__ */ new Set();
35575
- for (const { pattern, action } of rules) {
35576
- if (action !== "allow") continue;
35577
- const shadowing = shadowingRestrictions(pattern);
35578
- if (shadowing.length > 0) {
35579
- shadowedAllowPatterns.push(pattern);
35580
- for (const restriction of shadowing) withholdingPatterns.add(restriction);
35581
- continue;
35582
- }
35583
- allow.push(pattern);
35584
- }
35585
- return {
35586
- allow,
35587
- deny,
35588
- shadowedAllowPatterns,
35589
- unwrittenDenyPatterns,
35590
- unenforcedAllToolsDenyPatterns: collectUnenforcedAllToolsPatterns({
35591
- rules,
35592
- allToolsPatterns: writtenAllToolsDenyPatterns,
35593
- withholdingPatterns
35594
- }),
35595
- unenforcedAllToolsAskPatterns: collectUnenforcedAllToolsPatterns({
35596
- rules,
35597
- allToolsPatterns: allToolsAskPatterns,
35598
- withholdingPatterns
35599
- }),
35600
- intersectionBudgetExhausted: budget.remaining === 0
35601
- };
35602
- }
35603
- /**
35604
- * Report, for one command-only tool, every canonical rule its two lists could
35605
- * not carry. Every command-only adapter shares this reporting, so a rule
35606
- * dropped in one is worded the same way in all.
35607
- */
35608
- function warnAboutUnwrittenCommandRules({ toolLabel, surfaceLabel, foreignRestrictingCategories, shadowedAllowPatterns, unwrittenDenyPatterns = [], unwrittenDenyReason, unenforcedAllToolsDenyPatterns = [], unenforcedAllToolsAskPatterns = [], ignoredAllToolsAllowPatterns = [], intersectionBudgetExhausted = false, logger }) {
35609
- if (intersectionBudgetExhausted) warnWithFallback(logger, `${toolLabel} reached the limit on how much work one generation may spend comparing .rulesync/permissions.jsonc's allow rules against its deny and ask rules, so the allow rules left over were withheld rather than compared — the safe answer, but a wider one than the file asks for. Write fewer or shorter command patterns to have them all compared.`);
35610
- for (const category of foreignRestrictingCategories) warnWithFallback(logger, `${toolLabel} only models shell-command permissions (${surfaceLabel}); '${category}' deny and ask rules cannot be represented and were skipped.`);
35611
- if (unwrittenDenyPatterns.length > 0) warnWithFallback(logger, `${toolLabel} did not write the all-tools '*' deny rule(s) for ${unwrittenDenyPatterns.join(", ")} into its denylist.${unwrittenDenyReason === void 0 ? "" : ` ${unwrittenDenyReason}`} They restrict only by withholding the allow rules they cover; write them under 'bash' to have them enforced as commands.`);
35612
- if (unenforcedAllToolsDenyPatterns.length > 0) warnWithFallback(logger, `${toolLabel} wrote the all-tools '*' deny rule(s) for ${unenforcedAllToolsDenyPatterns.join(", ")} into its denylist as they stand, but they withheld none of the allow rules beside them. A pattern written under '*' need not name a command — 'secrets/**' there denies a path — and a denylist entry that names none blocks nothing; write it under 'bash' too if it is a command pattern.`);
35613
- if (unenforcedAllToolsAskPatterns.length > 0) warnWithFallback(logger, `${toolLabel} has no ask tier (${surfaceLabel}), so the all-tools '*' ask rule(s) for ${unenforcedAllToolsAskPatterns.join(", ")} restrict only by withholding the allow rules they cover — and they covered none. A pattern written under '*' need not name a command, so nothing observed says these ones do; write them under 'bash' if they are command patterns.`);
35614
- if (ignoredAllToolsAllowPatterns.length > 0) warnWithFallback(logger, `${toolLabel} reads the all-tools '*' category for its deny and ask rules only, so the allow rule(s) for ${ignoredAllToolsAllowPatterns.join(", ")} were skipped — a pattern written under '*' need not be a command. Write them under 'bash' to auto-approve them as commands.`);
35615
- if (shadowedAllowPatterns.length > 0) warnWithFallback(logger, `${toolLabel} was not given the allow rule(s) for ${shadowedAllowPatterns.join(", ")} because .rulesync/permissions.jsonc restricts the same commands elsewhere, and the stricter rule wins whatever its width.`);
35616
- }
35617
36257
  //#endregion
35618
36258
  //#region src/features/permissions/claudecode-permissions.ts
35619
36259
  /**
@@ -37375,7 +38015,7 @@ function mergeFilesystemCategoryRules({ categoryRules, logger }) {
37375
38015
  return merged;
37376
38016
  }
37377
38017
  function buildCodexBashRulesContent(config) {
37378
- const bashRules = config.permission.bash ?? {};
38018
+ const bashRules = bashRulesHonoringAllTools(config.permission);
37379
38019
  const entries = Object.entries(bashRules);
37380
38020
  const header = ["# Generated by Rulesync from .rulesync/permissions.jsonc (permission.bash)", "# https://developers.openai.com/codex/rules"];
37381
38021
  if (entries.length === 0) return [...header, "# No bash permission rules were configured."].join("\n");
@@ -38066,7 +38706,7 @@ var CursorPermissions = class CursorPermissions extends ToolPermissions {
38066
38706
  function convertRulesyncToCursorPermissions(config, logger) {
38067
38707
  const allow = [];
38068
38708
  const deny = [];
38069
- for (const [category, rules] of Object.entries(config.permission)) {
38709
+ for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) {
38070
38710
  const cursorType = toCursorType(category);
38071
38711
  for (const [pattern, action] of Object.entries(rules)) {
38072
38712
  const entry = buildCursorPermissionEntry(cursorType, toCursorPattern(category, pattern));
@@ -38885,7 +39525,7 @@ function convertRulesyncToDevinPermissions(config) {
38885
39525
  const allow = [];
38886
39526
  const ask = [];
38887
39527
  const deny = [];
38888
- for (const [category, rules] of Object.entries(config.permission)) {
39528
+ for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) {
38889
39529
  const scope = toDevinScope(category);
38890
39530
  for (const [pattern, action] of Object.entries(rules)) {
38891
39531
  const entry = buildDevinPermissionEntry(scope, pattern);
@@ -39173,6 +39813,19 @@ var GoosePermissions = class GoosePermissions extends ToolPermissions {
39173
39813
  isDeletable() {
39174
39814
  return false;
39175
39815
  }
39816
+ /**
39817
+ * `permission.yaml` is Goose's file, not one rulesync owns: rulesync merges
39818
+ * into it when it exists but has no business bringing it into existence to
39819
+ * hold nothing. When no rule maps, the `user` block holds three empty lists,
39820
+ * which would otherwise be written as a fresh permission.yaml that says
39821
+ * nothing — an absent file and empty lists both mean "no user override, so
39822
+ * Goose decides on its own". An existing file is still rewritten as before,
39823
+ * so user content is never dropped — the skip only applies when there is no
39824
+ * file yet.
39825
+ */
39826
+ shouldSkipCreationWhenPayloadEmpty() {
39827
+ return true;
39828
+ }
39176
39829
  static getSettablePaths(_options) {
39177
39830
  return {
39178
39831
  relativeDirPath: GOOSE_GLOBAL_DIR,
@@ -39257,7 +39910,7 @@ function convertRulesyncToGoosePermissionConfig({ config, logger }) {
39257
39910
  never_allow: []
39258
39911
  };
39259
39912
  const assigned = /* @__PURE__ */ new Map();
39260
- const orderedEntries = Object.entries(config.permission).toSorted(([a], [b]) => (a === "edit" ? 1 : 0) - (b === "edit" ? 1 : 0));
39913
+ const orderedEntries = Object.entries(honorAllToolsOnBash(config.permission)).toSorted(([a], [b]) => (a === "edit" ? 1 : 0) - (b === "edit" ? 1 : 0));
39261
39914
  for (const [category, rules] of orderedEntries) {
39262
39915
  const toolName = RULESYNC_TO_GOOSE_TOOL_NAME[category] ?? category;
39263
39916
  for (const [pattern, action] of Object.entries(rules)) {
@@ -39589,7 +40242,7 @@ function unmanagedEntries(existingPermission, key) {
39589
40242
  */
39590
40243
  function buildGrokPermissionArrays(config, existingPermission, logger) {
39591
40244
  const ranked = /* @__PURE__ */ new Map();
39592
- for (const [category, rules] of Object.entries(config.permission)) for (const [pattern, action] of Object.entries(rules)) {
40245
+ for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) for (const [pattern, action] of Object.entries(rules)) {
39593
40246
  const entry = buildGrokEntry(category, pattern);
39594
40247
  if (entry === null) {
39595
40248
  if (action === "deny" && logger) logger.warn(`Grok CLI has no permission tool for the '${category}' category; its 'deny' rule could not be represented and was skipped.`);
@@ -39678,6 +40331,8 @@ function deriveGrokPermissionMode(config) {
39678
40331
  function patternsByAction(category, action) {
39679
40332
  return Object.entries(category ?? {}).filter(([, value]) => value === action).map(([pattern]) => pattern);
39680
40333
  }
40334
+ /** The canonical category whose `deny` rules feed `security.website_blocklist`. */
40335
+ const WEBFETCH_PERMISSION_CATEGORY = "webfetch";
39681
40336
  function clonePermissionBlock(permission) {
39682
40337
  return Object.fromEntries(Object.entries(permission).map(([category, rules]) => [category, { ...rules }]));
39683
40338
  }
@@ -39690,18 +40345,39 @@ function ensureCategory(permission, category) {
39690
40345
  function removeEmptyCategories(permission) {
39691
40346
  for (const [category, rules] of Object.entries(permission)) if (Object.keys(rules).length === 0) delete permission[category];
39692
40347
  }
40348
+ /**
40349
+ * Make the native `command_allowlist` authoritative for the `bash` allow rules
40350
+ * it can speak for, and for those only. The allowlist is a list of shell-command
40351
+ * patterns, so it is generated from `bash` alone (see `fromRulesyncPermissions`);
40352
+ * an allow in any other category names something Hermes's allowlist cannot
40353
+ * carry, so its absence from the list says nothing about it and it is kept as
40354
+ * provenance wrote it. The same holds for a `bash` allow the generator withheld
40355
+ * because a stricter `*` or `bash` rule covers it: the allowlist never carried
40356
+ * it, so its absence is not a retraction and the rule is kept too.
40357
+ */
39693
40358
  function reconcileCommandAllowlist({ permission, commandAllowlist }) {
39694
- const nativeAllows = new Set(commandAllowlist);
39695
- const existingAllowCategories = /* @__PURE__ */ new Map();
39696
- for (const [category, rules] of Object.entries(permission)) for (const [pattern, action] of Object.entries(rules)) {
39697
- if (action !== "allow") continue;
39698
- existingAllowCategories.set(pattern, category);
39699
- if (!nativeAllows.has(pattern)) delete rules[pattern];
39700
- }
39701
- for (const pattern of nativeAllows) {
39702
- const existingCategory = existingAllowCategories.get(pattern);
39703
- if (existingCategory) ensureCategory(permission, existingCategory)[pattern] = "allow";
39704
- else ensureCategory(permission, "bash")[pattern] = "allow";
40359
+ const { rules: commandRules } = collectShellCommandRules(permission);
40360
+ const { shadowedAllowPatterns } = partitionCommandRules({
40361
+ rules: commandRules,
40362
+ writesAllToolsDeny: false
40363
+ });
40364
+ const withheld = new Set(shadowedAllowPatterns);
40365
+ const rules = ensureCategory(permission, SHELL_PERMISSION_CATEGORY);
40366
+ for (const [pattern, action] of Object.entries(rules)) if (action === "allow" && !withheld.has(pattern)) delete rules[pattern];
40367
+ for (const pattern of commandAllowlist) rules[pattern] = "allow";
40368
+ }
40369
+ /**
40370
+ * Report the restricting rules Hermes has no per-pattern primitive for: a
40371
+ * `deny` or `ask` in any category other than `bash`, `*`, and `webfetch`, and
40372
+ * an `ask` under `webfetch` — the blocklist carries a `webfetch` deny but has
40373
+ * no ask tier. (`bash` and `*` are reported by `warnAboutUnwrittenCommandRules`.)
40374
+ * Such rules survive only in the round-trip blob.
40375
+ */
40376
+ function warnAboutUnexpressedHermesRestrictions({ permissionBlock, foreignRestrictingCategories, logger }) {
40377
+ for (const category of foreignRestrictingCategories) {
40378
+ const isWebfetch = category === WEBFETCH_PERMISSION_CATEGORY;
40379
+ if (isWebfetch && patternsByAction(permissionBlock[category], "ask").length === 0) continue;
40380
+ warnWithFallback(logger, isWebfetch ? "Hermes Agent's security.website_blocklist has no ask tier, so the 'webfetch' ask rule(s) cannot be represented and were skipped; they survive only in the permissions.rulesync round-trip block." : `Hermes Agent has no per-pattern primitive for '${category}' deny and ask rules (it enforces command_allowlist, approvals.deny, and security.website_blocklist), so they were skipped; they survive only in the permissions.rulesync round-trip block.`);
39705
40381
  }
39706
40382
  }
39707
40383
  function reconcileNativeDenies({ permission, category, patterns }) {
@@ -39813,14 +40489,14 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
39813
40489
  const approvals = isRecord$1(config.approvals) ? config.approvals : {};
39814
40490
  reconcileNativeDenies({
39815
40491
  permission,
39816
- category: "bash",
40492
+ category: SHELL_PERMISSION_CATEGORY,
39817
40493
  patterns: isStringArray$2(approvals.deny) ? approvals.deny : []
39818
40494
  });
39819
40495
  const security = isRecord$1(config.security) ? config.security : {};
39820
40496
  const websiteBlocklist = isRecord$1(security.website_blocklist) ? security.website_blocklist : {};
39821
40497
  reconcileNativeDenies({
39822
40498
  permission,
39823
- category: "webfetch",
40499
+ category: WEBFETCH_PERMISSION_CATEGORY,
39824
40500
  patterns: websiteBlocklist.enabled === true && isStringArray$2(websiteBlocklist.domains) ? websiteBlocklist.domains : []
39825
40501
  });
39826
40502
  removeEmptyCategories(permission);
@@ -39840,12 +40516,32 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
39840
40516
  fileContent: JSON.stringify(imported, null, 2)
39841
40517
  });
39842
40518
  }
39843
- static fromRulesyncPermissions({ outputRoot, rulesyncPermissions, global = false }) {
40519
+ static fromRulesyncPermissions({ outputRoot, rulesyncPermissions, global = false, logger }) {
39844
40520
  const permissions = rulesyncPermissions.getJson();
39845
40521
  const permissionBlock = permissions.permission ?? {};
39846
- const commandAllowlist = Object.entries(permissionBlock).flatMap(([, patterns]) => patternsByAction(patterns, "allow"));
39847
- const bashDeny = patternsByAction(permissionBlock.bash, "deny");
39848
- const webfetchDeny = patternsByAction(permissionBlock.webfetch, "deny");
40522
+ const { rules, foreignRestrictingCategories, ignoredAllToolsAllowPatterns } = collectShellCommandRules(permissionBlock);
40523
+ const { allow: commandAllowlist, deny: bashDeny, shadowedAllowPatterns, unwrittenDenyPatterns, unenforcedAllToolsAskPatterns, intersectionBudgetExhausted } = partitionCommandRules({
40524
+ rules,
40525
+ writesAllToolsDeny: false
40526
+ });
40527
+ warnAboutUnexpressedHermesRestrictions({
40528
+ permissionBlock,
40529
+ foreignRestrictingCategories,
40530
+ logger
40531
+ });
40532
+ warnAboutUnwrittenCommandRules({
40533
+ toolLabel: "Hermes Agent",
40534
+ surfaceLabel: "command_allowlist/approvals.deny",
40535
+ foreignRestrictingCategories: [],
40536
+ shadowedAllowPatterns,
40537
+ unwrittenDenyPatterns,
40538
+ unwrittenDenyReason: "approvals.deny is a hard denylist of shell commands, and a pattern written under '*' need not be a command at all.",
40539
+ unenforcedAllToolsAskPatterns,
40540
+ ignoredAllToolsAllowPatterns,
40541
+ intersectionBudgetExhausted,
40542
+ logger
40543
+ });
40544
+ const webfetchDeny = patternsByAction(permissionBlock[WEBFETCH_PERMISSION_CATEGORY], "deny");
39849
40545
  let config = {};
39850
40546
  if (commandAllowlist.length > 0) config.command_allowlist = commandAllowlist;
39851
40547
  if (bashDeny.length > 0) config.approvals = { deny: bashDeny };
@@ -40112,7 +40808,7 @@ var JuniePermissions = class JuniePermissions extends ToolPermissions {
40112
40808
  */
40113
40809
  function convertRulesyncToJunieRules({ config, logger, existingRules, overrideSecretFile, overrideRuleDefaults }) {
40114
40810
  const ruleLists = {};
40115
- for (const [category, patterns] of Object.entries(config.permission)) {
40811
+ for (const [category, patterns] of Object.entries(honorAllToolsOnBash(config.permission))) {
40116
40812
  const group = CANONICAL_TO_JUNIE_GROUP[category];
40117
40813
  if (!group) {
40118
40814
  if (Object.keys(patterns).length > 0) logger?.warn(`Junie allowlist only models executables/fileEditing/mcpTools/readOutsideProject (canonical bash/edit/write/read/mcp); '${category}' rules cannot be represented and were skipped.`);
@@ -40362,7 +41058,7 @@ var KiloPermissions = class KiloPermissions extends ToolPermissions {
40362
41058
  const rulesyncJson = rulesyncPermissions.getJson();
40363
41059
  const kiloOverride = rulesyncJson.kilo;
40364
41060
  const incomingPermission = {
40365
- ...rulesyncJson.permission,
41061
+ ...honorAllToolsOnBash(rulesyncJson.permission),
40366
41062
  ...kiloOverride?.permission
40367
41063
  };
40368
41064
  const droppedDenyByKey = {};
@@ -40881,7 +41577,7 @@ function buildKiroPermissionsFromRulesync({ config, logger, existing }) {
40881
41577
  allowedCommands: [],
40882
41578
  deniedCommands: []
40883
41579
  };
40884
- for (const [category, rules] of Object.entries(config.permission)) for (const [pattern, action] of Object.entries(rules)) {
41580
+ for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) for (const [pattern, action] of Object.entries(rules)) {
40885
41581
  if (action === "ask") {
40886
41582
  logger?.warn(`Kiro permissions do not support "ask". Skipping ${category}:${pattern}`);
40887
41583
  continue;
@@ -41165,7 +41861,7 @@ var OpencodePermissions = class OpencodePermissions extends ToolPermissions {
41165
41861
  const rulesyncJson = rulesyncPermissions.getJson();
41166
41862
  const overridePermission = rulesyncJson.opencode?.permission ?? {};
41167
41863
  const sharedPermission = {};
41168
- for (const [category, value] of Object.entries(rulesyncJson.permission ?? {})) sharedPermission[toOpencodePermissionKey(category)] = value;
41864
+ for (const [category, value] of Object.entries(honorAllToolsOnBash(rulesyncJson.permission ?? {}))) sharedPermission[toOpencodePermissionKey(category)] = value;
41169
41865
  const permission = {};
41170
41866
  for (const [category, value] of Object.entries({
41171
41867
  ...sharedPermission,
@@ -42294,11 +42990,19 @@ var RooPermissions = class extends ToolPermissions {
42294
42990
  const paths = this.getSettablePaths();
42295
42991
  const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
42296
42992
  const existingContent = await readFileContentOrNull(filePath) ?? "{}";
42297
- const rules = rulesyncPermissions.getJson().permission[COMMAND_CATEGORY];
42993
+ const permission = rulesyncPermissions.getJson().permission;
42994
+ const bashStated = permission[COMMAND_CATEGORY] !== void 0;
42298
42995
  const patch = {};
42299
- if (rules !== void 0) {
42996
+ if (bashStated) {
42997
+ const { bash } = resolveShellCommandLists({
42998
+ permission,
42999
+ writesAllToolsDeny: true,
43000
+ toolLabel: this.getToolLabel(),
43001
+ surfaceLabel: `${this.getAllowedCommandsKey()}/${this.getDeniedCommandsKey()}`,
43002
+ logger
43003
+ });
42300
43004
  const { allowed, denied } = buildVscodeCommandLists({
42301
- rules,
43005
+ rules: bash,
42302
43006
  toolLabel: this.getToolLabel(),
42303
43007
  logger
42304
43008
  });
@@ -42309,7 +43013,7 @@ var RooPermissions = class extends ToolPermissions {
42309
43013
  outputRoot,
42310
43014
  relativeDirPath: paths.relativeDirPath,
42311
43015
  relativeFilePath: paths.relativeFilePath,
42312
- ownsCommandKeys: rules !== void 0,
43016
+ ownsCommandKeys: bashStated,
42313
43017
  fileContent: applySharedConfigPatch({
42314
43018
  fileKey: sharedConfigFileKey(paths),
42315
43019
  feature: "permissions",
@@ -42689,7 +43393,7 @@ function convertRulesyncToRovodevToolPermissions({ config, logger }) {
42689
43393
  config,
42690
43394
  logger
42691
43395
  });
42692
- for (const [category, rules] of Object.entries(config.permission)) {
43396
+ for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) {
42693
43397
  if (category === CATCH_ALL_PATTERN$1) {
42694
43398
  const toolWideDefault = convertAllToolsRules({
42695
43399
  rules,
@@ -43699,7 +44403,7 @@ var VibePermissions = class VibePermissions extends ToolPermissions {
43699
44403
  const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
43700
44404
  const existingContent = await readFileContentOrNull(filePath) ?? "";
43701
44405
  const config = parseVibeConfig(existingContent);
43702
- const permission = rulesyncPermissions.getJson().permission;
44406
+ const permission = honorAllToolsOnBash(rulesyncPermissions.getJson().permission);
43703
44407
  const vibeOverride = rulesyncPermissions.getJson().vibe;
43704
44408
  const tools = toVibeToolsRecord(config.tools);
43705
44409
  const diskShellPatterns = new Map([VIBE_SHELL_CATEGORY, ...VIBE_SHELL_ALIAS_TOOL_NAMES].map((vibeToolName) => [vibeToolName, toStringArray(readVibeToolConfig({
@@ -45040,6 +45744,19 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
45040
45744
  isDeletable() {
45041
45745
  return false;
45042
45746
  }
45747
+ /**
45748
+ * `settings.toml` is Warp's file, not one rulesync owns: rulesync merges into
45749
+ * it when it exists but has no business bringing it into existence to hold
45750
+ * nothing. When no rule maps, both command lists are dropped and the payload
45751
+ * is a bare `[agents.profiles]` table, which would otherwise be written as a
45752
+ * fresh settings file that says nothing — an absent file and an empty table
45753
+ * both mean "Warp's own defaults". An existing file is still rewritten as
45754
+ * before, so user content is never dropped — the skip only applies when
45755
+ * there is no file yet.
45756
+ */
45757
+ shouldSkipCreationWhenPayloadEmpty() {
45758
+ return true;
45759
+ }
45043
45760
  static getSettablePaths(_options) {
45044
45761
  return {
45045
45762
  relativeDirPath: warpSettingsDir(),
@@ -45651,6 +46368,7 @@ function buildZedToolPermissions({ permission, logger }) {
45651
46368
  for (const [category, rules] of Object.entries(permission)) {
45652
46369
  if (category === "*") {
45653
46370
  for (const [pattern, action] of Object.entries(rules)) if (pattern === "*") managedDefault = CANONICAL_TO_ZED_ACTION[action];
46371
+ else if (permission.bash?.[pattern] === "deny" || permission.bash?.[pattern] === "ask") continue;
45654
46372
  else logger?.warn(`Zed permissions: dropping the "*" category rule for pattern "${pattern}" — Zed's global tool-permission default takes no patterns; scope the rule to a tool category instead.`);
45655
46373
  continue;
45656
46374
  }
@@ -45805,7 +46523,7 @@ var ZedPermissions = class ZedPermissions extends ToolPermissions {
45805
46523
  const toolPermissions = asRecord(agent.tool_permissions);
45806
46524
  const existingTools = asRecord(toolPermissions.tools);
45807
46525
  const { managedDefault, managedTools, excludedCategories, inertMcpCategories } = buildZedToolPermissions({
45808
- permission: config.permission,
46526
+ permission: honorAllToolsOnBash(config.permission),
45809
46527
  logger
45810
46528
  });
45811
46529
  if (excludedCategories.length > 0) logger?.warn(`Zed permissions: dropping the ${excludedCategories.map((category) => `"${category}"`).join(", ")} ${excludedCategories.length === 1 ? "category" : "categories"} — Zed does not gate its read-only tools, so the entries would never be consulted. Zed's read-denial surface is \`private_files\`, which the ignore feature writes from \`.rulesync/.aiignore\`.`);
@@ -46314,6 +47032,24 @@ var PermissionsProcessor = class extends FeatureProcessor {
46314
47032
  }
46315
47033
  };
46316
47034
  //#endregion
47035
+ //#region src/constants/codebuddy-paths.ts
47036
+ /**
47037
+ * CodeBuddy Code configuration-layout conventions.
47038
+ *
47039
+ * CodeBuddy Code (`@tencent-ai/codebuddy-code`) is Tencent Cloud's terminal
47040
+ * coding agent. Its configuration surface mirrors Claude Code closely: a
47041
+ * root memory file plus a `.codebuddy/` tree.
47042
+ *
47043
+ * @see https://www.codebuddy.ai/docs/cli/memory
47044
+ * @see https://www.codebuddy.ai/docs/cli/codebuddy-dir
47045
+ */
47046
+ /** Root directory for CodeBuddy Code configuration, relative to the scope root. */
47047
+ const CODEBUDDY_DIR = ".codebuddy";
47048
+ const CODEBUDDY_RULE_FILE_NAME = "CODEBUDDY.md";
47049
+ const CODEBUDDY_LOCAL_RULE_FILE_NAME = "CODEBUDDY.local.md";
47050
+ /** Modular rules directory name under `.codebuddy/`. */
47051
+ const CODEBUDDY_RULES_DIR_NAME = "rules";
47052
+ //#endregion
46317
47053
  //#region src/features/skills/simulated-skill.ts
46318
47054
  const SimulatedSkillFrontmatterSchema = z.looseObject({
46319
47055
  name: z.string(),
@@ -46852,7 +47588,7 @@ var DirFeatureProcessor = class extends RulesyncSourceConsumer {
46852
47588
  * This only deletes directories that are no longer in the rulesync source, not directories that will be overwritten.
46853
47589
  */
46854
47590
  async removeOrphanAiDirs(existingDirs, generatedDirs) {
46855
- const generatedPaths = new Set(generatedDirs.map((d) => d.getDirPath()));
47591
+ const generatedPaths = new Set(generatedDirs.map((d) => caseFoldIdentity(d.getDirPath())));
46856
47592
  const orphanPaths = /* @__PURE__ */ new Set();
46857
47593
  const quotedOutputRoot = quoteForLog(this.outputRoot);
46858
47594
  for (const aiDir of existingDirs) {
@@ -46877,7 +47613,7 @@ var DirFeatureProcessor = class extends RulesyncSourceConsumer {
46877
47613
  this.logger.warn(verdict === "equal" ? `Refusing to delete ${quotedDirPath}: it is the root it was found in, not a directory inside that root` : `Refusing to delete ${quotedDirPath}: it is not inside ${quotedRoot}, the root it was found in`);
46878
47614
  continue;
46879
47615
  }
46880
- if (!generatedPaths.has(dirPath)) orphanPaths.add(dirPath);
47616
+ if (!generatedPaths.has(caseFoldIdentity(dirPath))) orphanPaths.add(dirPath);
46881
47617
  }
46882
47618
  return await this.deleteOrphanPaths({
46883
47619
  paths: orphanPaths,
@@ -46976,7 +47712,7 @@ var DirFeatureProcessor = class extends RulesyncSourceConsumer {
46976
47712
  const mainFile = aiDir.getMainFile();
46977
47713
  if (mainFile) generatedNames.add(toPosixPath(mainFile.name));
46978
47714
  for (const file of aiDir.getOtherFiles()) generatedNames.add(toPosixPath(file.relativeFilePathToDirPath));
46979
- const generatedNamesFolded = new Set([...generatedNames].map((name) => name.toLowerCase()));
47715
+ const generatedNamesFolded = new Set([...generatedNames].map((name) => caseFoldIdentity(name)));
46980
47716
  let existingNames;
46981
47717
  try {
46982
47718
  existingNames = await listFilePathsRecursively(dirPath, {
@@ -46991,7 +47727,7 @@ var DirFeatureProcessor = class extends RulesyncSourceConsumer {
46991
47727
  const posixName = toPosixPath(existingName);
46992
47728
  if (generatedNames.has(posixName)) continue;
46993
47729
  const filePath = join(dirPath, existingName);
46994
- if (generatedNamesFolded.has(posixName.toLowerCase())) {
47730
+ if (generatedNamesFolded.has(caseFoldIdentity(posixName))) {
46995
47731
  this.logger.warn(`Refusing to delete ${quoteForLog(filePath)}: this run wrote a file whose path differs from it only in case, which on a case-insensitive filesystem is the very file it wrote`);
46996
47732
  continue;
46997
47733
  }
@@ -47047,7 +47783,7 @@ var DirFeatureProcessor = class extends RulesyncSourceConsumer {
47047
47783
  generatedPaths.add(flatFilePath);
47048
47784
  for (const file of generatedDir.getOtherFiles()) generatedPaths.add(join(generatedDirPath, file.relativeFilePathToDirPath));
47049
47785
  }
47050
- const generatedPathsFolded = new Set([...generatedPaths].map((generatedPath) => generatedPath.toLowerCase()));
47786
+ const generatedPathsFolded = new Set([...generatedPaths].map((generatedPath) => caseFoldIdentity(generatedPath)));
47051
47787
  const orphanPaths = /* @__PURE__ */ new Set();
47052
47788
  const quotedOutputRoot = quoteForLog(this.outputRoot);
47053
47789
  for (const aiDir of existingFlatFiles) {
@@ -47077,7 +47813,7 @@ var DirFeatureProcessor = class extends RulesyncSourceConsumer {
47077
47813
  continue;
47078
47814
  }
47079
47815
  if (generatedPaths.has(filePath)) continue;
47080
- if (generatedPathsFolded.has(filePath.toLowerCase())) {
47816
+ if (generatedPathsFolded.has(caseFoldIdentity(filePath))) {
47081
47817
  this.logger.warn(`Refusing to delete ${quotedFilePath}: this run wrote a file whose path differs from it only in case, which on a case-insensitive filesystem is the very file it wrote`);
47082
47818
  continue;
47083
47819
  }
@@ -48909,6 +49645,191 @@ var CopilotcliSkill = class CopilotcliSkill extends ToolSkill {
48909
49645
  }
48910
49646
  };
48911
49647
  //#endregion
49648
+ //#region src/features/skills/crush-skill.ts
49649
+ const CrushSkillFrontmatterSchema = z.looseObject({
49650
+ name: z.string(),
49651
+ description: z.string(),
49652
+ "user-invocable": z.optional(z.boolean()),
49653
+ "disable-model-invocation": z.optional(z.boolean()),
49654
+ license: z.optional(z.string()),
49655
+ compatibility: z.optional(z.union([z.string(), z.looseObject({})])),
49656
+ metadata: z.optional(z.looseObject({}))
49657
+ });
49658
+ /**
49659
+ * Represents a Crush Agent Skill directory.
49660
+ *
49661
+ * Crush auto-discovers Agent Skills (`SKILL.md` per directory) from
49662
+ * `.crush/skills/` at project scope and `~/.config/crush/skills/` (or
49663
+ * `$CRUSH_SKILLS_DIR`) at global scope. Unless `$CRUSH_SKILLS_DIR` is set,
49664
+ * Crush also scans several shared directories it does not own (globally
49665
+ * `~/.config/agents/skills/`, `~/.agents/skills/`, `~/.claude/skills/`;
49666
+ * per-project `.agents/skills/`, `.claude/skills/`, `.cursor/skills/`, also
49667
+ * checked at a git worktree's common root); this class writes only to the
49668
+ * Crush-specific path above, leaving those shared roots to their own targets.
49669
+ *
49670
+ * Crush's `UserInvocable` field is a non-pointer Go `bool`, so an omitted
49671
+ * `user-invocable` (at both the root and the `crush:` section) resolves to
49672
+ * `false`: the skill stays reachable by the model but is hidden from Crush's
49673
+ * command palette. See `FromSkillCatalog` in `internal/commands/commands.go`.
49674
+ * @see https://github.com/charmbracelet/crush/blob/main/internal/config/load.go
49675
+ */
49676
+ var CrushSkill = class CrushSkill extends ToolSkill {
49677
+ constructor({ outputRoot = process.cwd(), relativeDirPath = CRUSH_SKILLS_PROJECT_DIR, dirName, frontmatter, body, otherFiles = [], validate = true, global = false }) {
49678
+ super({
49679
+ outputRoot,
49680
+ relativeDirPath,
49681
+ dirName,
49682
+ mainFile: {
49683
+ name: SKILL_FILE_NAME,
49684
+ body,
49685
+ frontmatter: { ...frontmatter }
49686
+ },
49687
+ otherFiles,
49688
+ global
49689
+ });
49690
+ if (validate) {
49691
+ const result = this.validate();
49692
+ if (!result.success) throw result.error;
49693
+ }
49694
+ }
49695
+ static getSettablePaths({ global = false } = {}) {
49696
+ return { relativeDirPath: global ? CRUSH_SKILLS_GLOBAL_DIR : CRUSH_SKILLS_PROJECT_DIR };
49697
+ }
49698
+ getFrontmatter() {
49699
+ return CrushSkillFrontmatterSchema.parse(this.requireMainFileFrontmatter());
49700
+ }
49701
+ getBody() {
49702
+ return this.mainFile?.body ?? "";
49703
+ }
49704
+ validate() {
49705
+ if (!this.mainFile) return {
49706
+ success: false,
49707
+ error: /* @__PURE__ */ new Error(`${this.getDirPath()}: ${SKILL_FILE_NAME} file does not exist`)
49708
+ };
49709
+ const result = CrushSkillFrontmatterSchema.safeParse(this.mainFile.frontmatter);
49710
+ if (!result.success) return {
49711
+ success: false,
49712
+ error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${this.getDirPath()}: ${formatError(result.error)}`)
49713
+ };
49714
+ return {
49715
+ success: true,
49716
+ error: null
49717
+ };
49718
+ }
49719
+ toRulesyncSkill() {
49720
+ const frontmatter = this.getFrontmatter();
49721
+ const crushSection = {
49722
+ ...frontmatter["user-invocable"] !== void 0 && { "user-invocable": frontmatter["user-invocable"] },
49723
+ ...frontmatter["disable-model-invocation"] !== void 0 && { "disable-model-invocation": frontmatter["disable-model-invocation"] },
49724
+ ...frontmatter.license !== void 0 && { license: frontmatter.license },
49725
+ ...frontmatter.compatibility !== void 0 && { compatibility: frontmatter.compatibility },
49726
+ ...frontmatter.metadata !== void 0 && { metadata: frontmatter.metadata }
49727
+ };
49728
+ const rulesyncFrontmatter = {
49729
+ name: frontmatter.name,
49730
+ description: frontmatter.description,
49731
+ targets: ["*"],
49732
+ ...Object.keys(crushSection).length > 0 && { crush: crushSection }
49733
+ };
49734
+ return new RulesyncSkill({
49735
+ outputRoot: this.outputRoot,
49736
+ relativeDirPath: RULESYNC_SKILLS_RELATIVE_DIR_PATH,
49737
+ dirName: this.getDirName(),
49738
+ frontmatter: rulesyncFrontmatter,
49739
+ body: this.getBody(),
49740
+ otherFiles: this.getOtherFiles(),
49741
+ validate: true,
49742
+ global: this.global
49743
+ });
49744
+ }
49745
+ static fromRulesyncSkill({ outputRoot = process.cwd(), rulesyncSkill, validate = true, global = false }) {
49746
+ const settablePaths = CrushSkill.getSettablePaths({ global });
49747
+ const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
49748
+ const crushSection = rulesyncFrontmatter.crush;
49749
+ const resolvedUserInvocable = resolveUserInvocable({
49750
+ rootFrontmatter: rulesyncFrontmatter,
49751
+ section: crushSection
49752
+ });
49753
+ const resolvedDisableModelInvocation = resolveDisableModelInvocation({
49754
+ rootFrontmatter: rulesyncFrontmatter,
49755
+ section: crushSection
49756
+ });
49757
+ const license = resolveLicense({
49758
+ rootFrontmatter: rulesyncFrontmatter,
49759
+ section: crushSection
49760
+ });
49761
+ const compatibility = resolveCompatibility({
49762
+ rootFrontmatter: rulesyncFrontmatter,
49763
+ section: crushSection
49764
+ });
49765
+ const metadata = resolveMetadata({
49766
+ rootFrontmatter: rulesyncFrontmatter,
49767
+ section: crushSection
49768
+ });
49769
+ const compatibilityString = compatibility === void 0 ? void 0 : toCompatibilityString(compatibility);
49770
+ const crushFrontmatter = {
49771
+ name: rulesyncFrontmatter.name,
49772
+ description: rulesyncFrontmatter.description,
49773
+ ...resolvedUserInvocable !== void 0 && { "user-invocable": resolvedUserInvocable },
49774
+ ...resolvedDisableModelInvocation !== void 0 && { "disable-model-invocation": resolvedDisableModelInvocation },
49775
+ ...license !== void 0 && { license },
49776
+ ...compatibilityString !== void 0 && compatibilityString.length > 0 && { compatibility: compatibilityString },
49777
+ ...metadata !== void 0 && { metadata: toStringMetadata(metadata) }
49778
+ };
49779
+ return new CrushSkill({
49780
+ outputRoot,
49781
+ relativeDirPath: settablePaths.relativeDirPath,
49782
+ dirName: rulesyncSkill.getDirName(),
49783
+ frontmatter: crushFrontmatter,
49784
+ body: rulesyncSkill.getBody(),
49785
+ otherFiles: rulesyncSkill.getOtherFiles(),
49786
+ validate,
49787
+ global
49788
+ });
49789
+ }
49790
+ static isTargetedByRulesyncSkill(rulesyncSkill) {
49791
+ const targets = rulesyncSkill.getFrontmatter().targets;
49792
+ return targets.includes("*") || targets.includes("crush");
49793
+ }
49794
+ static async fromDir(params) {
49795
+ const loaded = await this.loadSkillDirContent({
49796
+ ...params,
49797
+ getSettablePaths: CrushSkill.getSettablePaths
49798
+ });
49799
+ const result = CrushSkillFrontmatterSchema.safeParse(loaded.frontmatter);
49800
+ if (!result.success) {
49801
+ const skillDirPath = join(loaded.outputRoot, loaded.relativeDirPath, loaded.dirName);
49802
+ throw new Error(`Invalid frontmatter in ${join(skillDirPath, SKILL_FILE_NAME)}: ${formatError(result.error)}`);
49803
+ }
49804
+ return new CrushSkill({
49805
+ outputRoot: loaded.outputRoot,
49806
+ relativeDirPath: loaded.relativeDirPath,
49807
+ dirName: loaded.dirName,
49808
+ frontmatter: result.data,
49809
+ body: loaded.body,
49810
+ otherFiles: loaded.otherFiles,
49811
+ validate: true,
49812
+ global: loaded.global
49813
+ });
49814
+ }
49815
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, dirName, global = false }) {
49816
+ const settablePaths = CrushSkill.getSettablePaths({ global });
49817
+ return new CrushSkill({
49818
+ outputRoot,
49819
+ relativeDirPath: relativeDirPath ?? settablePaths.relativeDirPath,
49820
+ dirName,
49821
+ frontmatter: {
49822
+ name: "",
49823
+ description: ""
49824
+ },
49825
+ body: "",
49826
+ otherFiles: [],
49827
+ validate: false,
49828
+ global
49829
+ });
49830
+ }
49831
+ };
49832
+ //#endregion
48912
49833
  //#region src/features/skills/cursor-skill.ts
48913
49834
  const CursorSkillFrontmatterSchema = z.looseObject({
48914
49835
  name: z.string(),
@@ -49073,9 +49994,9 @@ const DeepagentsSkillFrontmatterSchema = z.looseObject({
49073
49994
  name: z.string(),
49074
49995
  description: z.string(),
49075
49996
  "allowed-tools": z.optional(z.union([z.string(), z.array(z.string())])),
49076
- license: z.optional(z.string()),
49077
- compatibility: z.optional(z.union([z.string(), z.looseObject({})])),
49078
- metadata: z.optional(z.looseObject({}))
49997
+ license: z.optional(z.unknown()),
49998
+ compatibility: z.optional(z.unknown()),
49999
+ metadata: z.optional(z.unknown())
49079
50000
  });
49080
50001
  var DeepagentsSkill = class DeepagentsSkill extends ToolSkill {
49081
50002
  constructor({ outputRoot = process.cwd(), relativeDirPath = DEEPAGENTS_SKILLS_DIR_PATH, dirName, frontmatter, body, otherFiles = [], validate = true, global = false }) {
@@ -50133,9 +51054,9 @@ var JunieSkill = class JunieSkill extends ToolSkill {
50133
51054
  const KiloSkillFrontmatterSchema = z.looseObject({
50134
51055
  name: z.string(),
50135
51056
  description: z.string(),
50136
- license: z.optional(z.string()),
50137
- compatibility: z.optional(z.union([z.string(), z.looseObject({})])),
50138
- metadata: z.optional(z.looseObject({})),
51057
+ license: z.optional(z.unknown()),
51058
+ compatibility: z.optional(z.unknown()),
51059
+ metadata: z.optional(z.unknown()),
50139
51060
  "allowed-tools": z.optional(z.array(z.string()))
50140
51061
  });
50141
51062
  var KiloSkill = class KiloSkill extends ToolSkill {
@@ -50572,10 +51493,11 @@ var KiroSkill = class KiroSkill extends ToolSkill {
50572
51493
  rootFrontmatter: rulesyncFrontmatter,
50573
51494
  section: kiroSection
50574
51495
  });
51496
+ const { name: _sectionName, description: _sectionDescription, ...section } = kiroSection ?? {};
50575
51497
  const kiroFrontmatter = {
50576
- ...kiroSection,
50577
51498
  name: rulesyncFrontmatter.name,
50578
51499
  description: rulesyncFrontmatter.description,
51500
+ ...section,
50579
51501
  ...license !== void 0 && { license },
50580
51502
  ...compatibility !== void 0 && { compatibility },
50581
51503
  ...metadata !== void 0 && { metadata }
@@ -50807,9 +51729,9 @@ var MusecodeSkill = class MusecodeSkill extends ToolSkill {
50807
51729
  const OpenCodeSkillFrontmatterSchema = z.looseObject({
50808
51730
  name: z.string(),
50809
51731
  description: z.string(),
50810
- license: z.optional(z.string()),
50811
- compatibility: z.optional(z.union([z.string(), z.looseObject({})])),
50812
- metadata: z.optional(z.looseObject({})),
51732
+ license: z.optional(z.unknown()),
51733
+ compatibility: z.optional(z.unknown()),
51734
+ metadata: z.optional(z.unknown()),
50813
51735
  "allowed-tools": z.optional(z.array(z.string()))
50814
51736
  });
50815
51737
  var OpenCodeSkill = class OpenCodeSkill extends ToolSkill {
@@ -52723,6 +53645,14 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
52723
53645
  supportsGlobal: true
52724
53646
  }
52725
53647
  }],
53648
+ ["crush", {
53649
+ class: CrushSkill,
53650
+ meta: {
53651
+ supportsProject: true,
53652
+ supportsSimulated: false,
53653
+ supportsGlobal: true
53654
+ }
53655
+ }],
52726
53656
  ["cursor", {
52727
53657
  class: CursorSkill,
52728
53658
  meta: {
@@ -60466,6 +61396,238 @@ var ClineRule = class ClineRule extends ToolRule {
60466
61396
  }
60467
61397
  };
60468
61398
  //#endregion
61399
+ //#region src/features/rules/codebuddy-rule.ts
61400
+ /**
61401
+ * Frontmatter schema for CodeBuddy Code modular rules.
61402
+ * @see https://www.codebuddy.ai/docs/cli/memory
61403
+ */
61404
+ const CodebuddyRuleFrontmatterSchema = z.object({
61405
+ description: z.optional(z.string()),
61406
+ paths: z.optional(z.array(z.string())),
61407
+ alwaysApply: z.optional(z.boolean())
61408
+ });
61409
+ /**
61410
+ * A universal glob (matching everything) is redundant on an Always Apply
61411
+ * rule and, paired with `alwaysApply: true`, is the same semantic conflict
61412
+ * `CursorRule.resolveCursorGlobs` avoids for Cursor: `alwaysApply` already
61413
+ * applies the rule everywhere, so also emitting an explicit
61414
+ * `paths: ["**\/*"]` is at best redundant and, on a subsequent
61415
+ * import/generate round-trip, misleadingly implies the rule is scoped by
61416
+ * path rather than always-on.
61417
+ */
61418
+ const UNIVERSAL_PATHS = /* @__PURE__ */ new Set(["**/*", "*"]);
61419
+ /**
61420
+ * Rule generator for CodeBuddy Code, Tencent Cloud's terminal coding agent
61421
+ * (`@tencent-ai/codebuddy-code`). Its configuration surface mirrors Claude
61422
+ * Code closely.
61423
+ *
61424
+ * Rules format:
61425
+ * - {project}/CODEBUDDY.md (root: true), also read from {project}/.codebuddy/CODEBUDDY.md
61426
+ * - {project}/.codebuddy/rules/*.md (root: false, with optional
61427
+ * `description` / `paths` / `alwaysApply` frontmatter)
61428
+ * - Global: ~/.codebuddy/CODEBUDDY.md and ~/.codebuddy/rules/*.md
61429
+ *
61430
+ * @see https://www.codebuddy.ai/docs/cli/memory
61431
+ * @see https://www.codebuddy.ai/docs/cli/codebuddy-dir
61432
+ */
61433
+ var CodebuddyRule = class CodebuddyRule extends ToolRule {
61434
+ frontmatter;
61435
+ body;
61436
+ static getSettablePaths({ global, excludeToolDir } = {}) {
61437
+ if (global) return {
61438
+ root: {
61439
+ relativeDirPath: buildToolPath(CODEBUDDY_DIR, ".", excludeToolDir),
61440
+ relativeFilePath: CODEBUDDY_RULE_FILE_NAME
61441
+ },
61442
+ nonRoot: { relativeDirPath: buildToolPath(CODEBUDDY_DIR, CODEBUDDY_RULES_DIR_NAME, excludeToolDir) }
61443
+ };
61444
+ return {
61445
+ root: {
61446
+ relativeDirPath: ".",
61447
+ relativeFilePath: CODEBUDDY_RULE_FILE_NAME
61448
+ },
61449
+ alternativeRoots: [{
61450
+ relativeDirPath: CODEBUDDY_DIR,
61451
+ relativeFilePath: CODEBUDDY_RULE_FILE_NAME
61452
+ }],
61453
+ nonRoot: { relativeDirPath: buildToolPath(CODEBUDDY_DIR, CODEBUDDY_RULES_DIR_NAME, excludeToolDir) }
61454
+ };
61455
+ }
61456
+ constructor({ frontmatter, body, ...rest }) {
61457
+ if (rest.validate) {
61458
+ const result = CodebuddyRuleFrontmatterSchema.safeParse(frontmatter);
61459
+ if (!result.success) throw new Error(`Invalid frontmatter in ${join(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}`);
61460
+ }
61461
+ super({
61462
+ ...rest,
61463
+ fileContent: rest.root ? body : CodebuddyRule.generateFileContent(body, frontmatter)
61464
+ });
61465
+ this.frontmatter = frontmatter;
61466
+ this.body = body;
61467
+ }
61468
+ static generateFileContent(body, frontmatter) {
61469
+ if (frontmatter.description === void 0 && frontmatter.paths === void 0 && frontmatter.alwaysApply === void 0) return body;
61470
+ return stringifyFrontmatter(body, {
61471
+ description: frontmatter.description,
61472
+ alwaysApply: frontmatter.alwaysApply,
61473
+ paths: frontmatter.paths
61474
+ });
61475
+ }
61476
+ static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false, relativeDirPath: overrideDirPath }) {
61477
+ const paths = this.getSettablePaths({ global });
61478
+ if (relativeFilePath === paths.root.relativeFilePath) {
61479
+ const rootDirPath = overrideDirPath ?? paths.root.relativeDirPath;
61480
+ const fileContent = await readFileContent(join(outputRoot, rootDirPath, paths.root.relativeFilePath));
61481
+ return new CodebuddyRule({
61482
+ outputRoot,
61483
+ relativeDirPath: rootDirPath,
61484
+ relativeFilePath: paths.root.relativeFilePath,
61485
+ frontmatter: {},
61486
+ body: fileContent.trim(),
61487
+ validate,
61488
+ root: true
61489
+ });
61490
+ }
61491
+ if (!paths.nonRoot) throw new Error(`nonRoot path is not set for ${relativeFilePath}`);
61492
+ const relativePath = join(paths.nonRoot.relativeDirPath, relativeFilePath);
61493
+ const filePath = join(outputRoot, relativePath);
61494
+ const { frontmatter, body: content } = parseFrontmatter(await readFileContent(filePath), filePath);
61495
+ const result = CodebuddyRuleFrontmatterSchema.safeParse(frontmatter);
61496
+ if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
61497
+ return new CodebuddyRule({
61498
+ outputRoot,
61499
+ relativeDirPath: paths.nonRoot.relativeDirPath,
61500
+ relativeFilePath,
61501
+ frontmatter: result.data,
61502
+ body: content.trim(),
61503
+ validate,
61504
+ root: false
61505
+ });
61506
+ }
61507
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
61508
+ const isRoot = relativeFilePath === this.getSettablePaths({ global }).root.relativeFilePath;
61509
+ return new CodebuddyRule({
61510
+ outputRoot,
61511
+ relativeDirPath,
61512
+ relativeFilePath,
61513
+ frontmatter: {},
61514
+ body: "",
61515
+ validate: false,
61516
+ root: isRoot
61517
+ });
61518
+ }
61519
+ static resolveCodebuddyPaths({ paths, alwaysApply }) {
61520
+ if (!paths || paths.length === 0) return;
61521
+ if (alwaysApply && paths.every((path) => UNIVERSAL_PATHS.has(path.trim()))) return;
61522
+ return paths;
61523
+ }
61524
+ static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
61525
+ const rulesyncFrontmatter = rulesyncRule.getFrontmatter();
61526
+ const root = rulesyncFrontmatter.root ?? false;
61527
+ const paths = this.getSettablePaths({ global });
61528
+ const body = rulesyncRule.getBody();
61529
+ if (root) return new CodebuddyRule({
61530
+ outputRoot,
61531
+ frontmatter: {},
61532
+ body,
61533
+ relativeDirPath: paths.root.relativeDirPath,
61534
+ relativeFilePath: paths.root.relativeFilePath,
61535
+ validate,
61536
+ root
61537
+ });
61538
+ if (!paths.nonRoot) throw new Error(`nonRoot path is not set for ${rulesyncRule.getRelativeFilePath()}`);
61539
+ const codebuddyPaths = rulesyncFrontmatter.codebuddy?.paths;
61540
+ const globs = rulesyncFrontmatter.globs;
61541
+ const alwaysApply = rulesyncFrontmatter.codebuddy?.alwaysApply;
61542
+ const pathsValue = CodebuddyRule.resolveCodebuddyPaths({
61543
+ paths: codebuddyPaths ?? (globs?.length ? globs : void 0),
61544
+ alwaysApply: alwaysApply === true
61545
+ });
61546
+ const codebuddyFrontmatter = {
61547
+ description: rulesyncFrontmatter.codebuddy?.description ?? rulesyncFrontmatter.description,
61548
+ paths: pathsValue,
61549
+ alwaysApply
61550
+ };
61551
+ return new CodebuddyRule({
61552
+ outputRoot,
61553
+ frontmatter: codebuddyFrontmatter,
61554
+ body,
61555
+ relativeDirPath: paths.nonRoot.relativeDirPath,
61556
+ relativeFilePath: rulesyncRule.getRelativeFilePath(),
61557
+ validate,
61558
+ root
61559
+ });
61560
+ }
61561
+ toRulesyncRule() {
61562
+ const targets = ["*"];
61563
+ if (this.isRoot()) {
61564
+ const rulesyncFrontmatter = {
61565
+ targets,
61566
+ root: true,
61567
+ description: this.description,
61568
+ globs: ["**/*"]
61569
+ };
61570
+ return new RulesyncRule({
61571
+ outputRoot: this.getOutputRoot(),
61572
+ frontmatter: rulesyncFrontmatter,
61573
+ body: this.body,
61574
+ relativeDirPath: RULESYNC_RULES_RELATIVE_DIR_PATH,
61575
+ relativeFilePath: this.getRelativeFilePath(),
61576
+ validate: true
61577
+ });
61578
+ }
61579
+ const isAlways = this.frontmatter.alwaysApply === true;
61580
+ const sourcePaths = this.frontmatter.paths ?? [];
61581
+ const globs = sourcePaths.length === 0 && isAlways ? ["**/*"] : sourcePaths;
61582
+ const rulesyncFrontmatter = {
61583
+ targets,
61584
+ root: false,
61585
+ description: this.frontmatter.description,
61586
+ globs,
61587
+ ...(this.frontmatter.paths !== void 0 || this.frontmatter.alwaysApply !== void 0 || this.frontmatter.description !== void 0) && { codebuddy: {
61588
+ paths: this.frontmatter.paths,
61589
+ alwaysApply: this.frontmatter.alwaysApply,
61590
+ description: this.frontmatter.description
61591
+ } }
61592
+ };
61593
+ return new RulesyncRule({
61594
+ outputRoot: this.getOutputRoot(),
61595
+ frontmatter: rulesyncFrontmatter,
61596
+ body: this.body,
61597
+ relativeDirPath: RULESYNC_RULES_RELATIVE_DIR_PATH,
61598
+ relativeFilePath: this.getRelativeFilePath(),
61599
+ validate: true
61600
+ });
61601
+ }
61602
+ validate() {
61603
+ if (!this.frontmatter) return {
61604
+ success: true,
61605
+ error: null
61606
+ };
61607
+ const result = CodebuddyRuleFrontmatterSchema.safeParse(this.frontmatter);
61608
+ if (result.success) return {
61609
+ success: true,
61610
+ error: null
61611
+ };
61612
+ else return {
61613
+ success: false,
61614
+ error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${join(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
61615
+ };
61616
+ }
61617
+ getFrontmatter() {
61618
+ return this.frontmatter;
61619
+ }
61620
+ getBody() {
61621
+ return this.body;
61622
+ }
61623
+ static isTargetedByRulesyncRule(rulesyncRule) {
61624
+ return this.isTargetedByRulesyncRuleDefault({
61625
+ rulesyncRule,
61626
+ toolTarget: "codebuddy"
61627
+ });
61628
+ }
61629
+ };
61630
+ //#endregion
60469
61631
  //#region src/features/rules/codexcli-rule.ts
60470
61632
  var CodexcliRule = class CodexcliRule extends ToolRule {
60471
61633
  constructor({ fileContent, root, ...rest }) {
@@ -60762,6 +61924,79 @@ var CopilotcliRule = class CopilotcliRule extends CopilotRule {
60762
61924
  }
60763
61925
  };
60764
61926
  //#endregion
61927
+ //#region src/features/rules/crush-rule.ts
61928
+ var CrushRule = class CrushRule extends ToolRule {
61929
+ constructor({ fileContent, root, ...rest }) {
61930
+ super({
61931
+ ...rest,
61932
+ fileContent,
61933
+ root: root ?? false
61934
+ });
61935
+ }
61936
+ static getSettablePaths({ global = false } = {}) {
61937
+ if (global) return { root: {
61938
+ relativeDirPath: CRUSH_GLOBAL_DIR,
61939
+ relativeFilePath: CRUSH_RULE_FILE_NAME
61940
+ } };
61941
+ return { root: {
61942
+ relativeDirPath: ".",
61943
+ relativeFilePath: CRUSH_RULE_FILE_NAME
61944
+ } };
61945
+ }
61946
+ static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, validate = true, global = false }) {
61947
+ const { root } = this.getSettablePaths({ global });
61948
+ const relativePath = join(root.relativeDirPath, root.relativeFilePath);
61949
+ const fileContent = await readFileContent(join(outputRoot, relativePath));
61950
+ return new CrushRule({
61951
+ outputRoot,
61952
+ relativeDirPath: root.relativeDirPath,
61953
+ relativeFilePath: root.relativeFilePath,
61954
+ fileContent,
61955
+ validate,
61956
+ root: true
61957
+ });
61958
+ }
61959
+ static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
61960
+ const { root } = this.getSettablePaths({ global });
61961
+ const isRoot = rulesyncRule.getFrontmatter().root ?? false;
61962
+ return new CrushRule({
61963
+ outputRoot,
61964
+ relativeDirPath: root.relativeDirPath,
61965
+ relativeFilePath: root.relativeFilePath,
61966
+ fileContent: rulesyncRule.getBody(),
61967
+ validate,
61968
+ root: isRoot
61969
+ });
61970
+ }
61971
+ toRulesyncRule() {
61972
+ return this.toRulesyncRuleDefault();
61973
+ }
61974
+ validate() {
61975
+ return {
61976
+ success: true,
61977
+ error: null
61978
+ };
61979
+ }
61980
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
61981
+ const { root } = this.getSettablePaths({ global });
61982
+ const isRoot = relativeFilePath === root.relativeFilePath && relativeDirPath === root.relativeDirPath;
61983
+ return new CrushRule({
61984
+ outputRoot,
61985
+ relativeDirPath,
61986
+ relativeFilePath,
61987
+ fileContent: "",
61988
+ validate: false,
61989
+ root: isRoot
61990
+ });
61991
+ }
61992
+ static isTargetedByRulesyncRule(rulesyncRule) {
61993
+ return this.isTargetedByRulesyncRuleDefault({
61994
+ rulesyncRule,
61995
+ toolTarget: "crush"
61996
+ });
61997
+ }
61998
+ };
61999
+ //#endregion
60765
62000
  //#region src/features/rules/cursor-rule.ts
60766
62001
  const CursorRuleFrontmatterSchema = z.object({
60767
62002
  description: z.optional(z.string()),
@@ -61369,13 +62604,34 @@ var DevinRule = class DevinRule extends ToolRule {
61369
62604
  };
61370
62605
  //#endregion
61371
62606
  //#region src/features/rules/factorydroid-rule.ts
62607
+ /**
62608
+ * Rule generator for Factory Droid.
62609
+ *
62610
+ * Factory Droid loads the root `AGENTS.md` (project) / `~/.factory/AGENTS.md`
62611
+ * (global) as coding guidelines, plus non-root rules referenced from it via
62612
+ * `.factory/rules/*.md`.
62613
+ *
62614
+ * Factory Droid also loads `DESIGN.md` (project only) as a second,
62615
+ * independent instruction surface: "Always-on design-system, UX, visual, and
62616
+ * interaction guidance", loaded separately from `AGENTS.md`'s coding
62617
+ * guidelines. Rulesync emits it from any non-root rule that opts in via a
62618
+ * `factorydroid.channel: design` frontmatter block — those rule bodies are
62619
+ * routed to `DESIGN.md` instead of `AGENTS.md`/`.factory/rules/*.md`, and
62620
+ * multiple opted-in rules concatenate in source order. Factory's docs describe
62621
+ * `DESIGN.md` at the repository root and in nested subdirectories, like
62622
+ * `AGENTS.md`, but document no personal/global home-directory equivalent, so
62623
+ * this channel is project scope only.
62624
+ * @see https://docs.factory.ai/cli/configuration/agents-md
62625
+ */
61372
62626
  var FactorydroidRule = class FactorydroidRule extends ToolRule {
61373
- constructor({ fileContent, root, ...rest }) {
62627
+ design;
62628
+ constructor({ fileContent, root, design = false, ...rest }) {
61374
62629
  super({
61375
62630
  ...rest,
61376
62631
  fileContent,
61377
62632
  root: root ?? false
61378
62633
  });
62634
+ this.design = design;
61379
62635
  }
61380
62636
  static getSettablePaths({ global, excludeToolDir } = {}) {
61381
62637
  if (global) return { root: {
@@ -61387,11 +62643,47 @@ var FactorydroidRule = class FactorydroidRule extends ToolRule {
61387
62643
  relativeDirPath: ".",
61388
62644
  relativeFilePath: FACTORYDROID_RULE_FILE_NAME
61389
62645
  },
61390
- nonRoot: { relativeDirPath: buildToolPath(FACTORYDROID_DIR, "rules", excludeToolDir) }
62646
+ nonRoot: { relativeDirPath: buildToolPath(FACTORYDROID_DIR, "rules", excludeToolDir) },
62647
+ design: {
62648
+ relativeDirPath: ".",
62649
+ relativeFilePath: FACTORYDROID_DESIGN_FILE_NAME
62650
+ }
61391
62651
  };
61392
62652
  }
61393
- static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
62653
+ /**
62654
+ * Extra fixed files this tool manages beyond the root/non-root rules. The
62655
+ * RulesProcessor enumerates these for import and deletion so a stale
62656
+ * `DESIGN.md` is cleaned up once no rule opts in anymore. Empty in global
62657
+ * mode: `DESIGN.md` has no documented home-directory equivalent.
62658
+ */
62659
+ static getExtraFixedFiles({ global = false } = {}) {
62660
+ if (global) return [];
62661
+ return [this.getSettablePaths({ global }).design];
62662
+ }
62663
+ /**
62664
+ * Factory Droid loads `DESIGN.md` itself, so listing it in the root rule's
62665
+ * TOON reference section would double-load the content (and misrepresent it
62666
+ * as a rule the model must remember to open).
62667
+ */
62668
+ isExcludedFromRootReferences() {
62669
+ return this.design;
62670
+ }
62671
+ static async fromFile({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, validate = true, global = false }) {
61394
62672
  const paths = this.getSettablePaths({ global });
62673
+ const design = !global ? paths.design : void 0;
62674
+ if (design !== void 0 && relativeDirPath === design.relativeDirPath && relativeFilePath === design.relativeFilePath) {
62675
+ const relativePath = join(design.relativeDirPath, design.relativeFilePath);
62676
+ const fileContent = await readFileContent(join(outputRoot, relativePath));
62677
+ return new FactorydroidRule({
62678
+ outputRoot,
62679
+ relativeDirPath: design.relativeDirPath,
62680
+ relativeFilePath: design.relativeFilePath,
62681
+ fileContent,
62682
+ validate,
62683
+ root: false,
62684
+ design: true
62685
+ });
62686
+ }
61395
62687
  if (relativeFilePath === paths.root.relativeFilePath) {
61396
62688
  const relativePath = join(paths.root.relativeDirPath, paths.root.relativeFilePath);
61397
62689
  const fileContent = await readFileContent(join(outputRoot, relativePath));
@@ -61418,18 +62710,34 @@ var FactorydroidRule = class FactorydroidRule extends ToolRule {
61418
62710
  }
61419
62711
  static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
61420
62712
  const paths = this.getSettablePaths({ global });
61421
- const isRoot = relativeFilePath === paths.root.relativeFilePath && relativeDirPath === paths.root.relativeDirPath;
62713
+ const design = !global ? paths.design : void 0;
62714
+ const isDesign = design !== void 0 && relativeDirPath === design.relativeDirPath && relativeFilePath === design.relativeFilePath;
62715
+ const isRoot = !isDesign && relativeFilePath === paths.root.relativeFilePath && relativeDirPath === paths.root.relativeDirPath;
61422
62716
  return new FactorydroidRule({
61423
62717
  outputRoot,
61424
62718
  relativeDirPath,
61425
62719
  relativeFilePath,
61426
62720
  fileContent: "",
61427
62721
  validate: false,
61428
- root: isRoot
62722
+ root: isRoot,
62723
+ design: isDesign
61429
62724
  });
61430
62725
  }
61431
62726
  static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
62727
+ const frontmatter = rulesyncRule.getFrontmatter();
61432
62728
  const paths = this.getSettablePaths({ global });
62729
+ if (!global && !frontmatter.root && frontmatter.factorydroid?.channel === "design") {
62730
+ const { design } = paths;
62731
+ return new FactorydroidRule({
62732
+ outputRoot,
62733
+ relativeDirPath: design.relativeDirPath,
62734
+ relativeFilePath: design.relativeFilePath,
62735
+ fileContent: rulesyncRule.getBody(),
62736
+ validate,
62737
+ root: false,
62738
+ design: true
62739
+ });
62740
+ }
61433
62741
  return new FactorydroidRule(this.buildToolRuleParamsAgentsmd({
61434
62742
  outputRoot,
61435
62743
  rulesyncRule,
@@ -61439,6 +62747,17 @@ var FactorydroidRule = class FactorydroidRule extends ToolRule {
61439
62747
  }));
61440
62748
  }
61441
62749
  toRulesyncRule() {
62750
+ if (this.design) return new RulesyncRule({
62751
+ outputRoot: process.cwd(),
62752
+ relativeDirPath: RULESYNC_RULES_RELATIVE_DIR_PATH,
62753
+ relativeFilePath: FACTORYDROID_DESIGN_FILE_NAME,
62754
+ frontmatter: {
62755
+ root: false,
62756
+ targets: ["factorydroid"],
62757
+ factorydroid: { channel: "design" }
62758
+ },
62759
+ body: this.getFileContent()
62760
+ });
61442
62761
  return this.toRulesyncRuleDefault();
61443
62762
  }
61444
62763
  validate() {
@@ -63956,6 +65275,16 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
63956
65275
  ruleDiscoveryMode: "auto"
63957
65276
  }
63958
65277
  }],
65278
+ ["codebuddy", {
65279
+ class: CodebuddyRule,
65280
+ meta: {
65281
+ extension: "md",
65282
+ supportsGlobal: true,
65283
+ ruleDiscoveryMode: "auto",
65284
+ localRootMode: "separate-local-file",
65285
+ localRootFileName: CODEBUDDY_LOCAL_RULE_FILE_NAME
65286
+ }
65287
+ }],
63959
65288
  ["codexcli", {
63960
65289
  class: CodexcliRule,
63961
65290
  meta: {
@@ -63981,6 +65310,15 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
63981
65310
  ruleDiscoveryMode: "auto"
63982
65311
  }
63983
65312
  }],
65313
+ ["crush", {
65314
+ class: CrushRule,
65315
+ meta: {
65316
+ extension: "md",
65317
+ supportsGlobal: true,
65318
+ ruleDiscoveryMode: "auto",
65319
+ collisionPolicy: "fold"
65320
+ }
65321
+ }],
63984
65322
  ["cursor", {
63985
65323
  class: CursorRule,
63986
65324
  meta: {
@@ -64349,6 +65687,10 @@ var RulesProcessor = class extends FeatureProcessor {
64349
65687
  outputFiles,
64350
65688
  convertedRules
64351
65689
  });
65690
+ await this.warnForDeactivatedImportOnlyRoots({
65691
+ toolRules,
65692
+ factory
65693
+ });
64352
65694
  return outputFiles;
64353
65695
  }
64354
65696
  /**
@@ -64570,6 +65912,41 @@ var RulesProcessor = class extends FeatureProcessor {
64570
65912
  }
64571
65913
  }
64572
65914
  /**
65915
+ * Warn when this generate run is about to write a root rule file that will
65916
+ * make the tool stop reading paths it currently reads instead — Junie's
65917
+ * `.junie/rules/*.md` and `.junie/playbook.md` become unreachable the
65918
+ * moment `.junie/AGENTS.md` exists, since Junie reads the root file
65919
+ * exclusively once it is present. `importOnlyRoots` with
65920
+ * `onlyWhenRootAbsent` already models exactly this shape for import; this
65921
+ * reuses the same declaration so the
65922
+ * `generate` path — which never calls `loadToolFiles` and so never reached
65923
+ * the existing import-side warning — surfaces it too. Without this, a repo
65924
+ * that only ever runs `generate` never sees any warning: the deactivated
65925
+ * files stay on disk, untouched and not gitignored, silently unread.
65926
+ */
65927
+ async warnForDeactivatedImportOnlyRoots({ toolRules, factory }) {
65928
+ const rootRule = toolRules.find((rule) => rule.isRoot());
65929
+ if (!rootRule) return;
65930
+ const settablePaths = factory.class.getSettablePaths({ global: this.global });
65931
+ const importOnlyRoots = "importOnlyRoots" in settablePaths ? settablePaths.importOnlyRoots : void 0;
65932
+ if (!importOnlyRoots || importOnlyRoots.length === 0) return;
65933
+ const existingPaths = [];
65934
+ for (const importOnlyRoot of importOnlyRoots) {
65935
+ if (importOnlyRoot.onlyWhenRootAbsent !== true) continue;
65936
+ const matchedPaths = await findFilesByGlobs(rootRelativeGlob(importOnlyRoot.relativeDirPath, importOnlyRoot.relativeFilePath ?? `*.${factory.meta.extension}`), {
65937
+ cwd: this.outputRoot,
65938
+ type: "file"
65939
+ });
65940
+ existingPaths.push(...matchedPaths);
65941
+ }
65942
+ if (existingPaths.length === 0) return;
65943
+ const rootFileRelativePath = join(rootRule.getRelativeDirPath(), rootRule.getRelativeFilePath());
65944
+ const names = existingPaths.map((filePath) => stripControlCharacters(relative(this.outputRoot, filePath)));
65945
+ const listedNames = names.slice(0, MAX_LISTED_SKIPPED_IMPORT_ONLY_PATHS);
65946
+ const remainingCount = names.length - listedNames.length;
65947
+ this.logger.warn(`Writing ${stripControlCharacters(rootFileRelativePath)} for ${this.toolTarget} means ${listedNames.join(", ")}${remainingCount > 0 ? ` and ${remainingCount} more` : ""} will no longer be read. Run \`rulesync import --targets ${this.toolTarget}\` first to carry that content into ${RULESYNC_RULES_RELATIVE_DIR_PATH}, or delete ${listedNames.length === 1 ? "it" : "them"} once you have checked the content is already in the root file.`);
65948
+ }
65949
+ /**
64573
65950
  * Handle localRoot rule generation based on tool target.
64574
65951
  * - `separate-local-file`: writes a dedicated `*.local.md` root file
64575
65952
  * (claudecode/legacy: `./CLAUDE.local.md`, rovodev: `./AGENTS.local.md`)
@@ -64821,6 +66198,26 @@ As this project's AI coding tool, you must follow the additional conventions bel
64821
66198
  }));
64822
66199
  }
64823
66200
  /**
66201
+ * Load and merge rulesync rule files from every configured input root's
66202
+ * `.rulesync/rules/` directory, by relative path, so that a rule with the
66203
+ * same target path from a later root replaces the earlier root's copy
66204
+ * (case-insensitive, matching the intra-root collision handling).
66205
+ *
66206
+ * This is the side-effect-free half of `loadRulesyncFiles`: it does not
66207
+ * warn about a missing root rule or validate `localRoot` placement, so it
66208
+ * is also safe to call from code paths — like
66209
+ * `warnForFoldImportDuplicationRisk` — that only need the merged rule set
66210
+ * and must not trigger `loadRulesyncFiles`'s generate-time checks.
66211
+ */
66212
+ async loadMergedRulesyncRules() {
66213
+ return mergeByCaseInsensitiveIdentity({
66214
+ perRoot: await Promise.all(this.inputRoots.map((root) => this.loadRulesyncFilesForRoot(root))),
66215
+ identity: (rule) => rule.getRelativeFilePath(),
66216
+ artifactName: "rule",
66217
+ logger: this.logger
66218
+ });
66219
+ }
66220
+ /**
64824
66221
  * Implementation of abstract method from FeatureProcessor
64825
66222
  * Load and parse rulesync rule files from every configured input root's
64826
66223
  * `.rulesync/rules/` directory, merging by relative path so that a rule
@@ -64828,12 +66225,7 @@ As this project's AI coding tool, you must follow the additional conventions bel
64828
66225
  * copy (case-insensitive, matching the intra-root collision handling).
64829
66226
  */
64830
66227
  async loadRulesyncFiles() {
64831
- const rulesyncRules = mergeByCaseInsensitiveIdentity({
64832
- perRoot: await Promise.all(this.inputRoots.map((root) => this.loadRulesyncFilesForRoot(root))),
64833
- identity: (rule) => rule.getRelativeFilePath(),
64834
- artifactName: "rule",
64835
- logger: this.logger
64836
- });
66228
+ const rulesyncRules = await this.loadMergedRulesyncRules();
64837
66229
  const factory = this.getFactory(this.toolTarget);
64838
66230
  const targetedRootRules = rulesyncRules.filter((rule) => rule.getFrontmatter().root).filter((rule) => factory.class.isTargetedByRulesyncRule(rule));
64839
66231
  if (targetedRootRules.length === 0 && rulesyncRules.length > 0) this.logger.warn(`No root rulesync rule file found for target '${this.toolTarget}'. Consider adding 'root: true' to one of your rule files in ${RULESYNC_RULES_RELATIVE_DIR_PATH}.`);
@@ -64890,6 +66282,46 @@ As this project's AI coding tool, you must follow the additional conventions bel
64890
66282
  });
64891
66283
  }
64892
66284
  /**
66285
+ * Warn when importing a `collisionPolicy: "fold"` target's root file while
66286
+ * `.rulesync/rules/` still holds non-root rules targeting it. A fold target
66287
+ * (codexcli, junie, and others) concatenates every targeted non-root rule
66288
+ * into its one root output file on `generate`. Importing that root file
66289
+ * back therefore re-reads the already-folded content as a single new
66290
+ * rulesync rule, while the original non-root rules stay in place
66291
+ * untouched — the next `generate` folds both together, duplicating the
66292
+ * content once per generate/import cycle with nothing to indicate why.
66293
+ *
66294
+ * This does not attempt to detect or drop the specific duplicated content
66295
+ * (the root file has no marker recording which rule contributed what); it
66296
+ * only surfaces that the cycle produces one, per the "at minimum, warn"
66297
+ * option recorded on issue #2743.
66298
+ *
66299
+ * Only the actual `rulesync import` call site invokes this (and only once
66300
+ * it has confirmed there is something to import) — `loadToolFiles` is also
66301
+ * the entry point for `rulesync convert` and `rulesync fetch`, neither of
66302
+ * which writes to `.rulesync/rules/` or carries this duplication risk.
66303
+ *
66304
+ * Reads via `loadMergedRulesyncRules` rather than `loadRulesyncFiles`
66305
+ * deliberately: this runs before the imported root file is written, so
66306
+ * `.rulesync/rules/` never yet has a root rule targeting this tool, and
66307
+ * `loadRulesyncFiles`'s "no root rule found" warning and `localRoot`
66308
+ * validation (which can throw) would fire spuriously on every fold-tool
66309
+ * import — including ones where nothing is actually misconfigured.
66310
+ *
66311
+ * In global mode, a `localRoot: true` rule is excluded from the
66312
+ * duplication check the same way `loadRulesyncFiles`'s global-mode branch
66313
+ * excludes it from `nonRootRules`: `generate` ignores `localRoot` entirely
66314
+ * in global mode, so such a rule is never actually folded into the global
66315
+ * root output and warning about it here would be inaccurate.
66316
+ */
66317
+ async warnForFoldImportDuplicationRisk() {
66318
+ const factory = this.getFactory(this.toolTarget);
66319
+ if (factory.meta.collisionPolicy !== "fold") return;
66320
+ const nonRootRules = (await this.loadMergedRulesyncRules()).filter((rule) => !rule.getFrontmatter().root && (!this.global || !rule.getFrontmatter().localRoot) && factory.class.isTargetedByRulesyncRule(rule));
66321
+ if (nonRootRules.length === 0) return;
66322
+ this.logger.warn(`Importing ${this.toolTarget}'s root file will re-add content already folded from ${formatRulePaths(nonRootRules)}: ${this.toolTarget} concatenates every non-root rule into its single root output file, so the imported copy duplicates them the next time you run \`rulesync generate --targets ${this.toolTarget}\`. Review the imported rule and remove the duplicated content, or remove the original non-root rule files, before generating again.`);
66323
+ }
66324
+ /**
64893
66325
  * Implementation of abstract method from FeatureProcessor
64894
66326
  * Load tool-specific rule configurations and parse them into ToolRule instances
64895
66327
  */
@@ -66953,6 +68385,7 @@ async function importRulesCore(params) {
66953
68385
  logger.warn(`No rule files found for ${tool}. Skipping import.`);
66954
68386
  return 0;
66955
68387
  }
68388
+ await rulesProcessor.warnForFoldImportDuplicationRisk();
66956
68389
  const rulesyncFiles = await rulesProcessor.convertToolFilesToRulesyncFiles(toolFiles);
66957
68390
  const { count: writtenCount } = await rulesProcessor.writeAiFiles(rulesyncFiles);
66958
68391
  if (config.getVerbose() && writtenCount > 0) logger.success(`Created ${writtenCount} rule files`);
@@ -67200,6 +68633,6 @@ async function importChecksCore(params) {
67200
68633
  return writtenCount;
67201
68634
  }
67202
68635
  //#endregion
67203
- export { loadYaml as $, ToolTargetSchema as $t, caseFoldIdentity as A, ALL_FEATURES as An, getHomeDirectory as At, RulesyncRuleFrontmatterSchema as B, removeDirectory as Bt, CLAUDECODE_DIR as C, RULESYNC_PERMISSIONS_SCHEMA_URL as Cn, assertWritablePathInsideRoot as Ct, CLAUDECODE_SKILLS_DIR_PATH as D, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as Dn, ensureDir as Dt, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as E, RULESYNC_SKILLS_RELATIVE_DIR_PATH as En, directoryExists as Et, RulesyncSubagent as F, hasDeceptiveHiddenCharacters as Fn, listFilePathsRecursively as Ft, getRulesyncSourceCandidates as G, resolvePath as Gt, RulesyncMcp as H, removeFile as Ht, RulesyncSubagentFrontmatterSchema as I, quoteForLog as In, listSubdirectoryNames as It, RulesyncCommand as J, writeFileBuffer as Jt, resolveRulesyncSourceWritePath as K, runWithDirectoryRollback as Kt, RulesyncSkill as L, stripControlCharacters as Ln, pathEscapesRoot as Lt, AUGMENTCODE_DIR as M, DEPRECATED_FEATURE_REPLACEMENTS as Mn, isFileSystemError as Mt, AUGMENTCODE_SETTINGS_LOCAL_FILE_NAME as N, formatError as Nn, isSymlink as Nt, FACTORYDROID_DIR as O, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as On, fileExists as Ot, getLocalSkillDirNames as P, truncateText as Pn, listDirectoryEntryNames as Pt, stringifyFrontmatter as Q, PACKAGING_TOOL_TARGETS as Qt, RulesyncSkillFrontmatterSchema as R, stripControlCharactersKeepingLineFeeds as Rn, readFileContent as Rt, CODEXCLI_DIR as S, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as Sn, assertTreeContainsNoSymlinks as St, CLAUDECODE_MEMORIES_DIR_NAME as T, RULESYNC_RULES_RELATIVE_DIR_PATH as Tn, createTempDirectory as Tt, RulesyncIgnore as U, removeFileStrict as Ut, RulesyncPermissions as V, removeDirectoryStrict as Vt, RulesyncHooks as W, removeTempDirectory as Wt, RulesyncCheck as X, ALL_TOOL_TARGETS as Xt, RulesyncCommandFrontmatterSchema as Y, writeFileContent as Yt, RulesyncCheckFrontmatterSchema as Z, ALL_TOOL_TARGETS_WITH_WILDCARD as Zt, CommandsProcessor as _, RULESYNC_MCP_RELATIVE_FILE_PATH as _n, withWarnOnceScope as _t, getProcessorRegistryEntry as a, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as an, CONFLICTING_TARGET_PAIRS as at, ChecksProcessor as b, RULESYNC_PERMISSIONS_FILE_NAME as bn, applyFileMode as bt, RulesProcessor as c, RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH as cn, SourceEntrySchema as ct, ELLIPSIS_WIDTH as d, RULESYNC_HOOKS_LEGACY_FILE_NAME as dn, JsonLogger as dt, CURATED_RULES_FEATURE_SUBDIR as en, SHARED_USER_MANAGED_CONFIG_PATHS as et, displayWidthOf as f, RULESYNC_HOOKS_RELATIVE_FILE_PATH as fn, WarningCollectingLogger as ft, HooksProcessor as g, RULESYNC_MCP_LEGACY_FILE_NAME as gn, resetRunWarningState as gt, IgnoreProcessor as h, RULESYNC_MCP_FILE_NAME as hn, withFallbackLoggerTarget as ht, inspectInputRoots as i, RULESYNC_CHECKS_RELATIVE_DIR_PATH as in, resolveEffectiveInputRoots as it, groupSpellingsByCaseFoldedIdentity as j, ALL_FEATURES_WITH_WILDCARD as jn, isFileNotFoundError as jt, FACTORYDROID_SETTINGS_LOCAL_FILE_NAME as k, parseCommaSeparatedList as kn, getFileSize as kt, SubagentsProcessor as l, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as ln, findControlCharacter as lt, McpProcessor as m, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as mn, warnOnConflictingFlags as mt, formatSourceLoadFailure as n, RULESYNC_AIIGNORE_FILE_NAME as nn, ConfigResolver as nt, convertFromTool as o, RULESYNC_CONFIG_RELATIVE_FILE_PATH as on, ConfigFileSchema as ot, shortenToWidth as p, RULESYNC_IGNORE_RELATIVE_FILE_PATH as pn, fallbackLogger as pt, parseJsonc as q, toPosixPath as qt, generate as r, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as rn, mergeInputRootConfigs as rt, isPackagingToolTarget as s, RULESYNC_CONFIG_SCHEMA_URL as sn, GITIGNORE_DESTINATION_KEY as st, importFromTool as t, MAX_FILE_SIZE as tn, SKILL_FILE_NAME as tt, SkillsProcessor as u, RULESYNC_HOOKS_FILE_NAME as un, ConsoleLogger as ut, QWENCODE_DIR as v, RULESYNC_MCP_SCHEMA_URL as vn, CLIError as vt, CLAUDECODE_LOCAL_RULE_FILE_NAME as w, RULESYNC_RELATIVE_DIR_PATH as wn, checkPathTraversal as wt, CODEXCLI_BASH_RULES_FILE_NAME as x, RULESYNC_PERMISSIONS_LEGACY_FILE_NAME as xn, assertDirectoryIfExists as xt, QWENCODE_LOCAL_RULE_FILE_NAME as y, RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH as yn, ErrorCodes as yt, RulesyncRule as z, stripHiddenCharacters as zn, readFileContentOrNull as zt };
68636
+ export { RulesyncCheckFrontmatterSchema as $, ALL_TOOL_TARGETS_WITH_WILDCARD as $t, FACTORYDROID_DIR as A, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as An, fileExists as At, RulesyncSkillFrontmatterSchema as B, stripControlCharacters as Bn, readFileContent as Bt, CODEXCLI_BASH_RULES_FILE_NAME as C, RULESYNC_PERMISSIONS_LEGACY_FILE_NAME as Cn, assertDirectoryIfExists as Ct, CLAUDECODE_MEMORIES_DIR_NAME as D, RULESYNC_RULES_RELATIVE_DIR_PATH as Dn, createTempDirectory as Dt, CLAUDECODE_LOCAL_RULE_FILE_NAME as E, RULESYNC_RELATIVE_DIR_PATH as En, checkPathTraversal as Et, AUGMENTCODE_SETTINGS_LOCAL_FILE_NAME as F, formatError as Fn, isSymlink as Ft, RulesyncIgnore as G, removeFileStrict as Gt, RulesyncRuleFrontmatterSchema as H, stripHiddenCharacters as Hn, removeDirectory as Ht, getLocalSkillDirNames as I, truncateText as In, listDirectoryEntryNames as It, resolveRulesyncSourceWritePath as J, runWithDirectoryRollback as Jt, RulesyncHooks as K, removeTempDirectory as Kt, RulesyncSubagent as L, hasDeceptiveHiddenCharacters as Ln, listFilePathsRecursively as Lt, caseFoldIdentity as M, ALL_FEATURES as Mn, getHomeDirectory as Mt, groupSpellingsByCaseFoldedIdentity as N, ALL_FEATURES_WITH_WILDCARD as Nn, isFileNotFoundError as Nt, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as O, RULESYNC_SKILLS_RELATIVE_DIR_PATH as On, directoryExists as Ot, AUGMENTCODE_DIR as P, DEPRECATED_FEATURE_REPLACEMENTS as Pn, isFileSystemError as Pt, RulesyncCheck as Q, ALL_TOOL_TARGETS as Qt, RulesyncSubagentFrontmatterSchema as R, hasEnclosingMarkOutsideKeycap as Rn, listSubdirectoryNames as Rt, ChecksProcessor as S, RULESYNC_PERMISSIONS_FILE_NAME as Sn, applyFileMode as St, CLAUDECODE_DIR as T, RULESYNC_PERMISSIONS_SCHEMA_URL as Tn, assertWritablePathInsideRoot as Tt, RulesyncPermissions as U, removeDirectoryStrict as Ut, RulesyncRule as V, stripControlCharactersKeepingLineFeeds as Vn, readFileContentOrNull as Vt, RulesyncMcp as W, removeFile as Wt, RulesyncCommand as X, writeFileBuffer as Xt, parseJsonc as Y, toPosixPath as Yt, RulesyncCommandFrontmatterSchema as Z, writeFileContent as Zt, IgnoreProcessor as _, RULESYNC_MCP_FILE_NAME as _n, withFallbackLoggerTarget as _t, getProcessorRegistryEntry as a, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as an, mergeInputRootConfigs as at, QWENCODE_DIR as b, RULESYNC_MCP_SCHEMA_URL as bn, CLIError as bt, RulesProcessor as c, RULESYNC_CONFIG_RELATIVE_FILE_PATH as cn, ConfigFileSchema as ct, CODEBUDDY_DIR as d, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as dn, findControlCharacter as dt, PACKAGING_TOOL_TARGETS as en, stringifyFrontmatter as et, CODEBUDDY_LOCAL_RULE_FILE_NAME as f, RULESYNC_HOOKS_FILE_NAME as fn, ConsoleLogger as ft, McpProcessor as g, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as gn, warnOnConflictingFlags as gt, shortenToWidth as h, RULESYNC_IGNORE_RELATIVE_FILE_PATH as hn, fallbackLogger as ht, inspectInputRoots as i, RULESYNC_AIIGNORE_FILE_NAME as in, ConfigResolver as it, FACTORYDROID_SETTINGS_LOCAL_FILE_NAME as j, parseCommaSeparatedList as jn, getFileSize as jt, CLAUDECODE_SKILLS_DIR_PATH as k, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as kn, ensureDir as kt, SubagentsProcessor as l, RULESYNC_CONFIG_SCHEMA_URL as ln, GITIGNORE_DESTINATION_KEY as lt, displayWidthOf as m, RULESYNC_HOOKS_RELATIVE_FILE_PATH as mn, WarningCollectingLogger as mt, formatSourceLoadFailure as n, CURATED_RULES_FEATURE_SUBDIR as nn, SHARED_USER_MANAGED_CONFIG_PATHS as nt, convertFromTool as o, RULESYNC_CHECKS_RELATIVE_DIR_PATH as on, resolveEffectiveInputRoots as ot, ELLIPSIS_WIDTH as p, RULESYNC_HOOKS_LEGACY_FILE_NAME as pn, JsonLogger as pt, getRulesyncSourceCandidates as q, resolvePath as qt, generate as r, MAX_FILE_SIZE as rn, SKILL_FILE_NAME as rt, isPackagingToolTarget as s, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as sn, CONFLICTING_TARGET_PAIRS as st, importFromTool as t, ToolTargetSchema as tn, loadYaml as tt, SkillsProcessor as u, RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH as un, SourceEntrySchema as ut, HooksProcessor as v, RULESYNC_MCP_LEGACY_FILE_NAME as vn, resetRunWarningState as vt, CODEXCLI_DIR as w, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as wn, assertTreeContainsNoSymlinks as wt, QWENCODE_LOCAL_RULE_FILE_NAME as x, RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH as xn, ErrorCodes as xt, CommandsProcessor as y, RULESYNC_MCP_RELATIVE_FILE_PATH as yn, withWarnOnceScope as yt, RulesyncSkill as z, quoteForLog as zn, pathEscapesRoot as zt };
67204
68637
 
67205
- //# sourceMappingURL=import-5_n-Y8N6.js.map
68638
+ //# sourceMappingURL=import-BKqbq4Ut.js.map