rulesync 43.1.2 → 43.2.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.
@@ -5522,24 +5522,27 @@ const AUGMENTCODE_HOOK_EVENTS = [
5522
5522
  * Hook events supported by Mistral Vibe (mistral-vibe).
5523
5523
  *
5524
5524
  * Vibe exposes three hook events in `.vibe/hooks.toml`: `pre_tool` ←
5525
- * `preToolUse`, `post_tool` ← `postToolUse`, and `post_agent` ← `stop` (fires
5526
- * after every assistant turn that ends without pending tool calls — the closest
5527
- * canonical equivalent to a "turn end"/"stop" event, matching how
5528
- * codexcli/copilot map their stop events). Only the tool events
5529
- * (`pre_tool`/`post_tool`) carry the `match` tool-name matcher (fnmatch glob or
5530
- * `re:` regex) and the `strict` flag; `post_agent` carries neither. Only
5531
- * `type: "command"` hooks are relevant.
5525
+ * `preToolUse`, `post_tool` ← `postToolUse`, and `post_agent` ← `stop` /
5526
+ * `subagentStop`. `post_agent` fires after every assistant turn that ends
5527
+ * without pending tool calls, for the main agent and (since v3.0.2) for every
5528
+ * subagent. v3.0.3 lets a `post_agent` hook take a `match` on the agent that
5529
+ * finished its turn: the reserved alias `main` selects the main agent, so
5530
+ * canonical `stop` becomes `post_agent` with `match = "main"`, and a canonical
5531
+ * `subagentStop` with a matcher becomes `post_agent` with that agent name as
5532
+ * `match`. Only the tool events (`pre_tool`/`post_tool`) carry the `strict`
5533
+ * flag. Only `type: "command"` hooks are relevant.
5532
5534
  *
5533
5535
  * v2.21.0 graduated hooks from experimental and renamed all three types
5534
5536
  * (`before_tool` → `pre_tool`, `after_tool` → `post_tool`, `post_agent_turn` →
5535
5537
  * `post_agent`). `HookType` is a strict enum, so an entry using an old name is
5536
5538
  * rejected outright and reported as a `HookConfigIssue`.
5537
- * @see https://github.com/mistralai/mistral-vibe/blob/main/README.md
5539
+ * @see https://github.com/mistralai/mistral-vibe/blob/v3.0.3/README.md
5538
5540
  */
5539
5541
  const VIBE_HOOK_EVENTS = [
5540
5542
  "preToolUse",
5541
5543
  "postToolUse",
5542
- "stop"
5544
+ "stop",
5545
+ "subagentStop"
5543
5546
  ];
5544
5547
  /**
5545
5548
  * Hook events supported by Codewhale.
@@ -7007,9 +7010,10 @@ const JUNIE_TO_CANONICAL_EVENT_NAMES = invertEventNames(CANONICAL_TO_JUNIE_EVENT
7007
7010
  /**
7008
7011
  * Map canonical camelCase event names to Mistral Vibe snake_case.
7009
7012
  *
7010
- * Vibe documents three hook events. The canonical `stop` event maps to Vibe's
7011
- * `post_agent` (fires after every assistant turn ending without pending tool
7012
- * calls) — the closest documented "turn end"/"stop" equivalent.
7013
+ * Vibe documents three hook events. The canonical `stop` and `subagentStop`
7014
+ * events both map to Vibe's `post_agent` (fires after every assistant turn
7015
+ * ending without pending tool calls); the adapter tells them apart with the
7016
+ * hook's `match` on the agent name (`main` for the main agent).
7013
7017
  *
7014
7018
  * These are the v2.21.0 names; the pre-2.21.0 spellings (`before_tool`,
7015
7019
  * `after_tool`, `post_agent_turn`) are rejected by Vibe's strict `HookType`
@@ -7019,7 +7023,8 @@ const JUNIE_TO_CANONICAL_EVENT_NAMES = invertEventNames(CANONICAL_TO_JUNIE_EVENT
7019
7023
  const CANONICAL_TO_VIBE_EVENT_NAMES = {
7020
7024
  preToolUse: "pre_tool",
7021
7025
  postToolUse: "post_tool",
7022
- stop: "post_agent"
7026
+ stop: "post_agent",
7027
+ subagentStop: "post_agent"
7023
7028
  };
7024
7029
  /**
7025
7030
  * Map Mistral Vibe snake_case event names to canonical camelCase.
@@ -7032,6 +7037,7 @@ const CANONICAL_TO_VIBE_EVENT_NAMES = {
7032
7037
  */
7033
7038
  const VIBE_TO_CANONICAL_EVENT_NAMES = {
7034
7039
  ...invertEventNames(CANONICAL_TO_VIBE_EVENT_NAMES),
7040
+ post_agent: "stop",
7035
7041
  before_tool: "preToolUse",
7036
7042
  after_tool: "postToolUse",
7037
7043
  post_agent_turn: "stop"
@@ -8382,6 +8388,32 @@ const CopilotPermissionsOverrideSchema = z.looseObject({
8382
8388
  sandbox: z.optional(CopilotSandboxSchema)
8383
8389
  });
8384
8390
  /**
8391
+ * The permission mode the GitHub Copilot CLI starts new interactive sessions
8392
+ * in (`defaultPermissionMode` of `~/.copilot/settings.json`): `manual` prompts
8393
+ * for every request not already allowed, `assisted` lets a model review
8394
+ * eligible requests (Assisted Permissions, non-experimental since v1.0.104),
8395
+ * and `allow-all` is equivalent to `--allow-all`. These are the CLI's own
8396
+ * values and differ from the VS Code `copilot` target's
8397
+ * {@link COPILOT_DEFAULT_PERMISSION_MODES}.
8398
+ *
8399
+ * @see https://docs.github.com/en/copilot/reference/copilot-cli-reference/cli-config-dir-reference
8400
+ */
8401
+ const COPILOTCLI_DEFAULT_PERMISSION_MODES = [
8402
+ "manual",
8403
+ "assisted",
8404
+ "allow-all"
8405
+ ];
8406
+ /**
8407
+ * Tool-scoped override block for the GitHub Copilot CLI.
8408
+ * `defaultPermissionMode` writes the user-scope `defaultPermissionMode` key of
8409
+ * `~/.copilot/settings.json`; it has no canonical category, and the repository
8410
+ * settings file does not accept it.
8411
+ */
8412
+ const CopilotcliPermissionsOverrideSchema = z.looseObject({
8413
+ permission: z.optional(ToolScopedPermissionSchema),
8414
+ defaultPermissionMode: z.optional(z.enum(COPILOTCLI_DEFAULT_PERMISSION_MODES))
8415
+ });
8416
+ /**
8385
8417
  * oh-my-pi's `tools.approvalMode` values: the tool tiers it auto-approves
8386
8418
  * before prompting. `always-ask` auto-approves only read-only tools,
8387
8419
  * `always-ask-cached` (Ask and remember) also prompts before file reads and
@@ -8398,11 +8430,14 @@ const OMP_APPROVAL_MODES = [
8398
8430
  ];
8399
8431
  /**
8400
8432
  * Tool-scoped override block for oh-my-pi (`omp`). `approvalMode` writes the
8401
- * `tools.approvalMode` key of `config.yml`, which has no canonical category.
8433
+ * `tools.approvalMode` key of `config.yml`, and `restrictedFolders` the
8434
+ * `tools.restrictedFolders` list (folders where every action prompts in every
8435
+ * approval mode, `yolo` included); neither has a canonical category.
8402
8436
  */
8403
8437
  const OmpPermissionsOverrideSchema = z.looseObject({
8404
8438
  permission: z.optional(ToolScopedPermissionSchema),
8405
- approvalMode: z.optional(z.enum(OMP_APPROVAL_MODES))
8439
+ approvalMode: z.optional(z.enum(OMP_APPROVAL_MODES)),
8440
+ restrictedFolders: z.optional(z.array(z.string()))
8406
8441
  });
8407
8442
  const KimiCodePermissionsOverrideSchema = z.looseObject({
8408
8443
  permission: z.optional(ToolScopedPermissionSchema),
@@ -8854,6 +8889,48 @@ const QoderPermissionsOverrideSchema = z.looseObject({
8854
8889
  agentsMdExcludes: z.optional(z.array(z.string()))
8855
8890
  });
8856
8891
  /**
8892
+ * Tool-scoped override block for CodeBuddy Code. The shared `permission` block
8893
+ * continues to drive `permissions.allow`/`ask`/`deny` of
8894
+ * `.codebuddy/settings.json` (project) / `~/.codebuddy/settings.json` (global).
8895
+ *
8896
+ * The other documented settings with no canonical category are authored here
8897
+ * and merged into the same file, key by key, over what the file already
8898
+ * holds:
8899
+ *
8900
+ * - `autoReview` (`policy` replaces the built-in security policy of the `auto`
8901
+ * permission mode, `extraPolicy` is appended to it) and the legacy
8902
+ * `autoMode` rules (`environment` / `allow` / `soft_deny` / `hard_deny`).
8903
+ * CodeBuddy reads both from the user and project-local settings only, never
8904
+ * from the shared project file, so they are written by `--global` generate
8905
+ * alone.
8906
+ * - `memory.parentDiscovery`: whether project instruction files
8907
+ * (`CODEBUDDY.md` / `AGENTS.md`) are also discovered in parent directories.
8908
+ *
8909
+ * Only the keys listed here are written; any other key of these groups is
8910
+ * skipped with a warning. Removing a key from the override leaves the
8911
+ * previously written value in place.
8912
+ *
8913
+ * @see https://www.codebuddy.ai/docs/cli/settings
8914
+ * @see https://www.codebuddy.ai/docs/cli/memory
8915
+ * @example
8916
+ * { "autoReview": { "extraPolicy": "Allow dev namespace releases" },
8917
+ * "memory": { "parentDiscovery": false } }
8918
+ */
8919
+ const CodebuddyPermissionsOverrideSchema = z.looseObject({
8920
+ permission: z.optional(ToolScopedPermissionSchema),
8921
+ autoReview: z.optional(z.looseObject({
8922
+ policy: z.optional(z.string()),
8923
+ extraPolicy: z.optional(z.string())
8924
+ })),
8925
+ autoMode: z.optional(z.looseObject({
8926
+ environment: z.optional(z.array(z.string())),
8927
+ allow: z.optional(z.array(z.string())),
8928
+ soft_deny: z.optional(z.array(z.string())),
8929
+ hard_deny: z.optional(z.array(z.string()))
8930
+ })),
8931
+ memory: z.optional(z.looseObject({ parentDiscovery: z.optional(z.boolean()) }))
8932
+ });
8933
+ /**
8857
8934
  * One native Factory Droid permission rule. `match.prefix` positions are either
8858
8935
  * a single token or a non-empty list of alternative tokens.
8859
8936
  *
@@ -9736,13 +9813,13 @@ const PermissionsConfigSchema = z.looseObject({
9736
9813
  zed: z.optional(ZedPermissionsOverrideSchema),
9737
9814
  devin: z.optional(DevinPermissionsOverrideSchema),
9738
9815
  qoder: z.optional(QoderPermissionsOverrideSchema),
9816
+ copilotcli: z.optional(CopilotcliPermissionsOverrideSchema),
9739
9817
  "antigravity-ide": z.optional(CanonicalPermissionsOverrideSchema),
9740
9818
  continue: z.optional(CanonicalPermissionsOverrideSchema),
9741
9819
  copilot: z.optional(CopilotPermissionsOverrideSchema),
9742
- codebuddy: z.optional(CanonicalPermissionsOverrideSchema),
9820
+ codebuddy: z.optional(CodebuddyPermissionsOverrideSchema),
9743
9821
  commandcode: z.optional(CanonicalPermissionsOverrideSchema),
9744
9822
  lettacode: z.optional(CanonicalPermissionsOverrideSchema),
9745
- copilotcli: z.optional(CanonicalPermissionsOverrideSchema),
9746
9823
  crush: z.optional(CanonicalPermissionsOverrideSchema),
9747
9824
  goose: z.optional(CanonicalPermissionsOverrideSchema),
9748
9825
  omp: z.optional(OmpPermissionsOverrideSchema),
@@ -10229,6 +10306,10 @@ const RulesyncRuleFrontmatterSchema = z.object({
10229
10306
  enabled: z.optional(z.boolean()),
10230
10307
  description: z.optional(z.string())
10231
10308
  })),
10309
+ cline: z.optional(z.looseObject({
10310
+ disabled: z.optional(z.boolean()),
10311
+ enabled: z.optional(z.boolean())
10312
+ })),
10232
10313
  continue: z.optional(z.looseObject({
10233
10314
  alwaysApply: z.optional(z.boolean()),
10234
10315
  description: z.optional(z.string()),
@@ -13708,7 +13789,11 @@ const SHARED_CONFIG_OWNERSHIP = {
13708
13789
  },
13709
13790
  permissions: {
13710
13791
  kind: "replace-owned-keys",
13711
- ownedKeys: ["allowedUrls", "deniedUrls"]
13792
+ ownedKeys: [
13793
+ "allowedUrls",
13794
+ "deniedUrls",
13795
+ "defaultPermissionMode"
13796
+ ]
13712
13797
  }
13713
13798
  }
13714
13799
  },
@@ -13887,7 +13972,12 @@ const SHARED_CONFIG_OWNERSHIP = {
13887
13972
  },
13888
13973
  permissions: {
13889
13974
  kind: "replace-owned-keys",
13890
- ownedKeys: ["permissions"]
13975
+ ownedKeys: [
13976
+ "permissions",
13977
+ "autoReview",
13978
+ "autoMode",
13979
+ "memory"
13980
+ ]
13891
13981
  }
13892
13982
  }
13893
13983
  },
@@ -14197,7 +14287,8 @@ const SHARED_CONFIG_OWNERSHIP = {
14197
14287
  "tools",
14198
14288
  "enabled_tools",
14199
14289
  "disabled_tools",
14200
- "enable_subagents"
14290
+ "enable_subagents",
14291
+ "outside_workdir"
14201
14292
  ]
14202
14293
  }
14203
14294
  }
@@ -30550,7 +30641,10 @@ var QwencodeSkill = class QwencodeSkill extends ToolSkill {
30550
30641
  }
30551
30642
  }
30552
30643
  static getSettablePaths({ global: _global = false } = {}) {
30553
- return { relativeDirPath: QWENCODE_SKILLS_DIR_PATH };
30644
+ return {
30645
+ relativeDirPath: QWENCODE_SKILLS_DIR_PATH,
30646
+ importOnlySkillRoots: [AGENTSMD_SKILLS_DIR_PATH]
30647
+ };
30554
30648
  }
30555
30649
  getFrontmatter() {
30556
30650
  return QwencodeSkillFrontmatterSchema.parse(this.requireMainFileFrontmatter());
@@ -39571,6 +39665,16 @@ var VibePluginSubagent = class VibePluginSubagent extends ToolSubagent {
39571
39665
  */
39572
39666
  const VIBE_PROMPTS_DIR_PATH = join(".vibe", "prompts");
39573
39667
  /**
39668
+ * The agent name Vibe reserves for `post_agent` hook matching (v3.0.3+).
39669
+ * `AgentProfile.from_toml` refuses a file whose stem matches it case-insensitively, so an
39670
+ * agent written as `main.toml` would never load.
39671
+ * @see https://github.com/mistralai/mistral-vibe/blob/v3.0.3/vibe/core/agents/models.py
39672
+ */
39673
+ const VIBE_RESERVED_AGENT_NAME = "main";
39674
+ function isReservedVibeAgentFileName(relativeFilePath) {
39675
+ return basename(relativeFilePath).replace(/\.(md|toml)$/, "").toLowerCase() === VIBE_RESERVED_AGENT_NAME;
39676
+ }
39677
+ /**
39574
39678
  * Vibe resolves a `system_prompt_id` by file name, and
39575
39679
  * `VibeConfigSchema._check_system_prompt` evaluates it during validation — an
39576
39680
  * unresolvable id raises and the agent is dropped at discovery. The id must
@@ -39685,9 +39789,17 @@ var VibeSubagent = class VibeSubagent extends ToolSubagent {
39685
39789
  * evaluates the property during validation, so an unresolvable
39686
39790
  * `system_prompt_id` raises and `AgentRegistry._try_load` drops the agent
39687
39791
  * with a warning.
39792
+ *
39793
+ * A subagent named `main` is skipped with a warning: Vibe reserves that name
39794
+ * for hook matching and does not load an agent file called `main.toml`.
39688
39795
  */
39689
- static fromRulesyncSubagents({ rulesyncSubagents, outputRoot = process.cwd(), global = false }) {
39796
+ static fromRulesyncSubagents({ rulesyncSubagents, outputRoot = process.cwd(), global = false, logger }) {
39690
39797
  return rulesyncSubagents.flatMap((rulesyncSubagent) => {
39798
+ const sourcePath = rulesyncSubagent.getRelativeFilePath();
39799
+ if (isReservedVibeAgentFileName(sourcePath)) {
39800
+ logger?.warn(`Skipping Vibe subagent ${sourcePath}: Vibe reserves the agent name '${VIBE_RESERVED_AGENT_NAME}' for hook matching and does not load an agent file named '${VIBE_RESERVED_AGENT_NAME}'. Rename the subagent file.`);
39801
+ return [];
39802
+ }
39691
39803
  const agentFile = VibeSubagent.fromRulesyncSubagent({
39692
39804
  outputRoot,
39693
39805
  relativeDirPath: VibeSubagent.getSettablePaths({ global }).relativeDirPath,
@@ -44974,17 +45086,49 @@ var ClaudecodeRule = class ClaudecodeRule extends ToolRule {
44974
45086
  * - `alwaysApply: true`: force the rule to always load (equivalent to a file
44975
45087
  * with no frontmatter at all).
44976
45088
  * - `description`: label surfaced in the rule toggle UI.
45089
+ * - `disabled: true` (or `enabled: false`): the rule is switched off. Since
45090
+ * Cline v4.1.26 (cline/cline#14605) the rule toggle writes this flag into the
45091
+ * rule file itself instead of keeping it only in extension state.
44977
45092
  *
44978
45093
  * Rules without frontmatter are always active. Uses `z.looseObject()` so
44979
- * forward-compatible fields added upstream are preserved on round-trip.
45094
+ * forward-compatible fields added upstream are preserved on round-trip: every
45095
+ * key other than `paths` / `alwaysApply` / `description` is carried in the
45096
+ * rulesync `cline:` section.
44980
45097
  *
44981
45098
  * @see https://docs.cline.bot/customization/cline-rules
45099
+ * @see https://github.com/cline/cline/blob/main/sdk/packages/core/src/extensions/config/user-instruction-config-loader.ts
44982
45100
  */
44983
45101
  const ClineRuleFrontmatterSchema = z.looseObject({
44984
45102
  paths: z.optional(z.union([z.array(z.string()), z.string()])),
44985
45103
  alwaysApply: z.optional(z.boolean()),
44986
45104
  description: z.optional(z.string())
44987
45105
  });
45106
+ const CLINE_RULE_FIELD_TYPES = {
45107
+ disabled: "boolean",
45108
+ enabled: "boolean"
45109
+ };
45110
+ const CANONICAL_CLINE_RULE_KEYS = [
45111
+ "paths",
45112
+ "alwaysApply",
45113
+ "description"
45114
+ ];
45115
+ /** Returns a copy of `record` without the canonical `paths` / `alwaysApply` / `description` keys. */
45116
+ function omitCanonicalClineRuleKeys(record) {
45117
+ return Object.fromEntries(Object.entries(record ?? {}).filter(([key]) => !CANONICAL_CLINE_RULE_KEYS.includes(key)));
45118
+ }
45119
+ /**
45120
+ * Splits a Cline rule frontmatter into the keys that have canonical rulesync
45121
+ * homes (`paths` / `alwaysApply` / `description`) and the rest, which are
45122
+ * lifted into the rulesync `cline:` section.
45123
+ */
45124
+ function extractClineSection({ frontmatter, relativeFilePath }) {
45125
+ return Object.fromEntries(Object.entries(omitCanonicalClineRuleKeys(frontmatter)).filter(([key, value]) => {
45126
+ const expected = Object.hasOwn(CLINE_RULE_FIELD_TYPES, key) ? CLINE_RULE_FIELD_TYPES[key] : void 0;
45127
+ if (expected === void 0 || value === void 0 || typeof value === expected) return true;
45128
+ warnWithFallback(void 0, `Cline rules: dropping ${key} from ${relativeFilePath}: Cline rejects the whole rule unless it is a ${expected}.`);
45129
+ return false;
45130
+ }));
45131
+ }
44988
45132
  function isNonRootParams$1(params) {
44989
45133
  return "frontmatter" in params && "body" in params;
44990
45134
  }
@@ -45059,6 +45203,10 @@ var ClineRule = class ClineRule extends ToolRule {
45059
45203
  toRulesyncRule() {
45060
45204
  if (this.isRoot()) return this.toRulesyncRuleDefault();
45061
45205
  const globs = this.frontmatter?.alwaysApply === true ? ["**/*"] : normalizePaths$1(this.frontmatter?.paths);
45206
+ const cline = extractClineSection({
45207
+ frontmatter: this.frontmatter,
45208
+ relativeFilePath: this.getRelativeFilePath()
45209
+ });
45062
45210
  return new RulesyncRule({
45063
45211
  outputRoot: process.cwd(),
45064
45212
  relativeDirPath: RULESYNC_RULES_RELATIVE_DIR_PATH,
@@ -45067,7 +45215,8 @@ var ClineRule = class ClineRule extends ToolRule {
45067
45215
  root: false,
45068
45216
  targets: ["*"],
45069
45217
  description: this.frontmatter?.description,
45070
- globs
45218
+ globs,
45219
+ ...Object.keys(cline).length > 0 && { cline }
45071
45220
  },
45072
45221
  body: this.body ?? this.getFileContent()
45073
45222
  });
@@ -45112,16 +45261,22 @@ var ClineRule = class ClineRule extends ToolRule {
45112
45261
  * universal match-everything globs mean "always", so they map to
45113
45262
  * `alwaysApply: true` instead of a `paths` list; specific globs become
45114
45263
  * conditional `paths`; an empty globs list leaves the rule always-active with
45115
- * no `paths`/`alwaysApply`.
45264
+ * no `paths`/`alwaysApply`. Every key of the rulesync `cline:` section
45265
+ * (e.g. `disabled: true`) is written into the frontmatter too; the canonical
45266
+ * `paths` / `alwaysApply` / `description` win over same-named keys there.
45116
45267
  */
45117
45268
  static buildNonRootRule({ outputRoot, rulesyncRule, validate, relativeDirPath }) {
45118
45269
  const rulesyncFrontmatter = rulesyncRule.getFrontmatter();
45119
45270
  const globs = normalizePaths$1(rulesyncFrontmatter.globs);
45120
45271
  const isUniversal = globs.length > 0 && globs.every((glob) => glob === "**/*" || glob === "*");
45272
+ const rawClineSection = rulesyncFrontmatter.cline ?? {};
45273
+ for (const key of CANONICAL_CLINE_RULE_KEYS) if (Object.hasOwn(rawClineSection, key)) warnWithFallback(void 0, `Cline rules: ignoring cline.${key} in ${rulesyncRule.getRelativeFilePath()}: set the canonical ${key === "description" ? "description" : "globs"} instead.`);
45274
+ const clineSection = omitCanonicalClineRuleKeys(rawClineSection);
45121
45275
  const frontmatter = {
45122
45276
  paths: globs.length > 0 && !isUniversal ? globs : void 0,
45123
45277
  alwaysApply: isUniversal ? true : void 0,
45124
- description: rulesyncFrontmatter.description || void 0
45278
+ description: rulesyncFrontmatter.description || void 0,
45279
+ ...clineSection
45125
45280
  };
45126
45281
  return new ClineRule({
45127
45282
  outputRoot,
@@ -55808,6 +55963,14 @@ function applyCommandPrefix({ def, converterConfig }) {
55808
55963
  if (isExecForm) return `${bracePlaceholder(converterConfig.projectDirVar)}/${stripSurroundingQuotes(relativeCommand)}`;
55809
55964
  return anchorInline(`"${converterConfig.projectDirVar}"/${relativeCommand}`);
55810
55965
  }
55966
+ /** Whether a field restricted to some events applies on this one. */
55967
+ function isEventApplicable({ events, eventName }) {
55968
+ return events === void 0 || events.has(eventName);
55969
+ }
55970
+ /** Quote and comma-join a set of names (events, hook types) for a warning. */
55971
+ function describeEvents(names) {
55972
+ return [...names].map((name) => `"${name}"`).join(", ");
55973
+ }
55811
55974
  /**
55812
55975
  * Whether a field registered for `command` hooks only applies to this hook.
55813
55976
  * Applied on both export and import: a value imported into a canonical field
@@ -55829,7 +55992,7 @@ function isFieldApplicable({ commandOnly, hookType }) {
55829
55992
  * silence — the mirror of the import side.
55830
55993
  */
55831
55994
  function emitPassthroughFields({ def, hookType, eventName, fields, isValid, warn }) {
55832
- for (const { canonical, tool, commandOnly, maxLength, collapseWhitespaceForLength, onlyWithFlag } of fields) {
55995
+ for (const { canonical, tool, commandOnly, maxLength, collapseWhitespaceForLength, onlyWithFlag, events } of fields) {
55833
55996
  const value = def[canonical];
55834
55997
  if (value === void 0) continue;
55835
55998
  if (!isFieldApplicable({
@@ -55839,6 +56002,13 @@ function emitPassthroughFields({ def, hookType, eventName, fields, isValid, warn
55839
56002
  warn?.(`Dropping "${canonical}" from a "${hookType}" hook on "${eventName}": this tool documents "${tool}" on "command" hooks only, so it is not generated.`);
55840
56003
  continue;
55841
56004
  }
56005
+ if (events !== void 0 && !isEventApplicable({
56006
+ events,
56007
+ eventName
56008
+ })) {
56009
+ if (value !== false) warn?.(`Dropping "${canonical}" from a "${hookType}" hook on "${eventName}": this tool honours "${tool}" on ${describeEvents(events)} only, so it is not generated.`);
56010
+ continue;
56011
+ }
55842
56012
  if (!isValid({
55843
56013
  value,
55844
56014
  canonical
@@ -55852,9 +56022,12 @@ function emitPassthroughFields({ def, hookType, eventName, fields, isValid, warn
55852
56022
  if (length > maxLength) warn?.(`"${canonical}" on a "${hookType}" hook on "${eventName}" is ${length} characters long${collapseWhitespaceForLength ? " after collapsing whitespace" : ""}; this tool caps "${tool}" at ${maxLength} characters and will shorten it.`);
55853
56023
  }
55854
56024
  }
55855
- return Object.fromEntries(fields.filter(({ canonical, commandOnly }) => isFieldApplicable({
56025
+ return Object.fromEntries(fields.filter(({ canonical, commandOnly, events }) => isFieldApplicable({
55856
56026
  commandOnly,
55857
56027
  hookType
56028
+ }) && isEventApplicable({
56029
+ events,
56030
+ eventName
55858
56031
  }) && isValid({
55859
56032
  value: def[canonical],
55860
56033
  canonical
@@ -55867,16 +56040,24 @@ function emitPassthroughFields({ def, hookType, eventName, fields, isValid, warn
55867
56040
  * kind has a rule an authored file can plausibly violate — turns a rejected
55868
56041
  * value into a warning instead of a silent drop.
55869
56042
  */
55870
- function importPassthroughFields({ h, hookType, fields, isValid, describeInvalid, warn }) {
55871
- const applicable = fields.filter(({ commandOnly }) => isFieldApplicable({
56043
+ function importPassthroughFields({ h, hookType, eventName, fields, isValid, describeInvalid, warn }) {
56044
+ const typeApplicable = fields.filter(({ commandOnly }) => isFieldApplicable({
55872
56045
  commandOnly,
55873
56046
  hookType
55874
56047
  }));
56048
+ const applicable = typeApplicable.filter(({ events }) => isEventApplicable({
56049
+ events,
56050
+ eventName
56051
+ }));
55875
56052
  const skipped = fields.filter(({ commandOnly }) => !isFieldApplicable({
55876
56053
  commandOnly,
55877
56054
  hookType
55878
56055
  }));
55879
56056
  for (const { tool } of skipped) if (h[tool] !== void 0) warn?.(`Dropping "${tool}" from an imported "${hookType}" hook: this tool documents it on "command" hooks only, so it is not imported.`);
56057
+ for (const { tool, events } of typeApplicable) if (events !== void 0 && !isEventApplicable({
56058
+ events,
56059
+ eventName
56060
+ }) && h[tool] !== void 0 && h[tool] !== false) warn?.(`Dropping "${tool}" from an imported "${hookType}" hook on "${eventName}": this tool honours it on ${describeEvents(events)} only, so it is not imported.`);
55880
56061
  for (const { tool, canonical } of applicable) if (describeInvalid !== void 0 && h[tool] !== void 0 && !isValid({
55881
56062
  value: h[tool],
55882
56063
  canonical
@@ -56044,11 +56225,11 @@ function isKeywordFieldApplicable({ spec, hookType, eventName, flags, direction,
56044
56225
  return false;
56045
56226
  }
56046
56227
  if (hookTypes !== void 0 && !hookTypes.has(hookType)) {
56047
- warn?.(`Dropping "${field}" from a "${hookType}" hook on "${eventName}": this tool documents "${tool}" on ${[...hookTypes].map((type) => `"${type}"`).join(", ")} hooks only, so it is not ${direction}.`);
56228
+ warn?.(`Dropping "${field}" from a "${hookType}" hook on "${eventName}": this tool documents "${tool}" on ${describeEvents(hookTypes)} hooks only, so it is not ${direction}.`);
56048
56229
  return false;
56049
56230
  }
56050
56231
  if (events !== void 0 && !events.has(eventName)) {
56051
- warn?.(`Dropping "${field}" from a "${hookType}" hook on "${eventName}": this tool honours "${tool}" on ${[...events].map((event) => `"${event}"`).join(", ")} only, so it is not ${direction}.`);
56232
+ warn?.(`Dropping "${field}" from a "${hookType}" hook on "${eventName}": this tool honours "${tool}" on ${describeEvents(events)} only, so it is not ${direction}.`);
56052
56233
  return false;
56053
56234
  }
56054
56235
  if (ignoredEvents?.has(eventName)) {
@@ -56494,6 +56675,7 @@ function importAllPassthroughFields({ h, hookType, eventName, converterConfig, w
56494
56675
  ...importPassthroughFields({
56495
56676
  h,
56496
56677
  hookType,
56678
+ eventName,
56497
56679
  fields: converterConfig.booleanPassthroughFields ?? [],
56498
56680
  isValid: isBooleanValue,
56499
56681
  warn
@@ -56508,6 +56690,7 @@ function importAllPassthroughFields({ h, hookType, eventName, converterConfig, w
56508
56690
  ...importPassthroughFields({
56509
56691
  h,
56510
56692
  hookType,
56693
+ eventName,
56511
56694
  fields: converterConfig.numberPassthroughFields ?? [],
56512
56695
  isValid: isImportableNumber,
56513
56696
  describeInvalid: describeInvalidScalar,
@@ -56516,6 +56699,7 @@ function importAllPassthroughFields({ h, hookType, eventName, converterConfig, w
56516
56699
  ...importPassthroughFields({
56517
56700
  h,
56518
56701
  hookType,
56702
+ eventName,
56519
56703
  fields: converterConfig.stringPassthroughFields ?? [],
56520
56704
  isValid: isImportableString,
56521
56705
  describeInvalid: describeInvalidScalar,
@@ -56524,6 +56708,7 @@ function importAllPassthroughFields({ h, hookType, eventName, converterConfig, w
56524
56708
  ...importPassthroughFields({
56525
56709
  h,
56526
56710
  hookType,
56711
+ eventName,
56527
56712
  fields: converterConfig.arrayPassthroughFields ?? [],
56528
56713
  isValid: isImportableArray,
56529
56714
  describeInvalid: describeInvalidArray,
@@ -56532,6 +56717,7 @@ function importAllPassthroughFields({ h, hookType, eventName, converterConfig, w
56532
56717
  ...importPassthroughFields({
56533
56718
  h,
56534
56719
  hookType,
56720
+ eventName,
56535
56721
  fields: converterConfig.recordPassthroughFields ?? [],
56536
56722
  isValid: isEmittableRecord,
56537
56723
  describeInvalid: describeInvalidRecord,
@@ -56540,6 +56726,7 @@ function importAllPassthroughFields({ h, hookType, eventName, converterConfig, w
56540
56726
  ...importPassthroughFields({
56541
56727
  h,
56542
56728
  hookType,
56729
+ eventName,
56543
56730
  fields: converterConfig.objectPassthroughFields ?? [],
56544
56731
  isValid: isImportableObject,
56545
56732
  describeInvalid: describeInvalidObject,
@@ -61372,6 +61559,11 @@ const GROKCLI_CONVERTER_CONFIG = {
61372
61559
  tool: "env",
61373
61560
  commandOnly: true
61374
61561
  }],
61562
+ booleanPassthroughFields: [{
61563
+ canonical: "failClosed",
61564
+ tool: "failClosed",
61565
+ events: /* @__PURE__ */ new Set(["preToolUse", "beforeSubmitPrompt"])
61566
+ }],
61375
61567
  arrayPassthroughFields: [{
61376
61568
  canonical: "args",
61377
61569
  tool: "args",
@@ -63021,6 +63213,15 @@ const LETTACODE_CONVERTER_CONFIG = {
63021
63213
  timeoutUnit: "milliseconds"
63022
63214
  };
63023
63215
  /**
63216
+ * Letta Code documents settings-file hooks as deprecated in favor of Mods
63217
+ * (https://docs.letta.com/reference/deprecated/hooks) but still ships the
63218
+ * loader, so rulesync keeps emitting them and tells the user once per run.
63219
+ * Mods are not modeled: they are JS/TS code modules loaded only from
63220
+ * `~/.letta/mods/` or an agent's memory directory, with no declarative or
63221
+ * project-local form a canonical hook could be translated into.
63222
+ */
63223
+ const LETTACODE_HOOKS_DEPRECATION_WARNING = "Letta Code documents settings.json hooks as deprecated in favor of Mods and may remove them in a future release; rulesync still writes them while Letta Code loads them. See https://docs.letta.com/reference/deprecated/hooks";
63224
+ /**
63024
63225
  * Letta Code keeps a boolean `disabled` switch beside the event arrays inside
63025
63226
  * `hooks`. rulesync owns the whole `hooks` key, so the switch is read back from
63026
63227
  * the existing file and carried into the generated one instead of being lost.
@@ -63079,6 +63280,11 @@ function parseLettacodeSettings$1(fileContent, filePath) {
63079
63280
  * and UI preferences, ...), so generation merges the `hooks` key into the
63080
63281
  * existing file (see `SHARED_CONFIG_OWNERSHIP`) instead of overwriting it.
63081
63282
  *
63283
+ * Letta Code marks this surface deprecated in favor of Mods (see
63284
+ * `LETTACODE_HOOKS_DEPRECATION_WARNING`); generation warns once per run when it
63285
+ * writes any hook.
63286
+ *
63287
+ * @see https://docs.letta.com/reference/deprecated/hooks
63082
63288
  * @see https://github.com/letta-ai/letta-code/blob/main/src/hooks/types.ts
63083
63289
  * @see https://github.com/letta-ai/letta-code/blob/main/src/hooks/loader.ts
63084
63290
  */
@@ -63123,6 +63329,7 @@ var LettacodeHooks = class LettacodeHooks extends ToolHooks {
63123
63329
  converterConfig: LETTACODE_CONVERTER_CONFIG,
63124
63330
  logger
63125
63331
  });
63332
+ if (Object.keys(hooks).length > 0) warnOnceWithFallback(logger, LETTACODE_HOOKS_DEPRECATION_WARNING);
63126
63333
  const fileContent = applySharedConfigPatch({
63127
63334
  fileKey: sharedConfigFileKey(paths),
63128
63335
  feature: "hooks",
@@ -65120,11 +65327,38 @@ const VIBE_DIR = ".vibe";
65120
65327
  const VIBE_HOOKS_FILE_NAME = "hooks.toml";
65121
65328
  /**
65122
65329
  * Vibe scopes the tool-name `match` glob and the `strict` flag to tool hooks
65123
- * (`pre_tool` / `post_tool`) only; `post_agent` fires after every
65124
- * assistant turn and carries no matcher.
65125
- * @see https://github.com/mistralai/mistral-vibe/blob/main/README.md
65330
+ * (`pre_tool` / `post_tool`). `post_agent` takes a `match` too since v3.0.3,
65331
+ * but on the name of the agent that finished its turn, not a tool name (see
65332
+ * {@link VIBE_MAIN_AGENT_MATCH}).
65333
+ * @see https://github.com/mistralai/mistral-vibe/blob/v3.0.3/README.md
65126
65334
  */
65127
65335
  const VIBE_TOOL_EVENTS = /* @__PURE__ */ new Set(["pre_tool", "post_tool"]);
65336
+ /** The Vibe event both canonical `stop` and `subagentStop` become. */
65337
+ const VIBE_POST_AGENT_EVENT = "post_agent";
65338
+ /**
65339
+ * The reserved agent alias a `post_agent` hook's `match` uses to run only
65340
+ * after the main agent. Without a `match`, `post_agent` runs after every
65341
+ * agent, subagents included (v3.0.2+), whereas canonical `stop` is
65342
+ * main-agent-only — so `stop` is written with this match.
65343
+ * @see https://github.com/mistralai/mistral-vibe/blob/v3.0.3/README.md
65344
+ */
65345
+ const VIBE_MAIN_AGENT_MATCH = "main";
65346
+ /**
65347
+ * The `post_agent` `match` for a canonical `subagentStop` hook, or null to
65348
+ * skip it. Vibe has no selector for "every subagent but not the main agent",
65349
+ * so a matcher naming the subagent is required.
65350
+ */
65351
+ function toVibeSubagentMatch({ matcher, logger }) {
65352
+ if (typeof matcher !== "string" || matcher === "" || matcher === "*") {
65353
+ logger?.warn("Vibe hooks: skipping a \"subagentStop\" hook without an agent-name matcher. Vibe's post_agent cannot select every subagent without also running after the main agent; set \"matcher\" to the subagent name.");
65354
+ return null;
65355
+ }
65356
+ if (matcher.toLowerCase() === VIBE_MAIN_AGENT_MATCH) {
65357
+ logger?.warn("Vibe hooks: skipping a \"subagentStop\" hook whose matcher is \"main\". Vibe reserves \"main\" for the main agent; use a \"stop\" hook instead.");
65358
+ return null;
65359
+ }
65360
+ return matcher;
65361
+ }
65128
65362
  const SUPPORTED_VIBE_EVENTS = new Set(VIBE_HOOK_EVENTS);
65129
65363
  /**
65130
65364
  * The `match` a canonical `Bash` matcher becomes. Vibe tests `match` against a
@@ -65148,7 +65382,8 @@ function fromVibeHookMatch(match) {
65148
65382
  /**
65149
65383
  * Build the flat `[[hooks]]` array for `.vibe/hooks.toml` from a canonical
65150
65384
  * hooks config. Vibe uses a flat array where each entry carries its own event
65151
- * `type`, tool-name `match` glob/regex, and `command`. Only `type: "command"`
65385
+ * `type`, `match` glob/regex (a tool name for tool hooks, an agent name
65386
+ * for `post_agent`), and `command`. Only `type: "command"`
65152
65387
  * canonical hooks are emitted: a Vibe hook is a shell command line. (v2.25.1
65153
65388
  * to v2.25.4 briefly exec'd it without a shell under the unified harness and
65154
65389
  * skipped any command containing a backslash; v2.25.5 restored shell
@@ -65156,7 +65391,7 @@ function fromVibeHookMatch(match) {
65156
65391
  * now run through the shell" — and dropped the backslash skip.)
65157
65392
  * @see https://github.com/mistralai/mistral-vibe/blob/v3.0.2/README.md
65158
65393
  */
65159
- function canonicalToVibeHooks({ config, toolOverride }) {
65394
+ function canonicalToVibeHooks({ config, toolOverride, logger }) {
65160
65395
  const shared = {};
65161
65396
  for (const [event, defs] of Object.entries(config.hooks)) if (SUPPORTED_VIBE_EVENTS.has(event)) shared[event] = defs;
65162
65397
  const effective = {
@@ -65164,30 +65399,40 @@ function canonicalToVibeHooks({ config, toolOverride }) {
65164
65399
  ...toolOverride
65165
65400
  };
65166
65401
  const hooks = [];
65402
+ const nextIndexByVibeEvent = /* @__PURE__ */ new Map();
65167
65403
  for (const [event, defs] of Object.entries(effective)) {
65168
65404
  if (!SUPPORTED_VIBE_EVENTS.has(event)) continue;
65169
65405
  const vibeEvent = lookupOwn({
65170
65406
  record: CANONICAL_TO_VIBE_EVENT_NAMES,
65171
65407
  key: event
65172
65408
  }) ?? event;
65173
- let index = 0;
65174
65409
  for (const def of defs) {
65175
65410
  if ((def.type ?? "command") !== "command") continue;
65176
65411
  if (typeof def.command !== "string") continue;
65177
- const name = typeof def.name === "string" ? def.name : `${vibeEvent}-${index}`;
65178
65412
  const isToolEvent = VIBE_TOOL_EVENTS.has(vibeEvent);
65413
+ let match;
65414
+ if (isToolEvent) match = toVibeHookMatch(def.matcher);
65415
+ else if (event === "subagentStop") {
65416
+ const subagentMatch = toVibeSubagentMatch({
65417
+ matcher: def.matcher,
65418
+ logger
65419
+ });
65420
+ if (subagentMatch === null) continue;
65421
+ match = subagentMatch;
65422
+ } else match = VIBE_MAIN_AGENT_MATCH;
65423
+ const index = nextIndexByVibeEvent.get(vibeEvent) ?? 0;
65179
65424
  const entry = {
65180
- name,
65425
+ name: typeof def.name === "string" ? def.name : `${vibeEvent}-${index}`,
65181
65426
  type: vibeEvent,
65427
+ match,
65182
65428
  command: def.command
65183
65429
  };
65184
- if (isToolEvent) entry.match = toVibeHookMatch(def.matcher);
65185
65430
  if (typeof def.timeout === "number") entry.timeout = def.timeout;
65186
65431
  const strict = def.strict;
65187
65432
  if (isToolEvent && typeof strict === "boolean") entry.strict = strict;
65188
65433
  if (typeof def.description === "string") entry.description = def.description;
65189
65434
  hooks.push(entry);
65190
- index += 1;
65435
+ nextIndexByVibeEvent.set(vibeEvent, index + 1);
65191
65436
  }
65192
65437
  }
65193
65438
  return { hooks };
@@ -65199,14 +65444,22 @@ function vibeEntryToCanonicalDef(raw) {
65199
65444
  const vibeEvent = typeof entry.type === "string" ? entry.type : void 0;
65200
65445
  if (vibeEvent === void 0) return null;
65201
65446
  if (isPrototypePollutionKey(vibeEvent)) return null;
65202
- const canonicalEvent = lookupOwn({
65447
+ let canonicalEvent = lookupOwn({
65203
65448
  record: VIBE_TO_CANONICAL_EVENT_NAMES,
65204
65449
  key: vibeEvent
65205
65450
  }) ?? vibeEvent;
65206
65451
  const def = { type: "command" };
65207
65452
  if (typeof entry.command === "string") def.command = entry.command;
65208
- const matcher = fromVibeHookMatch(entry.match);
65209
- if (matcher !== void 0) def.matcher = matcher;
65453
+ if (vibeEvent === VIBE_POST_AGENT_EVENT) {
65454
+ const agentMatch = typeof entry.match === "string" ? entry.match : "";
65455
+ if (agentMatch !== "" && agentMatch !== "*" && agentMatch.toLowerCase() !== VIBE_MAIN_AGENT_MATCH) {
65456
+ canonicalEvent = "subagentStop";
65457
+ def.matcher = agentMatch;
65458
+ }
65459
+ } else {
65460
+ const matcher = fromVibeHookMatch(entry.match);
65461
+ if (matcher !== void 0) def.matcher = matcher;
65462
+ }
65210
65463
  if (typeof entry.timeout === "number") def.timeout = entry.timeout;
65211
65464
  if (typeof entry.name === "string") def.name = entry.name;
65212
65465
  if (typeof entry.description === "string") def.description = entry.description;
@@ -65237,6 +65490,26 @@ function vibeHooksToCanonical(parsed) {
65237
65490
  }
65238
65491
  return canonical;
65239
65492
  }
65493
+ /** Vibe reads a `match` with a `re:` prefix or glob metacharacters as a pattern. */
65494
+ const VIBE_PATTERN_MATCH = /^re:|[*?[]/;
65495
+ /**
65496
+ * Move the imported `subagentStop` hooks into the `vibe` override when any of
65497
+ * them matches agents by a regex or glob rather than by a literal name. Such a
65498
+ * pattern can also select the main agent (a main session answers to its mode
65499
+ * name, e.g. `accept-edits`), and other tools would read it as their own
65500
+ * matcher syntax, so it is kept Vibe-only. The override replaces the shared
65501
+ * list for its event, so the whole list moves to keep every entry on Vibe.
65502
+ */
65503
+ function takeVibeOnlySubagentStop({ hooks, logger }) {
65504
+ const defs = lookupOwn({
65505
+ record: hooks,
65506
+ key: "subagentStop"
65507
+ });
65508
+ if (defs === void 0 || !defs.some((def) => typeof def.matcher === "string" && VIBE_PATTERN_MATCH.test(def.matcher))) return;
65509
+ delete hooks.subagentStop;
65510
+ logger?.warn("Vibe hooks: a post_agent hook matches agents by a pattern, so its subagentStop hooks are imported into the vibe override instead of the shared hooks.");
65511
+ return defs;
65512
+ }
65240
65513
  function parseVibeToml(fileContent) {
65241
65514
  const parsed = smolToml.parse(fileContent || smolToml.stringify({}));
65242
65515
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
@@ -65276,12 +65549,13 @@ var VibeHooks = class extends ToolHooks {
65276
65549
  validate
65277
65550
  });
65278
65551
  }
65279
- static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false }) {
65552
+ static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
65280
65553
  const paths = this.getSettablePaths({ global });
65281
65554
  const config = rulesyncHooks.getJson();
65282
65555
  const vibeHooks = canonicalToVibeHooks({
65283
65556
  config,
65284
- toolOverride: config.vibe?.hooks
65557
+ toolOverride: config.vibe?.hooks,
65558
+ logger
65285
65559
  });
65286
65560
  const fileContent = smolToml.stringify(vibeHooks);
65287
65561
  return new this({
@@ -65292,15 +65566,30 @@ var VibeHooks = class extends ToolHooks {
65292
65566
  validate
65293
65567
  });
65294
65568
  }
65295
- toRulesyncHooks() {
65569
+ toRulesyncHooks(options = {}) {
65296
65570
  const hooks = vibeHooksToCanonical(this.parseFileContentOrThrow({
65297
65571
  displayName: "Vibe",
65298
65572
  parse: (fileContent) => parseVibeToml(fileContent)
65299
65573
  }));
65300
- return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
65574
+ const vibeOnlySubagentStop = takeVibeOnlySubagentStop({
65575
+ hooks,
65576
+ logger: options.logger
65577
+ });
65578
+ const config = buildImportedHooksConfig({
65301
65579
  hooks,
65302
65580
  overrideKey: "vibe"
65303
- }), null, 2) });
65581
+ });
65582
+ if (vibeOnlySubagentStop !== void 0) {
65583
+ const override = config.vibe ?? {};
65584
+ config.vibe = {
65585
+ ...override,
65586
+ hooks: {
65587
+ ...override.hooks,
65588
+ subagentStop: vibeOnlySubagentStop
65589
+ }
65590
+ };
65591
+ }
65592
+ return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(config, null, 2) });
65304
65593
  }
65305
65594
  validate() {
65306
65595
  try {
@@ -76917,6 +77206,12 @@ const REASONIX_PLUGIN_FIELDS = [
76917
77206
  "load",
76918
77207
  "oauth_allow_missing_pkce_metadata"
76919
77208
  ];
77209
+ /**
77210
+ * The `load` mode that puts a server's tools in the provider schema. Reasonix
77211
+ * compares it case-insensitively after trimming (`normalizeMCPLoad`).
77212
+ * @see https://github.com/esengine/DeepSeek-Reasonix/blob/studio-v2.53.0/internal/contract/config/mcp_load.go
77213
+ */
77214
+ const REASONIX_MCP_LOAD_ALWAYS = "always";
76920
77215
  /** The `[[plugins]]` key that canonical `disabledTools` is written to and read from. */
76921
77216
  const REASONIX_DISABLED_TOOLS_KEY = "disabled_tools";
76922
77217
  var ReasonixMcp = class ReasonixMcp extends ToolMcp {
@@ -77081,7 +77376,9 @@ function hasExpectedType(value, expected) {
77081
77376
  * `concurrency` string decodes fine but is ignored in favor of the server-name
77082
77377
  * default, so it is dropped too rather than written as a setting that does
77083
77378
  * nothing. An unknown `load` string is read as `deferred`, the default, so it
77084
- * passes through as authored.
77379
+ * passes through as authored. A project `load = "always"` also passes through,
77380
+ * but is warned about in `writePassthroughFields`: see the v2.53.0 note on
77381
+ * `REASONIX_PLUGIN_FIELDS`.
77085
77382
  * @see https://github.com/esengine/DeepSeek-Reasonix/blob/v2.31.0/internal/contract/config/plugin_entry.go
77086
77383
  */
77087
77384
  function invalidPassthroughFieldReason(field, value) {
@@ -77121,6 +77418,7 @@ function writePassthroughFields({ name, server, plugin, global, logger }) {
77121
77418
  continue;
77122
77419
  }
77123
77420
  if (field === "oauth_allow_missing_pkce_metadata" && value === true) logger?.warn(`Reasonix MCP: "${name}" sets "oauth_allow_missing_pkce_metadata", which lets OAuth proceed with an authorization server that does not advertise PKCE support.` + (global ? "" : ` Reasonix honours it only from the user config, not a project reasonix.toml.`));
77421
+ if (field === "load" && !global && server.disabled !== true && typeof value === "string" && value.trim().toLowerCase() === REASONIX_MCP_LOAD_ALWAYS) logger?.warn(`Reasonix MCP: "${name}" sets "load" to "always" in the project reasonix.toml. Reasonix 2.53.0 and later ignore a project-declared load mode and defer the server; set it in the user config (the server's "load" or [tools.mcp_load]) instead.`);
77124
77422
  plugin[field] = value;
77125
77423
  }
77126
77424
  }
@@ -86059,6 +86357,41 @@ var ClinePermissions = class ClinePermissions extends ToolPermissions {
86059
86357
  // src/features/permissions/codebuddy-permissions.ts
86060
86358
  /** Top-level key of `.codebuddy/settings.json` that rulesync owns. */
86061
86359
  const CODEBUDDY_PERMISSIONS_KEY = "permissions";
86360
+ /**
86361
+ * The top-level settings groups the `codebuddy` override may author, each with
86362
+ * the only keys of that group rulesync writes and imports. A group's other
86363
+ * keys are kept as the file has them; an override key not listed here is
86364
+ * skipped with a warning.
86365
+ *
86366
+ * - `autoReview`: the `auto` permission mode's review policy (`policy`
86367
+ * replaces the built-in security policy, `extraPolicy` is appended to it).
86368
+ * - `autoMode`: the legacy natural-language `auto` rules, still read as
86369
+ * additional policy.
86370
+ * - `memory.parentDiscovery`: whether `CODEBUDDY.md` / `AGENTS.md` are also
86371
+ * discovered in parent directories of the launch directory.
86372
+ *
86373
+ * @see https://www.codebuddy.ai/docs/cli/settings
86374
+ * @see https://www.codebuddy.ai/docs/cli/memory
86375
+ */
86376
+ const CODEBUDDY_OVERRIDE_GROUP_KEYS = {
86377
+ autoReview: ["policy", "extraPolicy"],
86378
+ autoMode: [
86379
+ "environment",
86380
+ "allow",
86381
+ "soft_deny",
86382
+ "hard_deny"
86383
+ ],
86384
+ memory: ["parentDiscovery"]
86385
+ };
86386
+ const CODEBUDDY_OVERRIDE_GROUPS = Object.keys(CODEBUDDY_OVERRIDE_GROUP_KEYS);
86387
+ /**
86388
+ * Groups CodeBuddy reads from the user and project-local settings only: the
86389
+ * shared project `.codebuddy/settings.json` is not read for them, so a checked-in
86390
+ * repository cannot relax the review of its own actions. They are written and
86391
+ * imported in global mode alone.
86392
+ * @see https://www.codebuddy.ai/docs/cli/settings
86393
+ */
86394
+ const CODEBUDDY_USER_ONLY_GROUPS = /* @__PURE__ */ new Set(["autoReview", "autoMode"]);
86062
86395
  /** The rule lists under `permissions`, one per canonical action. */
86063
86396
  const CODEBUDDY_RULE_LISTS = [
86064
86397
  "allow",
@@ -86212,6 +86545,87 @@ function buildCodebuddyRuleLists({ config, existingPermissions, logger }) {
86212
86545
  return lists;
86213
86546
  }
86214
86547
  /**
86548
+ * Whether a settings value has the shape the `codebuddy` override schema
86549
+ * accepts for that key, so an off-spec value is left in the file instead of
86550
+ * being written or making the imported permissions file fail validation.
86551
+ */
86552
+ function isLiftableCodebuddyValue(key, value) {
86553
+ switch (key) {
86554
+ case "policy":
86555
+ case "extraPolicy": return typeof value === "string";
86556
+ case "parentDiscovery": return typeof value === "boolean";
86557
+ default: return isStringArray(value);
86558
+ }
86559
+ }
86560
+ /** Name every written override setting that can make the `auto` review more lenient. */
86561
+ function announceCodebuddyReviewSettings({ picked, group, where, logger }) {
86562
+ if (group === "autoReview" && typeof picked.policy === "string") logger?.warn(`CodeBuddy permissions: codebuddy.autoReview.policy was written to ${where}, so it replaces CodeBuddy's built-in security policy for the 'auto' permission mode.`);
86563
+ if (group === "autoReview" && typeof picked.extraPolicy === "string") logger?.warn(`CodeBuddy permissions: codebuddy.autoReview.extraPolicy was written to ${where}, so it is appended to the security policy CodeBuddy's 'auto' permission mode reviews actions against.`);
86564
+ if (group === "autoMode" && toStringArray$1(picked.allow).length > 0) logger?.warn(`CodeBuddy permissions: codebuddy.autoMode.allow was written to ${where}, so CodeBuddy's 'auto' permission mode leans toward approving the actions it describes.`);
86565
+ }
86566
+ /**
86567
+ * The top-level keys the `codebuddy` override contributes to the settings
86568
+ * patch, besides `permissions`. Each group is the existing group with the
86569
+ * authored keys laid over it; a group is only in the patch when authored, so
86570
+ * a hand-set value survives an unrelated regeneration.
86571
+ */
86572
+ function buildCodebuddyOverridePatch({ override, existing, global, where, logger }) {
86573
+ const patch = {};
86574
+ if (override === void 0) return patch;
86575
+ for (const group of CODEBUDDY_OVERRIDE_GROUPS) {
86576
+ const authored = override[group];
86577
+ if (!isRecord(authored)) continue;
86578
+ if (CODEBUDDY_USER_ONLY_GROUPS.has(group) && !global) {
86579
+ logger?.warn(`CodeBuddy permissions: skipping codebuddy.${group} in project mode — CodeBuddy does not read ${group} from the shared project .codebuddy/settings.json. Run 'rulesync generate --global' to write it to ~/.codebuddy/settings.json.`);
86580
+ continue;
86581
+ }
86582
+ const allowedKeys = CODEBUDDY_OVERRIDE_GROUP_KEYS[group];
86583
+ const picked = {};
86584
+ for (const [key, value] of Object.entries(authored)) {
86585
+ if (isPrototypePollutionKey(key) || value === void 0) continue;
86586
+ if (!allowedKeys.includes(key)) {
86587
+ logger?.warn(`CodeBuddy permissions: skipping codebuddy.${group}.${key} — Rulesync writes only ${allowedKeys.map((k) => `${group}.${k}`).join(", ")} from the codebuddy override; set other keys in ${where} by hand.`);
86588
+ continue;
86589
+ }
86590
+ picked[key] = value;
86591
+ }
86592
+ if (Object.keys(picked).length === 0) continue;
86593
+ const existingValue = existing[group];
86594
+ if (existingValue !== void 0 && !isRecord(existingValue)) {
86595
+ logger?.warn(`CodeBuddy permissions: skipping codebuddy.${group} — ${group} in ${where} is not an object, so Rulesync leaves it as it is. Fix it by hand to let Rulesync write ${group}.`);
86596
+ continue;
86597
+ }
86598
+ patch[group] = {
86599
+ ...existingValue,
86600
+ ...picked
86601
+ };
86602
+ announceCodebuddyReviewSettings({
86603
+ picked,
86604
+ group,
86605
+ where,
86606
+ logger
86607
+ });
86608
+ }
86609
+ return patch;
86610
+ }
86611
+ /**
86612
+ * Lift the settings the `codebuddy` override authors back out of a settings
86613
+ * file. `autoReview` and `autoMode` are lifted only from the user file, the
86614
+ * one of the two rulesync manages that CodeBuddy reads them from.
86615
+ */
86616
+ function liftCodebuddyOverride({ settings, global }) {
86617
+ const lifted = {};
86618
+ for (const group of CODEBUDDY_OVERRIDE_GROUPS) {
86619
+ if (CODEBUDDY_USER_ONLY_GROUPS.has(group) && !global) continue;
86620
+ const existingGroup = settings[group];
86621
+ if (!isRecord(existingGroup)) continue;
86622
+ const picked = {};
86623
+ for (const key of CODEBUDDY_OVERRIDE_GROUP_KEYS[group]) if (Object.hasOwn(existingGroup, key) && isLiftableCodebuddyValue(key, existingGroup[key])) picked[key] = existingGroup[key];
86624
+ if (Object.keys(picked).length > 0) lifted[group] = picked;
86625
+ }
86626
+ return lifted;
86627
+ }
86628
+ /**
86215
86629
  * CodeBuddy Code permissions.
86216
86630
  *
86217
86631
  * Rules live under the `permissions` key of `.codebuddy/settings.json`
@@ -86222,7 +86636,10 @@ function buildCodebuddyRuleLists({ config, existingPermissions, logger }) {
86222
86636
  * writes go through the shared-config gateway, the sibling keys of the rule
86223
86637
  * lists (`defaultMode`, `additionalDirectories`, `trustedDirectories`, ...)
86224
86638
  * are kept, and the file is never deleted. The gitignored
86225
- * `.codebuddy/settings.local.json` layer is left to CodeBuddy.
86639
+ * `.codebuddy/settings.local.json` layer is left to CodeBuddy. The
86640
+ * `codebuddy` override's other settings (`autoReview`, `autoMode`,
86641
+ * `memory.parentDiscovery`) go through `buildCodebuddyOverridePatch` /
86642
+ * `liftCodebuddyOverride`.
86226
86643
  *
86227
86644
  * @see https://www.codebuddy.ai/docs/cli/permissions
86228
86645
  * @see https://www.codebuddy.ai/docs/cli/settings
@@ -86261,8 +86678,9 @@ var CodebuddyPermissions = class CodebuddyPermissions extends ToolPermissions {
86261
86678
  relativePath: join(paths.relativeDirPath, paths.relativeFilePath)
86262
86679
  });
86263
86680
  const existingPermissions = isRecord(existing[CODEBUDDY_PERMISSIONS_KEY]) ? existing[CODEBUDDY_PERMISSIONS_KEY] : {};
86681
+ const config = rulesyncPermissions.getJson();
86264
86682
  const lists = buildCodebuddyRuleLists({
86265
- config: rulesyncPermissions.getJson(),
86683
+ config,
86266
86684
  existingPermissions,
86267
86685
  logger
86268
86686
  });
@@ -86277,7 +86695,16 @@ var CodebuddyPermissions = class CodebuddyPermissions extends ToolPermissions {
86277
86695
  fileKey: sharedConfigFileKey(paths),
86278
86696
  feature: "permissions",
86279
86697
  existingContent,
86280
- patch: { [CODEBUDDY_PERMISSIONS_KEY]: Object.keys(permissions).length > 0 ? permissions : void 0 },
86698
+ patch: {
86699
+ [CODEBUDDY_PERMISSIONS_KEY]: Object.keys(permissions).length > 0 ? permissions : void 0,
86700
+ ...buildCodebuddyOverridePatch({
86701
+ override: config.codebuddy,
86702
+ existing,
86703
+ global,
86704
+ where: `${join(paths.relativeDirPath, paths.relativeFilePath)}${global ? " in the home directory" : ""}`,
86705
+ logger
86706
+ })
86707
+ },
86281
86708
  filePath
86282
86709
  }),
86283
86710
  validate: true,
@@ -86298,7 +86725,14 @@ var CodebuddyPermissions = class CodebuddyPermissions extends ToolPermissions {
86298
86725
  const existing = rules[parsed.pattern];
86299
86726
  if (existing === void 0 || PERMISSION_ACTION_PRIORITY[action] > PERMISSION_ACTION_PRIORITY[existing]) rules[parsed.pattern] = action;
86300
86727
  }
86301
- return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({ permission }, null, 2) });
86728
+ const codebuddy = liftCodebuddyOverride({
86729
+ settings,
86730
+ global: this.global
86731
+ });
86732
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({
86733
+ permission,
86734
+ ...Object.keys(codebuddy).length > 0 && { codebuddy }
86735
+ }, null, 2) });
86302
86736
  }
86303
86737
  static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
86304
86738
  return new CodebuddyPermissions({
@@ -89264,7 +89698,7 @@ const AUTO_APPROVE_KEYS = {
89264
89698
  *
89265
89699
  * @see https://code.visualstudio.com/docs/agents/reference/ai-settings
89266
89700
  */
89267
- const DEFAULT_PERMISSION_MODE_KEY = "chat.permissions.default";
89701
+ const DEFAULT_PERMISSION_MODE_KEY$1 = "chat.permissions.default";
89268
89702
  /**
89269
89703
  * The prefix every Agent Host sandbox setting shares. A `copilot.sandbox` leaf
89270
89704
  * at path `a.b` is written to `chat.agent.sandbox.a.b`.
@@ -89552,7 +89986,7 @@ var CopilotPermissions = class CopilotPermissions extends ToolPermissions {
89552
89986
  });
89553
89987
  }
89554
89988
  const defaultPermissionMode = config.copilot?.defaultPermissionMode;
89555
- if (defaultPermissionMode !== void 0) patch[DEFAULT_PERMISSION_MODE_KEY] = defaultPermissionMode;
89989
+ if (defaultPermissionMode !== void 0) patch[DEFAULT_PERMISSION_MODE_KEY$1] = defaultPermissionMode;
89556
89990
  const sandbox = config.copilot?.sandbox;
89557
89991
  if (sandbox !== void 0) {
89558
89992
  Object.assign(patch, buildSandboxPatch({
@@ -89608,7 +90042,7 @@ var CopilotPermissions = class CopilotPermissions extends ToolPermissions {
89608
90042
  for (const [pattern, flag] of Object.entries(autoApprove)) rules[pattern] = flag ? "allow" : "deny";
89609
90043
  if (Object.keys(rules).length > 0) permission[category] = rules;
89610
90044
  }
89611
- const defaultPermissionMode = settings[DEFAULT_PERMISSION_MODE_KEY];
90045
+ const defaultPermissionMode = settings[DEFAULT_PERMISSION_MODE_KEY$1];
89612
90046
  const sandbox = readSandboxFromSettings(settings);
89613
90047
  const copilot = {
89614
90048
  ...isCopilotDefaultPermissionMode(defaultPermissionMode) && { defaultPermissionMode },
@@ -89641,6 +90075,16 @@ const WEBFETCH_CATEGORY$1 = "webfetch";
89641
90075
  /** `~/.copilot/settings.json` and `.github/copilot/settings.json` keys. */
89642
90076
  const ALLOWED_URLS_KEY = "allowedUrls";
89643
90077
  const DENIED_URLS_KEY = "deniedUrls";
90078
+ /**
90079
+ * `~/.copilot/settings.json` only: the startup permission mode for new
90080
+ * interactive sessions, authored through the `copilotcli.defaultPermissionMode`
90081
+ * override. It is a user setting — the repository settings file does not list
90082
+ * it among its repo-overridable keys.
90083
+ */
90084
+ const DEFAULT_PERMISSION_MODE_KEY = "defaultPermissionMode";
90085
+ function isCopilotcliDefaultPermissionMode(value) {
90086
+ return COPILOTCLI_DEFAULT_PERMISSION_MODES.some((mode) => mode === value);
90087
+ }
89644
90088
  function toUrlList(value) {
89645
90089
  if (!Array.isArray(value)) return [];
89646
90090
  return value.filter((entry) => typeof entry === "string");
@@ -89682,6 +90126,13 @@ function splitUrlRules(rules) {
89682
90126
  * top-level keys in the user `settings.json`, so only documented keys are ever
89683
90127
  * emitted there either.)
89684
90128
  *
90129
+ * The `copilotcli.defaultPermissionMode` override (`manual` | `assisted` |
90130
+ * `allow-all`) writes the user-scope `defaultPermissionMode` key in global mode
90131
+ * only; in project mode it is skipped with a warning, since the repository
90132
+ * settings file does not accept it. It is patched only when authored, so a mode
90133
+ * the user picked through `/settings` otherwise survives. Writing `allow-all`
90134
+ * warns, because it makes new sessions skip every approval prompt.
90135
+ *
89685
90136
  * @see https://docs.github.com/en/copilot/reference/copilot-cli-reference/cli-config-dir-reference
89686
90137
  * @see https://github.com/github/copilot-cli/blob/main/changelog.md
89687
90138
  */
@@ -89724,7 +90175,8 @@ var CopilotcliPermissions = class CopilotcliPermissions extends ToolPermissions
89724
90175
  const paths = CopilotcliPermissions.getSettablePaths({ global });
89725
90176
  const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
89726
90177
  const existingContent = await readFileContentOrNull(filePath) ?? "{}";
89727
- const rules = rulesyncPermissions.getJson().permission[WEBFETCH_CATEGORY$1];
90178
+ const config = rulesyncPermissions.getJson();
90179
+ const rules = config.permission[WEBFETCH_CATEGORY$1];
89728
90180
  const patch = {};
89729
90181
  if (rules !== void 0) {
89730
90182
  const { allowedUrls, deniedUrls } = splitUrlRules(rules);
@@ -89732,6 +90184,13 @@ var CopilotcliPermissions = class CopilotcliPermissions extends ToolPermissions
89732
90184
  if (global) patch[ALLOWED_URLS_KEY] = allowedUrls.length > 0 ? allowedUrls : void 0;
89733
90185
  else if (allowedUrls.length > 0) logger?.warn(`Copilot CLI permissions: dropping ${allowedUrls.length} "webfetch" allow rule(s) at project scope — ${join(paths.relativeDirPath, paths.relativeFilePath)} does not accept "${ALLOWED_URLS_KEY}" (repository settings may only add denials). Author allow rules in global scope (\`--global\`) instead.`);
89734
90186
  }
90187
+ const defaultPermissionMode = config.copilotcli?.defaultPermissionMode;
90188
+ if (defaultPermissionMode !== void 0) {
90189
+ if (global) {
90190
+ patch[DEFAULT_PERMISSION_MODE_KEY] = defaultPermissionMode;
90191
+ if (defaultPermissionMode === "allow-all") logger?.warn(`Copilot CLI permissions: writing "${DEFAULT_PERMISSION_MODE_KEY}": "allow-all" to ${join(paths.relativeDirPath, paths.relativeFilePath)} — new interactive sessions will start with every tool, path and URL approved without prompting (equivalent to \`--allow-all\`). Deny rules and enterprise policy still apply.`);
90192
+ } else logger?.warn(`Copilot CLI permissions: skipping "copilotcli.${DEFAULT_PERMISSION_MODE_KEY}" at project scope — ${join(paths.relativeDirPath, paths.relativeFilePath)} does not accept "${DEFAULT_PERMISSION_MODE_KEY}" (it is a user setting). Author it in global scope (\`--global\`) instead.`);
90193
+ }
89735
90194
  return new CopilotcliPermissions({
89736
90195
  outputRoot,
89737
90196
  relativeDirPath: paths.relativeDirPath,
@@ -89753,7 +90212,11 @@ var CopilotcliPermissions = class CopilotcliPermissions extends ToolPermissions
89753
90212
  for (const pattern of toUrlList(settings[DENIED_URLS_KEY])) rules[pattern] = "deny";
89754
90213
  if (this.global) for (const pattern of toUrlList(settings[ALLOWED_URLS_KEY])) rules[pattern] ??= "allow";
89755
90214
  const permission = Object.keys(rules).length > 0 ? { [WEBFETCH_CATEGORY$1]: rules } : {};
89756
- return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({ permission }, null, 2) });
90215
+ const defaultPermissionMode = this.global ? settings[DEFAULT_PERMISSION_MODE_KEY] : void 0;
90216
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({
90217
+ permission,
90218
+ ...isCopilotcliDefaultPermissionMode(defaultPermissionMode) && { copilotcli: { defaultPermissionMode } }
90219
+ }, null, 2) });
89757
90220
  }
89758
90221
  parseSettings() {
89759
90222
  const relativePath = join(this.getRelativeDirPath(), this.getRelativeFilePath());
@@ -95258,6 +95721,7 @@ var MimocodePermissions = class extends OpencodePermissions {
95258
95721
  // src/features/permissions/omp-permissions.ts
95259
95722
  const CATCH_ALL_PATTERN$5 = "*";
95260
95723
  const APPROVAL_MODE_KEY = "approvalMode";
95724
+ const RESTRICTED_FOLDERS_KEY = "restrictedFolders";
95261
95725
  const ACTION_TO_OMP_APPROVAL = {
95262
95726
  allow: "allow",
95263
95727
  ask: "prompt",
@@ -95296,9 +95760,9 @@ const MANAGED_OMP_TOOLS = /* @__PURE__ */ new Set([
95296
95760
  *
95297
95761
  * - `tools.approval.<tool>: allow | prompt | deny` is a per-tool policy honored
95298
95762
  * in every approval mode. A category's catch-all `*` rule maps here
95299
- * (`ask` → `prompt`). oh-my-pi has no per-path matcher, so a non-bash category
95300
- * with pattern-specific rules collapses to one action (deny > ask > allow, with
95301
- * an implicit `ask` when there is no catch-all) and a warning.
95763
+ * (`ask` → `prompt`). `tools.approval` has no per-path matcher, so a non-bash
95764
+ * category with pattern-specific rules collapses to one action (deny > ask >
95765
+ * allow, with an implicit `ask` when there is no catch-all) and a warning.
95302
95766
  * - `bash.patterns: [{ match, approval }]` is an ordered, first-match-wins list
95303
95767
  * of command globs (only `*` is a wildcard). Every `bash` rule except a
95304
95768
  * catch-all `allow` is written there, ordered deny → prompt → allow so that
@@ -95315,12 +95779,15 @@ const MANAGED_OMP_TOOLS = /* @__PURE__ */ new Set([
95315
95779
  *
95316
95780
  * The `omp.approvalMode` override writes `tools.approvalMode` (`always-ask`,
95317
95781
  * `always-ask-cached`, `write` or `yolo`); without it the key is left as it is in the file, and on
95318
- * import a recognized value is read back into the override.
95782
+ * import a recognized value is read back into the override. Likewise the
95783
+ * `omp.restrictedFolders` override writes `tools.restrictedFolders` (folders
95784
+ * where every action prompts in every approval mode, `yolo` included), is left
95785
+ * alone when absent, and is read back on import.
95319
95786
  *
95320
95787
  * `config.yml` carries every other oh-my-pi setting, so only `tools.approval`
95321
95788
  * (the keys rulesync manages), `bash.patterns` and an authored
95322
- * `tools.approvalMode` are rewritten; sibling keys such as `bash.enabled` are
95323
- * kept, and the file is never deleted.
95789
+ * `tools.approvalMode` or `tools.restrictedFolders` are rewritten; sibling
95790
+ * keys such as `bash.enabled` are kept, and the file is never deleted.
95324
95791
  *
95325
95792
  * @see https://github.com/can1357/oh-my-pi/blob/main/docs/settings.md
95326
95793
  * @see https://github.com/can1357/oh-my-pi/blob/main/docs/approval-mode.md
@@ -95369,6 +95836,7 @@ var OmpPermissions = class OmpPermissions extends ToolPermissions {
95369
95836
  logger
95370
95837
  });
95371
95838
  const approvalMode = config.omp?.approvalMode;
95839
+ const restrictedFolders = config.omp?.restrictedFolders;
95372
95840
  const existingTools = isRecord(existing.tools) ? existing.tools : {};
95373
95841
  const nextApproval = {
95374
95842
  ...isRecord(existingTools.approval) ? Object.fromEntries(Object.entries(existingTools.approval).filter(([tool]) => !MANAGED_OMP_TOOLS.has(tool) && !Object.hasOwn(approval, tool)).map(([tool, value]) => [tool, raisedApproval(value, floor)])) : {},
@@ -95376,6 +95844,7 @@ var OmpPermissions = class OmpPermissions extends ToolPermissions {
95376
95844
  };
95377
95845
  const toolsSiblings = withoutKey(existingTools, "approval");
95378
95846
  if (approvalMode !== void 0) toolsSiblings[APPROVAL_MODE_KEY] = approvalMode;
95847
+ if (restrictedFolders !== void 0) toolsSiblings[RESTRICTED_FOLDERS_KEY] = restrictedFolders;
95379
95848
  const nextTools = Object.keys(nextApproval).length > 0 ? {
95380
95849
  ...toolsSiblings,
95381
95850
  approval: nextApproval
@@ -95626,11 +96095,26 @@ function convertOmpToRulesync(config) {
95626
96095
  }
95627
96096
  if (Object.keys(bashRules).length > 0) permission.bash = bashRules;
95628
96097
  const approvalMode = tools[APPROVAL_MODE_KEY];
96098
+ const restrictedFolders = importRestrictedFolders(tools[RESTRICTED_FOLDERS_KEY]);
96099
+ const omp = {
96100
+ ...isOmpApprovalMode(approvalMode) && { approvalMode },
96101
+ ...restrictedFolders !== void 0 && { restrictedFolders }
96102
+ };
95629
96103
  return {
95630
96104
  permission,
95631
- ...isOmpApprovalMode(approvalMode) && { omp: { approvalMode } }
96105
+ ...Object.keys(omp).length > 0 && { omp }
95632
96106
  };
95633
96107
  }
96108
+ /**
96109
+ * `tools.restrictedFolders` as the override list: its string entries, or
96110
+ * `undefined` when it is unset or no entry survives (so a list of non-strings
96111
+ * is not imported as `[]` and then rewritten as an empty list).
96112
+ */
96113
+ function importRestrictedFolders(value) {
96114
+ if (!Array.isArray(value)) return;
96115
+ const folders = value.filter((entry) => typeof entry === "string");
96116
+ return folders.length === 0 && value.length > 0 ? void 0 : folders;
96117
+ }
95634
96118
  function isOmpApprovalMode(value) {
95635
96119
  return OMP_APPROVAL_MODES.some((mode) => mode === value);
95636
96120
  }
@@ -100727,19 +101211,31 @@ const CANONICAL_TO_VIBE_TOOL_NAMES = {
100727
101211
  };
100728
101212
  /**
100729
101213
  * The tables earlier rulesync versions wrote for canonical categories that
100730
- * Vibe 3.x no longer reads: `[tools.grep]` (Vibe warns that it "has no
100731
- * effect") and `[tools.task]` for the subagent tool v3.0.0 renamed to
100732
- * `agents.*`. A regenerate that configures the category removes them — and
101214
+ * Vibe 3.x does not read: `[tools.grep]` (Vibe warns that it "has no
101215
+ * effect"), `[tools.task]` for the subagent tool v3.0.0 renamed to
101216
+ * `agents.*`, and `[tools.external_directory]` — Vibe has no such tool, and
101217
+ * its unused-settings check skips unknown tool names as custom/MCP tools, so
101218
+ * that deny was silently inert. A regenerate that configures the category removes them — and
100733
101219
  * the matching `disabled_tools` entry — instead of leaving an inert table.
100734
101220
  * Import still reads them (see {@link VIBE_TO_CANONICAL_TOOL_NAMES}), so a
100735
101221
  * Vibe 2.x config is lifted into the canonical categories rather than lost.
100736
101222
  */
100737
101223
  const VIBE_RETIRED_TOOL_TABLES = {
100738
101224
  grep: "grep",
100739
- agent: "task"
101225
+ agent: "task",
101226
+ external_directory: "external_directory"
100740
101227
  };
100741
101228
  /** The canonical category Vibe's `enable_subagents` switch expresses. */
100742
101229
  const VIBE_SUBAGENT_CATEGORY = "agent";
101230
+ /** The canonical category Vibe's top-level `outside_workdir` setting expresses. */
101231
+ const VIBE_OUTSIDE_WORKDIR_CATEGORY = "external_directory";
101232
+ /**
101233
+ * The values of Vibe's `outside_workdir` (v3.0.3+). There is no allow value:
101234
+ * access outside the working directory is granted per path through a tool's
101235
+ * `allowlist` (`vibe-path:...`) or `--add-dir`.
101236
+ * @see https://github.com/mistralai/mistral-vibe/blob/v3.0.3/README.md
101237
+ */
101238
+ const VIBE_OUTSIDE_WORKDIR_VALUES = /* @__PURE__ */ new Set(["ask", "deny"]);
100743
101239
  const VIBE_TO_CANONICAL_TOOL_NAMES = {
100744
101240
  bash: "bash",
100745
101241
  read_file: "read",
@@ -100815,7 +101311,8 @@ const CANONICAL_PERMISSION_CATEGORIES = /* @__PURE__ */ new Set([
100815
101311
  "grep",
100816
101312
  "glob",
100817
101313
  "notebookedit",
100818
- "agent"
101314
+ "agent",
101315
+ "external_directory"
100819
101316
  ]);
100820
101317
  var VibePermissions = class VibePermissions extends ToolPermissions {
100821
101318
  toml;
@@ -100896,7 +101393,11 @@ var VibePermissions = class VibePermissions extends ToolPermissions {
100896
101393
  rules: permission[VIBE_SUBAGENT_CATEGORY],
100897
101394
  logger
100898
101395
  });
100899
- const tableCategories = Object.entries(permission).filter(([category]) => category !== VIBE_SUBAGENT_CATEGORY);
101396
+ const outsideWorkdirPatch = resolveOutsideWorkdirPatch({
101397
+ rules: permission[VIBE_OUTSIDE_WORKDIR_CATEGORY],
101398
+ logger
101399
+ });
101400
+ const tableCategories = Object.entries(permission).filter(([category]) => category !== VIBE_SUBAGENT_CATEGORY && category !== VIBE_OUTSIDE_WORKDIR_CATEGORY);
100900
101401
  for (const [category, rules] of tableCategories) {
100901
101402
  const vibeToolNames = resolveNames(category);
100902
101403
  if (vibeToolNames === void 0) {
@@ -100970,7 +101471,8 @@ var VibePermissions = class VibePermissions extends ToolPermissions {
100970
101471
  enabledTools
100971
101472
  }),
100972
101473
  disabled_tools: disabledTools.size > 0 ? [...disabledTools].toSorted() : void 0,
100973
- ...enableSubagentsPatch
101474
+ ...enableSubagentsPatch,
101475
+ ...outsideWorkdirPatch
100974
101476
  },
100975
101477
  filePath
100976
101478
  }),
@@ -101014,6 +101516,8 @@ var VibePermissions = class VibePermissions extends ToolPermissions {
101014
101516
  }
101015
101517
  for (const category of silencedCategories) ensurePermission(permission, category)["*"] = "deny";
101016
101518
  if (this.toml.enable_subagents === false) ensurePermission(permission, VIBE_SUBAGENT_CATEGORY)["*"] = "deny";
101519
+ const outsideWorkdir = this.toml.outside_workdir;
101520
+ if (typeof outsideWorkdir === "string" && VIBE_OUTSIDE_WORKDIR_VALUES.has(outsideWorkdir)) ensurePermission(permission, VIBE_OUTSIDE_WORKDIR_CATEGORY)["*"] = outsideWorkdir === "deny" ? "deny" : "ask";
101017
101521
  warnOnUnresolvableDisabledTools({
101018
101522
  entries: disabledToolEntries,
101019
101523
  tools: this.toml.tools
@@ -101090,7 +101594,7 @@ function retireLegacyToolTables({ permission, tools, disabledTools, logger }) {
101090
101594
  }
101091
101595
  if (disabledTools.delete(legacyToolName)) removed.push(`disabled_tools entry '${legacyToolName}'`);
101092
101596
  }
101093
- if (removed.length > 0) logger?.warn(`Removed ${removed.join(", ")} from the Vibe config: Vibe 3.x no longer reads [tools.grep] or the former 'task' subagent tool, so those entries had no effect.`);
101597
+ if (removed.length > 0) logger?.warn(`Removed ${removed.join(", ")} from the Vibe config: Vibe 3.x does not read [tools.grep], the former 'task' subagent tool or an 'external_directory' tool (the 'external_directory' category is written to outside_workdir instead), so those entries had no effect.`);
101094
101598
  }
101095
101599
  /**
101096
101600
  * Report the rules of a category that has no Vibe tool table. `grep` gets its
@@ -101126,6 +101630,30 @@ function resolveEnableSubagentsPatch({ rules, logger }) {
101126
101630
  return { enable_subagents: action === "deny" ? false : void 0 };
101127
101631
  }
101128
101632
  /**
101633
+ * Translate the canonical `external_directory` category into Vibe's top-level
101634
+ * `outside_workdir` setting (v3.0.3+), which decides whether a file or shell
101635
+ * tool call reaching outside the working directory prompts (`"ask"`, the
101636
+ * default) or is refused with a tool error (`"deny"`). A `*` deny or ask writes
101637
+ * that value; a `*` allow has no Vibe value, so it is warned about and the key
101638
+ * is removed, leaving Vibe's default prompt. Without a `*` rule the key is left
101639
+ * as the file has it. Pattern rules are skipped with a warning: path grants go
101640
+ * in a tool's `allowlist` (`vibe-path:...`) instead.
101641
+ * @see https://github.com/mistralai/mistral-vibe/blob/v3.0.3/README.md
101642
+ * @see https://github.com/mistralai/mistral-vibe/blob/v3.0.3/vibe/app_server/_unified_permissions.py
101643
+ */
101644
+ function resolveOutsideWorkdirPatch({ rules, logger }) {
101645
+ if (rules === void 0) return {};
101646
+ const patternCount = Object.keys(rules).filter((pattern) => pattern !== "*").length;
101647
+ if (patternCount > 0) logger?.warn(`Vibe's outside_workdir applies to every path outside the working directory; skipping ${patternCount} pattern rule(s) of the '${VIBE_OUTSIDE_WORKDIR_CATEGORY}' category. Grant a path through a tool's allowlist ("vibe-path:...") in the Vibe config instead.`);
101648
+ const action = rules["*"];
101649
+ if (action === void 0) return {};
101650
+ if (action === "allow") {
101651
+ logger?.warn(`Vibe's outside_workdir only takes "ask" or "deny"; the '${VIBE_OUTSIDE_WORKDIR_CATEGORY}' category's "*": "allow" leaves Vibe's default, which prompts before reaching outside the working directory.`);
101652
+ return { outside_workdir: void 0 };
101653
+ }
101654
+ return { outside_workdir: action };
101655
+ }
101656
+ /**
101129
101657
  * Apply the Vibe-scoped override's per-tool `sensitive_patterns` (patterns that
101130
101658
  * escalate to ASK even when the base permission is ALWAYS). rulesync owns this
101131
101659
  * list for any category the override names: a present list is set, an empty
@@ -106475,4 +107003,4 @@ async function importChecksCore(params) {
106475
107003
  }
106476
107004
  export { RulesyncPermissions as $, formatError as $n, isSymlink as $t, CODEBUDDY_DIR as A, MAX_FILE_SIZE as An, withFallbackLoggerTarget as At, CLAUDECODE_LOCAL_RULE_FILE_NAME as B, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as Bn, assertTreeContainsNoSymlinks as Bt, QWENCODE_LOCAL_RULE_FILE_NAME as C, PACKAGING_TOOL_TARGETS as Cn, getSourceTransport as Ct, FACTORYDROID_SETTINGS_LOCAL_FILE_NAME as D, DEPRECATED_FEATURE_REPLACEMENTS as Dn, WarningCollectingLogger as Dt, FACTORYDROID_DIR as E, ALL_FEATURES_WITH_WILDCARD as En, JsonLogger as Et, groupSpellingsByCaseFoldedIdentity as F, RULESYNC_CONFIG_SCHEMA_URL as Fn, PathOutsideRootError as Ft, toInstructionBudgetFile as G, RULESYNC_PERMISSIONS_SCHEMA_URL as Gn, ensureDir as Gt, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as H, RULESYNC_MCP_SCHEMA_URL as Hn, checkPathTraversal as Ht, CODEXCLI_BASH_RULES_FILE_NAME as I, RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH as In, applyFileMode as It, RulesyncSubagentFrontmatterSchema as J, RULESYNC_SKILLS_RELATIVE_DIR_PATH as Jn, findFilesByGlobs as Jt, getLocalSkillDirNames as K, RULESYNC_RELATIVE_DIR_PATH as Kn, fileExists as Kt, CODEXCLI_DIR as L, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as Ln, assertDirectoryIfExists as Lt, AUGMENTCODE_DIR as M, RULESYNC_CHECKS_RELATIVE_DIR_PATH as Mn, withWarnOnceScope as Mt, AUGMENTCODE_SETTINGS_LOCAL_FILE_NAME as N, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as Nn, CLIError as Nt, COMMANDCODE_DIR as O, CURATED_RULES_FEATURE_SUBDIR as On, fallbackLogger as Ot, caseFoldIdentity as P, RULESYNC_CONFIG_RELATIVE_FILE_PATH as Pn, ErrorCodes as Pt, RulesyncRuleFrontmatterSchema as Q, RULESYNC_XDG_CONFIG_HOME_DEFAULT_DIR_NAME as Qn, isFileSystemError as Qt, CLAUDECODE_AGENT_MEMORY_LOCAL_DIR_NAME as R, RULESYNC_HOOKS_RELATIVE_FILE_PATH as Rn, assertNoSymlinkBelowRoot as Rt, QWENCODE_DIR as S, ALL_TOOL_TARGETS_WITH_WILDCARD as Sn, assertTargetsFeaturesExclusive as St, LETTACODE_SETTINGS_LOCAL_FILE_NAME as T, ALL_FEATURES as Tn, ConsoleLogger as Tt, CLAUDECODE_SKILLS_DIR_PATH as U, RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH as Un, createTempDirectory as Ut, CLAUDECODE_MEMORIES_DIR_NAME as V, RULESYNC_MCP_RELATIVE_FILE_PATH as Vn, assertWritablePathInsideRoot as Vt, findProjectInstructionBudgetFindings as W, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as Wn, directoryExists as Wt, RulesyncSkillFrontmatterSchema as X, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as Xn, getHomeDirectory as Xt, RulesyncSkill as Y, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as Yn, getFileSizeAndExecutableBit as Yt, RulesyncRule as Z, RULESYNC_USER_CONFIG_DIR_NAME as Zn, isFileNotFoundError as Zt, SubagentsProcessor as _, toPosixPath as _n, resolveEffectiveInputRoots as _t, convertFromTool as a, readFileContent as an, stripControlCharactersKeepingLineFeeds as ar, parseJsonc as at, CommandsProcessor as b, mapWithConcurrency as bn, GITIGNORE_DESTINATION_KEY as bt, parseCommaSeparatedList as c, removeDirectoryStrict as cn, RulesyncCheck as ct, shortenToWidth as d, removeFileWithoutFollowingSymlinks as dn, loadYaml as dt, listDirectoryEntryNames as en, truncateText as er, RulesyncMcp as et, McpProcessor as f, removeTempDirectory as fn, isRecord as ft, RulesProcessor as g, splitPathSegments as gn, mergeInputRootConfigs as gt, ChecksProcessor as h, runWithDirectoryRollback as hn, ConfigResolver as ht, inspectInputRoots as i, posixRelativePathEscapesRoot as in, stripControlCharacters as ir, resolveRulesyncSourceWritePath as it, CODEBUDDY_LOCAL_RULE_FILE_NAME as j, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as jn, resetRunWarningState as jt, COMMANDCODE_SETTINGS_LOCAL_FILE_NAME as k, FEATURE_SOURCE_TREE_ENTRIES as kn, warnOnConflictingFlags as kt, ELLIPSIS_WIDTH as l, removeFile as ln, RulesyncCheckFrontmatterSchema as lt, HooksProcessor as m, resolvedRelativePath as mn, SKILL_FILE_NAME as mt, formatSourceLoadFailure as n, listSubdirectoryNames as nn, hasEnclosingMarkOutsideKeycap as nr, RulesyncHooks as nt, isPackagingToolTarget as o, readFileContentOrNull as on, stripControlCharactersKeepingLineFeedsAndTabs as or, RulesyncCommand as ot, IgnoreProcessor as p, resolvePath as pn, SHARED_USER_MANAGED_CONFIG_PATHS as pt, RulesyncSubagent as q, RULESYNC_RULES_RELATIVE_DIR_PATH as qn, fileExistsStrict as qt, generate as r, pathEscapesRoot as rn, quoteForLog as rr, getRulesyncSourceCandidates as rt, getProcessorRegistryEntry as s, removeDirectory as sn, stripHiddenCharacters as sr, RulesyncCommandFrontmatterSchema as st, importFromTool as t, listFilePathsRecursively as tn, hasDeceptiveHiddenCharacters as tr, RulesyncIgnore as tt, displayWidthOf as u, removeFileStrict as un, stringifyFrontmatter as ut, SkillsProcessor as v, writeFileBuffer as vn, CONFLICTING_TARGET_PAIRS as vt, LETTACODE_DIR as w, ToolTargetSchema as wn, findControlCharacter as wt, REASONIX_LOCAL_RULE_FILE_NAME as x, ALL_TOOL_TARGETS as xn, SourceEntrySchema as xt, CRUSH_LOCAL_RULE_FILE_NAME as y, writeFileContent as yn, ConfigFileSchema as yt, CLAUDECODE_DIR as z, RULESYNC_IGNORE_RELATIVE_FILE_PATH as zn, assertPathResolvesInsideRoot as zt };
106477
107005
 
106478
- //# sourceMappingURL=import-D2uYjAdc.js.map
107006
+ //# sourceMappingURL=import-DPXjeK8q.js.map