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.
@@ -5544,24 +5544,27 @@ const AUGMENTCODE_HOOK_EVENTS = [
5544
5544
  * Hook events supported by Mistral Vibe (mistral-vibe).
5545
5545
  *
5546
5546
  * Vibe exposes three hook events in `.vibe/hooks.toml`: `pre_tool` ←
5547
- * `preToolUse`, `post_tool` ← `postToolUse`, and `post_agent` ← `stop` (fires
5548
- * after every assistant turn that ends without pending tool calls — the closest
5549
- * canonical equivalent to a "turn end"/"stop" event, matching how
5550
- * codexcli/copilot map their stop events). Only the tool events
5551
- * (`pre_tool`/`post_tool`) carry the `match` tool-name matcher (fnmatch glob or
5552
- * `re:` regex) and the `strict` flag; `post_agent` carries neither. Only
5553
- * `type: "command"` hooks are relevant.
5547
+ * `preToolUse`, `post_tool` ← `postToolUse`, and `post_agent` ← `stop` /
5548
+ * `subagentStop`. `post_agent` fires after every assistant turn that ends
5549
+ * without pending tool calls, for the main agent and (since v3.0.2) for every
5550
+ * subagent. v3.0.3 lets a `post_agent` hook take a `match` on the agent that
5551
+ * finished its turn: the reserved alias `main` selects the main agent, so
5552
+ * canonical `stop` becomes `post_agent` with `match = "main"`, and a canonical
5553
+ * `subagentStop` with a matcher becomes `post_agent` with that agent name as
5554
+ * `match`. Only the tool events (`pre_tool`/`post_tool`) carry the `strict`
5555
+ * flag. Only `type: "command"` hooks are relevant.
5554
5556
  *
5555
5557
  * v2.21.0 graduated hooks from experimental and renamed all three types
5556
5558
  * (`before_tool` → `pre_tool`, `after_tool` → `post_tool`, `post_agent_turn` →
5557
5559
  * `post_agent`). `HookType` is a strict enum, so an entry using an old name is
5558
5560
  * rejected outright and reported as a `HookConfigIssue`.
5559
- * @see https://github.com/mistralai/mistral-vibe/blob/main/README.md
5561
+ * @see https://github.com/mistralai/mistral-vibe/blob/v3.0.3/README.md
5560
5562
  */
5561
5563
  const VIBE_HOOK_EVENTS = [
5562
5564
  "preToolUse",
5563
5565
  "postToolUse",
5564
- "stop"
5566
+ "stop",
5567
+ "subagentStop"
5565
5568
  ];
5566
5569
  /**
5567
5570
  * Hook events supported by Codewhale.
@@ -7029,9 +7032,10 @@ const JUNIE_TO_CANONICAL_EVENT_NAMES = invertEventNames(CANONICAL_TO_JUNIE_EVENT
7029
7032
  /**
7030
7033
  * Map canonical camelCase event names to Mistral Vibe snake_case.
7031
7034
  *
7032
- * Vibe documents three hook events. The canonical `stop` event maps to Vibe's
7033
- * `post_agent` (fires after every assistant turn ending without pending tool
7034
- * calls) — the closest documented "turn end"/"stop" equivalent.
7035
+ * Vibe documents three hook events. The canonical `stop` and `subagentStop`
7036
+ * events both map to Vibe's `post_agent` (fires after every assistant turn
7037
+ * ending without pending tool calls); the adapter tells them apart with the
7038
+ * hook's `match` on the agent name (`main` for the main agent).
7035
7039
  *
7036
7040
  * These are the v2.21.0 names; the pre-2.21.0 spellings (`before_tool`,
7037
7041
  * `after_tool`, `post_agent_turn`) are rejected by Vibe's strict `HookType`
@@ -7041,7 +7045,8 @@ const JUNIE_TO_CANONICAL_EVENT_NAMES = invertEventNames(CANONICAL_TO_JUNIE_EVENT
7041
7045
  const CANONICAL_TO_VIBE_EVENT_NAMES = {
7042
7046
  preToolUse: "pre_tool",
7043
7047
  postToolUse: "post_tool",
7044
- stop: "post_agent"
7048
+ stop: "post_agent",
7049
+ subagentStop: "post_agent"
7045
7050
  };
7046
7051
  /**
7047
7052
  * Map Mistral Vibe snake_case event names to canonical camelCase.
@@ -7054,6 +7059,7 @@ const CANONICAL_TO_VIBE_EVENT_NAMES = {
7054
7059
  */
7055
7060
  const VIBE_TO_CANONICAL_EVENT_NAMES = {
7056
7061
  ...invertEventNames(CANONICAL_TO_VIBE_EVENT_NAMES),
7062
+ post_agent: "stop",
7057
7063
  before_tool: "preToolUse",
7058
7064
  after_tool: "postToolUse",
7059
7065
  post_agent_turn: "stop"
@@ -8404,6 +8410,32 @@ const CopilotPermissionsOverrideSchema = zod_mini.z.looseObject({
8404
8410
  sandbox: zod_mini.z.optional(CopilotSandboxSchema)
8405
8411
  });
8406
8412
  /**
8413
+ * The permission mode the GitHub Copilot CLI starts new interactive sessions
8414
+ * in (`defaultPermissionMode` of `~/.copilot/settings.json`): `manual` prompts
8415
+ * for every request not already allowed, `assisted` lets a model review
8416
+ * eligible requests (Assisted Permissions, non-experimental since v1.0.104),
8417
+ * and `allow-all` is equivalent to `--allow-all`. These are the CLI's own
8418
+ * values and differ from the VS Code `copilot` target's
8419
+ * {@link COPILOT_DEFAULT_PERMISSION_MODES}.
8420
+ *
8421
+ * @see https://docs.github.com/en/copilot/reference/copilot-cli-reference/cli-config-dir-reference
8422
+ */
8423
+ const COPILOTCLI_DEFAULT_PERMISSION_MODES = [
8424
+ "manual",
8425
+ "assisted",
8426
+ "allow-all"
8427
+ ];
8428
+ /**
8429
+ * Tool-scoped override block for the GitHub Copilot CLI.
8430
+ * `defaultPermissionMode` writes the user-scope `defaultPermissionMode` key of
8431
+ * `~/.copilot/settings.json`; it has no canonical category, and the repository
8432
+ * settings file does not accept it.
8433
+ */
8434
+ const CopilotcliPermissionsOverrideSchema = zod_mini.z.looseObject({
8435
+ permission: zod_mini.z.optional(ToolScopedPermissionSchema),
8436
+ defaultPermissionMode: zod_mini.z.optional(zod_mini.z.enum(COPILOTCLI_DEFAULT_PERMISSION_MODES))
8437
+ });
8438
+ /**
8407
8439
  * oh-my-pi's `tools.approvalMode` values: the tool tiers it auto-approves
8408
8440
  * before prompting. `always-ask` auto-approves only read-only tools,
8409
8441
  * `always-ask-cached` (Ask and remember) also prompts before file reads and
@@ -8420,11 +8452,14 @@ const OMP_APPROVAL_MODES = [
8420
8452
  ];
8421
8453
  /**
8422
8454
  * Tool-scoped override block for oh-my-pi (`omp`). `approvalMode` writes the
8423
- * `tools.approvalMode` key of `config.yml`, which has no canonical category.
8455
+ * `tools.approvalMode` key of `config.yml`, and `restrictedFolders` the
8456
+ * `tools.restrictedFolders` list (folders where every action prompts in every
8457
+ * approval mode, `yolo` included); neither has a canonical category.
8424
8458
  */
8425
8459
  const OmpPermissionsOverrideSchema = zod_mini.z.looseObject({
8426
8460
  permission: zod_mini.z.optional(ToolScopedPermissionSchema),
8427
- approvalMode: zod_mini.z.optional(zod_mini.z.enum(OMP_APPROVAL_MODES))
8461
+ approvalMode: zod_mini.z.optional(zod_mini.z.enum(OMP_APPROVAL_MODES)),
8462
+ restrictedFolders: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string()))
8428
8463
  });
8429
8464
  const KimiCodePermissionsOverrideSchema = zod_mini.z.looseObject({
8430
8465
  permission: zod_mini.z.optional(ToolScopedPermissionSchema),
@@ -8876,6 +8911,48 @@ const QoderPermissionsOverrideSchema = zod_mini.z.looseObject({
8876
8911
  agentsMdExcludes: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string()))
8877
8912
  });
8878
8913
  /**
8914
+ * Tool-scoped override block for CodeBuddy Code. The shared `permission` block
8915
+ * continues to drive `permissions.allow`/`ask`/`deny` of
8916
+ * `.codebuddy/settings.json` (project) / `~/.codebuddy/settings.json` (global).
8917
+ *
8918
+ * The other documented settings with no canonical category are authored here
8919
+ * and merged into the same file, key by key, over what the file already
8920
+ * holds:
8921
+ *
8922
+ * - `autoReview` (`policy` replaces the built-in security policy of the `auto`
8923
+ * permission mode, `extraPolicy` is appended to it) and the legacy
8924
+ * `autoMode` rules (`environment` / `allow` / `soft_deny` / `hard_deny`).
8925
+ * CodeBuddy reads both from the user and project-local settings only, never
8926
+ * from the shared project file, so they are written by `--global` generate
8927
+ * alone.
8928
+ * - `memory.parentDiscovery`: whether project instruction files
8929
+ * (`CODEBUDDY.md` / `AGENTS.md`) are also discovered in parent directories.
8930
+ *
8931
+ * Only the keys listed here are written; any other key of these groups is
8932
+ * skipped with a warning. Removing a key from the override leaves the
8933
+ * previously written value in place.
8934
+ *
8935
+ * @see https://www.codebuddy.ai/docs/cli/settings
8936
+ * @see https://www.codebuddy.ai/docs/cli/memory
8937
+ * @example
8938
+ * { "autoReview": { "extraPolicy": "Allow dev namespace releases" },
8939
+ * "memory": { "parentDiscovery": false } }
8940
+ */
8941
+ const CodebuddyPermissionsOverrideSchema = zod_mini.z.looseObject({
8942
+ permission: zod_mini.z.optional(ToolScopedPermissionSchema),
8943
+ autoReview: zod_mini.z.optional(zod_mini.z.looseObject({
8944
+ policy: zod_mini.z.optional(zod_mini.z.string()),
8945
+ extraPolicy: zod_mini.z.optional(zod_mini.z.string())
8946
+ })),
8947
+ autoMode: zod_mini.z.optional(zod_mini.z.looseObject({
8948
+ environment: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
8949
+ allow: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
8950
+ soft_deny: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
8951
+ hard_deny: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string()))
8952
+ })),
8953
+ memory: zod_mini.z.optional(zod_mini.z.looseObject({ parentDiscovery: zod_mini.z.optional(zod_mini.z.boolean()) }))
8954
+ });
8955
+ /**
8879
8956
  * One native Factory Droid permission rule. `match.prefix` positions are either
8880
8957
  * a single token or a non-empty list of alternative tokens.
8881
8958
  *
@@ -9758,13 +9835,13 @@ const PermissionsConfigSchema = zod_mini.z.looseObject({
9758
9835
  zed: zod_mini.z.optional(ZedPermissionsOverrideSchema),
9759
9836
  devin: zod_mini.z.optional(DevinPermissionsOverrideSchema),
9760
9837
  qoder: zod_mini.z.optional(QoderPermissionsOverrideSchema),
9838
+ copilotcli: zod_mini.z.optional(CopilotcliPermissionsOverrideSchema),
9761
9839
  "antigravity-ide": zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
9762
9840
  continue: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
9763
9841
  copilot: zod_mini.z.optional(CopilotPermissionsOverrideSchema),
9764
- codebuddy: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
9842
+ codebuddy: zod_mini.z.optional(CodebuddyPermissionsOverrideSchema),
9765
9843
  commandcode: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
9766
9844
  lettacode: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
9767
- copilotcli: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
9768
9845
  crush: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
9769
9846
  goose: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
9770
9847
  omp: zod_mini.z.optional(OmpPermissionsOverrideSchema),
@@ -10251,6 +10328,10 @@ const RulesyncRuleFrontmatterSchema = zod_mini.z.object({
10251
10328
  enabled: zod_mini.z.optional(zod_mini.z.boolean()),
10252
10329
  description: zod_mini.z.optional(zod_mini.z.string())
10253
10330
  })),
10331
+ cline: zod_mini.z.optional(zod_mini.z.looseObject({
10332
+ disabled: zod_mini.z.optional(zod_mini.z.boolean()),
10333
+ enabled: zod_mini.z.optional(zod_mini.z.boolean())
10334
+ })),
10254
10335
  continue: zod_mini.z.optional(zod_mini.z.looseObject({
10255
10336
  alwaysApply: zod_mini.z.optional(zod_mini.z.boolean()),
10256
10337
  description: zod_mini.z.optional(zod_mini.z.string()),
@@ -13730,7 +13811,11 @@ const SHARED_CONFIG_OWNERSHIP = {
13730
13811
  },
13731
13812
  permissions: {
13732
13813
  kind: "replace-owned-keys",
13733
- ownedKeys: ["allowedUrls", "deniedUrls"]
13814
+ ownedKeys: [
13815
+ "allowedUrls",
13816
+ "deniedUrls",
13817
+ "defaultPermissionMode"
13818
+ ]
13734
13819
  }
13735
13820
  }
13736
13821
  },
@@ -13909,7 +13994,12 @@ const SHARED_CONFIG_OWNERSHIP = {
13909
13994
  },
13910
13995
  permissions: {
13911
13996
  kind: "replace-owned-keys",
13912
- ownedKeys: ["permissions"]
13997
+ ownedKeys: [
13998
+ "permissions",
13999
+ "autoReview",
14000
+ "autoMode",
14001
+ "memory"
14002
+ ]
13913
14003
  }
13914
14004
  }
13915
14005
  },
@@ -14219,7 +14309,8 @@ const SHARED_CONFIG_OWNERSHIP = {
14219
14309
  "tools",
14220
14310
  "enabled_tools",
14221
14311
  "disabled_tools",
14222
- "enable_subagents"
14312
+ "enable_subagents",
14313
+ "outside_workdir"
14223
14314
  ]
14224
14315
  }
14225
14316
  }
@@ -30572,7 +30663,10 @@ var QwencodeSkill = class QwencodeSkill extends ToolSkill {
30572
30663
  }
30573
30664
  }
30574
30665
  static getSettablePaths({ global: _global = false } = {}) {
30575
- return { relativeDirPath: QWENCODE_SKILLS_DIR_PATH };
30666
+ return {
30667
+ relativeDirPath: QWENCODE_SKILLS_DIR_PATH,
30668
+ importOnlySkillRoots: [AGENTSMD_SKILLS_DIR_PATH]
30669
+ };
30576
30670
  }
30577
30671
  getFrontmatter() {
30578
30672
  return QwencodeSkillFrontmatterSchema.parse(this.requireMainFileFrontmatter());
@@ -39593,6 +39687,16 @@ var VibePluginSubagent = class VibePluginSubagent extends ToolSubagent {
39593
39687
  */
39594
39688
  const VIBE_PROMPTS_DIR_PATH = (0, node_path.join)(".vibe", "prompts");
39595
39689
  /**
39690
+ * The agent name Vibe reserves for `post_agent` hook matching (v3.0.3+).
39691
+ * `AgentProfile.from_toml` refuses a file whose stem matches it case-insensitively, so an
39692
+ * agent written as `main.toml` would never load.
39693
+ * @see https://github.com/mistralai/mistral-vibe/blob/v3.0.3/vibe/core/agents/models.py
39694
+ */
39695
+ const VIBE_RESERVED_AGENT_NAME = "main";
39696
+ function isReservedVibeAgentFileName(relativeFilePath) {
39697
+ return (0, node_path.basename)(relativeFilePath).replace(/\.(md|toml)$/, "").toLowerCase() === VIBE_RESERVED_AGENT_NAME;
39698
+ }
39699
+ /**
39596
39700
  * Vibe resolves a `system_prompt_id` by file name, and
39597
39701
  * `VibeConfigSchema._check_system_prompt` evaluates it during validation — an
39598
39702
  * unresolvable id raises and the agent is dropped at discovery. The id must
@@ -39707,9 +39811,17 @@ var VibeSubagent = class VibeSubagent extends ToolSubagent {
39707
39811
  * evaluates the property during validation, so an unresolvable
39708
39812
  * `system_prompt_id` raises and `AgentRegistry._try_load` drops the agent
39709
39813
  * with a warning.
39814
+ *
39815
+ * A subagent named `main` is skipped with a warning: Vibe reserves that name
39816
+ * for hook matching and does not load an agent file called `main.toml`.
39710
39817
  */
39711
- static fromRulesyncSubagents({ rulesyncSubagents, outputRoot = process.cwd(), global = false }) {
39818
+ static fromRulesyncSubagents({ rulesyncSubagents, outputRoot = process.cwd(), global = false, logger }) {
39712
39819
  return rulesyncSubagents.flatMap((rulesyncSubagent) => {
39820
+ const sourcePath = rulesyncSubagent.getRelativeFilePath();
39821
+ if (isReservedVibeAgentFileName(sourcePath)) {
39822
+ 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.`);
39823
+ return [];
39824
+ }
39713
39825
  const agentFile = VibeSubagent.fromRulesyncSubagent({
39714
39826
  outputRoot,
39715
39827
  relativeDirPath: VibeSubagent.getSettablePaths({ global }).relativeDirPath,
@@ -44996,17 +45108,49 @@ var ClaudecodeRule = class ClaudecodeRule extends ToolRule {
44996
45108
  * - `alwaysApply: true`: force the rule to always load (equivalent to a file
44997
45109
  * with no frontmatter at all).
44998
45110
  * - `description`: label surfaced in the rule toggle UI.
45111
+ * - `disabled: true` (or `enabled: false`): the rule is switched off. Since
45112
+ * Cline v4.1.26 (cline/cline#14605) the rule toggle writes this flag into the
45113
+ * rule file itself instead of keeping it only in extension state.
44999
45114
  *
45000
45115
  * Rules without frontmatter are always active. Uses `z.looseObject()` so
45001
- * forward-compatible fields added upstream are preserved on round-trip.
45116
+ * forward-compatible fields added upstream are preserved on round-trip: every
45117
+ * key other than `paths` / `alwaysApply` / `description` is carried in the
45118
+ * rulesync `cline:` section.
45002
45119
  *
45003
45120
  * @see https://docs.cline.bot/customization/cline-rules
45121
+ * @see https://github.com/cline/cline/blob/main/sdk/packages/core/src/extensions/config/user-instruction-config-loader.ts
45004
45122
  */
45005
45123
  const ClineRuleFrontmatterSchema = zod_mini.z.looseObject({
45006
45124
  paths: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.array(zod_mini.z.string()), zod_mini.z.string()])),
45007
45125
  alwaysApply: zod_mini.z.optional(zod_mini.z.boolean()),
45008
45126
  description: zod_mini.z.optional(zod_mini.z.string())
45009
45127
  });
45128
+ const CLINE_RULE_FIELD_TYPES = {
45129
+ disabled: "boolean",
45130
+ enabled: "boolean"
45131
+ };
45132
+ const CANONICAL_CLINE_RULE_KEYS = [
45133
+ "paths",
45134
+ "alwaysApply",
45135
+ "description"
45136
+ ];
45137
+ /** Returns a copy of `record` without the canonical `paths` / `alwaysApply` / `description` keys. */
45138
+ function omitCanonicalClineRuleKeys(record) {
45139
+ return Object.fromEntries(Object.entries(record ?? {}).filter(([key]) => !CANONICAL_CLINE_RULE_KEYS.includes(key)));
45140
+ }
45141
+ /**
45142
+ * Splits a Cline rule frontmatter into the keys that have canonical rulesync
45143
+ * homes (`paths` / `alwaysApply` / `description`) and the rest, which are
45144
+ * lifted into the rulesync `cline:` section.
45145
+ */
45146
+ function extractClineSection({ frontmatter, relativeFilePath }) {
45147
+ return Object.fromEntries(Object.entries(omitCanonicalClineRuleKeys(frontmatter)).filter(([key, value]) => {
45148
+ const expected = Object.hasOwn(CLINE_RULE_FIELD_TYPES, key) ? CLINE_RULE_FIELD_TYPES[key] : void 0;
45149
+ if (expected === void 0 || value === void 0 || typeof value === expected) return true;
45150
+ warnWithFallback(void 0, `Cline rules: dropping ${key} from ${relativeFilePath}: Cline rejects the whole rule unless it is a ${expected}.`);
45151
+ return false;
45152
+ }));
45153
+ }
45010
45154
  function isNonRootParams$1(params) {
45011
45155
  return "frontmatter" in params && "body" in params;
45012
45156
  }
@@ -45081,6 +45225,10 @@ var ClineRule = class ClineRule extends ToolRule {
45081
45225
  toRulesyncRule() {
45082
45226
  if (this.isRoot()) return this.toRulesyncRuleDefault();
45083
45227
  const globs = this.frontmatter?.alwaysApply === true ? ["**/*"] : normalizePaths$1(this.frontmatter?.paths);
45228
+ const cline = extractClineSection({
45229
+ frontmatter: this.frontmatter,
45230
+ relativeFilePath: this.getRelativeFilePath()
45231
+ });
45084
45232
  return new RulesyncRule({
45085
45233
  outputRoot: process.cwd(),
45086
45234
  relativeDirPath: RULESYNC_RULES_RELATIVE_DIR_PATH,
@@ -45089,7 +45237,8 @@ var ClineRule = class ClineRule extends ToolRule {
45089
45237
  root: false,
45090
45238
  targets: ["*"],
45091
45239
  description: this.frontmatter?.description,
45092
- globs
45240
+ globs,
45241
+ ...Object.keys(cline).length > 0 && { cline }
45093
45242
  },
45094
45243
  body: this.body ?? this.getFileContent()
45095
45244
  });
@@ -45134,16 +45283,22 @@ var ClineRule = class ClineRule extends ToolRule {
45134
45283
  * universal match-everything globs mean "always", so they map to
45135
45284
  * `alwaysApply: true` instead of a `paths` list; specific globs become
45136
45285
  * conditional `paths`; an empty globs list leaves the rule always-active with
45137
- * no `paths`/`alwaysApply`.
45286
+ * no `paths`/`alwaysApply`. Every key of the rulesync `cline:` section
45287
+ * (e.g. `disabled: true`) is written into the frontmatter too; the canonical
45288
+ * `paths` / `alwaysApply` / `description` win over same-named keys there.
45138
45289
  */
45139
45290
  static buildNonRootRule({ outputRoot, rulesyncRule, validate, relativeDirPath }) {
45140
45291
  const rulesyncFrontmatter = rulesyncRule.getFrontmatter();
45141
45292
  const globs = normalizePaths$1(rulesyncFrontmatter.globs);
45142
45293
  const isUniversal = globs.length > 0 && globs.every((glob) => glob === "**/*" || glob === "*");
45294
+ const rawClineSection = rulesyncFrontmatter.cline ?? {};
45295
+ 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.`);
45296
+ const clineSection = omitCanonicalClineRuleKeys(rawClineSection);
45143
45297
  const frontmatter = {
45144
45298
  paths: globs.length > 0 && !isUniversal ? globs : void 0,
45145
45299
  alwaysApply: isUniversal ? true : void 0,
45146
- description: rulesyncFrontmatter.description || void 0
45300
+ description: rulesyncFrontmatter.description || void 0,
45301
+ ...clineSection
45147
45302
  };
45148
45303
  return new ClineRule({
45149
45304
  outputRoot,
@@ -55830,6 +55985,14 @@ function applyCommandPrefix({ def, converterConfig }) {
55830
55985
  if (isExecForm) return `${bracePlaceholder(converterConfig.projectDirVar)}/${stripSurroundingQuotes(relativeCommand)}`;
55831
55986
  return anchorInline(`"${converterConfig.projectDirVar}"/${relativeCommand}`);
55832
55987
  }
55988
+ /** Whether a field restricted to some events applies on this one. */
55989
+ function isEventApplicable({ events, eventName }) {
55990
+ return events === void 0 || events.has(eventName);
55991
+ }
55992
+ /** Quote and comma-join a set of names (events, hook types) for a warning. */
55993
+ function describeEvents(names) {
55994
+ return [...names].map((name) => `"${name}"`).join(", ");
55995
+ }
55833
55996
  /**
55834
55997
  * Whether a field registered for `command` hooks only applies to this hook.
55835
55998
  * Applied on both export and import: a value imported into a canonical field
@@ -55851,7 +56014,7 @@ function isFieldApplicable({ commandOnly, hookType }) {
55851
56014
  * silence — the mirror of the import side.
55852
56015
  */
55853
56016
  function emitPassthroughFields({ def, hookType, eventName, fields, isValid, warn }) {
55854
- for (const { canonical, tool, commandOnly, maxLength, collapseWhitespaceForLength, onlyWithFlag } of fields) {
56017
+ for (const { canonical, tool, commandOnly, maxLength, collapseWhitespaceForLength, onlyWithFlag, events } of fields) {
55855
56018
  const value = def[canonical];
55856
56019
  if (value === void 0) continue;
55857
56020
  if (!isFieldApplicable({
@@ -55861,6 +56024,13 @@ function emitPassthroughFields({ def, hookType, eventName, fields, isValid, warn
55861
56024
  warn?.(`Dropping "${canonical}" from a "${hookType}" hook on "${eventName}": this tool documents "${tool}" on "command" hooks only, so it is not generated.`);
55862
56025
  continue;
55863
56026
  }
56027
+ if (events !== void 0 && !isEventApplicable({
56028
+ events,
56029
+ eventName
56030
+ })) {
56031
+ 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.`);
56032
+ continue;
56033
+ }
55864
56034
  if (!isValid({
55865
56035
  value,
55866
56036
  canonical
@@ -55874,9 +56044,12 @@ function emitPassthroughFields({ def, hookType, eventName, fields, isValid, warn
55874
56044
  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.`);
55875
56045
  }
55876
56046
  }
55877
- return Object.fromEntries(fields.filter(({ canonical, commandOnly }) => isFieldApplicable({
56047
+ return Object.fromEntries(fields.filter(({ canonical, commandOnly, events }) => isFieldApplicable({
55878
56048
  commandOnly,
55879
56049
  hookType
56050
+ }) && isEventApplicable({
56051
+ events,
56052
+ eventName
55880
56053
  }) && isValid({
55881
56054
  value: def[canonical],
55882
56055
  canonical
@@ -55889,16 +56062,24 @@ function emitPassthroughFields({ def, hookType, eventName, fields, isValid, warn
55889
56062
  * kind has a rule an authored file can plausibly violate — turns a rejected
55890
56063
  * value into a warning instead of a silent drop.
55891
56064
  */
55892
- function importPassthroughFields({ h, hookType, fields, isValid, describeInvalid, warn }) {
55893
- const applicable = fields.filter(({ commandOnly }) => isFieldApplicable({
56065
+ function importPassthroughFields({ h, hookType, eventName, fields, isValid, describeInvalid, warn }) {
56066
+ const typeApplicable = fields.filter(({ commandOnly }) => isFieldApplicable({
55894
56067
  commandOnly,
55895
56068
  hookType
55896
56069
  }));
56070
+ const applicable = typeApplicable.filter(({ events }) => isEventApplicable({
56071
+ events,
56072
+ eventName
56073
+ }));
55897
56074
  const skipped = fields.filter(({ commandOnly }) => !isFieldApplicable({
55898
56075
  commandOnly,
55899
56076
  hookType
55900
56077
  }));
55901
56078
  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.`);
56079
+ for (const { tool, events } of typeApplicable) if (events !== void 0 && !isEventApplicable({
56080
+ events,
56081
+ eventName
56082
+ }) && 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.`);
55902
56083
  for (const { tool, canonical } of applicable) if (describeInvalid !== void 0 && h[tool] !== void 0 && !isValid({
55903
56084
  value: h[tool],
55904
56085
  canonical
@@ -56066,11 +56247,11 @@ function isKeywordFieldApplicable({ spec, hookType, eventName, flags, direction,
56066
56247
  return false;
56067
56248
  }
56068
56249
  if (hookTypes !== void 0 && !hookTypes.has(hookType)) {
56069
- 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}.`);
56250
+ warn?.(`Dropping "${field}" from a "${hookType}" hook on "${eventName}": this tool documents "${tool}" on ${describeEvents(hookTypes)} hooks only, so it is not ${direction}.`);
56070
56251
  return false;
56071
56252
  }
56072
56253
  if (events !== void 0 && !events.has(eventName)) {
56073
- 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}.`);
56254
+ warn?.(`Dropping "${field}" from a "${hookType}" hook on "${eventName}": this tool honours "${tool}" on ${describeEvents(events)} only, so it is not ${direction}.`);
56074
56255
  return false;
56075
56256
  }
56076
56257
  if (ignoredEvents?.has(eventName)) {
@@ -56516,6 +56697,7 @@ function importAllPassthroughFields({ h, hookType, eventName, converterConfig, w
56516
56697
  ...importPassthroughFields({
56517
56698
  h,
56518
56699
  hookType,
56700
+ eventName,
56519
56701
  fields: converterConfig.booleanPassthroughFields ?? [],
56520
56702
  isValid: isBooleanValue,
56521
56703
  warn
@@ -56530,6 +56712,7 @@ function importAllPassthroughFields({ h, hookType, eventName, converterConfig, w
56530
56712
  ...importPassthroughFields({
56531
56713
  h,
56532
56714
  hookType,
56715
+ eventName,
56533
56716
  fields: converterConfig.numberPassthroughFields ?? [],
56534
56717
  isValid: isImportableNumber,
56535
56718
  describeInvalid: describeInvalidScalar,
@@ -56538,6 +56721,7 @@ function importAllPassthroughFields({ h, hookType, eventName, converterConfig, w
56538
56721
  ...importPassthroughFields({
56539
56722
  h,
56540
56723
  hookType,
56724
+ eventName,
56541
56725
  fields: converterConfig.stringPassthroughFields ?? [],
56542
56726
  isValid: isImportableString,
56543
56727
  describeInvalid: describeInvalidScalar,
@@ -56546,6 +56730,7 @@ function importAllPassthroughFields({ h, hookType, eventName, converterConfig, w
56546
56730
  ...importPassthroughFields({
56547
56731
  h,
56548
56732
  hookType,
56733
+ eventName,
56549
56734
  fields: converterConfig.arrayPassthroughFields ?? [],
56550
56735
  isValid: isImportableArray,
56551
56736
  describeInvalid: describeInvalidArray,
@@ -56554,6 +56739,7 @@ function importAllPassthroughFields({ h, hookType, eventName, converterConfig, w
56554
56739
  ...importPassthroughFields({
56555
56740
  h,
56556
56741
  hookType,
56742
+ eventName,
56557
56743
  fields: converterConfig.recordPassthroughFields ?? [],
56558
56744
  isValid: isEmittableRecord,
56559
56745
  describeInvalid: describeInvalidRecord,
@@ -56562,6 +56748,7 @@ function importAllPassthroughFields({ h, hookType, eventName, converterConfig, w
56562
56748
  ...importPassthroughFields({
56563
56749
  h,
56564
56750
  hookType,
56751
+ eventName,
56565
56752
  fields: converterConfig.objectPassthroughFields ?? [],
56566
56753
  isValid: isImportableObject,
56567
56754
  describeInvalid: describeInvalidObject,
@@ -61394,6 +61581,11 @@ const GROKCLI_CONVERTER_CONFIG = {
61394
61581
  tool: "env",
61395
61582
  commandOnly: true
61396
61583
  }],
61584
+ booleanPassthroughFields: [{
61585
+ canonical: "failClosed",
61586
+ tool: "failClosed",
61587
+ events: /* @__PURE__ */ new Set(["preToolUse", "beforeSubmitPrompt"])
61588
+ }],
61397
61589
  arrayPassthroughFields: [{
61398
61590
  canonical: "args",
61399
61591
  tool: "args",
@@ -63043,6 +63235,15 @@ const LETTACODE_CONVERTER_CONFIG = {
63043
63235
  timeoutUnit: "milliseconds"
63044
63236
  };
63045
63237
  /**
63238
+ * Letta Code documents settings-file hooks as deprecated in favor of Mods
63239
+ * (https://docs.letta.com/reference/deprecated/hooks) but still ships the
63240
+ * loader, so rulesync keeps emitting them and tells the user once per run.
63241
+ * Mods are not modeled: they are JS/TS code modules loaded only from
63242
+ * `~/.letta/mods/` or an agent's memory directory, with no declarative or
63243
+ * project-local form a canonical hook could be translated into.
63244
+ */
63245
+ 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";
63246
+ /**
63046
63247
  * Letta Code keeps a boolean `disabled` switch beside the event arrays inside
63047
63248
  * `hooks`. rulesync owns the whole `hooks` key, so the switch is read back from
63048
63249
  * the existing file and carried into the generated one instead of being lost.
@@ -63101,6 +63302,11 @@ function parseLettacodeSettings$1(fileContent, filePath) {
63101
63302
  * and UI preferences, ...), so generation merges the `hooks` key into the
63102
63303
  * existing file (see `SHARED_CONFIG_OWNERSHIP`) instead of overwriting it.
63103
63304
  *
63305
+ * Letta Code marks this surface deprecated in favor of Mods (see
63306
+ * `LETTACODE_HOOKS_DEPRECATION_WARNING`); generation warns once per run when it
63307
+ * writes any hook.
63308
+ *
63309
+ * @see https://docs.letta.com/reference/deprecated/hooks
63104
63310
  * @see https://github.com/letta-ai/letta-code/blob/main/src/hooks/types.ts
63105
63311
  * @see https://github.com/letta-ai/letta-code/blob/main/src/hooks/loader.ts
63106
63312
  */
@@ -63145,6 +63351,7 @@ var LettacodeHooks = class LettacodeHooks extends ToolHooks {
63145
63351
  converterConfig: LETTACODE_CONVERTER_CONFIG,
63146
63352
  logger
63147
63353
  });
63354
+ if (Object.keys(hooks).length > 0) warnOnceWithFallback(logger, LETTACODE_HOOKS_DEPRECATION_WARNING);
63148
63355
  const fileContent = applySharedConfigPatch({
63149
63356
  fileKey: sharedConfigFileKey(paths),
63150
63357
  feature: "hooks",
@@ -65142,11 +65349,38 @@ const VIBE_DIR = ".vibe";
65142
65349
  const VIBE_HOOKS_FILE_NAME = "hooks.toml";
65143
65350
  /**
65144
65351
  * Vibe scopes the tool-name `match` glob and the `strict` flag to tool hooks
65145
- * (`pre_tool` / `post_tool`) only; `post_agent` fires after every
65146
- * assistant turn and carries no matcher.
65147
- * @see https://github.com/mistralai/mistral-vibe/blob/main/README.md
65352
+ * (`pre_tool` / `post_tool`). `post_agent` takes a `match` too since v3.0.3,
65353
+ * but on the name of the agent that finished its turn, not a tool name (see
65354
+ * {@link VIBE_MAIN_AGENT_MATCH}).
65355
+ * @see https://github.com/mistralai/mistral-vibe/blob/v3.0.3/README.md
65148
65356
  */
65149
65357
  const VIBE_TOOL_EVENTS = /* @__PURE__ */ new Set(["pre_tool", "post_tool"]);
65358
+ /** The Vibe event both canonical `stop` and `subagentStop` become. */
65359
+ const VIBE_POST_AGENT_EVENT = "post_agent";
65360
+ /**
65361
+ * The reserved agent alias a `post_agent` hook's `match` uses to run only
65362
+ * after the main agent. Without a `match`, `post_agent` runs after every
65363
+ * agent, subagents included (v3.0.2+), whereas canonical `stop` is
65364
+ * main-agent-only — so `stop` is written with this match.
65365
+ * @see https://github.com/mistralai/mistral-vibe/blob/v3.0.3/README.md
65366
+ */
65367
+ const VIBE_MAIN_AGENT_MATCH = "main";
65368
+ /**
65369
+ * The `post_agent` `match` for a canonical `subagentStop` hook, or null to
65370
+ * skip it. Vibe has no selector for "every subagent but not the main agent",
65371
+ * so a matcher naming the subagent is required.
65372
+ */
65373
+ function toVibeSubagentMatch({ matcher, logger }) {
65374
+ if (typeof matcher !== "string" || matcher === "" || matcher === "*") {
65375
+ 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.");
65376
+ return null;
65377
+ }
65378
+ if (matcher.toLowerCase() === VIBE_MAIN_AGENT_MATCH) {
65379
+ logger?.warn("Vibe hooks: skipping a \"subagentStop\" hook whose matcher is \"main\". Vibe reserves \"main\" for the main agent; use a \"stop\" hook instead.");
65380
+ return null;
65381
+ }
65382
+ return matcher;
65383
+ }
65150
65384
  const SUPPORTED_VIBE_EVENTS = new Set(VIBE_HOOK_EVENTS);
65151
65385
  /**
65152
65386
  * The `match` a canonical `Bash` matcher becomes. Vibe tests `match` against a
@@ -65170,7 +65404,8 @@ function fromVibeHookMatch(match) {
65170
65404
  /**
65171
65405
  * Build the flat `[[hooks]]` array for `.vibe/hooks.toml` from a canonical
65172
65406
  * hooks config. Vibe uses a flat array where each entry carries its own event
65173
- * `type`, tool-name `match` glob/regex, and `command`. Only `type: "command"`
65407
+ * `type`, `match` glob/regex (a tool name for tool hooks, an agent name
65408
+ * for `post_agent`), and `command`. Only `type: "command"`
65174
65409
  * canonical hooks are emitted: a Vibe hook is a shell command line. (v2.25.1
65175
65410
  * to v2.25.4 briefly exec'd it without a shell under the unified harness and
65176
65411
  * skipped any command containing a backslash; v2.25.5 restored shell
@@ -65178,7 +65413,7 @@ function fromVibeHookMatch(match) {
65178
65413
  * now run through the shell" — and dropped the backslash skip.)
65179
65414
  * @see https://github.com/mistralai/mistral-vibe/blob/v3.0.2/README.md
65180
65415
  */
65181
- function canonicalToVibeHooks({ config, toolOverride }) {
65416
+ function canonicalToVibeHooks({ config, toolOverride, logger }) {
65182
65417
  const shared = {};
65183
65418
  for (const [event, defs] of Object.entries(config.hooks)) if (SUPPORTED_VIBE_EVENTS.has(event)) shared[event] = defs;
65184
65419
  const effective = {
@@ -65186,30 +65421,40 @@ function canonicalToVibeHooks({ config, toolOverride }) {
65186
65421
  ...toolOverride
65187
65422
  };
65188
65423
  const hooks = [];
65424
+ const nextIndexByVibeEvent = /* @__PURE__ */ new Map();
65189
65425
  for (const [event, defs] of Object.entries(effective)) {
65190
65426
  if (!SUPPORTED_VIBE_EVENTS.has(event)) continue;
65191
65427
  const vibeEvent = lookupOwn({
65192
65428
  record: CANONICAL_TO_VIBE_EVENT_NAMES,
65193
65429
  key: event
65194
65430
  }) ?? event;
65195
- let index = 0;
65196
65431
  for (const def of defs) {
65197
65432
  if ((def.type ?? "command") !== "command") continue;
65198
65433
  if (typeof def.command !== "string") continue;
65199
- const name = typeof def.name === "string" ? def.name : `${vibeEvent}-${index}`;
65200
65434
  const isToolEvent = VIBE_TOOL_EVENTS.has(vibeEvent);
65435
+ let match;
65436
+ if (isToolEvent) match = toVibeHookMatch(def.matcher);
65437
+ else if (event === "subagentStop") {
65438
+ const subagentMatch = toVibeSubagentMatch({
65439
+ matcher: def.matcher,
65440
+ logger
65441
+ });
65442
+ if (subagentMatch === null) continue;
65443
+ match = subagentMatch;
65444
+ } else match = VIBE_MAIN_AGENT_MATCH;
65445
+ const index = nextIndexByVibeEvent.get(vibeEvent) ?? 0;
65201
65446
  const entry = {
65202
- name,
65447
+ name: typeof def.name === "string" ? def.name : `${vibeEvent}-${index}`,
65203
65448
  type: vibeEvent,
65449
+ match,
65204
65450
  command: def.command
65205
65451
  };
65206
- if (isToolEvent) entry.match = toVibeHookMatch(def.matcher);
65207
65452
  if (typeof def.timeout === "number") entry.timeout = def.timeout;
65208
65453
  const strict = def.strict;
65209
65454
  if (isToolEvent && typeof strict === "boolean") entry.strict = strict;
65210
65455
  if (typeof def.description === "string") entry.description = def.description;
65211
65456
  hooks.push(entry);
65212
- index += 1;
65457
+ nextIndexByVibeEvent.set(vibeEvent, index + 1);
65213
65458
  }
65214
65459
  }
65215
65460
  return { hooks };
@@ -65221,14 +65466,22 @@ function vibeEntryToCanonicalDef(raw) {
65221
65466
  const vibeEvent = typeof entry.type === "string" ? entry.type : void 0;
65222
65467
  if (vibeEvent === void 0) return null;
65223
65468
  if (isPrototypePollutionKey(vibeEvent)) return null;
65224
- const canonicalEvent = lookupOwn({
65469
+ let canonicalEvent = lookupOwn({
65225
65470
  record: VIBE_TO_CANONICAL_EVENT_NAMES,
65226
65471
  key: vibeEvent
65227
65472
  }) ?? vibeEvent;
65228
65473
  const def = { type: "command" };
65229
65474
  if (typeof entry.command === "string") def.command = entry.command;
65230
- const matcher = fromVibeHookMatch(entry.match);
65231
- if (matcher !== void 0) def.matcher = matcher;
65475
+ if (vibeEvent === VIBE_POST_AGENT_EVENT) {
65476
+ const agentMatch = typeof entry.match === "string" ? entry.match : "";
65477
+ if (agentMatch !== "" && agentMatch !== "*" && agentMatch.toLowerCase() !== VIBE_MAIN_AGENT_MATCH) {
65478
+ canonicalEvent = "subagentStop";
65479
+ def.matcher = agentMatch;
65480
+ }
65481
+ } else {
65482
+ const matcher = fromVibeHookMatch(entry.match);
65483
+ if (matcher !== void 0) def.matcher = matcher;
65484
+ }
65232
65485
  if (typeof entry.timeout === "number") def.timeout = entry.timeout;
65233
65486
  if (typeof entry.name === "string") def.name = entry.name;
65234
65487
  if (typeof entry.description === "string") def.description = entry.description;
@@ -65259,6 +65512,26 @@ function vibeHooksToCanonical(parsed) {
65259
65512
  }
65260
65513
  return canonical;
65261
65514
  }
65515
+ /** Vibe reads a `match` with a `re:` prefix or glob metacharacters as a pattern. */
65516
+ const VIBE_PATTERN_MATCH = /^re:|[*?[]/;
65517
+ /**
65518
+ * Move the imported `subagentStop` hooks into the `vibe` override when any of
65519
+ * them matches agents by a regex or glob rather than by a literal name. Such a
65520
+ * pattern can also select the main agent (a main session answers to its mode
65521
+ * name, e.g. `accept-edits`), and other tools would read it as their own
65522
+ * matcher syntax, so it is kept Vibe-only. The override replaces the shared
65523
+ * list for its event, so the whole list moves to keep every entry on Vibe.
65524
+ */
65525
+ function takeVibeOnlySubagentStop({ hooks, logger }) {
65526
+ const defs = lookupOwn({
65527
+ record: hooks,
65528
+ key: "subagentStop"
65529
+ });
65530
+ if (defs === void 0 || !defs.some((def) => typeof def.matcher === "string" && VIBE_PATTERN_MATCH.test(def.matcher))) return;
65531
+ delete hooks.subagentStop;
65532
+ 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.");
65533
+ return defs;
65534
+ }
65262
65535
  function parseVibeToml(fileContent) {
65263
65536
  const parsed = smol_toml.parse(fileContent || smol_toml.stringify({}));
65264
65537
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
@@ -65298,12 +65571,13 @@ var VibeHooks = class extends ToolHooks {
65298
65571
  validate
65299
65572
  });
65300
65573
  }
65301
- static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false }) {
65574
+ static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
65302
65575
  const paths = this.getSettablePaths({ global });
65303
65576
  const config = rulesyncHooks.getJson();
65304
65577
  const vibeHooks = canonicalToVibeHooks({
65305
65578
  config,
65306
- toolOverride: config.vibe?.hooks
65579
+ toolOverride: config.vibe?.hooks,
65580
+ logger
65307
65581
  });
65308
65582
  const fileContent = smol_toml.stringify(vibeHooks);
65309
65583
  return new this({
@@ -65314,15 +65588,30 @@ var VibeHooks = class extends ToolHooks {
65314
65588
  validate
65315
65589
  });
65316
65590
  }
65317
- toRulesyncHooks() {
65591
+ toRulesyncHooks(options = {}) {
65318
65592
  const hooks = vibeHooksToCanonical(this.parseFileContentOrThrow({
65319
65593
  displayName: "Vibe",
65320
65594
  parse: (fileContent) => parseVibeToml(fileContent)
65321
65595
  }));
65322
- return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
65596
+ const vibeOnlySubagentStop = takeVibeOnlySubagentStop({
65597
+ hooks,
65598
+ logger: options.logger
65599
+ });
65600
+ const config = buildImportedHooksConfig({
65323
65601
  hooks,
65324
65602
  overrideKey: "vibe"
65325
- }), null, 2) });
65603
+ });
65604
+ if (vibeOnlySubagentStop !== void 0) {
65605
+ const override = config.vibe ?? {};
65606
+ config.vibe = {
65607
+ ...override,
65608
+ hooks: {
65609
+ ...override.hooks,
65610
+ subagentStop: vibeOnlySubagentStop
65611
+ }
65612
+ };
65613
+ }
65614
+ return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(config, null, 2) });
65326
65615
  }
65327
65616
  validate() {
65328
65617
  try {
@@ -76939,6 +77228,12 @@ const REASONIX_PLUGIN_FIELDS = [
76939
77228
  "load",
76940
77229
  "oauth_allow_missing_pkce_metadata"
76941
77230
  ];
77231
+ /**
77232
+ * The `load` mode that puts a server's tools in the provider schema. Reasonix
77233
+ * compares it case-insensitively after trimming (`normalizeMCPLoad`).
77234
+ * @see https://github.com/esengine/DeepSeek-Reasonix/blob/studio-v2.53.0/internal/contract/config/mcp_load.go
77235
+ */
77236
+ const REASONIX_MCP_LOAD_ALWAYS = "always";
76942
77237
  /** The `[[plugins]]` key that canonical `disabledTools` is written to and read from. */
76943
77238
  const REASONIX_DISABLED_TOOLS_KEY = "disabled_tools";
76944
77239
  var ReasonixMcp = class ReasonixMcp extends ToolMcp {
@@ -77103,7 +77398,9 @@ function hasExpectedType(value, expected) {
77103
77398
  * `concurrency` string decodes fine but is ignored in favor of the server-name
77104
77399
  * default, so it is dropped too rather than written as a setting that does
77105
77400
  * nothing. An unknown `load` string is read as `deferred`, the default, so it
77106
- * passes through as authored.
77401
+ * passes through as authored. A project `load = "always"` also passes through,
77402
+ * but is warned about in `writePassthroughFields`: see the v2.53.0 note on
77403
+ * `REASONIX_PLUGIN_FIELDS`.
77107
77404
  * @see https://github.com/esengine/DeepSeek-Reasonix/blob/v2.31.0/internal/contract/config/plugin_entry.go
77108
77405
  */
77109
77406
  function invalidPassthroughFieldReason(field, value) {
@@ -77143,6 +77440,7 @@ function writePassthroughFields({ name, server, plugin, global, logger }) {
77143
77440
  continue;
77144
77441
  }
77145
77442
  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.`));
77443
+ 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.`);
77146
77444
  plugin[field] = value;
77147
77445
  }
77148
77446
  }
@@ -86081,6 +86379,41 @@ var ClinePermissions = class ClinePermissions extends ToolPermissions {
86081
86379
  // src/features/permissions/codebuddy-permissions.ts
86082
86380
  /** Top-level key of `.codebuddy/settings.json` that rulesync owns. */
86083
86381
  const CODEBUDDY_PERMISSIONS_KEY = "permissions";
86382
+ /**
86383
+ * The top-level settings groups the `codebuddy` override may author, each with
86384
+ * the only keys of that group rulesync writes and imports. A group's other
86385
+ * keys are kept as the file has them; an override key not listed here is
86386
+ * skipped with a warning.
86387
+ *
86388
+ * - `autoReview`: the `auto` permission mode's review policy (`policy`
86389
+ * replaces the built-in security policy, `extraPolicy` is appended to it).
86390
+ * - `autoMode`: the legacy natural-language `auto` rules, still read as
86391
+ * additional policy.
86392
+ * - `memory.parentDiscovery`: whether `CODEBUDDY.md` / `AGENTS.md` are also
86393
+ * discovered in parent directories of the launch directory.
86394
+ *
86395
+ * @see https://www.codebuddy.ai/docs/cli/settings
86396
+ * @see https://www.codebuddy.ai/docs/cli/memory
86397
+ */
86398
+ const CODEBUDDY_OVERRIDE_GROUP_KEYS = {
86399
+ autoReview: ["policy", "extraPolicy"],
86400
+ autoMode: [
86401
+ "environment",
86402
+ "allow",
86403
+ "soft_deny",
86404
+ "hard_deny"
86405
+ ],
86406
+ memory: ["parentDiscovery"]
86407
+ };
86408
+ const CODEBUDDY_OVERRIDE_GROUPS = Object.keys(CODEBUDDY_OVERRIDE_GROUP_KEYS);
86409
+ /**
86410
+ * Groups CodeBuddy reads from the user and project-local settings only: the
86411
+ * shared project `.codebuddy/settings.json` is not read for them, so a checked-in
86412
+ * repository cannot relax the review of its own actions. They are written and
86413
+ * imported in global mode alone.
86414
+ * @see https://www.codebuddy.ai/docs/cli/settings
86415
+ */
86416
+ const CODEBUDDY_USER_ONLY_GROUPS = /* @__PURE__ */ new Set(["autoReview", "autoMode"]);
86084
86417
  /** The rule lists under `permissions`, one per canonical action. */
86085
86418
  const CODEBUDDY_RULE_LISTS = [
86086
86419
  "allow",
@@ -86234,6 +86567,87 @@ function buildCodebuddyRuleLists({ config, existingPermissions, logger }) {
86234
86567
  return lists;
86235
86568
  }
86236
86569
  /**
86570
+ * Whether a settings value has the shape the `codebuddy` override schema
86571
+ * accepts for that key, so an off-spec value is left in the file instead of
86572
+ * being written or making the imported permissions file fail validation.
86573
+ */
86574
+ function isLiftableCodebuddyValue(key, value) {
86575
+ switch (key) {
86576
+ case "policy":
86577
+ case "extraPolicy": return typeof value === "string";
86578
+ case "parentDiscovery": return typeof value === "boolean";
86579
+ default: return isStringArray(value);
86580
+ }
86581
+ }
86582
+ /** Name every written override setting that can make the `auto` review more lenient. */
86583
+ function announceCodebuddyReviewSettings({ picked, group, where, logger }) {
86584
+ 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.`);
86585
+ 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.`);
86586
+ 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.`);
86587
+ }
86588
+ /**
86589
+ * The top-level keys the `codebuddy` override contributes to the settings
86590
+ * patch, besides `permissions`. Each group is the existing group with the
86591
+ * authored keys laid over it; a group is only in the patch when authored, so
86592
+ * a hand-set value survives an unrelated regeneration.
86593
+ */
86594
+ function buildCodebuddyOverridePatch({ override, existing, global, where, logger }) {
86595
+ const patch = {};
86596
+ if (override === void 0) return patch;
86597
+ for (const group of CODEBUDDY_OVERRIDE_GROUPS) {
86598
+ const authored = override[group];
86599
+ if (!isRecord(authored)) continue;
86600
+ if (CODEBUDDY_USER_ONLY_GROUPS.has(group) && !global) {
86601
+ 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.`);
86602
+ continue;
86603
+ }
86604
+ const allowedKeys = CODEBUDDY_OVERRIDE_GROUP_KEYS[group];
86605
+ const picked = {};
86606
+ for (const [key, value] of Object.entries(authored)) {
86607
+ if (isPrototypePollutionKey(key) || value === void 0) continue;
86608
+ if (!allowedKeys.includes(key)) {
86609
+ 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.`);
86610
+ continue;
86611
+ }
86612
+ picked[key] = value;
86613
+ }
86614
+ if (Object.keys(picked).length === 0) continue;
86615
+ const existingValue = existing[group];
86616
+ if (existingValue !== void 0 && !isRecord(existingValue)) {
86617
+ 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}.`);
86618
+ continue;
86619
+ }
86620
+ patch[group] = {
86621
+ ...existingValue,
86622
+ ...picked
86623
+ };
86624
+ announceCodebuddyReviewSettings({
86625
+ picked,
86626
+ group,
86627
+ where,
86628
+ logger
86629
+ });
86630
+ }
86631
+ return patch;
86632
+ }
86633
+ /**
86634
+ * Lift the settings the `codebuddy` override authors back out of a settings
86635
+ * file. `autoReview` and `autoMode` are lifted only from the user file, the
86636
+ * one of the two rulesync manages that CodeBuddy reads them from.
86637
+ */
86638
+ function liftCodebuddyOverride({ settings, global }) {
86639
+ const lifted = {};
86640
+ for (const group of CODEBUDDY_OVERRIDE_GROUPS) {
86641
+ if (CODEBUDDY_USER_ONLY_GROUPS.has(group) && !global) continue;
86642
+ const existingGroup = settings[group];
86643
+ if (!isRecord(existingGroup)) continue;
86644
+ const picked = {};
86645
+ for (const key of CODEBUDDY_OVERRIDE_GROUP_KEYS[group]) if (Object.hasOwn(existingGroup, key) && isLiftableCodebuddyValue(key, existingGroup[key])) picked[key] = existingGroup[key];
86646
+ if (Object.keys(picked).length > 0) lifted[group] = picked;
86647
+ }
86648
+ return lifted;
86649
+ }
86650
+ /**
86237
86651
  * CodeBuddy Code permissions.
86238
86652
  *
86239
86653
  * Rules live under the `permissions` key of `.codebuddy/settings.json`
@@ -86244,7 +86658,10 @@ function buildCodebuddyRuleLists({ config, existingPermissions, logger }) {
86244
86658
  * writes go through the shared-config gateway, the sibling keys of the rule
86245
86659
  * lists (`defaultMode`, `additionalDirectories`, `trustedDirectories`, ...)
86246
86660
  * are kept, and the file is never deleted. The gitignored
86247
- * `.codebuddy/settings.local.json` layer is left to CodeBuddy.
86661
+ * `.codebuddy/settings.local.json` layer is left to CodeBuddy. The
86662
+ * `codebuddy` override's other settings (`autoReview`, `autoMode`,
86663
+ * `memory.parentDiscovery`) go through `buildCodebuddyOverridePatch` /
86664
+ * `liftCodebuddyOverride`.
86248
86665
  *
86249
86666
  * @see https://www.codebuddy.ai/docs/cli/permissions
86250
86667
  * @see https://www.codebuddy.ai/docs/cli/settings
@@ -86283,8 +86700,9 @@ var CodebuddyPermissions = class CodebuddyPermissions extends ToolPermissions {
86283
86700
  relativePath: (0, node_path.join)(paths.relativeDirPath, paths.relativeFilePath)
86284
86701
  });
86285
86702
  const existingPermissions = isRecord(existing[CODEBUDDY_PERMISSIONS_KEY]) ? existing[CODEBUDDY_PERMISSIONS_KEY] : {};
86703
+ const config = rulesyncPermissions.getJson();
86286
86704
  const lists = buildCodebuddyRuleLists({
86287
- config: rulesyncPermissions.getJson(),
86705
+ config,
86288
86706
  existingPermissions,
86289
86707
  logger
86290
86708
  });
@@ -86299,7 +86717,16 @@ var CodebuddyPermissions = class CodebuddyPermissions extends ToolPermissions {
86299
86717
  fileKey: sharedConfigFileKey(paths),
86300
86718
  feature: "permissions",
86301
86719
  existingContent,
86302
- patch: { [CODEBUDDY_PERMISSIONS_KEY]: Object.keys(permissions).length > 0 ? permissions : void 0 },
86720
+ patch: {
86721
+ [CODEBUDDY_PERMISSIONS_KEY]: Object.keys(permissions).length > 0 ? permissions : void 0,
86722
+ ...buildCodebuddyOverridePatch({
86723
+ override: config.codebuddy,
86724
+ existing,
86725
+ global,
86726
+ where: `${(0, node_path.join)(paths.relativeDirPath, paths.relativeFilePath)}${global ? " in the home directory" : ""}`,
86727
+ logger
86728
+ })
86729
+ },
86303
86730
  filePath
86304
86731
  }),
86305
86732
  validate: true,
@@ -86320,7 +86747,14 @@ var CodebuddyPermissions = class CodebuddyPermissions extends ToolPermissions {
86320
86747
  const existing = rules[parsed.pattern];
86321
86748
  if (existing === void 0 || PERMISSION_ACTION_PRIORITY[action] > PERMISSION_ACTION_PRIORITY[existing]) rules[parsed.pattern] = action;
86322
86749
  }
86323
- return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({ permission }, null, 2) });
86750
+ const codebuddy = liftCodebuddyOverride({
86751
+ settings,
86752
+ global: this.global
86753
+ });
86754
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({
86755
+ permission,
86756
+ ...Object.keys(codebuddy).length > 0 && { codebuddy }
86757
+ }, null, 2) });
86324
86758
  }
86325
86759
  static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
86326
86760
  return new CodebuddyPermissions({
@@ -89286,7 +89720,7 @@ const AUTO_APPROVE_KEYS = {
89286
89720
  *
89287
89721
  * @see https://code.visualstudio.com/docs/agents/reference/ai-settings
89288
89722
  */
89289
- const DEFAULT_PERMISSION_MODE_KEY = "chat.permissions.default";
89723
+ const DEFAULT_PERMISSION_MODE_KEY$1 = "chat.permissions.default";
89290
89724
  /**
89291
89725
  * The prefix every Agent Host sandbox setting shares. A `copilot.sandbox` leaf
89292
89726
  * at path `a.b` is written to `chat.agent.sandbox.a.b`.
@@ -89574,7 +90008,7 @@ var CopilotPermissions = class CopilotPermissions extends ToolPermissions {
89574
90008
  });
89575
90009
  }
89576
90010
  const defaultPermissionMode = config.copilot?.defaultPermissionMode;
89577
- if (defaultPermissionMode !== void 0) patch[DEFAULT_PERMISSION_MODE_KEY] = defaultPermissionMode;
90011
+ if (defaultPermissionMode !== void 0) patch[DEFAULT_PERMISSION_MODE_KEY$1] = defaultPermissionMode;
89578
90012
  const sandbox = config.copilot?.sandbox;
89579
90013
  if (sandbox !== void 0) {
89580
90014
  Object.assign(patch, buildSandboxPatch({
@@ -89630,7 +90064,7 @@ var CopilotPermissions = class CopilotPermissions extends ToolPermissions {
89630
90064
  for (const [pattern, flag] of Object.entries(autoApprove)) rules[pattern] = flag ? "allow" : "deny";
89631
90065
  if (Object.keys(rules).length > 0) permission[category] = rules;
89632
90066
  }
89633
- const defaultPermissionMode = settings[DEFAULT_PERMISSION_MODE_KEY];
90067
+ const defaultPermissionMode = settings[DEFAULT_PERMISSION_MODE_KEY$1];
89634
90068
  const sandbox = readSandboxFromSettings(settings);
89635
90069
  const copilot = {
89636
90070
  ...isCopilotDefaultPermissionMode(defaultPermissionMode) && { defaultPermissionMode },
@@ -89663,6 +90097,16 @@ const WEBFETCH_CATEGORY$1 = "webfetch";
89663
90097
  /** `~/.copilot/settings.json` and `.github/copilot/settings.json` keys. */
89664
90098
  const ALLOWED_URLS_KEY = "allowedUrls";
89665
90099
  const DENIED_URLS_KEY = "deniedUrls";
90100
+ /**
90101
+ * `~/.copilot/settings.json` only: the startup permission mode for new
90102
+ * interactive sessions, authored through the `copilotcli.defaultPermissionMode`
90103
+ * override. It is a user setting — the repository settings file does not list
90104
+ * it among its repo-overridable keys.
90105
+ */
90106
+ const DEFAULT_PERMISSION_MODE_KEY = "defaultPermissionMode";
90107
+ function isCopilotcliDefaultPermissionMode(value) {
90108
+ return COPILOTCLI_DEFAULT_PERMISSION_MODES.some((mode) => mode === value);
90109
+ }
89666
90110
  function toUrlList(value) {
89667
90111
  if (!Array.isArray(value)) return [];
89668
90112
  return value.filter((entry) => typeof entry === "string");
@@ -89704,6 +90148,13 @@ function splitUrlRules(rules) {
89704
90148
  * top-level keys in the user `settings.json`, so only documented keys are ever
89705
90149
  * emitted there either.)
89706
90150
  *
90151
+ * The `copilotcli.defaultPermissionMode` override (`manual` | `assisted` |
90152
+ * `allow-all`) writes the user-scope `defaultPermissionMode` key in global mode
90153
+ * only; in project mode it is skipped with a warning, since the repository
90154
+ * settings file does not accept it. It is patched only when authored, so a mode
90155
+ * the user picked through `/settings` otherwise survives. Writing `allow-all`
90156
+ * warns, because it makes new sessions skip every approval prompt.
90157
+ *
89707
90158
  * @see https://docs.github.com/en/copilot/reference/copilot-cli-reference/cli-config-dir-reference
89708
90159
  * @see https://github.com/github/copilot-cli/blob/main/changelog.md
89709
90160
  */
@@ -89746,7 +90197,8 @@ var CopilotcliPermissions = class CopilotcliPermissions extends ToolPermissions
89746
90197
  const paths = CopilotcliPermissions.getSettablePaths({ global });
89747
90198
  const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
89748
90199
  const existingContent = await readFileContentOrNull(filePath) ?? "{}";
89749
- const rules = rulesyncPermissions.getJson().permission[WEBFETCH_CATEGORY$1];
90200
+ const config = rulesyncPermissions.getJson();
90201
+ const rules = config.permission[WEBFETCH_CATEGORY$1];
89750
90202
  const patch = {};
89751
90203
  if (rules !== void 0) {
89752
90204
  const { allowedUrls, deniedUrls } = splitUrlRules(rules);
@@ -89754,6 +90206,13 @@ var CopilotcliPermissions = class CopilotcliPermissions extends ToolPermissions
89754
90206
  if (global) patch[ALLOWED_URLS_KEY] = allowedUrls.length > 0 ? allowedUrls : void 0;
89755
90207
  else if (allowedUrls.length > 0) logger?.warn(`Copilot CLI permissions: dropping ${allowedUrls.length} "webfetch" allow rule(s) at project scope — ${(0, node_path.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.`);
89756
90208
  }
90209
+ const defaultPermissionMode = config.copilotcli?.defaultPermissionMode;
90210
+ if (defaultPermissionMode !== void 0) {
90211
+ if (global) {
90212
+ patch[DEFAULT_PERMISSION_MODE_KEY] = defaultPermissionMode;
90213
+ if (defaultPermissionMode === "allow-all") logger?.warn(`Copilot CLI permissions: writing "${DEFAULT_PERMISSION_MODE_KEY}": "allow-all" to ${(0, node_path.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.`);
90214
+ } else logger?.warn(`Copilot CLI permissions: skipping "copilotcli.${DEFAULT_PERMISSION_MODE_KEY}" at project scope — ${(0, node_path.join)(paths.relativeDirPath, paths.relativeFilePath)} does not accept "${DEFAULT_PERMISSION_MODE_KEY}" (it is a user setting). Author it in global scope (\`--global\`) instead.`);
90215
+ }
89757
90216
  return new CopilotcliPermissions({
89758
90217
  outputRoot,
89759
90218
  relativeDirPath: paths.relativeDirPath,
@@ -89775,7 +90234,11 @@ var CopilotcliPermissions = class CopilotcliPermissions extends ToolPermissions
89775
90234
  for (const pattern of toUrlList(settings[DENIED_URLS_KEY])) rules[pattern] = "deny";
89776
90235
  if (this.global) for (const pattern of toUrlList(settings[ALLOWED_URLS_KEY])) rules[pattern] ??= "allow";
89777
90236
  const permission = Object.keys(rules).length > 0 ? { [WEBFETCH_CATEGORY$1]: rules } : {};
89778
- return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({ permission }, null, 2) });
90237
+ const defaultPermissionMode = this.global ? settings[DEFAULT_PERMISSION_MODE_KEY] : void 0;
90238
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({
90239
+ permission,
90240
+ ...isCopilotcliDefaultPermissionMode(defaultPermissionMode) && { copilotcli: { defaultPermissionMode } }
90241
+ }, null, 2) });
89779
90242
  }
89780
90243
  parseSettings() {
89781
90244
  const relativePath = (0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath());
@@ -95280,6 +95743,7 @@ var MimocodePermissions = class extends OpencodePermissions {
95280
95743
  // src/features/permissions/omp-permissions.ts
95281
95744
  const CATCH_ALL_PATTERN$5 = "*";
95282
95745
  const APPROVAL_MODE_KEY = "approvalMode";
95746
+ const RESTRICTED_FOLDERS_KEY = "restrictedFolders";
95283
95747
  const ACTION_TO_OMP_APPROVAL = {
95284
95748
  allow: "allow",
95285
95749
  ask: "prompt",
@@ -95318,9 +95782,9 @@ const MANAGED_OMP_TOOLS = /* @__PURE__ */ new Set([
95318
95782
  *
95319
95783
  * - `tools.approval.<tool>: allow | prompt | deny` is a per-tool policy honored
95320
95784
  * in every approval mode. A category's catch-all `*` rule maps here
95321
- * (`ask` → `prompt`). oh-my-pi has no per-path matcher, so a non-bash category
95322
- * with pattern-specific rules collapses to one action (deny > ask > allow, with
95323
- * an implicit `ask` when there is no catch-all) and a warning.
95785
+ * (`ask` → `prompt`). `tools.approval` has no per-path matcher, so a non-bash
95786
+ * category with pattern-specific rules collapses to one action (deny > ask >
95787
+ * allow, with an implicit `ask` when there is no catch-all) and a warning.
95324
95788
  * - `bash.patterns: [{ match, approval }]` is an ordered, first-match-wins list
95325
95789
  * of command globs (only `*` is a wildcard). Every `bash` rule except a
95326
95790
  * catch-all `allow` is written there, ordered deny → prompt → allow so that
@@ -95337,12 +95801,15 @@ const MANAGED_OMP_TOOLS = /* @__PURE__ */ new Set([
95337
95801
  *
95338
95802
  * The `omp.approvalMode` override writes `tools.approvalMode` (`always-ask`,
95339
95803
  * `always-ask-cached`, `write` or `yolo`); without it the key is left as it is in the file, and on
95340
- * import a recognized value is read back into the override.
95804
+ * import a recognized value is read back into the override. Likewise the
95805
+ * `omp.restrictedFolders` override writes `tools.restrictedFolders` (folders
95806
+ * where every action prompts in every approval mode, `yolo` included), is left
95807
+ * alone when absent, and is read back on import.
95341
95808
  *
95342
95809
  * `config.yml` carries every other oh-my-pi setting, so only `tools.approval`
95343
95810
  * (the keys rulesync manages), `bash.patterns` and an authored
95344
- * `tools.approvalMode` are rewritten; sibling keys such as `bash.enabled` are
95345
- * kept, and the file is never deleted.
95811
+ * `tools.approvalMode` or `tools.restrictedFolders` are rewritten; sibling
95812
+ * keys such as `bash.enabled` are kept, and the file is never deleted.
95346
95813
  *
95347
95814
  * @see https://github.com/can1357/oh-my-pi/blob/main/docs/settings.md
95348
95815
  * @see https://github.com/can1357/oh-my-pi/blob/main/docs/approval-mode.md
@@ -95391,6 +95858,7 @@ var OmpPermissions = class OmpPermissions extends ToolPermissions {
95391
95858
  logger
95392
95859
  });
95393
95860
  const approvalMode = config.omp?.approvalMode;
95861
+ const restrictedFolders = config.omp?.restrictedFolders;
95394
95862
  const existingTools = isRecord(existing.tools) ? existing.tools : {};
95395
95863
  const nextApproval = {
95396
95864
  ...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)])) : {},
@@ -95398,6 +95866,7 @@ var OmpPermissions = class OmpPermissions extends ToolPermissions {
95398
95866
  };
95399
95867
  const toolsSiblings = withoutKey(existingTools, "approval");
95400
95868
  if (approvalMode !== void 0) toolsSiblings[APPROVAL_MODE_KEY] = approvalMode;
95869
+ if (restrictedFolders !== void 0) toolsSiblings[RESTRICTED_FOLDERS_KEY] = restrictedFolders;
95401
95870
  const nextTools = Object.keys(nextApproval).length > 0 ? {
95402
95871
  ...toolsSiblings,
95403
95872
  approval: nextApproval
@@ -95648,11 +96117,26 @@ function convertOmpToRulesync(config) {
95648
96117
  }
95649
96118
  if (Object.keys(bashRules).length > 0) permission.bash = bashRules;
95650
96119
  const approvalMode = tools[APPROVAL_MODE_KEY];
96120
+ const restrictedFolders = importRestrictedFolders(tools[RESTRICTED_FOLDERS_KEY]);
96121
+ const omp = {
96122
+ ...isOmpApprovalMode(approvalMode) && { approvalMode },
96123
+ ...restrictedFolders !== void 0 && { restrictedFolders }
96124
+ };
95651
96125
  return {
95652
96126
  permission,
95653
- ...isOmpApprovalMode(approvalMode) && { omp: { approvalMode } }
96127
+ ...Object.keys(omp).length > 0 && { omp }
95654
96128
  };
95655
96129
  }
96130
+ /**
96131
+ * `tools.restrictedFolders` as the override list: its string entries, or
96132
+ * `undefined` when it is unset or no entry survives (so a list of non-strings
96133
+ * is not imported as `[]` and then rewritten as an empty list).
96134
+ */
96135
+ function importRestrictedFolders(value) {
96136
+ if (!Array.isArray(value)) return;
96137
+ const folders = value.filter((entry) => typeof entry === "string");
96138
+ return folders.length === 0 && value.length > 0 ? void 0 : folders;
96139
+ }
95656
96140
  function isOmpApprovalMode(value) {
95657
96141
  return OMP_APPROVAL_MODES.some((mode) => mode === value);
95658
96142
  }
@@ -100749,19 +101233,31 @@ const CANONICAL_TO_VIBE_TOOL_NAMES = {
100749
101233
  };
100750
101234
  /**
100751
101235
  * The tables earlier rulesync versions wrote for canonical categories that
100752
- * Vibe 3.x no longer reads: `[tools.grep]` (Vibe warns that it "has no
100753
- * effect") and `[tools.task]` for the subagent tool v3.0.0 renamed to
100754
- * `agents.*`. A regenerate that configures the category removes them — and
101236
+ * Vibe 3.x does not read: `[tools.grep]` (Vibe warns that it "has no
101237
+ * effect"), `[tools.task]` for the subagent tool v3.0.0 renamed to
101238
+ * `agents.*`, and `[tools.external_directory]` — Vibe has no such tool, and
101239
+ * its unused-settings check skips unknown tool names as custom/MCP tools, so
101240
+ * that deny was silently inert. A regenerate that configures the category removes them — and
100755
101241
  * the matching `disabled_tools` entry — instead of leaving an inert table.
100756
101242
  * Import still reads them (see {@link VIBE_TO_CANONICAL_TOOL_NAMES}), so a
100757
101243
  * Vibe 2.x config is lifted into the canonical categories rather than lost.
100758
101244
  */
100759
101245
  const VIBE_RETIRED_TOOL_TABLES = {
100760
101246
  grep: "grep",
100761
- agent: "task"
101247
+ agent: "task",
101248
+ external_directory: "external_directory"
100762
101249
  };
100763
101250
  /** The canonical category Vibe's `enable_subagents` switch expresses. */
100764
101251
  const VIBE_SUBAGENT_CATEGORY = "agent";
101252
+ /** The canonical category Vibe's top-level `outside_workdir` setting expresses. */
101253
+ const VIBE_OUTSIDE_WORKDIR_CATEGORY = "external_directory";
101254
+ /**
101255
+ * The values of Vibe's `outside_workdir` (v3.0.3+). There is no allow value:
101256
+ * access outside the working directory is granted per path through a tool's
101257
+ * `allowlist` (`vibe-path:...`) or `--add-dir`.
101258
+ * @see https://github.com/mistralai/mistral-vibe/blob/v3.0.3/README.md
101259
+ */
101260
+ const VIBE_OUTSIDE_WORKDIR_VALUES = /* @__PURE__ */ new Set(["ask", "deny"]);
100765
101261
  const VIBE_TO_CANONICAL_TOOL_NAMES = {
100766
101262
  bash: "bash",
100767
101263
  read_file: "read",
@@ -100837,7 +101333,8 @@ const CANONICAL_PERMISSION_CATEGORIES = /* @__PURE__ */ new Set([
100837
101333
  "grep",
100838
101334
  "glob",
100839
101335
  "notebookedit",
100840
- "agent"
101336
+ "agent",
101337
+ "external_directory"
100841
101338
  ]);
100842
101339
  var VibePermissions = class VibePermissions extends ToolPermissions {
100843
101340
  toml;
@@ -100918,7 +101415,11 @@ var VibePermissions = class VibePermissions extends ToolPermissions {
100918
101415
  rules: permission[VIBE_SUBAGENT_CATEGORY],
100919
101416
  logger
100920
101417
  });
100921
- const tableCategories = Object.entries(permission).filter(([category]) => category !== VIBE_SUBAGENT_CATEGORY);
101418
+ const outsideWorkdirPatch = resolveOutsideWorkdirPatch({
101419
+ rules: permission[VIBE_OUTSIDE_WORKDIR_CATEGORY],
101420
+ logger
101421
+ });
101422
+ const tableCategories = Object.entries(permission).filter(([category]) => category !== VIBE_SUBAGENT_CATEGORY && category !== VIBE_OUTSIDE_WORKDIR_CATEGORY);
100922
101423
  for (const [category, rules] of tableCategories) {
100923
101424
  const vibeToolNames = resolveNames(category);
100924
101425
  if (vibeToolNames === void 0) {
@@ -100992,7 +101493,8 @@ var VibePermissions = class VibePermissions extends ToolPermissions {
100992
101493
  enabledTools
100993
101494
  }),
100994
101495
  disabled_tools: disabledTools.size > 0 ? [...disabledTools].toSorted() : void 0,
100995
- ...enableSubagentsPatch
101496
+ ...enableSubagentsPatch,
101497
+ ...outsideWorkdirPatch
100996
101498
  },
100997
101499
  filePath
100998
101500
  }),
@@ -101036,6 +101538,8 @@ var VibePermissions = class VibePermissions extends ToolPermissions {
101036
101538
  }
101037
101539
  for (const category of silencedCategories) ensurePermission(permission, category)["*"] = "deny";
101038
101540
  if (this.toml.enable_subagents === false) ensurePermission(permission, VIBE_SUBAGENT_CATEGORY)["*"] = "deny";
101541
+ const outsideWorkdir = this.toml.outside_workdir;
101542
+ if (typeof outsideWorkdir === "string" && VIBE_OUTSIDE_WORKDIR_VALUES.has(outsideWorkdir)) ensurePermission(permission, VIBE_OUTSIDE_WORKDIR_CATEGORY)["*"] = outsideWorkdir === "deny" ? "deny" : "ask";
101039
101543
  warnOnUnresolvableDisabledTools({
101040
101544
  entries: disabledToolEntries,
101041
101545
  tools: this.toml.tools
@@ -101112,7 +101616,7 @@ function retireLegacyToolTables({ permission, tools, disabledTools, logger }) {
101112
101616
  }
101113
101617
  if (disabledTools.delete(legacyToolName)) removed.push(`disabled_tools entry '${legacyToolName}'`);
101114
101618
  }
101115
- 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.`);
101619
+ 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.`);
101116
101620
  }
101117
101621
  /**
101118
101622
  * Report the rules of a category that has no Vibe tool table. `grep` gets its
@@ -101148,6 +101652,30 @@ function resolveEnableSubagentsPatch({ rules, logger }) {
101148
101652
  return { enable_subagents: action === "deny" ? false : void 0 };
101149
101653
  }
101150
101654
  /**
101655
+ * Translate the canonical `external_directory` category into Vibe's top-level
101656
+ * `outside_workdir` setting (v3.0.3+), which decides whether a file or shell
101657
+ * tool call reaching outside the working directory prompts (`"ask"`, the
101658
+ * default) or is refused with a tool error (`"deny"`). A `*` deny or ask writes
101659
+ * that value; a `*` allow has no Vibe value, so it is warned about and the key
101660
+ * is removed, leaving Vibe's default prompt. Without a `*` rule the key is left
101661
+ * as the file has it. Pattern rules are skipped with a warning: path grants go
101662
+ * in a tool's `allowlist` (`vibe-path:...`) instead.
101663
+ * @see https://github.com/mistralai/mistral-vibe/blob/v3.0.3/README.md
101664
+ * @see https://github.com/mistralai/mistral-vibe/blob/v3.0.3/vibe/app_server/_unified_permissions.py
101665
+ */
101666
+ function resolveOutsideWorkdirPatch({ rules, logger }) {
101667
+ if (rules === void 0) return {};
101668
+ const patternCount = Object.keys(rules).filter((pattern) => pattern !== "*").length;
101669
+ 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.`);
101670
+ const action = rules["*"];
101671
+ if (action === void 0) return {};
101672
+ if (action === "allow") {
101673
+ 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.`);
101674
+ return { outside_workdir: void 0 };
101675
+ }
101676
+ return { outside_workdir: action };
101677
+ }
101678
+ /**
101151
101679
  * Apply the Vibe-scoped override's per-tool `sensitive_patterns` (patterns that
101152
101680
  * escalate to ASK even when the base permission is ALWAYS). rulesync owns this
101153
101681
  * list for any category the override names: a present list is set, an empty