rulesync 32.0.0 → 34.0.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.
@@ -408,7 +408,11 @@ const DeprecatedIgnoreFeatureSchema = zod_mini.z.literal("ignore").check((0, zod
408
408
  const FeatureSchema = zod_mini.z.union([zod_mini.z.enum(ACTIVE_FEATURES), DeprecatedIgnoreFeatureSchema]);
409
409
  zod_mini.z.array(FeatureSchema);
410
410
  const FeatureWithWildcardSchema = zod_mini.z.union([zod_mini.z.enum([...ACTIVE_FEATURES, "*"]), DeprecatedIgnoreFeatureSchema]);
411
- const GitignoreDestinationSchema = zod_mini.z.enum(["gitignore", "gitattributes"]);
411
+ const GitignoreDestinationSchema = zod_mini.z.enum([
412
+ "gitignore",
413
+ "gitattributes",
414
+ "gitexclude"
415
+ ]);
412
416
  const FlattenedCommandNamingSchema = zod_mini.z.enum(["basename", "path"]);
413
417
  const FeatureOptionsSchema = zod_mini.z.record(zod_mini.z.string(), zod_mini.z.unknown());
414
418
  const FeatureValueSchema = zod_mini.z.union([
@@ -453,36 +457,36 @@ const isFeatureValueEnabled = (value) => {
453
457
  const parseCommaSeparatedList = (value) => value.split(",").map((s) => s.trim()).filter(Boolean);
454
458
  //#endregion
455
459
  //#region src/constants/rulesync-paths.ts
456
- const { join: join$417 } = node_path.posix;
460
+ const { join: join$418 } = node_path.posix;
457
461
  const RULESYNC_CONFIG_RELATIVE_FILE_PATH = "rulesync.jsonc";
458
462
  const RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH = "rulesync.local.jsonc";
459
463
  const RULESYNC_USER_CONFIG_DIR_NAME = "rulesync";
460
464
  const RULESYNC_XDG_CONFIG_HOME_DEFAULT_DIR_NAME = ".config";
461
465
  const RULESYNC_RELATIVE_DIR_PATH = ".rulesync";
462
466
  const RULES_FEATURE_SUBDIR = "rules";
463
- const CURATED_RULES_FEATURE_SUBDIR = join$417(RULES_FEATURE_SUBDIR, ".curated");
467
+ const CURATED_RULES_FEATURE_SUBDIR = join$418(RULES_FEATURE_SUBDIR, ".curated");
464
468
  const COMMANDS_FEATURE_SUBDIR = "commands";
465
469
  const SUBAGENTS_FEATURE_SUBDIR = "subagents";
466
470
  const CHECKS_FEATURE_SUBDIR = "checks";
467
471
  const SKILLS_FEATURE_SUBDIR = "skills";
468
- const CURATED_SKILLS_FEATURE_SUBDIR = join$417(SKILLS_FEATURE_SUBDIR, ".curated");
469
- const RULESYNC_RULES_RELATIVE_DIR_PATH = join$417(RULESYNC_RELATIVE_DIR_PATH, RULES_FEATURE_SUBDIR);
470
- const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$417(RULESYNC_RELATIVE_DIR_PATH, CURATED_RULES_FEATURE_SUBDIR);
471
- const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$417(RULESYNC_RELATIVE_DIR_PATH, COMMANDS_FEATURE_SUBDIR);
472
- const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$417(RULESYNC_RELATIVE_DIR_PATH, SUBAGENTS_FEATURE_SUBDIR);
473
- const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$417(RULESYNC_RELATIVE_DIR_PATH, CHECKS_FEATURE_SUBDIR);
474
- const RULESYNC_MCP_RELATIVE_FILE_PATH = join$417(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
475
- const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$417(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
476
- const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$417(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
477
- join$417(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
478
- const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$417(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
479
- const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$417(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
472
+ const CURATED_SKILLS_FEATURE_SUBDIR = join$418(SKILLS_FEATURE_SUBDIR, ".curated");
473
+ const RULESYNC_RULES_RELATIVE_DIR_PATH = join$418(RULESYNC_RELATIVE_DIR_PATH, RULES_FEATURE_SUBDIR);
474
+ const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$418(RULESYNC_RELATIVE_DIR_PATH, CURATED_RULES_FEATURE_SUBDIR);
475
+ const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$418(RULESYNC_RELATIVE_DIR_PATH, COMMANDS_FEATURE_SUBDIR);
476
+ const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$418(RULESYNC_RELATIVE_DIR_PATH, SUBAGENTS_FEATURE_SUBDIR);
477
+ const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$418(RULESYNC_RELATIVE_DIR_PATH, CHECKS_FEATURE_SUBDIR);
478
+ const RULESYNC_MCP_RELATIVE_FILE_PATH = join$418(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
479
+ const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$418(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
480
+ const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$418(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
481
+ join$418(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
482
+ const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$418(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
483
+ const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$418(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
480
484
  const RULESYNC_AIIGNORE_FILE_NAME = ".aiignore";
481
- const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$417(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
485
+ const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$418(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
482
486
  const RULESYNC_IGNORE_RELATIVE_FILE_PATH = ".rulesyncignore";
483
487
  const RULESYNC_OVERVIEW_FILE_NAME = "overview.md";
484
- const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$417(RULESYNC_RELATIVE_DIR_PATH, SKILLS_FEATURE_SUBDIR);
485
- const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$417(RULESYNC_RELATIVE_DIR_PATH, CURATED_SKILLS_FEATURE_SUBDIR);
488
+ const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$418(RULESYNC_RELATIVE_DIR_PATH, SKILLS_FEATURE_SUBDIR);
489
+ const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$418(RULESYNC_RELATIVE_DIR_PATH, CURATED_SKILLS_FEATURE_SUBDIR);
486
490
  const RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync.lock";
487
491
  const RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync-npm.lock.json";
488
492
  const RULESYNC_MCP_FILE_NAME = "mcp.jsonc";
@@ -528,6 +532,7 @@ const rulesProcessorToolTargetTuple = [
528
532
  "claudecode-legacy",
529
533
  "cline",
530
534
  "codebuddy",
535
+ "codebuff",
531
536
  "codewhale",
532
537
  "codexcli",
533
538
  "commandcode",
@@ -1407,6 +1412,13 @@ async function getFileSize(filepath) {
1407
1412
  throw new Error(`Failed to get file size for "${filepath}": ${formatError(error)}`, { cause: error });
1408
1413
  }
1409
1414
  }
1415
+ /**
1416
+ * Whether a file carries any execute bit. Windows reports none, so a file
1417
+ * there is never seen as executable.
1418
+ */
1419
+ async function hasExecutableBit(filepath) {
1420
+ return ((await (0, node_fs_promises.stat)(filepath)).mode & 73) !== 0;
1421
+ }
1410
1422
  async function isSymlink(filepath) {
1411
1423
  try {
1412
1424
  return (await (0, node_fs_promises.lstat)(filepath)).isSymbolicLink();
@@ -3179,6 +3191,9 @@ var Config = class Config {
3179
3191
  const featureValue = value[feature];
3180
3192
  if (featureValue && typeof featureValue === "object" && isFeatureValueEnabled(featureValue)) return featureValue;
3181
3193
  }
3194
+ getRootGitignoreDestination() {
3195
+ return this.gitignoreDestination;
3196
+ }
3182
3197
  getGitignoreDestination(target, feature) {
3183
3198
  const rootLevel = this.gitignoreDestination;
3184
3199
  if (!isRulesyncConfigTargetsObject(this.targets)) return rootLevel;
@@ -3196,7 +3211,8 @@ var Config = class Config {
3196
3211
  return toolLevel ?? rootLevel;
3197
3212
  }
3198
3213
  static parseGitignoreDestination(value) {
3199
- if (value === "gitignore" || value === "gitattributes") return value;
3214
+ const result = GitignoreDestinationSchema.safeParse(value);
3215
+ return result.success ? result.data : void 0;
3200
3216
  }
3201
3217
  /**
3202
3218
  * Check if per-target features configuration is being used.
@@ -5266,12 +5282,20 @@ const JUNIE_HOOK_EVENTS = [
5266
5282
  "sessionEnd"
5267
5283
  ];
5268
5284
  /**
5269
- * Hook events supported by the GitLab Duo CLI. Only `SessionStart` is
5270
- * documented; its optional regex matcher filters on the session source
5271
- * (`startup`, `resume`). Only `type: "command"` hooks are supported.
5285
+ * Hook events supported by the GitLab Duo CLI. `SessionStart` is documented;
5286
+ * `PreToolUse` and `SessionEnd` ship in gitlab-lsp (`HOOK_EVENT_NAMES`, v9.28.0+)
5287
+ * but are not on docs.gitlab.com yet. The optional regex matcher filters on the
5288
+ * session source (`startup`, `resume`) for `SessionStart`, on `tool_name` for
5289
+ * `PreToolUse`, and on the end reason for `SessionEnd`. Only `type: "command"`
5290
+ * hooks are supported.
5272
5291
  * @see https://docs.gitlab.com/user/gitlab_duo_cli/customize/
5292
+ * @see https://gitlab.com/gitlab-org/editor-extensions/gitlab-lsp/-/blob/main/packages/core/hooks/src/schema.ts
5273
5293
  */
5274
- const GITLABDUO_HOOK_EVENTS = ["sessionStart"];
5294
+ const GITLABDUO_HOOK_EVENTS = [
5295
+ "sessionStart",
5296
+ "preToolUse",
5297
+ "sessionEnd"
5298
+ ];
5275
5299
  /**
5276
5300
  * Hook events supported by Qwen Code.
5277
5301
  *
@@ -5612,7 +5636,10 @@ const CORTEXCODE_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CA
5612
5636
  * or `agent` hooks with a `timeout` in seconds. The hooks reference documents ten events; the
5613
5637
  * plugins reference lists the full set, stating that plugin hooks "respond to
5614
5638
  * the same lifecycle events as user-defined hooks", so every event listed
5615
- * there is supported here.
5639
+ * there is supported here. The hooks reference also names `Setup`
5640
+ * (bootstrap/maintenance), and the published package's settings schema
5641
+ * (`@tencent-ai/codebuddy-code` 2.167.0) accepts `UserPromptExpansion`, whose
5642
+ * matcher filters on `command_name` as in Claude Code.
5616
5643
  *
5617
5644
  * @see https://www.codebuddy.ai/docs/cli/hooks
5618
5645
  * @see https://www.codebuddy.ai/docs/cli/plugins-reference
@@ -5643,7 +5670,9 @@ const CODEBUDDY_HOOK_EVENTS = [
5643
5670
  "postCompact",
5644
5671
  "elicitation",
5645
5672
  "elicitationResult",
5646
- "sessionEnd"
5673
+ "sessionEnd",
5674
+ "setup",
5675
+ "userPromptExpansion"
5647
5676
  ];
5648
5677
  /**
5649
5678
  * CodeBuddy Code events that fire on every occurrence and take no `matcher`.
@@ -5692,7 +5721,9 @@ const CANONICAL_TO_CODEBUDDY_EVENT_NAMES = {
5692
5721
  postCompact: "PostCompact",
5693
5722
  elicitation: "Elicitation",
5694
5723
  elicitationResult: "ElicitationResult",
5695
- sessionEnd: "SessionEnd"
5724
+ sessionEnd: "SessionEnd",
5725
+ setup: "Setup",
5726
+ userPromptExpansion: "UserPromptExpansion"
5696
5727
  };
5697
5728
  const CODEBUDDY_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_CODEBUDDY_EVENT_NAMES).map(([k, v]) => [v, k]));
5698
5729
  /**
@@ -6546,7 +6577,11 @@ const KIRO_IDE_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANO
6546
6577
  /**
6547
6578
  * Map canonical camelCase event names to GitLab Duo CLI PascalCase.
6548
6579
  */
6549
- const CANONICAL_TO_GITLABDUO_EVENT_NAMES = { sessionStart: "SessionStart" };
6580
+ const CANONICAL_TO_GITLABDUO_EVENT_NAMES = {
6581
+ sessionStart: "SessionStart",
6582
+ preToolUse: "PreToolUse",
6583
+ sessionEnd: "SessionEnd"
6584
+ };
6550
6585
  /**
6551
6586
  * Map GitLab Duo CLI PascalCase event names to canonical camelCase.
6552
6587
  */
@@ -8287,6 +8322,48 @@ const ReasonixPermissionsOverrideSchema = zod_mini.z.looseObject({
8287
8322
  ]))
8288
8323
  });
8289
8324
  /**
8325
+ * Tool-scoped override block for Qoder. `outputStyle` is written to the
8326
+ * top-level `outputStyle` key of `.qoder/settings.json` (project) /
8327
+ * `~/.qoder/settings.json` (global): the name of the active output style, a
8328
+ * custom style from `.qoder/output-styles/` (see the `qoder.channel:
8329
+ * output-style` rule option) or a `plugin-name:style-name` reference. The
8330
+ * shared `permission` block continues to drive `permissions.allow`/`ask`/`deny`.
8331
+ *
8332
+ * @see https://docs.qoder.com/cli/output-styles
8333
+ * @see https://docs.qoder.com/cli/settings-reference
8334
+ * @example
8335
+ * { "outputStyle": "concise-review" }
8336
+ */
8337
+ const QoderPermissionsOverrideSchema = zod_mini.z.looseObject({
8338
+ permission: zod_mini.z.optional(ToolScopedPermissionSchema),
8339
+ outputStyle: zod_mini.z.optional(zod_mini.z.string().check(zod_mini.z.trim(), zod_mini.z.minLength(1)))
8340
+ });
8341
+ /**
8342
+ * One native Factory Droid permission rule. `match.prefix` positions are either
8343
+ * a single token or a non-empty list of alternative tokens.
8344
+ *
8345
+ * @see https://docs.factory.com/autonomy-and-safety/permission-rules
8346
+ */
8347
+ const FactorydroidPermissionRuleSchema = zod_mini.z.looseObject({
8348
+ id: zod_mini.z.string(),
8349
+ decision: zod_mini.z.enum([
8350
+ "allow",
8351
+ "ask",
8352
+ "block"
8353
+ ]),
8354
+ match: zod_mini.z.looseObject({ prefix: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string()).check(zod_mini.z.minLength(1))])).check(zod_mini.z.minLength(1))) }),
8355
+ tests: zod_mini.z.optional(zod_mini.z.looseObject({
8356
+ match: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
8357
+ noMatch: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string()))
8358
+ })),
8359
+ reason: zod_mini.z.optional(zod_mini.z.string()),
8360
+ enabled: zod_mini.z.optional(zod_mini.z.boolean())
8361
+ });
8362
+ const FactorydroidPermissionRulesSchema = zod_mini.z.looseObject({
8363
+ version: zod_mini.z.number(),
8364
+ rules: zod_mini.z.array(FactorydroidPermissionRuleSchema)
8365
+ });
8366
+ /**
8290
8367
  * Tool-scoped override block for Factory Droid. Factory Droid's `settings.json`
8291
8368
  * exposes security controls with no canonical per-command allow/ask/deny slot —
8292
8369
  * `commandBlocklist` (a hard-block tier that can never be approved, distinct from
@@ -8294,7 +8371,8 @@ const ReasonixPermissionsOverrideSchema = zod_mini.z.looseObject({
8294
8371
  * (`enabled`/`mode`/`filesystem`/`network`), `mcpPolicy`,
8295
8372
  * `mcpAutonomyOverrides` (per-MCP-tool autonomy levels), `enableDroidShield`,
8296
8373
  * and autonomy settings (`sessionDefaultSettings`, `maxAutonomyLevel`,
8297
- * `subagentAutonomyLevel`, `interactionMode`). Fields placed here are merged
8374
+ * `subagentAutonomyLevel`, `interactionMode`), and the native versioned
8375
+ * `permissionRules` (allow/ask/block). Fields placed here are merged
8298
8376
  * into `settings.json` and
8299
8377
  * emitted only for Factory Droid, while the shared `permission` block continues
8300
8378
  * to drive `commandAllowlist`/`commandDenylist`. Kept `looseObject` passthrough.
@@ -8305,7 +8383,8 @@ const ReasonixPermissionsOverrideSchema = zod_mini.z.looseObject({
8305
8383
  const FactorydroidPermissionsOverrideSchema = zod_mini.z.looseObject({
8306
8384
  permission: zod_mini.z.optional(ToolScopedPermissionSchema),
8307
8385
  commandBlocklist: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
8308
- disabledSkills: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string()))
8386
+ disabledSkills: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
8387
+ permissionRules: zod_mini.z.optional(FactorydroidPermissionRulesSchema)
8309
8388
  });
8310
8389
  /**
8311
8390
  * Tool-scoped override block for Warp. Warp's `[agents.profiles]` table exposes
@@ -9110,6 +9189,7 @@ const PermissionsConfigSchema = zod_mini.z.looseObject({
9110
9189
  codewhale: zod_mini.z.optional(CodewhalePermissionsOverrideSchema),
9111
9190
  zed: zod_mini.z.optional(ZedPermissionsOverrideSchema),
9112
9191
  devin: zod_mini.z.optional(DevinPermissionsOverrideSchema),
9192
+ qoder: zod_mini.z.optional(QoderPermissionsOverrideSchema),
9113
9193
  "antigravity-ide": zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
9114
9194
  continue: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
9115
9195
  copilot: zod_mini.z.optional(CopilotPermissionsOverrideSchema),
@@ -9121,7 +9201,6 @@ const PermissionsConfigSchema = zod_mini.z.looseObject({
9121
9201
  goose: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
9122
9202
  omp: zod_mini.z.optional(OmpPermissionsOverrideSchema),
9123
9203
  pool: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
9124
- qoder: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
9125
9204
  grokcli: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
9126
9205
  "kimi-code": zod_mini.z.optional(KimiCodePermissionsOverrideSchema),
9127
9206
  roo: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
@@ -9642,7 +9721,9 @@ const RulesyncRuleFrontmatterSchema = zod_mini.z.object({
9642
9721
  qoder: zod_mini.z.optional(zod_mini.z.looseObject({
9643
9722
  trigger: zod_mini.z.optional(zod_mini.z.string()),
9644
9723
  glob: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())])),
9645
- description: zod_mini.z.optional(zod_mini.z.string())
9724
+ description: zod_mini.z.optional(zod_mini.z.string()),
9725
+ channel: zod_mini.z.optional(zod_mini.z.enum(["output-style"])),
9726
+ name: zod_mini.z.optional(zod_mini.z.string().check(zod_mini.z.trim(), zod_mini.z.minLength(1)))
9646
9727
  })),
9647
9728
  augmentcode: zod_mini.z.optional(zod_mini.z.looseObject({
9648
9729
  type: zod_mini.z.optional(zod_mini.z.string()),
@@ -13011,7 +13092,7 @@ const SHARED_CONFIG_OWNERSHIP = {
13011
13092
  },
13012
13093
  permissions: {
13013
13094
  kind: "replace-owned-keys",
13014
- ownedKeys: ["permissions"]
13095
+ ownedKeys: ["permissions", "outputStyle"]
13015
13096
  }
13016
13097
  }
13017
13098
  },
@@ -21766,22 +21847,29 @@ function mergeOpencodeV2Entries({ config, legacyKey, pluralKey, lower }) {
21766
21847
  const entry = asOpencodeEntries(value);
21767
21848
  const lowered = entry === null ? null : lower(entry);
21768
21849
  if (Object.hasOwn(merged, name) || lowered === null) continue;
21769
- Object.defineProperty(merged, name, {
21770
- value: lowered,
21771
- enumerable: true,
21772
- configurable: true,
21773
- writable: true
21774
- });
21850
+ setOwnEntry(merged, name, lowered);
21775
21851
  }
21776
21852
  return Object.keys(merged).length > 0 ? merged : legacy;
21777
21853
  }
21778
21854
  /**
21855
+ * Sets `target[key]` as an own, enumerable property. A plain assignment to a
21856
+ * key named `__proto__` would set the prototype instead.
21857
+ */
21858
+ function setOwnEntry(target, key, value) {
21859
+ Object.defineProperty(target, key, {
21860
+ value,
21861
+ enumerable: true,
21862
+ configurable: true,
21863
+ writable: true
21864
+ });
21865
+ }
21866
+ /**
21779
21867
  * Returns the inline agent entries of an OpenCode config in the V1 `agent`
21780
21868
  * shape. Since v1.18.24 OpenCode V1 also reads the V2 `agents` spelling and
21781
21869
  * lowers it, so a layout that does so (`readsV2ConfigSpellings`) gets those
21782
21870
  * entries too.
21783
21871
  *
21784
- * @see https://github.com/anomalyco/opencode/blob/v1.18.34/packages/opencode/src/config/v2-compat.ts
21872
+ * @see https://github.com/anomalyco/opencode/blob/v1.18.35/packages/opencode/src/config/v2-compat.ts
21785
21873
  */
21786
21874
  function getOpencodeAgentEntries({ config, layout = OPENCODE_LAYOUT }) {
21787
21875
  if (!layout.readsV2ConfigSpellings) return asOpencodeEntries(config.agent);
@@ -21800,7 +21888,7 @@ function getOpencodeAgentEntries({ config, layout = OPENCODE_LAYOUT }) {
21800
21888
  * Other fields are kept: V1 drops a V2 `subagent`, but rulesync reads it as
21801
21889
  * `subtask` everywhere (as V2 does), so it is left for the command importer.
21802
21890
  *
21803
- * @see https://github.com/anomalyco/opencode/blob/v1.18.34/packages/opencode/src/config/v2-compat.ts
21891
+ * @see https://github.com/anomalyco/opencode/blob/v1.18.35/packages/opencode/src/config/v2-compat.ts
21804
21892
  */
21805
21893
  function getOpencodeCommandEntries({ config, layout = OPENCODE_LAYOUT }) {
21806
21894
  if (!layout.readsV2ConfigSpellings) return asOpencodeEntries(config.command);
@@ -21823,7 +21911,7 @@ function getOpencodeCommandEntries({ config, layout = OPENCODE_LAYOUT }) {
21823
21911
  * reads V2 spellings, a flat V2 `skills` array is accepted too: its entries
21824
21912
  * that are not `http(s)://` URLs are the paths, as OpenCode V1 lowers them.
21825
21913
  *
21826
- * @see https://github.com/anomalyco/opencode/blob/v1.18.34/packages/opencode/src/config/v2-compat.ts
21914
+ * @see https://github.com/anomalyco/opencode/blob/v1.18.35/packages/opencode/src/config/v2-compat.ts
21827
21915
  */
21828
21916
  function getOpencodeSkillPaths({ config, layout = OPENCODE_LAYOUT }) {
21829
21917
  if (Array.isArray(config.skills)) {
@@ -21833,6 +21921,160 @@ function getOpencodeSkillPaths({ config, layout = OPENCODE_LAYOUT }) {
21833
21921
  const skills = asOpencodeEntries(config.skills);
21834
21922
  return skills !== null && Array.isArray(skills.paths) ? skills.paths : [];
21835
21923
  }
21924
+ /**
21925
+ * Whether a record is itself an MCP server entry rather than a V2 container
21926
+ * (`mcp.servers`, `mcp.timeout`): a server — or a server literally named
21927
+ * `type` / `enabled` — carries a scalar `type` or `enabled`. Mirrors OpenCode
21928
+ * V1's `isDirectServer`.
21929
+ */
21930
+ function isDirectOpencodeMcpServer(value) {
21931
+ return ["type", "enabled"].some((key) => Object.hasOwn(value, key) && (value[key] === null || typeof value[key] !== "object" || Array.isArray(value[key])));
21932
+ }
21933
+ const OPENCODE_V2_TIMEOUT_KEYS = [
21934
+ "startup",
21935
+ "catalog",
21936
+ "execution"
21937
+ ];
21938
+ /**
21939
+ * Whether an MCP server entry uses a V2-only per-server spelling, so that
21940
+ * OpenCode V1 lowers it: `disabled`, `codemode`, an object `timeout`, or a
21941
+ * snake_case `oauth` key.
21942
+ */
21943
+ function isOpencodeV2NativeServer(value) {
21944
+ const oauth = value.oauth;
21945
+ return Object.hasOwn(value, "disabled") || Object.hasOwn(value, "codemode") || isRecord$1(value.timeout) || isRecord$1(oauth) && [
21946
+ "client_id",
21947
+ "client_secret",
21948
+ "callback_port",
21949
+ "redirect_uri"
21950
+ ].some((key) => Object.hasOwn(oauth, key));
21951
+ }
21952
+ /**
21953
+ * Lowers a V2 timeout object (`{ startup, catalog, execution }`) to V1's single
21954
+ * number, the way OpenCode V1 does: only a `catalog` equal to `execution`, with
21955
+ * no `startup`, has a V1 equivalent. Anything else — including a value that is
21956
+ * not a positive integer — is `undefined` (dropped).
21957
+ */
21958
+ function lowerOpencodeV2Timeout(value) {
21959
+ if (!hasPositiveIntegerTimeouts(value) || value.startup !== void 0 || typeof value.catalog !== "number") return;
21960
+ return value.catalog === value.execution ? value.catalog : void 0;
21961
+ }
21962
+ const OPENCODE_V2_OAUTH_KEYS = /* @__PURE__ */ new Map([
21963
+ ["client_id", "clientId"],
21964
+ ["client_secret", "clientSecret"],
21965
+ ["callback_port", "callbackPort"],
21966
+ ["redirect_uri", "redirectUri"]
21967
+ ]);
21968
+ /**
21969
+ * V2 `oauth` keys that have no V1 spelling. OpenCode V1 drops them when it
21970
+ * lowers a server, so they are dropped (and reported) here too rather than
21971
+ * being written back as a snake_case key among camelCase ones.
21972
+ */
21973
+ const OPENCODE_V2_UNSUPPORTED_OAUTH_KEYS = /* @__PURE__ */ new Set(["auth_server_metadata_url"]);
21974
+ function lowerOpencodeV2OAuth(oauth, path, onDropped) {
21975
+ const lowered = {};
21976
+ for (const [key, value] of Object.entries(oauth)) {
21977
+ if (OPENCODE_V2_UNSUPPORTED_OAUTH_KEYS.has(key)) {
21978
+ onDropped?.(`${path}.${key}`, "unsupported");
21979
+ continue;
21980
+ }
21981
+ setOwnEntry(lowered, OPENCODE_V2_OAUTH_KEYS.get(key) ?? key, value);
21982
+ }
21983
+ return lowered;
21984
+ }
21985
+ /**
21986
+ * Lowers one V2-spelled MCP server entry to V1's shape, as OpenCode V1 does:
21987
+ * `disabled` becomes `enabled` (an explicit `enabled` wins), an object
21988
+ * `timeout` becomes a number or is dropped, and snake_case `oauth` keys become
21989
+ * camelCase. `codemode` is kept: Rulesync passes it through where V1 drops it.
21990
+ *
21991
+ * Like V1, only an entry that is a local or remote server is lowered, and one
21992
+ * whose `enabled` or `disabled` is not a boolean is left as it is. Unlike V1,
21993
+ * such an entry is not dropped but passed on, so the schema rejects it loudly.
21994
+ */
21995
+ function lowerOpencodeV2Server(value, path, onDropped) {
21996
+ if (!isRecord$1(value) || value.type !== "local" && value.type !== "remote" || Object.hasOwn(value, "enabled") && typeof value.enabled !== "boolean" || Object.hasOwn(value, "disabled") && typeof value.disabled !== "boolean") return value;
21997
+ const { disabled, timeout, ...rest } = value;
21998
+ const lowered = { ...rest };
21999
+ if (!Object.hasOwn(value, "enabled")) lowered.enabled = disabled !== true;
22000
+ else if (value.enabled === disabled) onDropped?.(`${path}.disabled`, "conflict");
22001
+ const lowTimeout = isRecord$1(timeout) ? lowerOpencodeV2Timeout(timeout) : timeout;
22002
+ if (lowTimeout !== void 0) lowered.timeout = lowTimeout;
22003
+ else if (isRecord$1(timeout) && Object.keys(timeout).length > 0) onDropped?.(`${path}.timeout`, "unsupported");
22004
+ if (isRecord$1(value.oauth)) lowered.oauth = lowerOpencodeV2OAuth(value.oauth, `${path}.oauth`, onDropped);
22005
+ return lowered;
22006
+ }
22007
+ function isPositiveInteger(value) {
22008
+ return typeof value === "number" && Number.isInteger(value) && value > 0;
22009
+ }
22010
+ /** Whether each V2 timeout value a record carries is a positive integer. */
22011
+ function hasPositiveIntegerTimeouts(value) {
22012
+ return OPENCODE_V2_TIMEOUT_KEYS.every((key) => value[key] === void 0 || isPositiveInteger(value[key]));
22013
+ }
22014
+ /**
22015
+ * Whether `mcp.timeout` is V2's global timeout object rather than a server
22016
+ * named `timeout`. As in OpenCode V1, each of its values must be a positive
22017
+ * integer; anything else is read as a server, which the schema then rejects.
22018
+ */
22019
+ function isOpencodeV2GlobalTimeout(value) {
22020
+ return isRecord$1(value) && !isDirectOpencodeMcpServer(value) && (Object.keys(value).length === 0 || OPENCODE_V2_TIMEOUT_KEYS.some((key) => Object.hasOwn(value, key))) && hasPositiveIntegerTimeouts(value);
22021
+ }
22022
+ function getOpencodeV2McpEnvelope(mcp) {
22023
+ const nested = mcp.servers;
22024
+ return isRecord$1(nested) && !isDirectOpencodeMcpServer(nested) ? nested : null;
22025
+ }
22026
+ /**
22027
+ * Returns the paths of the V2-only containers in an OpenCode `mcp` value that
22028
+ * a write in V1's flat shape does not keep: a non-empty `mcp.servers`
22029
+ * envelope, and a non-empty global `mcp.timeout`.
22030
+ */
22031
+ function findOpencodeV2McpContainers(mcp) {
22032
+ if (!isRecord$1(mcp)) return [];
22033
+ const envelope = getOpencodeV2McpEnvelope(mcp);
22034
+ return [...envelope !== null && Object.keys(envelope).length > 0 ? ["mcp.servers"] : [], ...isOpencodeV2GlobalTimeout(mcp.timeout) && Object.keys(mcp.timeout).length > 0 ? ["mcp.timeout"] : []];
22035
+ }
22036
+ /**
22037
+ * Lowers OpenCode V2 `mcp` spellings found in a V1 config to the flat V1 map,
22038
+ * the way OpenCode V1 itself does since v1.18.24 (`normalizeMcp`):
22039
+ *
22040
+ * - entries of the `mcp.servers` envelope are merged into the map, a flat entry
22041
+ * of the same name winning;
22042
+ * - V2-spelled server entries (`disabled`, an object `timeout`, snake_case
22043
+ * `oauth` keys) are lowered by {@link lowerOpencodeV2Server};
22044
+ * - the global `mcp.timeout` object is skipped. V1 moves it to
22045
+ * `experimental.mcp_timeout`, a default request timeout that has no
22046
+ * canonical counterpart, so it is not read as a server either.
22047
+ *
22048
+ * Unlike V1, an envelope entry that is not a valid server is not dropped: it
22049
+ * is passed on, so the schema rejects it loudly rather than it vanishing
22050
+ * without a word. Each value that is dropped — a non-empty global timeout, a
22051
+ * per-server timeout object or `oauth` key with no V1 equivalent, or an
22052
+ * envelope entry shadowed by a different flat entry — is reported through
22053
+ * `onDropped`.
22054
+ *
22055
+ * @see https://github.com/anomalyco/opencode/blob/v1.18.35/packages/opencode/src/config/v2-compat.ts
22056
+ * @see https://opencode.ai/v2/docs/mcp-servers/
22057
+ */
22058
+ function lowerOpencodeV2Mcp(mcp, onDropped) {
22059
+ if (!isRecord$1(mcp)) return mcp;
22060
+ const envelope = getOpencodeV2McpEnvelope(mcp);
22061
+ const globalTimeout = isOpencodeV2GlobalTimeout(mcp.timeout);
22062
+ if (findOpencodeV2McpContainers(mcp).includes("mcp.timeout")) onDropped?.("mcp.timeout", "unsupported");
22063
+ const servers = {};
22064
+ for (const [name, value] of Object.entries(mcp)) {
22065
+ if (name === "servers" && envelope !== null || name === "timeout" && globalTimeout) continue;
22066
+ setOwnEntry(servers, name, isRecord$1(value) && isOpencodeV2NativeServer(value) ? lowerOpencodeV2Server(value, `mcp.${name}`, onDropped) : value);
22067
+ }
22068
+ for (const [name, value] of Object.entries(envelope ?? {})) {
22069
+ const path = `mcp.servers.${name}`;
22070
+ if (Object.hasOwn(servers, name)) {
22071
+ if (!(0, node_util.isDeepStrictEqual)(servers[name], value)) onDropped?.(path, "conflict");
22072
+ continue;
22073
+ }
22074
+ setOwnEntry(servers, name, lowerOpencodeV2Server(value, path, onDropped));
22075
+ }
22076
+ return servers;
22077
+ }
21836
22078
  /** Matches a string whose entire value is an OpenCode `{file:...}` reference. */
21837
22079
  const OPENCODE_FILE_TEMPLATE_PATTERN = /^\s*\{file:(.+?)\}\s*$/;
21838
22080
  /**
@@ -22264,6 +22506,7 @@ const QODER_DIR = ".qoder";
22264
22506
  const QODER_RULE_FILE_NAME = "AGENTS.md";
22265
22507
  const QODER_LOCAL_RULE_FILE_NAME = "AGENTS.local.md";
22266
22508
  const QODER_RULES_DIR_PATH = (0, node_path.join)(QODER_DIR, "rules");
22509
+ const QODER_OUTPUT_STYLES_DIR_PATH = (0, node_path.join)(QODER_DIR, "output-styles");
22267
22510
  const QODER_SETTINGS_FILE_NAME = "settings.json";
22268
22511
  const QODER_PROJECT_MCP_FILE_NAME = ".mcp.json";
22269
22512
  const QODER_COMMANDS_DIR_PATH = (0, node_path.join)(QODER_DIR, "commands");
@@ -29638,6 +29881,11 @@ const CORTEXCODE_SPEC = {
29638
29881
  canonical: "enabled",
29639
29882
  tool: "enabled"
29640
29883
  }],
29884
+ stringPassthroughFields: [{
29885
+ canonical: "statusMessage",
29886
+ tool: "statusMessage",
29887
+ commandOnly: true
29888
+ }],
29641
29889
  objectPassthroughFields: [{
29642
29890
  canonical: "source",
29643
29891
  tool: "source",
@@ -29652,7 +29900,7 @@ const CORTEXCODE_SPEC = {
29652
29900
  * (project scope) and of the dedicated `~/.snowflake/cortex/hooks.json` (user
29653
29901
  * scope), both in the Claude-Code shape: `{ "<Event>": [{ "matcher"?:
29654
29902
  * "<regex>", "hooks": [{ "type": "command" | "prompt", ..., "timeout"?:
29655
- * <seconds>, "enabled"?: <boolean>, "source"?: { "source", "ref"? } }] }] }`.
29903
+ * <seconds>, "enabled"?: <boolean>, "statusMessage"?: <string> (command only), "source"?: { "source", "ref"? } }] }] }`.
29656
29904
  * The project file also holds settings rulesync does not own, so generation
29657
29905
  * merges the `hooks` key into
29658
29906
  * either file (see `SHARED_CONFIG_OWNERSHIP`) instead of overwriting it, and
@@ -30591,7 +30839,8 @@ const FACTORYDROID_OVERRIDE_KEYS = [
30591
30839
  "allowManagedHooksOnly",
30592
30840
  "disabledSkills",
30593
30841
  "modelPolicy",
30594
- "missionPolicy"
30842
+ "missionPolicy",
30843
+ "permissionRules"
30595
30844
  ];
30596
30845
  //#endregion
30597
30846
  //#region src/utils/factorydroid-settings.ts
@@ -30806,7 +31055,7 @@ const GITLABDUO_CONVERTER_CONFIG = {
30806
31055
  *
30807
31056
  * Hooks live in a dedicated file, `<project>/.gitlab/duo/hooks.json` (project,
30808
31057
  * opt-in via `--enable-project-hooks` / `GITLAB_ENABLE_PROJECT_HOOKS=true`) or
30809
- * `~/.gitlab/duo/hooks.json` (user), shaped as `{ "hooks": { "SessionStart": [...] } }`.
31058
+ * `~/.gitlab/duo/hooks.json` (user), shaped as `{ "hooks": { "SessionStart": [...], "PreToolUse": [...] } }`.
30810
31059
  * rulesync owns the whole file, so it is regenerated and deleted as a unit.
30811
31060
  */
30812
31061
  var GitlabduoHooks = class GitlabduoHooks extends ToolHooks {
@@ -36460,6 +36709,7 @@ var ClineIgnore = class ClineIgnore extends ToolIgnore {
36460
36709
  const CODEBUFF_IGNORE_FILE_NAME = ".codebuffignore";
36461
36710
  const CODEBUFF_AGENTS_DIR = ".agents";
36462
36711
  const CODEBUFF_MCP_FILE_NAME = "mcp.json";
36712
+ const CODEBUFF_GLOBAL_RULE_FILE_NAME = ".AGENTS.md";
36463
36713
  //#endregion
36464
36714
  //#region src/features/ignore/codebuff-ignore.ts
36465
36715
  /**
@@ -43361,11 +43611,45 @@ const CANONICAL_TO_GITLABDUO_TRANSPORT = {
43361
43611
  sse: "sse"
43362
43612
  };
43363
43613
  /**
43614
+ * Map the canonical `timeout` (milliseconds) onto GitLab Duo's `timeoutMs`, a
43615
+ * per-server tool-call inactivity timeout (gitlab-lsp v9.25.0+) that must be a
43616
+ * positive integer. An explicit `timeoutMs` on the same server wins; a
43617
+ * `timeout` Duo would reject is dropped with a warning.
43618
+ *
43619
+ * @see https://gitlab.com/gitlab-org/editor-extensions/gitlab-lsp/-/commit/e8c4a76a66e8f37d4c97fc03ffe983bfadf43f76
43620
+ */
43621
+ function toGitlabduoTimeout({ serverName, server, logger }) {
43622
+ const { timeout, ...rest } = server;
43623
+ if (timeout === void 0 || rest.timeoutMs !== void 0) return rest;
43624
+ if (typeof timeout !== "number" || !Number.isInteger(timeout) || timeout <= 0) {
43625
+ logger?.warn(`GitLab Duo CLI MCP server "${serverName}": dropping timeout ${JSON.stringify(timeout)}, as timeoutMs must be a positive integer of milliseconds.`);
43626
+ return rest;
43627
+ }
43628
+ return {
43629
+ ...rest,
43630
+ timeoutMs: timeout
43631
+ };
43632
+ }
43633
+ /**
43634
+ * Read GitLab Duo's `timeoutMs` back into the canonical `timeout`
43635
+ * (milliseconds). A server that already carries a `timeout` is left as is, so
43636
+ * both keys round-trip and `timeoutMs` keeps winning on the next generate.
43637
+ */
43638
+ function fromGitlabduoTimeout(server) {
43639
+ if (typeof server !== "object" || server === null || Array.isArray(server)) return server;
43640
+ const { timeoutMs, ...rest } = server;
43641
+ if (timeoutMs === void 0 || rest.timeout !== void 0) return server;
43642
+ return {
43643
+ ...rest,
43644
+ timeout: timeoutMs
43645
+ };
43646
+ }
43647
+ /**
43364
43648
  * Convert one canonical server into GitLab Duo's shape. Every documented
43365
43649
  * example spells the transport out as `type` (`stdio`, `http` or `sse`), so it
43366
43650
  * is always written, inferred from `command` / `url` when the source leaves it
43367
- * implicit. Every other field — `args`, `env`, `approvedTools`, ... — is carried
43368
- * through as authored.
43651
+ * implicit. The canonical `timeout` becomes `timeoutMs`; every other field —
43652
+ * `args`, `env`, `approvedTools`, ... — is carried through as authored.
43369
43653
  */
43370
43654
  function toGitlabduoServer({ serverName, serverConfig, logger }) {
43371
43655
  if (declaresNoTransport(serverConfig)) return warnAndSkipMcpServer({
@@ -43374,7 +43658,7 @@ function toGitlabduoServer({ serverName, serverConfig, logger }) {
43374
43658
  reason: "no command or url",
43375
43659
  logger
43376
43660
  });
43377
- const { type, transport, httpUrl: _httpUrl, url: _url, ...rest } = serverConfig;
43661
+ const { type, transport, httpUrl: _httpUrl, url: _url, ...others } = serverConfig;
43378
43662
  const declared = type ?? transport;
43379
43663
  const mapped = declared ? CANONICAL_TO_GITLABDUO_TRANSPORT[declared] : isRemoteMcpServer(serverConfig) ? "http" : "stdio";
43380
43664
  if (!mapped) return warnAndSkipMcpServer({
@@ -43383,8 +43667,13 @@ function toGitlabduoServer({ serverName, serverConfig, logger }) {
43383
43667
  reason: `the unsupported transport "${declared}" (GitLab Duo CLI accepts stdio, http and sse)`,
43384
43668
  logger
43385
43669
  });
43670
+ const rest = toGitlabduoTimeout({
43671
+ serverName,
43672
+ server: others,
43673
+ logger
43674
+ });
43386
43675
  if (mapped === "stdio") {
43387
- const { command, args } = rest;
43676
+ const { command, args } = serverConfig;
43388
43677
  if (Array.isArray(command)) {
43389
43678
  const [executable, ...leadingArgs] = command;
43390
43679
  return {
@@ -43473,7 +43762,13 @@ var GitlabduoMcp = class GitlabduoMcp extends ToolMcp {
43473
43762
  });
43474
43763
  }
43475
43764
  toRulesyncMcp() {
43476
- return this.toRulesyncMcpDefault();
43765
+ const { mcpServers, ...others } = this.json;
43766
+ if (typeof mcpServers !== "object" || mcpServers === null || Array.isArray(mcpServers)) return this.toRulesyncMcpDefault();
43767
+ const servers = Object.fromEntries(Object.entries(mcpServers).map(([name, server]) => [name, fromGitlabduoTimeout(server)]));
43768
+ return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({
43769
+ ...others,
43770
+ mcpServers: servers
43771
+ }) });
43477
43772
  }
43478
43773
  validate() {
43479
43774
  return {
@@ -45581,18 +45876,21 @@ var KiroMcp = class KiroMcp extends ToolMcp {
45581
45876
  };
45582
45877
  //#endregion
45583
45878
  //#region src/features/mcp/opencode-mcp.ts
45879
+ const OpencodeMcpV2DisabledSchema = zod_mini.z.optional(zod_mini.z.boolean());
45584
45880
  const OpencodeMcpLocalServerSchema = zod_mini.z.looseObject({
45585
45881
  type: zod_mini.z.literal("local"),
45586
45882
  command: zod_mini.z.array(zod_mini.z.string()),
45587
45883
  environment: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.string())),
45588
45884
  enabled: zod_mini.z._default(zod_mini.z.boolean(), true),
45885
+ disabled: OpencodeMcpV2DisabledSchema,
45589
45886
  cwd: zod_mini.z.optional(zod_mini.z.string())
45590
45887
  });
45591
45888
  const OpencodeMcpRemoteServerSchema = zod_mini.z.looseObject({
45592
45889
  type: zod_mini.z.literal("remote"),
45593
45890
  url: zod_mini.z.string(),
45594
45891
  headers: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.string())),
45595
- enabled: zod_mini.z._default(zod_mini.z.boolean(), true)
45892
+ enabled: zod_mini.z._default(zod_mini.z.boolean(), true),
45893
+ disabled: OpencodeMcpV2DisabledSchema
45596
45894
  });
45597
45895
  const OPENCODE_PASSTHROUGH_SERVER_FIELDS = [
45598
45896
  "timeout",
@@ -45604,7 +45902,7 @@ const OPENCODE_MCP_TRANSPORT_KEYS = [
45604
45902
  ...Object.keys(OpencodeMcpLocalServerSchema.def.shape),
45605
45903
  ...Object.keys(OpencodeMcpRemoteServerSchema.def.shape),
45606
45904
  ...OPENCODE_PASSTHROUGH_SERVER_FIELDS
45607
- ].filter((key) => key !== "enabled");
45905
+ ].filter((key) => key !== "enabled" && key !== "disabled");
45608
45906
  /**
45609
45907
  * A bare toggle entry: `{"enabled": <bool>}` with no transport of its own,
45610
45908
  * disabling a server another config layer defines. It is the third member of
@@ -45732,7 +46030,7 @@ const OPENCODE_TOGGLE_KEPT_KEYS = /* @__PURE__ */ new Set([
45732
46030
  function warnAboutToggleDroppedKeys(serverName, serverConfig, logger) {
45733
46031
  const dropped = Object.keys(serverConfig).filter((key) => !OPENCODE_TOGGLE_KEPT_KEYS.has(key));
45734
46032
  if (dropped.length === 0) return;
45735
- logger?.warn(`OpenCode MCP: "${serverName}" declares no transport, so it is written as a toggle entry and ${dropped.toSorted().join(", ")} ${dropped.length === 1 ? "is" : "are"} dropped.`);
46033
+ logger?.warn(`OpenCode MCP: ${quoteValueForWarning(serverName)} declares no transport, so it is written as a toggle entry and ${dropped.toSorted().map(quoteValueForWarning).join(", ")} ${dropped.length === 1 ? "is" : "are"} dropped.`);
45736
46034
  }
45737
46035
  /**
45738
46036
  * Convert standard MCP format to OpenCode native format
@@ -45812,9 +46110,35 @@ var OpencodeMcp = class extends ToolMcp {
45812
46110
  /** Directory layout; OpenCode forks (MiMo Code) override it. */
45813
46111
  static layout = OPENCODE_LAYOUT;
45814
46112
  json;
45815
- constructor(params) {
46113
+ /**
46114
+ * With a `logger`, the V2 values that lowering the content drops are warned
46115
+ * about (see {@link OpencodeMcp.lowerConfig}); `fromFile` passes one so the
46116
+ * warnings come from the lowering that builds the parsed view rather than
46117
+ * from an extra pass of their own. (`validate()`, which the parent
46118
+ * constructor calls before this view exists, lowers separately and silently.)
46119
+ */
46120
+ constructor({ logger, ...params }) {
45816
46121
  super(params);
45817
- this.json = OpencodeConfigSchema.parse((0, jsonc_parser.parse)(this.fileContent || "{}"));
46122
+ this.json = OpencodeConfigSchema.parse(this.constructor.lowerConfig((0, jsonc_parser.parse)(this.fileContent || "{}"), logger));
46123
+ }
46124
+ /**
46125
+ * Lowers the OpenCode V2 `mcp` spellings of a parsed config for a layout
46126
+ * that reads them (see {@link lowerOpencodeV2Mcp}), so every reader — import,
46127
+ * the toggle lookup on generate, and the rules feature's `instructions`
46128
+ * write to the same file — accepts a V2-shaped `mcp`. Only the parsed view
46129
+ * is lowered; content written back keeps whatever `mcp` a feature that does
46130
+ * not own the key found there. With a `logger`, each V2 value the lowering
46131
+ * drops is warned about.
46132
+ */
46133
+ static lowerConfig(json, logger) {
46134
+ if (!this.layout.readsV2ConfigSpellings || !isRecord$1(json) || json.mcp === void 0) return json;
46135
+ return {
46136
+ ...json,
46137
+ mcp: lowerOpencodeV2Mcp(json.mcp, (path, reason) => {
46138
+ const quoted = quoteValueForWarning(path);
46139
+ logger?.warn(reason === "conflict" ? `OpenCode MCP: ${quoted} conflicts with a V1 value that takes precedence (the flat "mcp" entry of the same name, or "enabled"), so it is dropped on import.` : `OpenCode MCP: ${quoted} has no Rulesync equivalent, so it is dropped on import.`);
46140
+ })
46141
+ };
45818
46142
  }
45819
46143
  getJson() {
45820
46144
  return this.json;
@@ -45831,7 +46155,7 @@ var OpencodeMcp = class extends ToolMcp {
45831
46155
  relativeFilePath: this.layout.jsonFileName
45832
46156
  };
45833
46157
  }
45834
- static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
46158
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false, logger }) {
45835
46159
  const basePaths = this.getSettablePaths({ global });
45836
46160
  const jsonDir = (0, node_path.join)(outputRoot, basePaths.relativeDirPath);
45837
46161
  let fileContent = null;
@@ -45853,7 +46177,8 @@ var OpencodeMcp = class extends ToolMcp {
45853
46177
  relativeDirPath: basePaths.relativeDirPath,
45854
46178
  relativeFilePath,
45855
46179
  fileContent: JSON.stringify(newJson, null, 2),
45856
- validate
46180
+ validate,
46181
+ logger
45857
46182
  });
45858
46183
  }
45859
46184
  static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
@@ -45872,7 +46197,12 @@ var OpencodeMcp = class extends ToolMcp {
45872
46197
  mcpServers: rulesyncMcp.getMcpServers(),
45873
46198
  replacement: "{env:$1}"
45874
46199
  });
45875
- const existingMcp = OpencodeConfigSchema.safeParse((0, jsonc_parser.parse)(fileContent || "{}"));
46200
+ const existingJson = (0, jsonc_parser.parse)(fileContent || "{}");
46201
+ const existingMcp = OpencodeConfigSchema.safeParse(this.lowerConfig(existingJson));
46202
+ if (this.layout.readsV2ConfigSpellings && isRecord$1(existingJson)) {
46203
+ const containers = findOpencodeV2McpContainers(existingJson.mcp);
46204
+ if (containers.length > 0) logger?.warn(`OpenCode MCP: the whole "mcp" key is rewritten in the flat V1 shape, so ${containers.map(quoteValueForWarning).join(" and ")} ${containers.length === 1 ? "is" : "are"} not kept.`);
46205
+ }
45876
46206
  const { mcp: convertedMcp, tools: mcpTools } = convertToOpencodeFormat(transformedServers, (existingMcp.success ? existingMcp.data.mcp : void 0) ?? {}, logger);
45877
46207
  return new this({
45878
46208
  outputRoot,
@@ -45972,7 +46302,7 @@ var OpencodeMcp = class extends ToolMcp {
45972
46302
  }
45973
46303
  validate() {
45974
46304
  const json = parseJsonc$6(this.fileContent || "{}");
45975
- const result = OpencodeConfigSchema.safeParse(json);
46305
+ const result = OpencodeConfigSchema.safeParse(this.constructor.lowerConfig(json));
45976
46306
  if (!result.success) return {
45977
46307
  success: false,
45978
46308
  error: result.error
@@ -46545,6 +46875,15 @@ const CONSUMED_CANONICAL_KEYS = /* @__PURE__ */ new Set([
46545
46875
  ]);
46546
46876
  const MILLISECONDS_PER_SECOND = 1e3;
46547
46877
  /**
46878
+ * The only keys a project override entry for a user-level server carries:
46879
+ * Pi reads a `.pi/mcp.json` entry without `command`, `url` or `type` as an
46880
+ * override of `enabled`, `exposure` and `toolExposure` of the user-level server
46881
+ * with the same name, and keeps the rest of that server.
46882
+ *
46883
+ * @see https://github.com/earendil-works/pi/blob/v1.1.0/packages/coding-agent/docs/mcp.md
46884
+ */
46885
+ const PI_OVERRIDE_PASSTHROUGH_KEYS = ["exposure", "toolExposure"];
46886
+ /**
46548
46887
  * Pi rejects a server whose name has any other character, and treats names
46549
46888
  * that differ only in `-` and `_` as one server (its tools share the
46550
46889
  * `mcp__<name>` namespace with `-` replaced by `_`), rejecting the second.
@@ -46580,17 +46919,12 @@ function hasUnexpandedEnvVarRef(serverConfig) {
46580
46919
  /**
46581
46920
  * The transport fields Pi reads for one server: `type` `stdio` with a string
46582
46921
  * `command` plus `args`, or `type` `http` with a `url`. A server Pi cannot
46583
- * start or reach — no transport, a remote transport without a URL, an SSE or
46584
- * WebSocket server, or a stdio entry without a command — is rejected by Pi, so
46585
- * it is skipped with a warning (`null`).
46922
+ * start or reach — a remote transport without a URL, an SSE or WebSocket
46923
+ * server, or a stdio entry without a command — is rejected by Pi, so it is
46924
+ * skipped with a warning (`null`). A server with no transport at all is a
46925
+ * project override instead (see `convertOverrideToPi`).
46586
46926
  */
46587
46927
  function convertTransportToPi({ serverName, serverConfig, logger }) {
46588
- if (declaresNoTransport(serverConfig)) return warnAndSkipMcpServer({
46589
- toolName: "Pi",
46590
- serverName,
46591
- reason: "no transport",
46592
- logger
46593
- });
46594
46928
  if (isRemoteMcpServer(serverConfig)) {
46595
46929
  const url = resolveRemoteMcpUrl(serverConfig);
46596
46930
  const stated = serverConfig.type ?? serverConfig.transport;
@@ -46631,56 +46965,117 @@ function convertTransportToPi({ serverName, serverConfig, logger }) {
46631
46965
  };
46632
46966
  }
46633
46967
  /**
46634
- * Convert the canonical server map to Pi's `mcpServers` shape. Besides the
46635
- * transport fields, Pi switches a server off with `enabled: false` and reads
46636
- * `timeout` in seconds (canonical: milliseconds). A server name Pi rejects is
46637
- * skipped with a warning, and so is `auth` in the project file, which Pi
46638
- * accepts only in the global one (it drops the whole server otherwise).
46968
+ * A transport-less server in the project file becomes a Pi override entry for
46969
+ * the user-level server of the same name. `disabled` maps to `enabled` both
46970
+ * ways (an explicit `disabled: false` is "Enable in this project"), and any key
46971
+ * Pi ignores on an override is dropped with a warning. The global file has no
46972
+ * layer below it to override, so the entry is skipped there.
46973
+ */
46974
+ function convertOverrideToPi({ serverName, serverConfig, global, logger }) {
46975
+ if (global) return warnAndSkipMcpServer({
46976
+ toolName: "Pi",
46977
+ serverName,
46978
+ reason: "no transport",
46979
+ logger
46980
+ });
46981
+ const record = serverConfig;
46982
+ const override = {};
46983
+ if (typeof serverConfig.disabled === "boolean") override.enabled = !serverConfig.disabled;
46984
+ for (const key of PI_OVERRIDE_PASSTHROUGH_KEYS) if (record[key] !== void 0) override[key] = record[key];
46985
+ if (Object.keys(override).length === 0) return warnAndSkipMcpServer({
46986
+ toolName: "Pi",
46987
+ serverName,
46988
+ reason: "no transport and nothing a Pi project override sets (disabled, exposure, toolExposure)",
46989
+ logger
46990
+ });
46991
+ const droppedKeys = Object.keys(record).filter((key) => key !== "disabled" && !PI_OVERRIDE_PASSTHROUGH_KEYS.includes(key) && !PROTOTYPE_POLLUTION_KEYS.has(key));
46992
+ if (droppedKeys.length > 0) logger?.warn(`Pi MCP: dropping ${droppedKeys.map((key) => quoteValueForWarning(key)).join(", ")} of the project override ${quoteValueForWarning(serverName)}: Pi reads only enabled, exposure and toolExposure from an entry without a transport.`);
46993
+ return override;
46994
+ }
46995
+ /**
46996
+ * Whether Pi rejects the server name, either outright or because an earlier
46997
+ * server already took its `mcp__<name>` namespace; warns when it does.
46998
+ */
46999
+ function isRejectedServerName({ serverName, namespaces, logger }) {
47000
+ if (!PI_SERVER_NAME_PATTERN.test(serverName)) {
47001
+ warnAndSkipMcpServer({
47002
+ toolName: "Pi",
47003
+ serverName,
47004
+ reason: "a name Pi rejects (use letters, digits, \"_\" and \"-\")",
47005
+ logger
47006
+ });
47007
+ return true;
47008
+ }
47009
+ const clash = namespaces.get(toPiNamespace(serverName));
47010
+ if (clash !== void 0) {
47011
+ warnAndSkipMcpServer({
47012
+ toolName: "Pi",
47013
+ serverName,
47014
+ reason: `a name Pi treats as the same server as ${quoteValueForWarning(clash)} ("-" and "_" are equivalent)`,
47015
+ logger
47016
+ });
47017
+ return true;
47018
+ }
47019
+ return false;
47020
+ }
47021
+ /**
47022
+ * Convert one canonical server to its Pi entry, or `null` when it is skipped.
47023
+ * Besides the transport fields, Pi switches a server off with `enabled: false`
47024
+ * and reads `timeout` in seconds (canonical: milliseconds). `auth` is dropped
47025
+ * with a warning from the project file, since Pi accepts it only in the global
47026
+ * one (it drops the whole server otherwise). A server without a transport is a
47027
+ * project override entry instead.
47028
+ */
47029
+ function convertServerToPi({ serverName, serverConfig, global, logger }) {
47030
+ if (declaresNoTransport(serverConfig)) return convertOverrideToPi({
47031
+ serverName,
47032
+ serverConfig,
47033
+ global,
47034
+ logger
47035
+ });
47036
+ const converted = convertTransportToPi({
47037
+ serverName,
47038
+ serverConfig,
47039
+ logger
47040
+ });
47041
+ if (!converted) return null;
47042
+ for (const [key, value] of Object.entries(serverConfig)) {
47043
+ if (CONSUMED_CANONICAL_KEYS.has(key) || PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
47044
+ converted[key] = value;
47045
+ }
47046
+ if (serverConfig.timeout !== void 0) {
47047
+ if (Number.isFinite(serverConfig.timeout) && serverConfig.timeout > 0) converted.timeout = serverConfig.timeout / MILLISECONDS_PER_SECOND;
47048
+ else logger?.warn(`Pi MCP: dropping the "timeout" of ${quoteValueForWarning(serverName)}: expected a positive number of milliseconds.`);
47049
+ }
47050
+ if (serverConfig.disabled === true) converted.enabled = false;
47051
+ if (!global && "auth" in converted) {
47052
+ delete converted.auth;
47053
+ logger?.warn(`Pi MCP: dropping the "auth" of ${quoteValueForWarning(serverName)}: Pi accepts it only in the global ~/.pi/agent/mcp.json.`);
47054
+ }
47055
+ if (hasUnexpandedEnvVarRef(converted)) logger?.warn(`Pi MCP: ${quoteValueForWarning(serverName)} carries an environment variable reference Pi does not expand. Pi expands only \${VAR} and $VAR in env and headers values, and only a leading ~/ in command, args and cwd.`);
47056
+ return converted;
47057
+ }
47058
+ /**
47059
+ * Convert the canonical server map to Pi's `mcpServers` shape. A server name
47060
+ * Pi rejects is skipped with a warning.
46639
47061
  */
46640
47062
  function convertToPiFormat({ mcpServers, global, logger }) {
46641
47063
  const result = {};
46642
47064
  const namespaces = /* @__PURE__ */ new Map();
46643
47065
  for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
46644
47066
  if (PROTOTYPE_POLLUTION_KEYS.has(serverName) || !isRecord$1(serverConfig)) continue;
46645
- if (!PI_SERVER_NAME_PATTERN.test(serverName)) {
46646
- warnAndSkipMcpServer({
46647
- toolName: "Pi",
46648
- serverName,
46649
- reason: "a name Pi rejects (use letters, digits, \"_\" and \"-\")",
46650
- logger
46651
- });
46652
- continue;
46653
- }
46654
- const clash = namespaces.get(toPiNamespace(serverName));
46655
- if (clash !== void 0) {
46656
- warnAndSkipMcpServer({
46657
- toolName: "Pi",
46658
- serverName,
46659
- reason: `a name Pi treats as the same server as ${quoteValueForWarning(clash)} ("-" and "_" are equivalent)`,
46660
- logger
46661
- });
46662
- continue;
46663
- }
46664
- const converted = convertTransportToPi({
47067
+ if (isRejectedServerName({
47068
+ serverName,
47069
+ namespaces,
47070
+ logger
47071
+ })) continue;
47072
+ const converted = convertServerToPi({
46665
47073
  serverName,
46666
47074
  serverConfig,
47075
+ global,
46667
47076
  logger
46668
47077
  });
46669
47078
  if (!converted) continue;
46670
- for (const [key, value] of Object.entries(serverConfig)) {
46671
- if (CONSUMED_CANONICAL_KEYS.has(key) || PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
46672
- converted[key] = value;
46673
- }
46674
- if (serverConfig.timeout !== void 0) {
46675
- if (Number.isFinite(serverConfig.timeout) && serverConfig.timeout > 0) converted.timeout = serverConfig.timeout / MILLISECONDS_PER_SECOND;
46676
- else logger?.warn(`Pi MCP: dropping the "timeout" of ${quoteValueForWarning(serverName)}: expected a positive number of milliseconds.`);
46677
- }
46678
- if (serverConfig.disabled === true) converted.enabled = false;
46679
- if (!global && "auth" in converted) {
46680
- delete converted.auth;
46681
- logger?.warn(`Pi MCP: dropping the "auth" of ${quoteValueForWarning(serverName)}: Pi accepts it only in the global ~/.pi/agent/mcp.json.`);
46682
- }
46683
- if (hasUnexpandedEnvVarRef(converted)) logger?.warn(`Pi MCP: ${quoteValueForWarning(serverName)} carries an environment variable reference Pi does not expand. Pi expands only \${VAR} and $VAR in env and headers values, and only a leading ~/ in command, args and cwd.`);
46684
47079
  namespaces.set(toPiNamespace(serverName), serverName);
46685
47080
  result[serverName] = converted;
46686
47081
  }
@@ -46689,16 +47084,19 @@ function convertToPiFormat({ mcpServers, global, logger }) {
46689
47084
  /**
46690
47085
  * Convert Pi's `mcpServers` back to the canonical shape: `enabled: false`
46691
47086
  * becomes `disabled: true` and `timeout` (seconds) becomes milliseconds; every
46692
- * other key is already canonical.
47087
+ * other key is already canonical. On a transport-less override entry an
47088
+ * explicit `enabled: true` is kept as `disabled: false`, since it switches on a
47089
+ * user-level server the user turned off.
46693
47090
  */
46694
47091
  function convertFromPiFormat(mcpServers) {
46695
47092
  const result = {};
46696
47093
  for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
46697
47094
  if (PROTOTYPE_POLLUTION_KEYS.has(serverName) || !isRecord$1(serverConfig)) continue;
46698
47095
  const { enabled, timeout, ...rest } = serverConfig;
46699
- const converted = enabled === false ? {
47096
+ const keepsEnabled = enabled === true && declaresNoTransport(rest);
47097
+ const converted = enabled === false || keepsEnabled ? {
46700
47098
  ...rest,
46701
- disabled: true
47099
+ disabled: !enabled
46702
47100
  } : rest;
46703
47101
  if (typeof timeout === "number") converted.timeout = timeout * MILLISECONDS_PER_SECOND;
46704
47102
  else if (timeout !== void 0) converted.timeout = timeout;
@@ -46774,8 +47172,11 @@ var PiMcp = class PiMcp extends ToolMcp {
46774
47172
  });
46775
47173
  }
46776
47174
  toRulesyncMcp() {
46777
- const mcpServers = isMcpServers(this.json.mcpServers) ? this.json.mcpServers : {};
46778
- return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers: convertFromPiFormat(mcpServers) }, null, 2) });
47175
+ const { shared, toolOnly } = splitMcpServersByTransport(convertFromPiFormat(isMcpServers(this.json.mcpServers) ? this.json.mcpServers : {}));
47176
+ return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({
47177
+ mcpServers: shared,
47178
+ ...Object.keys(toolOnly).length > 0 && { pi: { mcpServers: toolOnly } }
47179
+ }, null, 2) });
46779
47180
  }
46780
47181
  validate() {
46781
47182
  return {
@@ -64568,9 +64969,11 @@ const DEFAULT_TOOLS_KEY = "defaultTools";
64568
64969
  * the built-in tools enabled at startup (v0.84.2) — authored through the `pi`
64569
64970
  * permissions override.
64570
64971
  *
64571
- * Scope semantics are the **opposite** of the union-merge most targets use:
64572
- * a project `defaultTools` array *replaces* the global array rather than adding
64573
- * to it, so the two scopes are written independently and never combined.
64972
+ * Scope layering is Pi's own rather than the union-merge most targets use:
64973
+ * a project `defaultTools` array with a plain name *replaces* the global
64974
+ * array, while one made only of `+name` / `-name` entries (Pi v0.99.0+) changes
64975
+ * the global selection instead. Entries are written verbatim, so the two
64976
+ * scopes are written independently and never combined by rulesync.
64574
64977
  * Project scope writes `.pi/settings.json`; global scope writes
64575
64978
  * `~/.pi/agent/settings.json`.
64576
64979
  *
@@ -65230,6 +65633,13 @@ function convertPoolToRulesync({ tools, paths }) {
65230
65633
  //#region src/features/permissions/qoder-permissions.ts
65231
65634
  /** Top-level key of `.qoder/settings.json` that rulesync owns. */
65232
65635
  const QODER_PERMISSIONS_KEY = "permissions";
65636
+ /**
65637
+ * Top-level key the `qoder` override authors: the active output style. Qoder
65638
+ * also reads `general.outputStyle`, but the top-level key wins, so that is the
65639
+ * one written and imported.
65640
+ * @see https://docs.qoder.com/cli/settings-reference
65641
+ */
65642
+ const QODER_OUTPUT_STYLE_KEY = "outputStyle";
65233
65643
  const CATCH_ALL_PATTERN$2 = "*";
65234
65644
  /** Prefix of the fully qualified MCP tool names (`mcp__<server>__<tool>`). */
65235
65645
  const MCP_TOOL_PREFIX = "mcp__";
@@ -65389,9 +65799,11 @@ function buildQoderRuleLists({ config, existingPermissions, logger }) {
65389
65799
  * through the shared-config gateway, the sibling keys of `permissions`
65390
65800
  * (`additionalDirectories`, `trustDirectories`) are kept, and the file is
65391
65801
  * never deleted. `.qoder/settings.local.json` (where `/allow` and `/deny`
65392
- * persist) is left to the user.
65802
+ * persist) is left to the user. The `qoder` override's `outputStyle` is
65803
+ * written to (and imported from) the top-level `outputStyle` key.
65393
65804
  *
65394
65805
  * @see https://docs.qoder.com/en/cli/permissions
65806
+ * @see https://docs.qoder.com/cli/output-styles
65395
65807
  */
65396
65808
  var QoderPermissions = class QoderPermissions extends ToolPermissions {
65397
65809
  constructor(params) {
@@ -65430,11 +65842,14 @@ var QoderPermissions = class QoderPermissions extends ToolPermissions {
65430
65842
  relativePath: (0, node_path.join)(paths.relativeDirPath, paths.relativeFilePath)
65431
65843
  });
65432
65844
  const existingPermissions = isRecord$1(existing[QODER_PERMISSIONS_KEY]) ? existing[QODER_PERMISSIONS_KEY] : {};
65845
+ const config = rulesyncPermissions.getJson();
65433
65846
  const lists = buildQoderRuleLists({
65434
- config: rulesyncPermissions.getJson(),
65847
+ config,
65435
65848
  existingPermissions,
65436
65849
  logger
65437
65850
  });
65851
+ const outputStyle = config.qoder?.outputStyle?.trim();
65852
+ if (outputStyle !== void 0) logger?.warn(`Qoder permissions: qoder.outputStyle = ${JSON.stringify(outputStyle)} was written to ${(0, node_path.join)(paths.relativeDirPath, paths.relativeFilePath)}${global ? " in the home directory" : ""}, so Qoder appends that style's instructions to the system prompt of every session it applies to.`);
65438
65853
  const permissions = { ...existingPermissions };
65439
65854
  for (const action of ACTIONS) if (lists[action].length > 0) permissions[action] = lists[action];
65440
65855
  else delete permissions[action];
@@ -65446,7 +65861,10 @@ var QoderPermissions = class QoderPermissions extends ToolPermissions {
65446
65861
  fileKey: sharedConfigFileKey(paths),
65447
65862
  feature: "permissions",
65448
65863
  existingContent,
65449
- patch: { [QODER_PERMISSIONS_KEY]: Object.keys(permissions).length > 0 ? permissions : void 0 },
65864
+ patch: {
65865
+ [QODER_PERMISSIONS_KEY]: Object.keys(permissions).length > 0 ? permissions : void 0,
65866
+ ...outputStyle !== void 0 && { [QODER_OUTPUT_STYLE_KEY]: outputStyle }
65867
+ },
65450
65868
  filePath
65451
65869
  }),
65452
65870
  validate: true,
@@ -65467,7 +65885,12 @@ var QoderPermissions = class QoderPermissions extends ToolPermissions {
65467
65885
  const existing = rules[parsed.pattern];
65468
65886
  if (existing === void 0 || ACTION_RANK[action] > ACTION_RANK[existing]) rules[parsed.pattern] = action;
65469
65887
  }
65470
- return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({ permission }, null, 2) });
65888
+ const outputStyle = settings[QODER_OUTPUT_STYLE_KEY];
65889
+ const hasOutputStyle = typeof outputStyle === "string" && outputStyle.trim() !== "";
65890
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({
65891
+ permission,
65892
+ ...hasOutputStyle && { qoder: { outputStyle } }
65893
+ }, null, 2) });
65471
65894
  }
65472
65895
  validate() {
65473
65896
  return {
@@ -84515,8 +84938,8 @@ var DevinSubagent = class extends ToolSubagent {
84515
84938
  * Derive the subagent name from this instance's relative file path.
84516
84939
  *
84517
84940
  * The tool-side layout is `<name>/AGENT.md`, so the name is the parent
84518
- * directory of the file. If the path is unexpectedly flat (e.g. a legacy
84519
- * `<name>.md`), fall back to the basename without extension.
84941
+ * directory of the file. Devin also reads a flat `<name>.md`, which import
84942
+ * picks up too; its name is the basename without extension.
84520
84943
  */
84521
84944
  getSubagentName() {
84522
84945
  const relativeFilePath = this.getRelativeFilePath();
@@ -84609,8 +85032,9 @@ var DevinSubagent = class extends ToolSubagent {
84609
85032
  /**
84610
85033
  * Subagent inside a Devin plugin bundle (`<plugin>/agents/<name>/AGENT.md`).
84611
85034
  * Plugin subagents use the same profile format as project ones and are
84612
- * exposed as `<plugin>:<name>`. Devin also reads a flat `agents/<name>.md`;
84613
- * the directory form is written to match the `devin` target.
85035
+ * exposed as `<plugin>:<name>`. Devin also reads a flat `agents/<name>.md`,
85036
+ * which import picks up too; the directory form is written to match the
85037
+ * `devin` target.
84614
85038
  *
84615
85039
  * @see https://docs.devin.ai/cli/extensibility/plugins/overview
84616
85040
  */
@@ -88172,6 +88596,17 @@ var ZoocodeSubagent = class extends RooSubagent {
88172
88596
  };
88173
88597
  //#endregion
88174
88598
  //#region src/features/subagents/subagents-processor.ts
88599
+ /**
88600
+ * The globs import scans for a tool: its import-only patterns when it reads
88601
+ * more layouts than Rulesync writes, otherwise the generation pattern.
88602
+ */
88603
+ function importFilePatternsOf(meta) {
88604
+ return meta.importFilePatterns ?? meta.filePattern;
88605
+ }
88606
+ /** Whether unparseable files are left out of this import or orphan sweep. */
88607
+ function skipsInvalidFiles(meta, forDeletion) {
88608
+ return meta.skipInvalidFiles === true || !forDeletion && meta.skipInvalidImportFiles === true;
88609
+ }
88175
88610
  const SubagentsProcessorToolTargetSchema = zod_mini.z.enum(subagentsProcessorToolTargetTuple);
88176
88611
  /**
88177
88612
  * Factory Map mapping tool targets to their subagent factories.
@@ -88368,7 +88803,9 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
88368
88803
  supportsProject: true,
88369
88804
  supportsSimulated: false,
88370
88805
  supportsGlobal: true,
88371
- filePattern: "*/AGENT.md"
88806
+ filePattern: "*/AGENT.md",
88807
+ importFilePatterns: ["*.md", "*/AGENT.md"],
88808
+ skipInvalidImportFiles: true
88372
88809
  }
88373
88810
  }],
88374
88811
  ["devin-plugin", {
@@ -88377,7 +88814,9 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
88377
88814
  supportsProject: true,
88378
88815
  supportsSimulated: false,
88379
88816
  supportsGlobal: false,
88380
- filePattern: "*/AGENT.md"
88817
+ filePattern: "*/AGENT.md",
88818
+ importFilePatterns: ["*.md", "*/AGENT.md"],
88819
+ skipInvalidImportFiles: true
88381
88820
  }
88382
88821
  }],
88383
88822
  ["factorydroid", {
@@ -88856,6 +89295,7 @@ var SubagentsProcessor = class extends FeatureProcessor {
88856
89295
  const factory = this.getFactory(this.toolTarget);
88857
89296
  const paths = factory.class.getSettablePaths({ global: this.global });
88858
89297
  const roots = forDeletion ? [paths.relativeDirPath] : [paths.relativeDirPath, ...paths.importDirPaths ?? []];
89298
+ const filePatterns = forDeletion ? factory.meta.filePattern : importFilePatternsOf(factory.meta);
88859
89299
  const toolSubagents = [];
88860
89300
  const claimedRelativeFilePaths = /* @__PURE__ */ new Map();
88861
89301
  const claimedImportIdentities = new ClaimedIdentities();
@@ -88872,7 +89312,7 @@ var SubagentsProcessor = class extends FeatureProcessor {
88872
89312
  this.logger.warn(`Skipping the orphan sweep for ${quoteForLog(baseDir)}; nothing under it is deleted: ${stripControlCharacters(formatError(error))}`);
88873
89313
  continue;
88874
89314
  }
88875
- const subagentFilePaths = (await findFilesByGlobs(factory.meta.filePattern, {
89315
+ const subagentFilePaths = (await findFilesByGlobs(filePatterns, {
88876
89316
  cwd: baseDir,
88877
89317
  followSymbolicLinks: !forDeletion
88878
89318
  })).toSorted((left, right) => compareRelativePathsShallowestFirst((0, node_path.relative)(baseDir, left), (0, node_path.relative)(baseDir, right)));
@@ -88886,7 +89326,7 @@ var SubagentsProcessor = class extends FeatureProcessor {
88886
89326
  })));
88887
89327
  ownedFilePaths = subagentFilePaths.filter((_, index) => ownership[index]);
88888
89328
  }
88889
- const parsedFiles = factory.meta.skipInvalidFiles ? (await Promise.all(ownedFilePaths.map(async (path) => {
89329
+ const parsedFiles = skipsInvalidFiles(factory.meta, forDeletion) ? (await Promise.all(ownedFilePaths.map(async (path) => {
88890
89330
  try {
88891
89331
  return {
88892
89332
  path,
@@ -91762,6 +92202,85 @@ var CodebuddyRule = class CodebuddyRule extends ToolRule {
91762
92202
  }
91763
92203
  };
91764
92204
  //#endregion
92205
+ //#region src/features/rules/codebuff-rule.ts
92206
+ /**
92207
+ * Rule generator for Codebuff (Freebuff), global scope only.
92208
+ *
92209
+ * Codebuff reads one user-level instructions file from the home directory:
92210
+ * the first hidden file matching `AGENTS.md` or `CLAUDE.md` (case-insensitive,
92211
+ * in that priority order), so the root rule is written to `~/.AGENTS.md`. There
92212
+ * is no user-level rules directory, so non-root rules fold into that file.
92213
+ *
92214
+ * Project scope is not supported here: Codebuff reads the project-root
92215
+ * `AGENTS.md`, which the `agentsmd` target already writes. The RulesProcessor
92216
+ * keeps this target out of project mode (`supportsProject: false`), so the
92217
+ * settable paths are the home-directory ones regardless of `global`.
92218
+ *
92219
+ * @see https://github.com/CodebuffAI/freebuff/blob/main/sdk/src/run-state.ts
92220
+ * @see https://github.com/CodebuffAI/freebuff/blob/main/common/src/constants/knowledge.ts
92221
+ */
92222
+ var CodebuffRule = class CodebuffRule extends ToolRule {
92223
+ static getSettablePaths(_options = {}) {
92224
+ return { root: {
92225
+ relativeDirPath: ".",
92226
+ relativeFilePath: CODEBUFF_GLOBAL_RULE_FILE_NAME
92227
+ } };
92228
+ }
92229
+ static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true }) {
92230
+ const { root } = this.getSettablePaths({ global: true });
92231
+ if (relativeFilePath !== root.relativeFilePath) throw new Error(`CodebuffRule only supports the root rule: ${relativeFilePath}`);
92232
+ const fileContent = await readFileContent((0, node_path.join)(outputRoot, root.relativeDirPath, root.relativeFilePath));
92233
+ return new CodebuffRule({
92234
+ outputRoot,
92235
+ relativeDirPath: root.relativeDirPath,
92236
+ relativeFilePath: root.relativeFilePath,
92237
+ fileContent,
92238
+ validate,
92239
+ root: true
92240
+ });
92241
+ }
92242
+ static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true }) {
92243
+ const { root } = this.getSettablePaths({ global: true });
92244
+ const frontmatter = rulesyncRule.getFrontmatter();
92245
+ return new CodebuffRule({
92246
+ outputRoot,
92247
+ relativeDirPath: root.relativeDirPath,
92248
+ relativeFilePath: root.relativeFilePath,
92249
+ fileContent: rulesyncRule.getBody(),
92250
+ validate,
92251
+ root: frontmatter.root ?? false,
92252
+ description: frontmatter.description,
92253
+ globs: frontmatter.globs
92254
+ });
92255
+ }
92256
+ toRulesyncRule() {
92257
+ return this.toRulesyncRuleDefault();
92258
+ }
92259
+ validate() {
92260
+ return {
92261
+ success: true,
92262
+ error: null
92263
+ };
92264
+ }
92265
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
92266
+ const { root } = this.getSettablePaths({ global: true });
92267
+ return new CodebuffRule({
92268
+ outputRoot,
92269
+ relativeDirPath,
92270
+ relativeFilePath,
92271
+ fileContent: "",
92272
+ validate: false,
92273
+ root: relativeDirPath === root.relativeDirPath && relativeFilePath === root.relativeFilePath
92274
+ });
92275
+ }
92276
+ static isTargetedByRulesyncRule(rulesyncRule) {
92277
+ return this.isTargetedByRulesyncRuleDefault({
92278
+ rulesyncRule,
92279
+ toolTarget: "codebuff"
92280
+ });
92281
+ }
92282
+ };
92283
+ //#endregion
91765
92284
  //#region src/features/rules/codewhale-rule.ts
91766
92285
  /**
91767
92286
  * Rule generator for Codewhale.
@@ -93399,7 +93918,7 @@ var DshRule = class extends NestedAgentsmdRule {
93399
93918
  * `getExtraFixedFiles`): Droid loads "direct `.md` children of each
93400
93919
  * `output-styles` directory".
93401
93920
  */
93402
- const OUTPUT_STYLE_FILE_GLOB = "*.md";
93921
+ const OUTPUT_STYLE_FILE_GLOB$1 = "*.md";
93403
93922
  /**
93404
93923
  * Rule generator for Factory Droid.
93405
93924
  *
@@ -93521,7 +94040,7 @@ var FactorydroidRule = class FactorydroidRule extends ToolRule {
93521
94040
  static getExtraFixedFiles({ global = false } = {}) {
93522
94041
  return [...this.getChannelPaths({ global }).map(({ path }) => path), {
93523
94042
  relativeDirPath: this.getOutputStylesDirPath(),
93524
- relativeFilePath: OUTPUT_STYLE_FILE_GLOB,
94043
+ relativeFilePath: OUTPUT_STYLE_FILE_GLOB$1,
93525
94044
  importOnly: true
93526
94045
  }];
93527
94046
  }
@@ -95376,6 +95895,12 @@ const UNIVERSAL_GLOBS$1 = /* @__PURE__ */ new Set([
95376
95895
  * Any other mode is kept in the `qoder` section so it survives a round trip.
95377
95896
  */
95378
95897
  const INFERABLE_TRIGGERS = /* @__PURE__ */ new Set(["always_on", "glob"]);
95898
+ /**
95899
+ * The file-name glob `getExtraFixedFiles` returns for the output-styles
95900
+ * directory, so import enumerates every style in it (never deletion: the
95901
+ * directory also holds hand-written styles).
95902
+ */
95903
+ const OUTPUT_STYLE_FILE_GLOB = "*.md";
95379
95904
  function normalizeGlobList(value) {
95380
95905
  if (value === void 0) return [];
95381
95906
  return (typeof value === "string" ? splitBraceAwareList(value) : value).map((glob) => glob.trim()).filter((glob) => glob !== "");
@@ -95409,22 +95934,59 @@ function resolveTrigger(frontmatter) {
95409
95934
  * A `qoder.trigger` (for example `model_decision` or `manual`) always takes
95410
95935
  * precedence and round-trips through import.
95411
95936
  *
95937
+ * A non-root rule with `qoder.channel: output-style` becomes a custom output
95938
+ * style instead, `.qoder/output-styles/<rule file name>.md` (both scopes),
95939
+ * with the rule's `description` and optional `qoder.name` as the style's
95940
+ * `description` / `name` frontmatter.
95941
+ *
95412
95942
  * @see https://docs.qoder.com/en/cli/04-extending-qoder-cli/memory
95943
+ * @see https://docs.qoder.com/cli/output-styles
95413
95944
  */
95414
95945
  var QoderRule = class QoderRule extends ToolRule {
95415
95946
  frontmatter;
95416
95947
  body;
95417
- constructor({ frontmatter, body, ...rest }) {
95948
+ outputStyle;
95949
+ constructor({ frontmatter, body, outputStyle = false, ...rest }) {
95418
95950
  if (rest.validate) {
95419
95951
  const result = QoderRuleFrontmatterSchema.safeParse(frontmatter);
95420
95952
  if (!result.success) throw new Error(`Invalid frontmatter in ${(0, node_path.join)(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}`);
95421
95953
  }
95422
95954
  super({
95423
95955
  ...rest,
95424
- fileContent: rest.root ? body : stringifyFrontmatter(body, frontmatter)
95956
+ fileContent: rest.root || outputStyle && Object.keys(frontmatter).length === 0 ? body : stringifyFrontmatter(body, frontmatter)
95425
95957
  });
95426
95958
  this.frontmatter = frontmatter;
95427
95959
  this.body = body;
95960
+ this.outputStyle = outputStyle;
95961
+ }
95962
+ /**
95963
+ * Whether the given path is a custom output style: a direct `.md` child of
95964
+ * `.qoder/output-styles/`, at either scope.
95965
+ */
95966
+ static isOutputStylePath({ relativeDirPath, relativeFilePath }) {
95967
+ return relativeDirPath === QODER_OUTPUT_STYLES_DIR_PATH && (0, node_path.extname)(relativeFilePath) === ".md" && (0, node_path.basename)(relativeFilePath) === relativeFilePath;
95968
+ }
95969
+ /** Whether a rule opts into the `output-style` channel (non-root only). */
95970
+ static isOutputStyleRule(rulesyncRule) {
95971
+ const frontmatter = rulesyncRule.getFrontmatter();
95972
+ return !frontmatter.root && frontmatter.qoder?.channel === "output-style";
95973
+ }
95974
+ /**
95975
+ * The output-styles directory, enumerated for import only: it also holds
95976
+ * styles users write by hand (including personal ones in
95977
+ * `~/.qoder/output-styles/`) that rulesync cannot tell from its own, so
95978
+ * `--delete` leaves it alone.
95979
+ */
95980
+ static getExtraFixedFiles(_params = {}) {
95981
+ return [{
95982
+ relativeDirPath: QODER_OUTPUT_STYLES_DIR_PATH,
95983
+ relativeFilePath: OUTPUT_STYLE_FILE_GLOB,
95984
+ importOnly: true
95985
+ }];
95986
+ }
95987
+ /** Qoder loads output styles itself; they are not rules to reference. */
95988
+ isExcludedFromRootReferences() {
95989
+ return this.outputStyle;
95428
95990
  }
95429
95991
  static getSettablePaths({ global = false, excludeToolDir } = {}) {
95430
95992
  return {
@@ -95437,6 +95999,23 @@ var QoderRule = class QoderRule extends ToolRule {
95437
95999
  }
95438
96000
  static async fromFile({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, validate = true, global = false }) {
95439
96001
  const paths = this.getSettablePaths({ global });
96002
+ if (this.isOutputStylePath({
96003
+ relativeDirPath,
96004
+ relativeFilePath
96005
+ })) {
96006
+ const filePath = (0, node_path.join)(outputRoot, QODER_OUTPUT_STYLES_DIR_PATH, relativeFilePath);
96007
+ const { frontmatter, body } = parseFrontmatter(await readFileContent(filePath), filePath);
96008
+ return new QoderRule({
96009
+ outputRoot,
96010
+ relativeDirPath: QODER_OUTPUT_STYLES_DIR_PATH,
96011
+ relativeFilePath,
96012
+ frontmatter,
96013
+ body: body.trim(),
96014
+ validate,
96015
+ root: false,
96016
+ outputStyle: true
96017
+ });
96018
+ }
95440
96019
  if (relativeDirPath !== void 0 ? relativeDirPath !== QODER_RULES_DIR_PATH : relativeFilePath === paths.root.relativeFilePath) {
95441
96020
  const rootDirPath = relativeDirPath ?? paths.root.relativeDirPath;
95442
96021
  const fileContent = await readFileContent((0, node_path.join)(outputRoot, rootDirPath, paths.root.relativeFilePath));
@@ -95469,6 +96048,11 @@ var QoderRule = class QoderRule extends ToolRule {
95469
96048
  const root = rulesyncFrontmatter.root ?? false;
95470
96049
  const paths = this.getSettablePaths({ global });
95471
96050
  const body = rulesyncRule.getBody();
96051
+ if (this.isOutputStyleRule(rulesyncRule)) return this.buildOutputStyle({
96052
+ outputRoot,
96053
+ rulesyncRule,
96054
+ validate
96055
+ });
95472
96056
  if (root) return new QoderRule({
95473
96057
  outputRoot,
95474
96058
  relativeDirPath: paths.root.relativeDirPath,
@@ -95488,8 +96072,32 @@ var QoderRule = class QoderRule extends ToolRule {
95488
96072
  root: false
95489
96073
  });
95490
96074
  }
96075
+ /**
96076
+ * Write an `output-style` rule as `.qoder/output-styles/<name>.md`. The file
96077
+ * name is the rule's own, so a rule nested under `.rulesync/rules/` is
96078
+ * refused: Qoder reads only the files directly in `output-styles/`.
96079
+ */
96080
+ static buildOutputStyle({ outputRoot, rulesyncRule, validate }) {
96081
+ const { description, qoder } = rulesyncRule.getFrontmatter();
96082
+ const sourcePath = rulesyncRule.getRelativeFilePath();
96083
+ if ((0, node_path.basename)(sourcePath) !== sourcePath) throw new Error(`Rule '${sourcePath}' sets qoder.channel: output-style but is nested in a subdirectory: Qoder reads only the files directly in .qoder/output-styles/, so move it to the top of ${RULESYNC_RULES_RELATIVE_DIR_PATH}.`);
96084
+ const name = qoder?.name;
96085
+ return new QoderRule({
96086
+ outputRoot,
96087
+ relativeDirPath: QODER_OUTPUT_STYLES_DIR_PATH,
96088
+ relativeFilePath: `${(0, node_path.basename)(sourcePath, (0, node_path.extname)(sourcePath))}.md`,
96089
+ frontmatter: {
96090
+ ...name !== void 0 && { name },
96091
+ ...description !== void 0 && { description }
96092
+ },
96093
+ body: rulesyncRule.getBody(),
96094
+ validate,
96095
+ root: false,
96096
+ outputStyle: true
96097
+ });
96098
+ }
95491
96099
  static buildFrontmatter(rulesyncFrontmatter, relativeFilePath) {
95492
- const { trigger: storedTrigger, glob: storedGlob, ...section } = rulesyncFrontmatter.qoder ?? {};
96100
+ const { trigger: storedTrigger, glob: storedGlob, channel: _channel, name: _name, ...section } = rulesyncFrontmatter.qoder ?? {};
95493
96101
  const specificGlobs = normalizeGlobList(storedGlob ?? rulesyncFrontmatter.globs).filter((glob) => !UNIVERSAL_GLOBS$1.has(glob));
95494
96102
  let trigger = storedTrigger ?? (specificGlobs.length > 0 ? "glob" : "always_on");
95495
96103
  const description = section.description ?? rulesyncFrontmatter.description;
@@ -95507,6 +96115,25 @@ var QoderRule = class QoderRule extends ToolRule {
95507
96115
  }
95508
96116
  toRulesyncRule() {
95509
96117
  if (this.isRoot()) return this.toRulesyncRuleDefault();
96118
+ if (this.outputStyle) {
96119
+ const { name, description } = this.frontmatter;
96120
+ return new RulesyncRule({
96121
+ outputRoot: this.getOutputRoot(),
96122
+ relativeDirPath: RULESYNC_RULES_RELATIVE_DIR_PATH,
96123
+ relativeFilePath: this.getRelativeFilePath(),
96124
+ frontmatter: {
96125
+ root: false,
96126
+ targets: ["qoder"],
96127
+ ...typeof description === "string" && { description },
96128
+ qoder: {
96129
+ channel: "output-style",
96130
+ ...typeof name === "string" && name.trim() !== "" && { name }
96131
+ }
96132
+ },
96133
+ body: this.body,
96134
+ validate: true
96135
+ });
96136
+ }
95510
96137
  const trigger = resolveTrigger(this.frontmatter);
95511
96138
  const { trigger: _trigger, alwaysApply: _alwaysApply, description, glob, paths, ...unmapped } = this.frontmatter;
95512
96139
  const fileGlobs = normalizeGlobList(glob ?? paths);
@@ -95555,6 +96182,10 @@ var QoderRule = class QoderRule extends ToolRule {
95555
96182
  return this.body;
95556
96183
  }
95557
96184
  static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
96185
+ const outputStyle = this.isOutputStylePath({
96186
+ relativeDirPath,
96187
+ relativeFilePath
96188
+ });
95558
96189
  return new QoderRule({
95559
96190
  outputRoot,
95560
96191
  relativeDirPath,
@@ -95562,7 +96193,8 @@ var QoderRule = class QoderRule extends ToolRule {
95562
96193
  frontmatter: {},
95563
96194
  body: "",
95564
96195
  validate: false,
95565
- root: relativeDirPath !== QODER_RULES_DIR_PATH && relativeFilePath === "AGENTS.md"
96196
+ root: !outputStyle && relativeDirPath !== QODER_RULES_DIR_PATH && relativeFilePath === "AGENTS.md",
96197
+ outputStyle
95566
96198
  });
95567
96199
  }
95568
96200
  static isTargetedByRulesyncRule(rulesyncRule) {
@@ -97249,6 +97881,16 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
97249
97881
  localRootFileName: CODEBUDDY_LOCAL_RULE_FILE_NAME
97250
97882
  }
97251
97883
  }],
97884
+ ["codebuff", {
97885
+ class: CodebuffRule,
97886
+ meta: {
97887
+ extension: "md",
97888
+ supportsProject: false,
97889
+ supportsGlobal: true,
97890
+ ruleDiscoveryMode: "auto",
97891
+ collisionPolicy: "fold"
97892
+ }
97893
+ }],
97252
97894
  ["codewhale", {
97253
97895
  class: CodewhaleRule,
97254
97896
  meta: {
@@ -97679,7 +98321,9 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
97679
98321
  }]
97680
98322
  ]);
97681
98323
  const allToolTargetKeys = [...toolRuleFactories.keys()];
97682
- const rulesProcessorToolTargets = allToolTargetKeys;
98324
+ const rulesProcessorToolTargets = allToolTargetKeys.filter((target) => {
98325
+ return toolRuleFactories.get(target)?.meta.supportsProject !== false;
98326
+ });
97683
98327
  const rulesProcessorToolTargetsGlobal = allToolTargetKeys.filter((target) => {
97684
98328
  return toolRuleFactories.get(target)?.meta.supportsGlobal ?? false;
97685
98329
  });
@@ -97758,7 +98402,7 @@ var RulesProcessor = class extends FeatureProcessor {
97758
98402
  const factory = this.getFactory(this.toolTarget);
97759
98403
  const { meta } = factory;
97760
98404
  this.warnForIncludeRootOption(meta);
97761
- this.warnForFactorydroidRuleOptions({
98405
+ this.warnForOutputStyleRuleOptions({
97762
98406
  rules: alignedRules,
97763
98407
  factory
97764
98408
  });
@@ -97992,22 +98636,25 @@ var RulesProcessor = class extends FeatureProcessor {
97992
98636
  }) !== "auto") this.logger.warn("The rules option `ruleDiscoveryMode` has no effect with `includeRoot: false` on claudecode: the rule references it adds are written into CLAUDE.md, which is not generated.");
97993
98637
  }
97994
98638
  /**
97995
- * Warn about `factorydroid` frontmatter that does not do what it looks like
97996
- * it does: an `output-style` rule that also targets other tools reaches them
97997
- * as an ordinary rule (so presentation instructions end up in their project
97998
- * rules), and `factorydroid.name` is read only with `channel: output-style`.
97999
- * Checked while generating for factorydroid only, so each warning prints once.
98639
+ * Warn about `factorydroid` / `qoder` frontmatter that does not do what it
98640
+ * looks like it does: an `output-style` rule that also targets other tools
98641
+ * reaches them as an ordinary rule (so presentation instructions end up in
98642
+ * their project rules), and `<tool>.name` is read only with
98643
+ * `channel: output-style`. Checked while generating for that tool only, so
98644
+ * each warning prints once.
98000
98645
  */
98001
- warnForFactorydroidRuleOptions({ rules, factory }) {
98002
- if (this.toolTarget !== "factorydroid") return;
98646
+ warnForOutputStyleRuleOptions({ rules, factory }) {
98647
+ const toolTarget = this.toolTarget;
98648
+ if (toolTarget !== "factorydroid" && toolTarget !== "qoder") return;
98003
98649
  for (const rule of rules) {
98004
98650
  const frontmatter = rule.getFrontmatter();
98005
- if (frontmatter.root || !frontmatter.factorydroid || !factory.class.isTargetedByRulesyncRule(rule)) continue;
98006
- const { channel, name } = frontmatter.factorydroid;
98651
+ const section = frontmatter[toolTarget];
98652
+ if (frontmatter.root || !section || !factory.class.isTargetedByRulesyncRule(rule)) continue;
98653
+ const { channel, name } = section;
98007
98654
  const fileName = stripControlCharacters(rule.getRelativeFilePath());
98008
98655
  const targets = frontmatter.targets ?? ["*"];
98009
- if (channel === "output-style" && targets.some((target) => target !== "factorydroid")) this.logger.warn(`${fileName} sets factorydroid.channel: output-style but also targets other tools, which receive it as an ordinary rule. Set \`targets: ["factorydroid"]\` to keep the output style to Factory Droid.`);
98010
- if (channel !== "output-style" && name !== void 0) this.logger.warn(`${fileName} sets factorydroid.name, which is read only with factorydroid.channel: output-style; ignoring it.`);
98656
+ if (channel === "output-style" && targets.some((target) => target !== toolTarget)) this.logger.warn(`${fileName} sets ${toolTarget}.channel: output-style but also targets other tools, which receive it as an ordinary rule. Set \`targets: ["${toolTarget}"]\` to keep the output style to ${toolTarget === "qoder" ? "Qoder" : "Factory Droid"}.`);
98657
+ if (channel !== "output-style" && name !== void 0) this.logger.warn(`${fileName} sets ${toolTarget}.name, which is read only with ${toolTarget}.channel: output-style; ignoring it.`);
98011
98658
  }
98012
98659
  }
98013
98660
  /**
@@ -102454,6 +103101,12 @@ Object.defineProperty(exports, "hasEnclosingMarkOutsideKeycap", {
102454
103101
  return hasEnclosingMarkOutsideKeycap;
102455
103102
  }
102456
103103
  });
103104
+ Object.defineProperty(exports, "hasExecutableBit", {
103105
+ enumerable: true,
103106
+ get: function() {
103107
+ return hasExecutableBit;
103108
+ }
103109
+ });
102457
103110
  Object.defineProperty(exports, "importFromTool", {
102458
103111
  enumerable: true,
102459
103112
  get: function() {
@@ -102610,6 +103263,12 @@ Object.defineProperty(exports, "resolveRulesyncSourceWritePath", {
102610
103263
  return resolveRulesyncSourceWritePath;
102611
103264
  }
102612
103265
  });
103266
+ Object.defineProperty(exports, "resolvedPathEscapesRoot", {
103267
+ enumerable: true,
103268
+ get: function() {
103269
+ return resolvedPathEscapesRoot;
103270
+ }
103271
+ });
102613
103272
  Object.defineProperty(exports, "runWithDirectoryRollback", {
102614
103273
  enumerable: true,
102615
103274
  get: function() {