rulesync 16.24.1 → 16.26.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.
@@ -444,34 +444,34 @@ const isFeatureValueEnabled = (value) => {
444
444
  const parseCommaSeparatedList = (value) => value.split(",").map((s) => s.trim()).filter(Boolean);
445
445
  //#endregion
446
446
  //#region src/constants/rulesync-paths.ts
447
- const { join: join$304 } = node_path.posix;
447
+ const { join: join$305 } = node_path.posix;
448
448
  const RULESYNC_CONFIG_RELATIVE_FILE_PATH = "rulesync.jsonc";
449
449
  const RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH = "rulesync.local.jsonc";
450
450
  const RULESYNC_RELATIVE_DIR_PATH = ".rulesync";
451
451
  const RULES_FEATURE_SUBDIR = "rules";
452
- const CURATED_RULES_FEATURE_SUBDIR = join$304(RULES_FEATURE_SUBDIR, ".curated");
452
+ const CURATED_RULES_FEATURE_SUBDIR = join$305(RULES_FEATURE_SUBDIR, ".curated");
453
453
  const COMMANDS_FEATURE_SUBDIR = "commands";
454
454
  const SUBAGENTS_FEATURE_SUBDIR = "subagents";
455
455
  const CHECKS_FEATURE_SUBDIR = "checks";
456
456
  const SKILLS_FEATURE_SUBDIR = "skills";
457
- const CURATED_SKILLS_FEATURE_SUBDIR = join$304(SKILLS_FEATURE_SUBDIR, ".curated");
458
- const RULESYNC_RULES_RELATIVE_DIR_PATH = join$304(RULESYNC_RELATIVE_DIR_PATH, RULES_FEATURE_SUBDIR);
459
- const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$304(RULESYNC_RELATIVE_DIR_PATH, CURATED_RULES_FEATURE_SUBDIR);
460
- const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$304(RULESYNC_RELATIVE_DIR_PATH, COMMANDS_FEATURE_SUBDIR);
461
- const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$304(RULESYNC_RELATIVE_DIR_PATH, SUBAGENTS_FEATURE_SUBDIR);
462
- const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$304(RULESYNC_RELATIVE_DIR_PATH, CHECKS_FEATURE_SUBDIR);
463
- const RULESYNC_MCP_RELATIVE_FILE_PATH = join$304(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
464
- const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$304(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
465
- const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$304(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
466
- join$304(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
467
- const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$304(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
468
- const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$304(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
457
+ const CURATED_SKILLS_FEATURE_SUBDIR = join$305(SKILLS_FEATURE_SUBDIR, ".curated");
458
+ const RULESYNC_RULES_RELATIVE_DIR_PATH = join$305(RULESYNC_RELATIVE_DIR_PATH, RULES_FEATURE_SUBDIR);
459
+ const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$305(RULESYNC_RELATIVE_DIR_PATH, CURATED_RULES_FEATURE_SUBDIR);
460
+ const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$305(RULESYNC_RELATIVE_DIR_PATH, COMMANDS_FEATURE_SUBDIR);
461
+ const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$305(RULESYNC_RELATIVE_DIR_PATH, SUBAGENTS_FEATURE_SUBDIR);
462
+ const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$305(RULESYNC_RELATIVE_DIR_PATH, CHECKS_FEATURE_SUBDIR);
463
+ const RULESYNC_MCP_RELATIVE_FILE_PATH = join$305(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
464
+ const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$305(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
465
+ const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$305(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
466
+ join$305(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
467
+ const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$305(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
468
+ const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$305(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
469
469
  const RULESYNC_AIIGNORE_FILE_NAME = ".aiignore";
470
- const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$304(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
470
+ const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$305(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
471
471
  const RULESYNC_IGNORE_RELATIVE_FILE_PATH = ".rulesyncignore";
472
472
  const RULESYNC_OVERVIEW_FILE_NAME = "overview.md";
473
- const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$304(RULESYNC_RELATIVE_DIR_PATH, SKILLS_FEATURE_SUBDIR);
474
- const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$304(RULESYNC_RELATIVE_DIR_PATH, CURATED_SKILLS_FEATURE_SUBDIR);
473
+ const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$305(RULESYNC_RELATIVE_DIR_PATH, SKILLS_FEATURE_SUBDIR);
474
+ const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$305(RULESYNC_RELATIVE_DIR_PATH, CURATED_SKILLS_FEATURE_SUBDIR);
475
475
  const RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync.lock";
476
476
  const RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync-npm.lock.json";
477
477
  const RULESYNC_MCP_FILE_NAME = "mcp.jsonc";
@@ -738,7 +738,8 @@ const hooksProcessorToolTargetTuple = [
738
738
  "vibe",
739
739
  "qwencode",
740
740
  "reasonix",
741
- "grokcli"
741
+ "grokcli",
742
+ "zcode"
742
743
  ];
743
744
  const permissionsProcessorToolTargetTuple = [
744
745
  "amp",
@@ -2418,6 +2419,7 @@ const ConfigParamsSchema = zod_mini.z.object({
2418
2419
  simulateCommands: (0, zod_mini.optional)(zod_mini.z.boolean()),
2419
2420
  simulateSubagents: (0, zod_mini.optional)(zod_mini.z.boolean()),
2420
2421
  simulateSkills: (0, zod_mini.optional)(zod_mini.z.boolean()),
2422
+ preserveUnownedHooks: (0, zod_mini.optional)(zod_mini.z.boolean()),
2421
2423
  deriveSubprojectPathFromGlobs: (0, zod_mini.optional)(zod_mini.z.boolean()),
2422
2424
  flattenedCommandNaming: (0, zod_mini.optional)(FlattenedCommandNamingSchema),
2423
2425
  /**
@@ -2566,6 +2568,7 @@ var Config = class Config {
2566
2568
  simulateCommands;
2567
2569
  simulateSubagents;
2568
2570
  simulateSkills;
2571
+ preserveUnownedHooks;
2569
2572
  deriveSubprojectPathFromGlobs;
2570
2573
  flattenedCommandNaming;
2571
2574
  language;
@@ -2592,7 +2595,7 @@ var Config = class Config {
2592
2595
  inputRoots;
2593
2596
  configFilePath;
2594
2597
  sources;
2595
- constructor({ outputRoots, targets, features, verbose, delete: isDelete, global, silent, simulateCommands, simulateSubagents, simulateSkills, deriveSubprojectPathFromGlobs, flattenedCommandNaming, language, gitignoreTargetsOnly, gitignoreDestination, dryRun, check, inputRoot, inputRoots, configFilePath, sources, configFileTargets }) {
2598
+ constructor({ outputRoots, targets, features, verbose, delete: isDelete, global, silent, simulateCommands, simulateSubagents, simulateSkills, preserveUnownedHooks, deriveSubprojectPathFromGlobs, flattenedCommandNaming, language, gitignoreTargetsOnly, gitignoreDestination, dryRun, check, inputRoot, inputRoots, configFilePath, sources, configFileTargets }) {
2596
2599
  assertTargetsFeaturesExclusive({
2597
2600
  targets,
2598
2601
  features
@@ -2624,6 +2627,7 @@ var Config = class Config {
2624
2627
  this.simulateCommands = simulateCommands ?? false;
2625
2628
  this.simulateSubagents = simulateSubagents ?? false;
2626
2629
  this.simulateSkills = simulateSkills ?? false;
2630
+ this.preserveUnownedHooks = preserveUnownedHooks ?? false;
2627
2631
  this.deriveSubprojectPathFromGlobs = deriveSubprojectPathFromGlobs ?? false;
2628
2632
  this.flattenedCommandNaming = flattenedCommandNaming ?? "basename";
2629
2633
  this.language = language;
@@ -2802,6 +2806,9 @@ var Config = class Config {
2802
2806
  getSimulateCommands() {
2803
2807
  return this.simulateCommands;
2804
2808
  }
2809
+ getPreserveUnownedHooks() {
2810
+ return this.preserveUnownedHooks;
2811
+ }
2805
2812
  getFlattenedCommandNaming() {
2806
2813
  return this.flattenedCommandNaming;
2807
2814
  }
@@ -2881,6 +2888,7 @@ const getDefaults = () => ({
2881
2888
  simulateCommands: false,
2882
2889
  simulateSubagents: false,
2883
2890
  simulateSkills: false,
2891
+ preserveUnownedHooks: false,
2884
2892
  deriveSubprojectPathFromGlobs: false,
2885
2893
  flattenedCommandNaming: "basename",
2886
2894
  gitignoreTargetsOnly: true,
@@ -2929,6 +2937,7 @@ const mergeConfigs = (baseConfig, localConfig) => {
2929
2937
  simulateCommands: localConfig.simulateCommands ?? baseConfig.simulateCommands,
2930
2938
  simulateSubagents: localConfig.simulateSubagents ?? baseConfig.simulateSubagents,
2931
2939
  simulateSkills: localConfig.simulateSkills ?? baseConfig.simulateSkills,
2940
+ preserveUnownedHooks: localConfig.preserveUnownedHooks ?? baseConfig.preserveUnownedHooks,
2932
2941
  deriveSubprojectPathFromGlobs: localConfig.deriveSubprojectPathFromGlobs ?? baseConfig.deriveSubprojectPathFromGlobs,
2933
2942
  flattenedCommandNaming: localConfig.flattenedCommandNaming ?? baseConfig.flattenedCommandNaming,
2934
2943
  language: localConfig.language ?? baseConfig.language,
@@ -3152,6 +3161,11 @@ var ConfigResolver = class {
3152
3161
  file: configByFile.simulateSkills,
3153
3162
  fallback: getDefaults().simulateSkills
3154
3163
  }),
3164
+ preserveUnownedHooks: pick({
3165
+ cli: void 0,
3166
+ file: configByFile.preserveUnownedHooks,
3167
+ fallback: getDefaults().preserveUnownedHooks
3168
+ }),
3155
3169
  deriveSubprojectPathFromGlobs: pick({
3156
3170
  cli: deriveSubprojectPathFromGlobs,
3157
3171
  file: configByFile.deriveSubprojectPathFromGlobs,
@@ -4711,6 +4725,48 @@ const KIMI_CODE_NATIVE_HOOK_EVENTS = [
4711
4725
  ];
4712
4726
  const KIMI_CODE_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_KIMI_CODE_EVENT_NAMES).map(([canonical, kimiCode]) => [kimiCode, canonical]));
4713
4727
  /**
4728
+ * Hook events supported by ZCode.
4729
+ *
4730
+ * ZCode's configuration-file hooks expose exactly seven PascalCase events, all
4731
+ * of which have a clean canonical equivalent: `SessionStart`, `PreToolUse`,
4732
+ * `PostToolUse`, `PostToolUseFailure`, `PermissionRequest`, `Stop`, and
4733
+ * `UserPromptSubmit` ← `beforeSubmitPrompt`. An optional matcher is honored on
4734
+ * all of them except `UserPromptSubmit` and `Stop`, which expose no value to
4735
+ * match against. ZCode reads the matcher in one of two ways: a value made up
4736
+ * solely of letters, digits, underscores and `|` is an exact name list
4737
+ * (`Write|Edit` matches those two tool names, not the regex alternation), and
4738
+ * any other character makes the whole value a case-sensitive JavaScript regex.
4739
+ * Rulesync passes matchers through verbatim, so an authored value keeps
4740
+ * whichever reading ZCode gives it — worth knowing before escaping one.
4741
+ * Configuration hooks are read only from the user config
4742
+ * `~/.zcode/cli/config.json` (workspace config hooks are never executed) and
4743
+ * additionally require `hooks.enabled: true` to run.
4744
+ *
4745
+ * ZCode also accepts a native `process` hook type (an argv run without a
4746
+ * shell) which has no canonical equivalent; see ZcodeHooks.
4747
+ *
4748
+ * @see https://zcode.z.ai/en/docs
4749
+ */
4750
+ const ZCODE_HOOK_EVENTS = [
4751
+ "sessionStart",
4752
+ "beforeSubmitPrompt",
4753
+ "preToolUse",
4754
+ "postToolUse",
4755
+ "postToolUseFailure",
4756
+ "permissionRequest",
4757
+ "stop"
4758
+ ];
4759
+ const CANONICAL_TO_ZCODE_EVENT_NAMES = {
4760
+ sessionStart: "SessionStart",
4761
+ beforeSubmitPrompt: "UserPromptSubmit",
4762
+ preToolUse: "PreToolUse",
4763
+ postToolUse: "PostToolUse",
4764
+ postToolUseFailure: "PostToolUseFailure",
4765
+ permissionRequest: "PermissionRequest",
4766
+ stop: "Stop"
4767
+ };
4768
+ const ZCODE_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_ZCODE_EVENT_NAMES).map(([k, v]) => [v, k]));
4769
+ /**
4714
4770
  * Hook events supported by Hermes Agent's native Shell Hooks system.
4715
4771
  *
4716
4772
  * Hermes validates hook events against a fixed `VALID_HOOKS` set — 37 entries as
@@ -4862,6 +4918,7 @@ const HooksConfigSchema = zod_mini.z.looseObject({
4862
4918
  reasonix: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
4863
4919
  grokcli: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
4864
4920
  "kimi-code": zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
4921
+ zcode: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
4865
4922
  qwencode: zod_mini.z.optional(zod_mini.z.looseObject({
4866
4923
  hooks: zod_mini.z.optional(hooksRecordSchema),
4867
4924
  disableAllHooks: zod_mini.z.optional(zod_mini.z.boolean())
@@ -6852,12 +6909,37 @@ const DeepagentsStartupOverrideSchema = zod_mini.z.looseObject({
6852
6909
  read_project_dotenv: zod_mini.z.optional(zod_mini.z.boolean())
6853
6910
  });
6854
6911
  /**
6912
+ * deepagents-cli's Python extension gate under `[extensions]` in
6913
+ * `~/.deepagents/config.toml`. `discover_extensions` auto-loads `*.py` from the
6914
+ * user's `~/.deepagents/extensions/` and from the checked-out project's
6915
+ * `<root>/.deepagents/extensions/`, so these two keys decide whether
6916
+ * repository-authored Python is imported into the agent process at all —
6917
+ * the same class of knob as `[startup].mode`, and one that ships defaulting to
6918
+ * a prompt a user can accept. Loose so a key added upstream passes through
6919
+ * verbatim.
6920
+ *
6921
+ * `extensions.extra_paths` is deliberately absent: it names machine-local files
6922
+ * and directories, which do not belong in a committed
6923
+ * `.rulesync/permissions.jsonc`, and it only ever widens what loads.
6924
+ *
6925
+ * @see https://docs.langchain.com/oss/deepagents/code/configuration
6926
+ */
6927
+ const DeepagentsExtensionsOverrideSchema = zod_mini.z.looseObject({
6928
+ enabled: zod_mini.z.optional(zod_mini.z.boolean()),
6929
+ trust: zod_mini.z.optional(zod_mini.z.enum([
6930
+ "ask",
6931
+ "always",
6932
+ "never"
6933
+ ]))
6934
+ });
6935
+ /**
6855
6936
  * The `[shell].allow_list` array itself is rulesync-owned, driven by the
6856
6937
  * shared `permission.bash` block, so it has no key here.
6857
6938
  */
6858
6939
  const DeepagentsPermissionsOverrideSchema = zod_mini.z.looseObject({
6859
6940
  permission: zod_mini.z.optional(ToolScopedPermissionSchema),
6860
- startup: zod_mini.z.optional(DeepagentsStartupOverrideSchema)
6941
+ startup: zod_mini.z.optional(DeepagentsStartupOverrideSchema),
6942
+ extensions: zod_mini.z.optional(DeepagentsExtensionsOverrideSchema)
6861
6943
  });
6862
6944
  /**
6863
6945
  * The actions Junie's allowlist accepts. Verified against the shipped Junie
@@ -7868,8 +7950,9 @@ const RulesyncRuleFrontmatterSchema = zod_mini.z.object({
7868
7950
  agentsmd: zod_mini.z.optional(zod_mini.z.looseObject({ subprojectPath: zod_mini.z.optional(zod_mini.z.string()) })),
7869
7951
  claudecode: zod_mini.z.optional(zod_mini.z.looseObject({ paths: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())) })),
7870
7952
  codebuddy: zod_mini.z.optional(zod_mini.z.looseObject({
7871
- paths: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
7953
+ paths: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())])),
7872
7954
  alwaysApply: zod_mini.z.optional(zod_mini.z.boolean()),
7955
+ enabled: zod_mini.z.optional(zod_mini.z.boolean()),
7873
7956
  description: zod_mini.z.optional(zod_mini.z.string())
7874
7957
  })),
7875
7958
  cursor: zod_mini.z.optional(zod_mini.z.looseObject({
@@ -9162,6 +9245,11 @@ const RulesyncSkillFrontmatterSchema = zod_mini.z.looseObject({
9162
9245
  license: zod_mini.z.optional(zod_mini.z.string()),
9163
9246
  compatibility: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.looseObject({})])),
9164
9247
  metadata: zod_mini.z.optional(zod_mini.z.looseObject({}))
9248
+ })),
9249
+ zcode: zod_mini.z.optional(zod_mini.z.looseObject({
9250
+ when_to_use: zod_mini.z.optional(zod_mini.z.string()),
9251
+ license: zod_mini.z.optional(zod_mini.z.string()),
9252
+ metadata: zod_mini.z.optional(zod_mini.z.looseObject({}))
9165
9253
  }))
9166
9254
  });
9167
9255
  /**
@@ -12412,14 +12500,15 @@ const KIMI_CODE_CONFIG_DECLARATION = {
12412
12500
  };
12413
12501
  /**
12414
12502
  * ZCode's settings file, which also carries model/theme/permission keys
12415
- * rulesync does not own. The workspace copy (`<project>/.zcode/config.json`)
12416
- * and the user copy (`~/.zcode/cli/config.json`) are the same file with the
12417
- * same owners, so the declaration is written once and shared a policy edit
12418
- * cannot land on one scope only. `mcp` is owned as a whole key because the
12419
- * writer recomputes it from the existing file (non-`servers` siblings carried
12420
- * over) before applying the patch.
12421
- */
12422
- const ZCODE_CONFIG_DECLARATION = {
12503
+ * rulesync does not own. Both copies are the user's primary ZCode config, so
12504
+ * every writer refuses to read-modify-write a file it could not parse rather
12505
+ * than replacing it with generated output. `mcp` and `hooks` are owned as
12506
+ * whole keys because their writers recompute each from the existing file
12507
+ * (non-owned siblings carried over) before applying the patch. ZCode never
12508
+ * executes workspace config hooks, so the workspace copy is declared with
12509
+ * `mcp` alone and the user copy adds `hooks`.
12510
+ */
12511
+ const ZCODE_WORKSPACE_CONFIG_DECLARATION = {
12423
12512
  format: "json",
12424
12513
  invalidRootPolicy: "error",
12425
12514
  features: { mcp: {
@@ -12427,6 +12516,16 @@ const ZCODE_CONFIG_DECLARATION = {
12427
12516
  ownedKeys: ["mcp"]
12428
12517
  } }
12429
12518
  };
12519
+ const ZCODE_USER_CONFIG_DECLARATION = {
12520
+ ...ZCODE_WORKSPACE_CONFIG_DECLARATION,
12521
+ features: {
12522
+ ...ZCODE_WORKSPACE_CONFIG_DECLARATION.features,
12523
+ hooks: {
12524
+ kind: "replace-owned-keys",
12525
+ ownedKeys: ["hooks"]
12526
+ }
12527
+ }
12528
+ };
12430
12529
  /**
12431
12530
  * What the two Claude Code settings files have in common: both are plain JSON,
12432
12531
  * and both validate against the one published schema, which the gateway
@@ -12684,8 +12783,8 @@ const SHARED_CONFIG_OWNERSHIP = {
12684
12783
  ownedKeys: ["mcp_servers", "schema_version"]
12685
12784
  } }
12686
12785
  },
12687
- ".zcode/config.json": ZCODE_CONFIG_DECLARATION,
12688
- ".zcode/cli/config.json": ZCODE_CONFIG_DECLARATION,
12786
+ ".zcode/config.json": ZCODE_WORKSPACE_CONFIG_DECLARATION,
12787
+ ".zcode/cli/config.json": ZCODE_USER_CONFIG_DECLARATION,
12689
12788
  ".kiro/agents/default.json": {
12690
12789
  format: "json",
12691
12790
  features: {
@@ -17733,6 +17832,20 @@ const ROO_MCP_FILE_NAME = "mcp.json";
17733
17832
  */
17734
17833
  const ROO_MODE_SLUG_PATTERN = /^[a-zA-Z0-9-]+$/;
17735
17834
  /**
17835
+ * `~/.roo/rules/AGENTS.md` — where the root rule goes in global scope.
17836
+ *
17837
+ * Roo/Zoo Code discover the agent-rules files (`AGENTS.md`, `AGENT.md`,
17838
+ * `AGENTS.local.md`) in the **workspace only**: `loadAllAgentRulesFiles` reads
17839
+ * `cwd`, plus subdirectories holding a `.roo` folder when `enableSubfolderRules`
17840
+ * is on. There is no home-directory branch, so a `~/AGENTS.md` is never read.
17841
+ * The global rules directory `~/.roo/rules/` *is* loaded, so the root rule is
17842
+ * written there alongside the non-root files rather than to an unread path.
17843
+ * The basename is kept so the file still reads as the root overview.
17844
+ * @see https://github.com/Zoo-Code-Org/Zoo-Code/blob/main/src/core/prompts/sections/custom-instructions.ts
17845
+ * @see https://github.com/Zoo-Code-Org/Zoo-Code/blob/main/src/services/roo-config/index.ts
17846
+ */
17847
+ const ROO_GLOBAL_ROOT_RULE_FILE_NAME = "AGENTS.md";
17848
+ /**
17736
17849
  * `.roo/rules-{mode}/` — the mode-specific rule directory Roo/Zoo Code load
17737
17850
  * INSTEAD of `.roo/rules/` while that mode is active. The relative path is the
17738
17851
  * same in global scope, where it resolves under `~/.roo/`.
@@ -18349,6 +18462,8 @@ const ZCODE_SKILLS_DIR_PATH = (0, node_path.join)(ZCODE_DIR, "skills");
18349
18462
  const ZCODE_CONFIG_FILE_NAME = "config.json";
18350
18463
  const ZCODE_GLOBAL_CONFIG_DIR_PATH = (0, node_path.join)(ZCODE_DIR, "cli");
18351
18464
  const ZCODE_MCP_SERVERS_KEY = "servers";
18465
+ const ZCODE_HOOKS_CONFIG_KEY = "hooks";
18466
+ const ZCODE_HOOKS_EVENTS_KEY = "events";
18352
18467
  const ZCODE_AGENTS_DIR_PATH = (0, node_path.join)(ZCODE_DIR, "agents");
18353
18468
  //#endregion
18354
18469
  //#region src/features/commands/zcode-command.ts
@@ -19221,19 +19336,124 @@ function generateAmpPluginCode({ config, supportedEvents, eventMap }) {
19221
19336
  lines.push("}", "");
19222
19337
  return lines.join("\n");
19223
19338
  }
19339
+ /**
19340
+ * Name of the ownership record written next to the hooks destination it
19341
+ * describes (e.g. `.claude/.rulesync-hooks-lock.json`). Only written when hook
19342
+ * preservation is enabled, so a project that never opts in never sees it.
19343
+ */
19344
+ const HOOKS_OWNERSHIP_LOCK_FILE_NAME = ".rulesync-hooks-lock.json";
19345
+ const OwnedHookRefSchema = zod_mini.z.object({
19346
+ event: zod_mini.z.string(),
19347
+ matcher: zod_mini.z.optional(zod_mini.z.string()),
19348
+ identity: zod_mini.z.string()
19349
+ });
19350
+ const HooksOwnershipLockSchema = zod_mini.z.object({
19351
+ lockfileVersion: zod_mini.z.number(),
19352
+ owned: zod_mini.z.array(OwnedHookRefSchema)
19353
+ });
19354
+ /**
19355
+ * The comparable form of a hook entry: identical keys mean "the same hook, in
19356
+ * the same place". Encoded as JSON so no separator can collide with a matcher
19357
+ * or an identity that happens to contain one.
19358
+ */
19359
+ function ownedHookKey({ event, matcher, identity }) {
19360
+ return JSON.stringify([
19361
+ event,
19362
+ matcher ?? null,
19363
+ identity
19364
+ ]);
19365
+ }
19366
+ /**
19367
+ * Read the previous run's owned set. Anything unreadable — missing, empty,
19368
+ * malformed, or written by a different lock version — yields an empty set,
19369
+ * which makes every existing handler look third-party and therefore preserved.
19370
+ */
19371
+ function parseHooksOwnershipLock(content) {
19372
+ if (content === null || content.trim() === "") return /* @__PURE__ */ new Set();
19373
+ let parsed;
19374
+ try {
19375
+ parsed = JSON.parse(content);
19376
+ } catch {
19377
+ return /* @__PURE__ */ new Set();
19378
+ }
19379
+ const result = HooksOwnershipLockSchema.safeParse(parsed);
19380
+ if (!result.success || result.data.lockfileVersion !== 1) return /* @__PURE__ */ new Set();
19381
+ return new Set(result.data.owned.map((ref) => ownedHookKey(ref)));
19382
+ }
19383
+ function compareOwnedHookRefs(a, b) {
19384
+ const left = ownedHookKey(a);
19385
+ const right = ownedHookKey(b);
19386
+ if (left === right) return 0;
19387
+ return left < right ? -1 : 1;
19388
+ }
19389
+ /** Serialize the owned set in a stable order so regenerating produces no diff. */
19390
+ function serializeHooksOwnershipLock(owned) {
19391
+ return `${JSON.stringify({
19392
+ lockfileVersion: 1,
19393
+ owned: [...owned].toSorted(compareOwnedHookRefs)
19394
+ }, null, 2)}\n`;
19395
+ }
19396
+ /**
19397
+ * The ownership record itself. It is a rulesync by-product rather than tool
19398
+ * configuration, so it is deletable: dropping the target should take it along.
19399
+ */
19400
+ var HooksOwnershipLockFile = class extends ToolFile {
19401
+ isDeletable() {
19402
+ return true;
19403
+ }
19404
+ validate() {
19405
+ return {
19406
+ success: true,
19407
+ error: null
19408
+ };
19409
+ }
19410
+ };
19411
+ /** Build the lock file that records what this run generated. */
19412
+ function buildHooksOwnershipLockFile({ outputRoot, relativeDirPath, owned }) {
19413
+ return new HooksOwnershipLockFile({
19414
+ outputRoot,
19415
+ relativeDirPath,
19416
+ relativeFilePath: HOOKS_OWNERSHIP_LOCK_FILE_NAME,
19417
+ fileContent: serializeHooksOwnershipLock(owned),
19418
+ validate: false
19419
+ });
19420
+ }
19224
19421
  //#endregion
19225
19422
  //#region src/features/hooks/tool-hooks.ts
19226
19423
  var ToolHooks = class extends ToolFile {
19424
+ ownedHookRefs;
19227
19425
  constructor(params) {
19228
19426
  super({
19229
19427
  ...params,
19230
19428
  validate: true
19231
19429
  });
19430
+ this.ownedHookRefs = params.ownedHookRefs;
19232
19431
  if (params.validate) {
19233
19432
  const result = this.validate();
19234
19433
  if (!result.success) throw result.error;
19235
19434
  }
19236
19435
  }
19436
+ /**
19437
+ * The ownership record for this destination, or nothing when preservation is
19438
+ * off — in which case rulesync owns the whole list and needs no record.
19439
+ */
19440
+ getOwnershipLockFiles() {
19441
+ if (this.ownedHookRefs === void 0) return [];
19442
+ return [buildHooksOwnershipLockFile({
19443
+ outputRoot: this.getOutputRoot(),
19444
+ relativeDirPath: this.getRelativeDirPath(),
19445
+ owned: this.ownedHookRefs
19446
+ })];
19447
+ }
19448
+ /**
19449
+ * Whether the adapter can keep handlers it did not generate. Destinations
19450
+ * rulesync owns outright (plugin bundles) must answer `false`: there is no
19451
+ * third party writing into them, and preserving there would only make
19452
+ * removals impossible.
19453
+ */
19454
+ static supportsPreserveUnowned() {
19455
+ return false;
19456
+ }
19237
19457
  static getSettablePaths(_options) {
19238
19458
  throw new Error("Please implement this method in the subclass.");
19239
19459
  }
@@ -20670,6 +20890,292 @@ var AugmentcodeHooks = class AugmentcodeHooks extends ToolHooks {
20670
20890
  }
20671
20891
  };
20672
20892
  //#endregion
20893
+ //#region src/features/hooks/preserve-unowned-hook-commands.ts
20894
+ /**
20895
+ * Read the `hooks` value from a destination JSON file.
20896
+ *
20897
+ * `malformed` separates "the file has no hooks" from "the file could not be
20898
+ * read", so a caller that is about to preserve content can say out loud that
20899
+ * it is replacing a file it failed to parse.
20900
+ */
20901
+ function parseExistingHooksValue(existingContent) {
20902
+ if (existingContent.trim() === "") return {
20903
+ hooks: void 0,
20904
+ malformed: false
20905
+ };
20906
+ try {
20907
+ const parsed = JSON.parse(existingContent);
20908
+ return {
20909
+ hooks: isPlainObject$1(parsed) ? parsed.hooks : void 0,
20910
+ malformed: false
20911
+ };
20912
+ } catch {
20913
+ return {
20914
+ hooks: void 0,
20915
+ malformed: true
20916
+ };
20917
+ }
20918
+ }
20919
+ /**
20920
+ * Produce the destination hooks list, optionally keeping handlers rulesync does
20921
+ * not own.
20922
+ *
20923
+ * Ownership is decided by `previouslyOwned` — the identities recorded in the
20924
+ * destination's ownership lock on the previous run — and never by guessing from
20925
+ * the command text. That is what makes removal work: a handler rulesync wrote
20926
+ * before and no longer generates is retracted, while a handler rulesync has
20927
+ * never written is left alone. Without a lock (the first run after opting in)
20928
+ * nothing is retracted, which is the non-destructive direction.
20929
+ *
20930
+ * Callers must pass the destination's shape. Plugin destinations are fully
20931
+ * owned by rulesync and must not call this.
20932
+ */
20933
+ function mergeGeneratedHookLists({ existingContent, generatedHooks, shape, preserveUnowned, previouslyOwned, logger }) {
20934
+ const owned = collectOwnedRefs({
20935
+ generatedHooks,
20936
+ shape
20937
+ });
20938
+ if (!preserveUnowned) return {
20939
+ hooks: generatedHooks,
20940
+ owned
20941
+ };
20942
+ const { hooks: existingHooks, malformed } = parseExistingHooksValue(existingContent);
20943
+ if (malformed) logger?.warn("Replacing hooks wholesale: the existing file is not valid JSON, so no third-party hook in it can be preserved.");
20944
+ return {
20945
+ hooks: preserveUnownedHookCommands({
20946
+ existingHooks,
20947
+ generatedHooks,
20948
+ shape,
20949
+ owned,
20950
+ previouslyOwned: previouslyOwned ?? /* @__PURE__ */ new Set(),
20951
+ logger
20952
+ }),
20953
+ owned
20954
+ };
20955
+ }
20956
+ /**
20957
+ * Merge the destination's existing handlers into the generated set.
20958
+ *
20959
+ * Each existing handler falls into exactly one of three cases:
20960
+ * - this run generates it → the generated copy stands, the existing one is dropped;
20961
+ * - a previous run generated it and this one does not → it is retracted, with a warning;
20962
+ * - neither → it is unowned, and kept.
20963
+ */
20964
+ function preserveUnownedHookCommands({ existingHooks, generatedHooks, shape, owned, previouslyOwned, logger }) {
20965
+ const result = cloneGeneratedHooks({
20966
+ generatedHooks,
20967
+ shape
20968
+ });
20969
+ if (!isPlainObject$1(existingHooks)) return result;
20970
+ const seen = new Set(owned.map((ref) => ownedHookKey(ref)));
20971
+ for (const [event, existingValue] of Object.entries(existingHooks)) {
20972
+ if (isPrototypePollutionKey(event)) continue;
20973
+ if (!Array.isArray(existingValue)) {
20974
+ warnSkip({
20975
+ logger,
20976
+ event,
20977
+ expected: "an array of hook entries"
20978
+ });
20979
+ continue;
20980
+ }
20981
+ const merged = shape === "matcher-groups" ? mergeMatcherGroups({
20982
+ existing: existingValue,
20983
+ generated: result[event] ?? [],
20984
+ event,
20985
+ seen,
20986
+ previouslyOwned,
20987
+ logger
20988
+ }) : mergeFlatHandlers({
20989
+ existing: existingValue,
20990
+ generated: result[event] ?? [],
20991
+ event,
20992
+ seen,
20993
+ previouslyOwned,
20994
+ logger
20995
+ });
20996
+ if (merged.length === 0) delete result[event];
20997
+ else result[event] = merged;
20998
+ }
20999
+ return result;
21000
+ }
21001
+ function cloneGeneratedHooks({ generatedHooks, shape }) {
21002
+ const result = Object.create(null);
21003
+ for (const [event, value] of Object.entries(generatedHooks)) {
21004
+ if (isPrototypePollutionKey(event) || !Array.isArray(value)) continue;
21005
+ result[event] = shape === "matcher-groups" ? value.map((group) => cloneMatcherGroup(group)) : value.map((handler) => cloneHandler(handler));
21006
+ }
21007
+ return result;
21008
+ }
21009
+ /** The identities of every handler in the generated set, in destination order. */
21010
+ function collectOwnedRefs({ generatedHooks, shape }) {
21011
+ const owned = [];
21012
+ for (const [event, value] of Object.entries(generatedHooks)) {
21013
+ if (isPrototypePollutionKey(event) || !Array.isArray(value)) continue;
21014
+ if (shape === "flat") {
21015
+ for (const handler of value) if (isPlainObject$1(handler)) owned.push({
21016
+ event,
21017
+ identity: handlerIdentity(handler)
21018
+ });
21019
+ continue;
21020
+ }
21021
+ for (const group of value) {
21022
+ if (!isMatcherGroup(group)) continue;
21023
+ const matcher = matcherKey(group.matcher);
21024
+ for (const handler of group.hooks) if (isPlainObject$1(handler)) owned.push({
21025
+ event,
21026
+ matcher,
21027
+ identity: handlerIdentity(handler)
21028
+ });
21029
+ }
21030
+ }
21031
+ return owned;
21032
+ }
21033
+ function mergeMatcherGroups({ existing, generated, event, seen, previouslyOwned, logger }) {
21034
+ const merged = [...generated];
21035
+ for (const group of existing) {
21036
+ if (!isMatcherGroup(group)) {
21037
+ warnSkip({
21038
+ logger,
21039
+ event,
21040
+ expected: "a matcher group"
21041
+ });
21042
+ continue;
21043
+ }
21044
+ const matcher = matcherKey(group.matcher);
21045
+ const preserved = group.hooks.filter((handler) => shouldPreserve({
21046
+ handler,
21047
+ event,
21048
+ matcher,
21049
+ seen,
21050
+ previouslyOwned,
21051
+ logger
21052
+ })).map((handler) => cloneHandler(handler));
21053
+ if (preserved.length === 0) continue;
21054
+ const target = merged.find((candidate) => isMatcherGroup(candidate) && matcherKey(candidate.matcher) === matcher);
21055
+ if (target !== void 0 && isMatcherGroup(target)) target.hooks.push(...preserved);
21056
+ else merged.push({
21057
+ ...omitPrototypePollutionKeys(group),
21058
+ hooks: preserved
21059
+ });
21060
+ }
21061
+ return merged;
21062
+ }
21063
+ function mergeFlatHandlers({ existing, generated, event, seen, previouslyOwned, logger }) {
21064
+ const preserved = [];
21065
+ for (const handler of existing) {
21066
+ if (isMatcherGroup(handler)) {
21067
+ warnSkip({
21068
+ logger,
21069
+ event,
21070
+ expected: "a flat handler"
21071
+ });
21072
+ continue;
21073
+ }
21074
+ if (!shouldPreserve({
21075
+ handler,
21076
+ event,
21077
+ matcher: void 0,
21078
+ seen,
21079
+ previouslyOwned,
21080
+ logger
21081
+ })) continue;
21082
+ preserved.push(cloneHandler(handler));
21083
+ }
21084
+ return [...generated, ...preserved];
21085
+ }
21086
+ function isMatcherGroup(value) {
21087
+ return isPlainObject$1(value) && Array.isArray(value.hooks);
21088
+ }
21089
+ function cloneMatcherGroup(group) {
21090
+ if (!isMatcherGroup(group)) return cloneHandler(group);
21091
+ return {
21092
+ ...group,
21093
+ hooks: group.hooks.map((handler) => cloneHandler(handler))
21094
+ };
21095
+ }
21096
+ function cloneHandler(handler) {
21097
+ return isPlainObject$1(handler) ? omitPrototypePollutionKeys(handler) : handler;
21098
+ }
21099
+ /**
21100
+ * Decide one existing handler, and remember it so an exact duplicate later in
21101
+ * the same event is not appended twice.
21102
+ */
21103
+ function shouldPreserve({ handler, event, matcher, seen, previouslyOwned, logger }) {
21104
+ if (!isPlainObject$1(handler)) {
21105
+ warnSkip({
21106
+ logger,
21107
+ event,
21108
+ expected: "a hook object"
21109
+ });
21110
+ return false;
21111
+ }
21112
+ const identity = handlerIdentity(handler);
21113
+ const key = ownedHookKey({
21114
+ event,
21115
+ matcher,
21116
+ identity
21117
+ });
21118
+ if (seen.has(key)) return false;
21119
+ if (previouslyOwned.has(key)) {
21120
+ logger?.warn(`Removing hook rulesync no longer generates on ${event}: ${identity}`);
21121
+ return false;
21122
+ }
21123
+ seen.add(key);
21124
+ logger?.warn(`Preserving unowned hook on ${event}: ${identity}`);
21125
+ return true;
21126
+ }
21127
+ /**
21128
+ * A handler's identity is its action, not its whole shape: two entries running
21129
+ * the same command are the same hook even if their timeouts differ, so the
21130
+ * generated one replaces the existing one instead of doubling it. Shapes with
21131
+ * no recognizable action fall back to their full structure, which keeps them
21132
+ * comparable across runs — the alternative, no identity at all, made them
21133
+ * accumulate on every generate.
21134
+ */
21135
+ function handlerIdentity(handler) {
21136
+ const type = typeof handler.type === "string" ? handler.type : inferredType(handler);
21137
+ switch (type) {
21138
+ case "http": return taggedIdentity("http", handler.url) ?? structuralIdentity(handler);
21139
+ case "mcp_tool": return mcpToolIdentity(handler) ?? structuralIdentity(handler);
21140
+ case "prompt":
21141
+ case "agent": return taggedIdentity(type, handler.prompt) ?? structuralIdentity(handler);
21142
+ case "function": return taggedIdentity("function", handler.name) ?? structuralIdentity(handler);
21143
+ default: return taggedIdentity("command", handler.command) ?? taggedIdentity("prompt", handler.prompt) ?? structuralIdentity(handler);
21144
+ }
21145
+ }
21146
+ function inferredType(handler) {
21147
+ return typeof handler.url === "string" ? "http" : "command";
21148
+ }
21149
+ function taggedIdentity(kind, value) {
21150
+ return typeof value === "string" && value !== "" ? `${kind}:${value}` : void 0;
21151
+ }
21152
+ function mcpToolIdentity(handler) {
21153
+ const server = typeof handler.server === "string" ? handler.server : "";
21154
+ const tool = typeof handler.tool === "string" ? handler.tool : "";
21155
+ if (server === "" && tool === "") return;
21156
+ return `mcp_tool:${server}:${tool}:${stableStringify$1(handler.input)}`;
21157
+ }
21158
+ function structuralIdentity(handler) {
21159
+ return `json:${stableStringify$1(handler)}`;
21160
+ }
21161
+ /** Key-order-independent JSON, so an identity survives a rewritten file. */
21162
+ function stableStringify$1(value) {
21163
+ if (Array.isArray(value)) return `[${value.map((entry) => stableStringify$1(entry)).join(",")}]`;
21164
+ if (isPlainObject$1(value)) return `{${Object.entries(value).filter(([key]) => !isPrototypePollutionKey(key)).toSorted(([left], [right]) => left === right ? 0 : left < right ? -1 : 1).map(([key, entry]) => `${JSON.stringify(key)}:${stableStringify$1(entry)}`).join(",")}}`;
21165
+ return JSON.stringify(value) ?? "null";
21166
+ }
21167
+ /**
21168
+ * Matcher groups are matched by the whole matcher value, not just strings: a
21169
+ * non-string matcher is a shape rulesync does not emit, and collapsing every
21170
+ * such group onto one key would merge unrelated third-party groups together.
21171
+ */
21172
+ function matcherKey(matcher) {
21173
+ return matcher === void 0 ? "" : stableStringify$1(matcher);
21174
+ }
21175
+ function warnSkip({ logger, event, expected }) {
21176
+ logger?.warn(`Skipping existing hook entry on ${event}: expected ${expected}`);
21177
+ }
21178
+ //#endregion
20673
21179
  //#region src/features/hooks/claudecode-hooks.ts
20674
21180
  const CLAUDE_CONVERTER_CONFIG = {
20675
21181
  supportedEvents: CLAUDE_HOOK_EVENTS,
@@ -20738,7 +21244,7 @@ const CLAUDE_CONVERTER_CONFIG = {
20738
21244
  commandOnly: true
20739
21245
  }]
20740
21246
  };
20741
- var ClaudecodeHooks = class extends ToolHooks {
21247
+ var ClaudecodeHooks = class ClaudecodeHooks extends ToolHooks {
20742
21248
  constructor(params) {
20743
21249
  super({
20744
21250
  ...params,
@@ -20773,7 +21279,13 @@ var ClaudecodeHooks = class extends ToolHooks {
20773
21279
  validate
20774
21280
  });
20775
21281
  }
20776
- static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
21282
+ static supportsPreserveUnowned() {
21283
+ return true;
21284
+ }
21285
+ static async getAuxiliaryFiles({ toolHooks } = {}) {
21286
+ return toolHooks instanceof ClaudecodeHooks ? toolHooks.getOwnershipLockFiles() : [];
21287
+ }
21288
+ static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, preserveUnowned = false, logger }) {
20777
21289
  const paths = this.getSettablePaths({ global });
20778
21290
  const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
20779
21291
  const existingContent = await readFileContentOrNull(filePath) ?? JSON.stringify({}, null, 2);
@@ -20784,11 +21296,20 @@ var ClaudecodeHooks = class extends ToolHooks {
20784
21296
  converterConfig: this.getConverterConfig(),
20785
21297
  logger
20786
21298
  });
21299
+ const preserving = preserveUnowned && this.supportsPreserveUnowned();
21300
+ const merged = mergeGeneratedHookLists({
21301
+ existingContent,
21302
+ generatedHooks: claudeHooks,
21303
+ shape: "matcher-groups",
21304
+ preserveUnowned: preserving,
21305
+ previouslyOwned: preserving ? parseHooksOwnershipLock(await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, HOOKS_OWNERSHIP_LOCK_FILE_NAME))) : void 0,
21306
+ logger
21307
+ });
20787
21308
  const fileContent = applySharedConfigPatch({
20788
21309
  fileKey: CLAUDE_SETTINGS_SHARED_FILE_KEY,
20789
21310
  feature: "hooks",
20790
21311
  existingContent,
20791
- patch: { hooks: claudeHooks },
21312
+ patch: { hooks: merged.hooks },
20792
21313
  filePath
20793
21314
  });
20794
21315
  return new this({
@@ -20796,6 +21317,7 @@ var ClaudecodeHooks = class extends ToolHooks {
20796
21317
  relativeDirPath: paths.relativeDirPath,
20797
21318
  relativeFilePath: paths.relativeFilePath,
20798
21319
  fileContent,
21320
+ ownedHookRefs: preserving ? merged.owned : void 0,
20799
21321
  validate
20800
21322
  });
20801
21323
  }
@@ -20853,6 +21375,14 @@ var ClaudecodePluginHooks = class extends ClaudecodeHooks {
20853
21375
  projectDirVar: "$CLAUDE_PLUGIN_ROOT"
20854
21376
  };
20855
21377
  }
21378
+ /**
21379
+ * The plugin bundle is generated in full by rulesync — nothing else writes
21380
+ * into it — so preserving there would buy nothing and cost the ability to
21381
+ * remove a hook.
21382
+ */
21383
+ static supportsPreserveUnowned() {
21384
+ return false;
21385
+ }
20856
21386
  static getSettablePaths() {
20857
21387
  return {
20858
21388
  relativeDirPath: CLAUDECODE_PLUGIN_HOOKS_DIR,
@@ -21283,21 +21813,30 @@ var CodexcliHooks = class CodexcliHooks extends ToolHooks {
21283
21813
  validate
21284
21814
  });
21285
21815
  }
21286
- static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
21816
+ static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, preserveUnowned = false, logger }) {
21287
21817
  const paths = CodexcliHooks.getSettablePaths({ global });
21818
+ const existingContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
21288
21819
  const config = rulesyncHooks.getJson();
21289
- const codexHooks = canonicalToToolHooks({
21290
- config,
21291
- toolOverrideHooks: config.codexcli?.hooks,
21292
- converterConfig: CODEXCLI_CONVERTER_CONFIG,
21820
+ const merged = mergeGeneratedHookLists({
21821
+ existingContent,
21822
+ generatedHooks: canonicalToToolHooks({
21823
+ config,
21824
+ toolOverrideHooks: config.codexcli?.hooks,
21825
+ converterConfig: CODEXCLI_CONVERTER_CONFIG,
21826
+ logger
21827
+ }),
21828
+ shape: "matcher-groups",
21829
+ preserveUnowned,
21830
+ previouslyOwned: preserveUnowned ? parseHooksOwnershipLock(await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, HOOKS_OWNERSHIP_LOCK_FILE_NAME))) : void 0,
21293
21831
  logger
21294
21832
  });
21295
- const fileContent = JSON.stringify({ hooks: codexHooks }, null, 2);
21833
+ const fileContent = JSON.stringify({ hooks: merged.hooks }, null, 2);
21296
21834
  return new CodexcliHooks({
21297
21835
  outputRoot,
21298
21836
  relativeDirPath: paths.relativeDirPath,
21299
21837
  relativeFilePath: paths.relativeFilePath,
21300
21838
  fileContent,
21839
+ ownedHookRefs: preserveUnowned ? merged.owned : void 0,
21301
21840
  validate
21302
21841
  });
21303
21842
  }
@@ -21333,8 +21872,11 @@ var CodexcliHooks = class CodexcliHooks extends ToolHooks {
21333
21872
  validate: false
21334
21873
  });
21335
21874
  }
21336
- static async getAuxiliaryFiles({ outputRoot = process.cwd() } = {}) {
21337
- return [await CodexcliConfigToml.fromOutputRoot({ outputRoot })];
21875
+ static supportsPreserveUnowned() {
21876
+ return true;
21877
+ }
21878
+ static async getAuxiliaryFiles({ outputRoot = process.cwd(), toolHooks } = {}) {
21879
+ return [await CodexcliConfigToml.fromOutputRoot({ outputRoot }), ...toolHooks instanceof CodexcliHooks ? toolHooks.getOwnershipLockFiles() : []];
21338
21880
  }
21339
21881
  };
21340
21882
  //#endregion
@@ -21939,7 +22481,7 @@ var CursorHooks = class CursorHooks extends ToolHooks {
21939
22481
  validate
21940
22482
  });
21941
22483
  }
21942
- static fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false }) {
22484
+ static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, preserveUnowned = false, logger }) {
21943
22485
  const config = rulesyncHooks.getJson();
21944
22486
  const cursorSupported = new Set(CURSOR_HOOK_EVENTS);
21945
22487
  const sharedHooks = {};
@@ -21966,21 +22508,36 @@ var CursorHooks = class CursorHooks extends ToolHooks {
21966
22508
  }));
21967
22509
  if (mappedDefs.length > 0) mappedHooks[cursorEventName] = mappedDefs;
21968
22510
  }
22511
+ const paths = CursorHooks.getSettablePaths({ global });
22512
+ const merged = mergeGeneratedHookLists({
22513
+ existingContent: await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "",
22514
+ generatedHooks: mappedHooks,
22515
+ shape: "flat",
22516
+ preserveUnowned,
22517
+ previouslyOwned: preserveUnowned ? parseHooksOwnershipLock(await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, HOOKS_OWNERSHIP_LOCK_FILE_NAME))) : void 0,
22518
+ logger
22519
+ });
21969
22520
  const cursorConfig = {
21970
22521
  version: config.version ?? 1,
21971
- hooks: mappedHooks
22522
+ hooks: merged.hooks
21972
22523
  };
21973
22524
  const fileContent = JSON.stringify(cursorConfig, null, 2);
21974
- const paths = CursorHooks.getSettablePaths({ global });
21975
22525
  return new CursorHooks({
21976
22526
  outputRoot,
21977
22527
  relativeDirPath: paths.relativeDirPath,
21978
22528
  relativeFilePath: paths.relativeFilePath,
21979
22529
  fileContent,
22530
+ ownedHookRefs: preserveUnowned ? merged.owned : void 0,
21980
22531
  validate,
21981
22532
  rulesyncHooks
21982
22533
  });
21983
22534
  }
22535
+ static supportsPreserveUnowned() {
22536
+ return true;
22537
+ }
22538
+ static async getAuxiliaryFiles({ toolHooks } = {}) {
22539
+ return toolHooks instanceof CursorHooks ? toolHooks.getOwnershipLockFiles() : [];
22540
+ }
21984
22541
  toRulesyncHooks() {
21985
22542
  const content = this.getFileContent();
21986
22543
  const parsed = JSON.parse(content);
@@ -22348,7 +22905,7 @@ var DevinHooks = class DevinHooks extends ToolHooks {
22348
22905
  * adapter so the settings reader in `src/utils/` can name the same list without
22349
22906
  * importing a feature.
22350
22907
  *
22351
- * @see https://docs.factory.ai/cli/configuration/settings
22908
+ * @see https://docs.factory.ai/droid-cli/settings
22352
22909
  */
22353
22910
  const FACTORYDROID_OVERRIDE_KEYS = [
22354
22911
  "commandBlocklist",
@@ -22356,6 +22913,8 @@ const FACTORYDROID_OVERRIDE_KEYS = [
22356
22913
  "sandbox",
22357
22914
  "mcpPolicy",
22358
22915
  "mcpAutonomyOverrides",
22916
+ "mcpAutonomyUrlOverrides",
22917
+ "builtInToolAutonomyOverrides",
22359
22918
  "enableDroidShield",
22360
22919
  "sessionDefaultSettings",
22361
22920
  "maxAutonomyLevel",
@@ -22363,7 +22922,10 @@ const FACTORYDROID_OVERRIDE_KEYS = [
22363
22922
  "interactionMode",
22364
22923
  "extraKnownMarketplaces",
22365
22924
  "enabledPlugins",
22925
+ "strictEnabledPlugins",
22926
+ "strictKnownMarketplaces",
22366
22927
  "hooksDisabled",
22928
+ "allowManagedHooksOnly",
22367
22929
  "disabledSkills",
22368
22930
  "modelPolicy",
22369
22931
  "missionPolicy"
@@ -25220,6 +25782,186 @@ var VibeHooks = class VibeHooks extends ToolHooks {
25220
25782
  }
25221
25783
  };
25222
25784
  //#endregion
25785
+ //#region src/features/hooks/zcode-hooks.ts
25786
+ /**
25787
+ * Single spelling of the config.json codec/policy, matching the one in
25788
+ * ZcodeMcp: fail closed on an unparseable root rather than replacing the
25789
+ * user's primary ZCode config with generated output.
25790
+ */
25791
+ function parseZcodeConfig$1(fileContent, filePath) {
25792
+ return parseSharedConfig({
25793
+ format: "json",
25794
+ fileContent,
25795
+ filePath,
25796
+ invalidRootPolicy: "error"
25797
+ });
25798
+ }
25799
+ /**
25800
+ * Drop ZCode's native `process` hooks from an `events` map before the shared
25801
+ * import converter runs. The converter would coerce the unknown type to
25802
+ * `command` and import the executable alone — losing the `args` vector that
25803
+ * *is* the process hook's command line — so the imported hook would run
25804
+ * something else than the file states. Matcher groups left without hooks are
25805
+ * dropped with them.
25806
+ */
25807
+ function stripProcessHooks({ events, logger }) {
25808
+ if (!isRecord$1(events)) return events;
25809
+ const result = Object.create(null);
25810
+ for (const [eventName, entries] of Object.entries(events)) {
25811
+ if (!Array.isArray(entries)) {
25812
+ result[eventName] = entries;
25813
+ continue;
25814
+ }
25815
+ const kept = [];
25816
+ for (const entry of entries) {
25817
+ if (!isRecord$1(entry) || !Array.isArray(entry.hooks)) {
25818
+ kept.push(entry);
25819
+ continue;
25820
+ }
25821
+ const hooks = entry.hooks.filter((hook) => {
25822
+ if (isRecord$1(hook) && hook.type === "process") {
25823
+ logger?.warn(`Skipping a ZCode "process" hook on "${eventName}" while importing: it has no canonical equivalent, and importing its executable alone would change what it runs.`);
25824
+ return false;
25825
+ }
25826
+ return true;
25827
+ });
25828
+ if (hooks.length > 0) kept.push({
25829
+ ...entry,
25830
+ hooks
25831
+ });
25832
+ }
25833
+ result[eventName] = kept;
25834
+ }
25835
+ return result;
25836
+ }
25837
+ const ZCODE_CONVERTER_CONFIG = {
25838
+ supportedEvents: ZCODE_HOOK_EVENTS,
25839
+ canonicalToToolEventNames: CANONICAL_TO_ZCODE_EVENT_NAMES,
25840
+ toolToCanonicalEventNames: ZCODE_TO_CANONICAL_EVENT_NAMES,
25841
+ projectDirVar: "",
25842
+ noMatcherEvents: /* @__PURE__ */ new Set(["beforeSubmitPrompt", "stop"]),
25843
+ supportedHookTypes: /* @__PURE__ */ new Set(["command"]),
25844
+ booleanPassthroughFields: [{
25845
+ canonical: "async",
25846
+ tool: "async"
25847
+ }, {
25848
+ canonical: "enabled",
25849
+ tool: "enabled"
25850
+ }],
25851
+ stringPassthroughFields: [{
25852
+ canonical: "statusMessage",
25853
+ tool: "statusMessage"
25854
+ }, {
25855
+ canonical: "shell",
25856
+ tool: "shell",
25857
+ commandOnly: true
25858
+ }],
25859
+ wildcardMatcherMeansAll: true
25860
+ };
25861
+ /**
25862
+ * ZCode hooks.
25863
+ *
25864
+ * ZCode reads configuration-file hooks from the `hooks` block of its user
25865
+ * config file, `~/.zcode/cli/config.json`. Workspace config hooks are never
25866
+ * executed — the workspace file is ignored regardless of `hooks.enabled` — so
25867
+ * rulesync treats ZCode hooks as global-only. The event map is nested under
25868
+ * `hooks.events` beside the user-tunable `enabled` and `timeoutMs` siblings,
25869
+ * which are carried over while `events` is replaced.
25870
+ *
25871
+ * @see https://zcode.z.ai/en/docs
25872
+ */
25873
+ var ZcodeHooks = class ZcodeHooks extends ToolHooks {
25874
+ json;
25875
+ constructor(params) {
25876
+ super(params);
25877
+ this.json = parseZcodeConfig$1(this.fileContent ?? "{}", (0, node_path.join)(this.relativeDirPath, this.relativeFilePath));
25878
+ }
25879
+ isDeletable() {
25880
+ return false;
25881
+ }
25882
+ static getSettablePaths(_options = {}) {
25883
+ return {
25884
+ relativeDirPath: ZCODE_GLOBAL_CONFIG_DIR_PATH,
25885
+ relativeFilePath: ZCODE_CONFIG_FILE_NAME
25886
+ };
25887
+ }
25888
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
25889
+ const paths = this.getSettablePaths({ global });
25890
+ const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{}";
25891
+ return new ZcodeHooks({
25892
+ outputRoot,
25893
+ relativeDirPath: paths.relativeDirPath,
25894
+ relativeFilePath: paths.relativeFilePath,
25895
+ fileContent,
25896
+ validate,
25897
+ global
25898
+ });
25899
+ }
25900
+ static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
25901
+ const paths = this.getSettablePaths({ global });
25902
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
25903
+ const existingContent = await readFileContentOrNull(filePath) ?? "{}";
25904
+ const existing = parseZcodeConfig$1(existingContent, filePath);
25905
+ const config = rulesyncHooks.getJson();
25906
+ const events = canonicalToToolHooks({
25907
+ config,
25908
+ toolOverrideHooks: config.zcode?.hooks,
25909
+ converterConfig: ZCODE_CONVERTER_CONFIG,
25910
+ logger
25911
+ });
25912
+ const existingHooks = isRecord$1(existing["hooks"]) ? existing[ZCODE_HOOKS_CONFIG_KEY] : {};
25913
+ const shouldStateEnabled = Object.keys(events).length > 0 && existingHooks.enabled === void 0;
25914
+ return new ZcodeHooks({
25915
+ outputRoot,
25916
+ relativeDirPath: paths.relativeDirPath,
25917
+ relativeFilePath: paths.relativeFilePath,
25918
+ fileContent: applySharedConfigPatch({
25919
+ fileKey: sharedConfigFileKey(paths),
25920
+ feature: "hooks",
25921
+ existingContent,
25922
+ patch: { [ZCODE_HOOKS_CONFIG_KEY]: {
25923
+ ...existingHooks,
25924
+ ...shouldStateEnabled ? { enabled: true } : {},
25925
+ [ZCODE_HOOKS_EVENTS_KEY]: events
25926
+ } },
25927
+ filePath
25928
+ }),
25929
+ validate,
25930
+ global
25931
+ });
25932
+ }
25933
+ toRulesyncHooks({ logger } = {}) {
25934
+ const hooks = toolHooksToCanonical({
25935
+ hooks: stripProcessHooks({
25936
+ events: (isRecord$1(this.json["hooks"]) ? this.json[ZCODE_HOOKS_CONFIG_KEY] : {})[ZCODE_HOOKS_EVENTS_KEY],
25937
+ logger
25938
+ }),
25939
+ converterConfig: ZCODE_CONVERTER_CONFIG,
25940
+ logger
25941
+ });
25942
+ return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
25943
+ hooks,
25944
+ overrideKey: "zcode"
25945
+ }), null, 2) });
25946
+ }
25947
+ validate() {
25948
+ return {
25949
+ success: true,
25950
+ error: null
25951
+ };
25952
+ }
25953
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
25954
+ return new ZcodeHooks({
25955
+ outputRoot,
25956
+ relativeDirPath,
25957
+ relativeFilePath,
25958
+ fileContent: JSON.stringify({ [ZCODE_HOOKS_CONFIG_KEY]: { [ZCODE_HOOKS_EVENTS_KEY]: {} } }, null, 2),
25959
+ validate: false,
25960
+ global
25961
+ });
25962
+ }
25963
+ };
25964
+ //#endregion
25223
25965
  //#region src/features/hooks/hooks-processor.ts
25224
25966
  const HooksProcessorToolTargetSchema = zod_mini.z.enum(hooksProcessorToolTargetTuple);
25225
25967
  /**
@@ -25291,8 +26033,12 @@ const HOOKS_OVERRIDE_KEY_ALIASES = {
25291
26033
  "kiro-cli": KIRO_HOOKS_OVERRIDE_KEY,
25292
26034
  "kiro-ide": KIRO_HOOKS_OVERRIDE_KEY
25293
26035
  };
25294
- /** The targets writing the standalone `.kiro/hooks/*.json` v1 format. */
25295
- const KIRO_STANDALONE_HOOKS_TARGETS = /* @__PURE__ */ new Set(["kiro-cli", "kiro-ide"]);
26036
+ /** The targets whose hooks format carries a per-hook on-disk enable flag. */
26037
+ const PER_HOOK_ENABLED_TARGETS = /* @__PURE__ */ new Set([
26038
+ "kiro-cli",
26039
+ "kiro-ide",
26040
+ "zcode"
26041
+ ]);
25296
26042
  const toolHooksFactories = /* @__PURE__ */ new Map([
25297
26043
  ["amp", {
25298
26044
  class: AmpHooks,
@@ -25641,6 +26387,17 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
25641
26387
  supportedEvents: GROKCLI_HOOK_EVENTS,
25642
26388
  supportedHookTypes: ["command", "http"],
25643
26389
  supportsMatcher: true
26390
+ }],
26391
+ ["zcode", {
26392
+ class: ZcodeHooks,
26393
+ meta: {
26394
+ supportsProject: false,
26395
+ supportsGlobal: true,
26396
+ supportsImport: true
26397
+ },
26398
+ supportedEvents: ZCODE_HOOK_EVENTS,
26399
+ supportedHookTypes: ["command"],
26400
+ supportsMatcher: true
25644
26401
  }]
25645
26402
  ]);
25646
26403
  const hooksProcessorToolTargets = [...toolHooksFactories.entries()].filter(([, f]) => f.meta.supportsProject).map(([t]) => t);
@@ -25650,7 +26407,8 @@ const hooksProcessorToolTargetsGlobalImportable = [...toolHooksFactories.entries
25650
26407
  var HooksProcessor = class extends FeatureProcessor {
25651
26408
  toolTarget;
25652
26409
  global;
25653
- constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, global = false, dryRun = false, logger }) {
26410
+ preserveUnownedHooks;
26411
+ constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, global = false, dryRun = false, preserveUnownedHooks = false, logger }) {
25654
26412
  super({
25655
26413
  outputRoot,
25656
26414
  inputRoots,
@@ -25661,6 +26419,7 @@ var HooksProcessor = class extends FeatureProcessor {
25661
26419
  if (!result.success) throw new Error(`Invalid tool target for HooksProcessor: ${toolTarget}. ${formatError(result.error)}`);
25662
26420
  this.toolTarget = result.data;
25663
26421
  this.global = global;
26422
+ this.preserveUnownedHooks = preserveUnownedHooks;
25664
26423
  }
25665
26424
  async loadRulesyncFiles() {
25666
26425
  const relativePaths = getRulesyncSourceCandidates({ paths: RulesyncHooks.getSettablePaths() }).map((candidate) => candidate.relativeFilePath);
@@ -25752,14 +26511,14 @@ var HooksProcessor = class extends FeatureProcessor {
25752
26511
  }
25753
26512
  for (const [hookType, events] of unsupportedTypeToEvents) this.logger.warn(`Skipped ${hookType}-type hook(s) for ${this.toolTarget} (not supported): ${Array.from(events).join(", ")}`);
25754
26513
  }
25755
- if (!KIRO_STANDALONE_HOOKS_TARGETS.has(this.toolTarget)) {
26514
+ if (!PER_HOOK_ENABLED_TARGETS.has(this.toolTarget)) {
25756
26515
  const skippedEvents = new Set(unsupportedEventNames({
25757
26516
  factory,
25758
26517
  sharedHooks,
25759
26518
  effectiveHooks
25760
26519
  }));
25761
26520
  const eventsWithDisabledHooks = Object.entries(sharedHooks).filter(([event, defs]) => !skippedEvents.has(event) && defs.some((def) => def.enabled === false)).map(([event]) => event);
25762
- if (eventsWithDisabledHooks.length > 0) this.logger.warn(`Emitting "enabled: false" hook(s) as active for ${this.toolTarget} (only the kiro-cli / kiro-ide standalone hooks format supports the flag): ${eventsWithDisabledHooks.join(", ")}`);
26521
+ if (eventsWithDisabledHooks.length > 0) this.logger.warn(`Emitting "enabled: false" hook(s) as active for ${this.toolTarget} (only the kiro-cli / kiro-ide / zcode hooks formats support the flag): ${eventsWithDisabledHooks.join(", ")}`);
25763
26522
  }
25764
26523
  const eventsWithUnsupportedMatcher = unsupportedMatcherEventNames({
25765
26524
  factory,
@@ -25771,6 +26530,7 @@ var HooksProcessor = class extends FeatureProcessor {
25771
26530
  rulesyncHooks,
25772
26531
  validate: true,
25773
26532
  global: this.global,
26533
+ preserveUnowned: this.preserveUnownedHooks,
25774
26534
  logger: withToolTargetPrefix({
25775
26535
  logger: this.logger,
25776
26536
  toolTarget: this.toolTarget
@@ -26206,6 +26966,7 @@ var ClineIgnore = class ClineIgnore extends ToolIgnore {
26206
26966
  //#region src/constants/crush-paths.ts
26207
26967
  const CRUSH_RULE_FILE_NAME = "CRUSH.md";
26208
26968
  const CRUSH_GLOBAL_DIR = (0, node_path.join)(".config", "crush");
26969
+ const CRUSH_LOCAL_RULE_FILE_NAME = "CRUSH.local.md";
26209
26970
  const CRUSH_IGNORE_FILE_NAME = ".crushignore";
26210
26971
  const CRUSH_SKILLS_PROJECT_DIR = (0, node_path.join)(".crush", "skills");
26211
26972
  const CRUSH_SKILLS_GLOBAL_DIR = (0, node_path.join)(CRUSH_GLOBAL_DIR, "skills");
@@ -32426,9 +33187,16 @@ function lookupTransport(map, key) {
32426
33187
  }
32427
33188
  /**
32428
33189
  * Read the "put this server's own instructions into the agent's prompt" flag
32429
- * off an unfiltered canonical entry, under either spelling. Anything other
32430
- * than a literal `true` reads as not enabled, matching Rovo Dev, where the key
32431
- * is absent by default.
33190
+ * off an unfiltered canonical entry, under either spelling. `undefined` means
33191
+ * the entry says nothing, which is Rovo Dev's own default.
33192
+ *
33193
+ * The flag is a **tri-state**, not a switch that only exists when on. Atlassian
33194
+ * inverted the default on 2026-09-02: a server's instructions are now surfaced
33195
+ * when the key is absent or `true`, and `false` is what suppresses them — the
33196
+ * opposite of the opt-in wording this adapter was first written against, where
33197
+ * absent and `false` meant the same thing. So `false` has to travel end to end;
33198
+ * collapsing it away is what makes a suppression impossible to author, and
33199
+ * erases one on import.
32432
33200
  *
32433
33201
  * The canonical key decides whenever it is present, the way codex resolves the
32434
33202
  * same two-spelling conflict for `experimental_environment`. OR-ing the two
@@ -32436,28 +33204,25 @@ function lookupTransport(map, key) {
32436
33204
  * `enable_instructions: true` copied out of Atlassian's docs — fail-open, on
32437
33205
  * the one key whose whole purpose is a trust decision.
32438
33206
  *
33207
+ * A non-boolean under either spelling reads as `undefined` rather than as a
33208
+ * suppression: Rovo Dev surfaces the instructions for anything that is not
33209
+ * `false`, so writing `false` there would invent a restriction the tool is not
33210
+ * applying. (The canonical key is a strict boolean in the schema, so only the
33211
+ * raw spelling can carry one.)
33212
+ *
32439
33213
  * `isPlainObject` rather than `isRecord`: this walks a user-supplied key set,
32440
33214
  * so a `constructor` entry must not resolve up the prototype chain.
32441
33215
  */
32442
33216
  function readEnableInstructions(rawServer) {
32443
- if (!isPlainObject$1(rawServer)) return false;
32444
- if (rawServer.rovodevEnableInstructions !== void 0) return rawServer.rovodevEnableInstructions === true;
32445
- return rawServer.enable_instructions === true;
32446
- }
32447
- /**
32448
- * Names that were emitted with `enable_instructions: true` during one
32449
- * `fromRulesyncMcp` call, so the run can say so once rather than per server.
32450
- * Atlassian gates this key on trust, and it is the only thing generate writes
32451
- * that widens what steers the model — the quietest possible write is the wrong
32452
- * one for it.
32453
- */
32454
- function warnEnabledInstructions(names, logger) {
32455
- if (names.length === 0) return;
32456
- logger?.warn(`Rovo Dev MCP: writing enable_instructions: true for ${names.join(", ")}. Rovo Dev pastes ${names.length === 1 ? "that server's" : "those servers'"} own instructions into the agent's system prompt, so enable it only for servers you trust.`);
33217
+ if (!isPlainObject$1(rawServer)) return;
33218
+ const canonical = rawServer.rovodevEnableInstructions;
33219
+ if (canonical !== void 0) return typeof canonical === "boolean" ? canonical : void 0;
33220
+ const raw = rawServer.enable_instructions;
33221
+ return typeof raw === "boolean" ? raw : void 0;
32457
33222
  }
32458
33223
  function toRovodevServer(name, server, logger) {
32459
33224
  const { type, transport, disabled: _disabled, rovodevEnableInstructions, ...rest } = server;
32460
- if (rovodevEnableInstructions === true) rest.enable_instructions = true;
33225
+ if (typeof rovodevEnableInstructions === "boolean") rest.enable_instructions = rovodevEnableInstructions;
32461
33226
  const declared = typeof transport === "string" ? transport : typeof type === "string" ? type : void 0;
32462
33227
  if (declared === void 0) return rest;
32463
33228
  const mapped = lookupTransport(CANONICAL_TO_ROVODEV_TRANSPORT, declared);
@@ -32472,7 +33237,7 @@ function toRovodevServer(name, server, logger) {
32472
33237
  }
32473
33238
  function fromRovodevServer(server) {
32474
33239
  const { transport, enable_instructions: enableInstructions, ...rest } = server;
32475
- if (enableInstructions === true) rest.rovodevEnableInstructions = true;
33240
+ if (typeof enableInstructions === "boolean") rest.rovodevEnableInstructions = enableInstructions;
32476
33241
  if (typeof transport !== "string") return rest;
32477
33242
  const mapped = lookupTransport(ROVODEV_TO_CANONICAL_TRANSPORT, transport);
32478
33243
  return mapped === void 0 ? rest : {
@@ -32913,10 +33678,10 @@ var RovodevMcp = class RovodevMcp extends ToolMcp {
32913
33678
  canWriteDisableToggle = false;
32914
33679
  }
32915
33680
  const mcpServers = Object.fromEntries(Object.entries(rulesyncMcp.getMcpServers()).map(([name, server]) => {
32916
- const rawServer = rulesyncMcp.getRawMcpServer(name);
33681
+ const enableInstructions = readEnableInstructions(rulesyncMcp.getRawMcpServer(name));
32917
33682
  const record = {
32918
33683
  ...server,
32919
- ...readEnableInstructions(rawServer) && { rovodevEnableInstructions: true }
33684
+ ...enableInstructions !== void 0 && { rovodevEnableInstructions: enableInstructions }
32920
33685
  };
32921
33686
  if (record.disabled === true && !canWriteDisableToggle) {
32922
33687
  logger?.warn(`Rovo Dev MCP: skipping disabled server "${name}" because config.yml cannot be parsed, so mcp.disabledMcpServers cannot be written to switch it off.`);
@@ -32925,7 +33690,6 @@ var RovodevMcp = class RovodevMcp extends ToolMcp {
32925
33690
  const converted = toRovodevServer(name, record, logger);
32926
33691
  return converted === null ? null : [name, converted];
32927
33692
  }).filter((entry) => entry !== null));
32928
- warnEnabledInstructions(Object.entries(mcpServers).filter(([, server]) => server.enable_instructions === true).map(([name]) => name), logger);
32929
33693
  const rovodevConfig = {
32930
33694
  ...json,
32931
33695
  mcpServers
@@ -39069,6 +39833,34 @@ const DEEPAGENTS_STARTUP_BOOLEAN_DEFAULTS = {
39069
39833
  yolo_switcher: true,
39070
39834
  read_project_dotenv: true
39071
39835
  };
39836
+ /**
39837
+ * `[extensions]` gates dcode's Python extension system. `discover_extensions`
39838
+ * auto-loads `*.py` from the user's `~/.deepagents/extensions/` and from the
39839
+ * checked-out project's `<root>/.deepagents/extensions/`, so this table decides
39840
+ * whether a cloned repository's Python is imported into the agent process.
39841
+ */
39842
+ const EXTENSIONS_TABLE_KEY = "extensions";
39843
+ /**
39844
+ * Keys lifted back into the override on import. `extensions.extra_paths` is
39845
+ * deliberately absent: it names machine-local files and directories, which do
39846
+ * not belong in a committed `.rulesync/permissions.jsonc`, and it only ever
39847
+ * widens what loads.
39848
+ */
39849
+ const DEEPAGENTS_EXTENSIONS_KEYS = ["enabled", "trust"];
39850
+ /** `TrustPolicy` in `extensions/settings.py`; dcode reads nothing else. */
39851
+ const DEEPAGENTS_EXTENSION_TRUST_POLICIES = [
39852
+ "ask",
39853
+ "always",
39854
+ "never"
39855
+ ];
39856
+ /**
39857
+ * `[extensions].enabled` defaults to `true` upstream, so writing `true` on a
39858
+ * machine that has not set it changes nothing — the same reasoning as
39859
+ * `DEEPAGENTS_STARTUP_BOOLEAN_DEFAULTS`.
39860
+ *
39861
+ * @see https://github.com/langchain-ai/deepagents `config_manifest.py`
39862
+ */
39863
+ const DEEPAGENTS_EXTENSIONS_ENABLED_DEFAULT = true;
39072
39864
  const ALLOW_ALL_SENTINEL = "all";
39073
39865
  const RECOMMENDED_SENTINEL = "recommended";
39074
39866
  const GLOB_CHARACTERS_PATTERN = /[*?[\]]/;
@@ -39138,7 +39930,10 @@ const TRAILING_ARGUMENT_WILDCARD_PATTERN = /:\*$/;
39138
39930
  * `DeepagentsPermissionsOverrideSchema`): `[startup].mode`
39139
39931
  * (`manual` / `auto` / `yolo`), `[startup].yolo_switcher` and
39140
39932
  * `[startup].read_project_dotenv` are merged into the file on generate and
39141
- * lifted back into the override on import.
39933
+ * lifted back into the override on import. The same override carries
39934
+ * `[extensions].enabled` and `[extensions].trust` (`ask` / `always` / `never`),
39935
+ * which decide whether the Python in a checked-out project's
39936
+ * `.deepagents/extensions/` is imported into the agent process.
39142
39937
  *
39143
39938
  * On **import** the allowlist comes back as `bash` `allow` rules named by the
39144
39939
  * executable — skipping any entry dcode could not match in the first place, so
@@ -39240,6 +40035,13 @@ var DeepagentsPermissions = class DeepagentsPermissions extends ToolPermissions
39240
40035
  filePath,
39241
40036
  logger
39242
40037
  });
40038
+ const extensionsOverride = config.deepagents?.extensions;
40039
+ if (isPlainObject$1(extensionsOverride) && Object.keys(extensionsOverride).length > 0) mergeExtensionsOverride({
40040
+ settings,
40041
+ extensionsOverride,
40042
+ filePath,
40043
+ logger
40044
+ });
39243
40045
  return new DeepagentsPermissions({
39244
40046
  outputRoot,
39245
40047
  relativeDirPath: paths.relativeDirPath,
@@ -39264,8 +40066,15 @@ var DeepagentsPermissions = class DeepagentsPermissions extends ToolPermissions
39264
40066
  startup: isPlainObject$1(settings[STARTUP_TABLE_KEY]) ? settings[STARTUP_TABLE_KEY] : {},
39265
40067
  selfPath
39266
40068
  });
40069
+ const extensionsOverride = liftExtensionsOverride({
40070
+ extensions: isPlainObject$1(settings[EXTENSIONS_TABLE_KEY]) ? settings[EXTENSIONS_TABLE_KEY] : {},
40071
+ selfPath
40072
+ });
39267
40073
  const result = { ...config };
39268
- if (Object.keys(startupOverride).length > 0) result.deepagents = { startup: startupOverride };
40074
+ const deepagents = {};
40075
+ if (Object.keys(startupOverride).length > 0) deepagents.startup = startupOverride;
40076
+ if (Object.keys(extensionsOverride).length > 0) deepagents.extensions = extensionsOverride;
40077
+ if (Object.keys(deepagents).length > 0) result.deepagents = deepagents;
39269
40078
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
39270
40079
  }
39271
40080
  validate() {
@@ -39352,57 +40161,192 @@ function warnAboutUnwrittenBashRules({ allowAll, requestedAllowAll, askPatterns,
39352
40161
  if (willWrite && allowAll) warnWithFallback(logger, "The bash '*' allow rule became allow_list = [\"all\"] for deepagents-cli, which auto-approves every command and skips its dangerous-pattern check (command substitution, redirects, process substitution). List the executables you want instead if that is more than you meant.");
39353
40162
  }
39354
40163
  /**
39355
- * Lift the `[startup]` keys rulesync models back into the `deepagents`
39356
- * override, keeping only values dcode itself accepts.
40164
+ * Lift the keys rulesync models from one `config.toml` table back into the
40165
+ * `deepagents` override, keeping only values dcode itself accepts.
40166
+ *
40167
+ * A value outside what the option's own parser reads is `Invalid` upstream —
40168
+ * dcode ignores it and falls back to its default. Importing it anyway would
40169
+ * record a setting the tool is not applying and, because the canonical schema
40170
+ * is stricter than TOML, would write a `.rulesync/permissions.jsonc` the next
40171
+ * `rulesync generate` cannot even read.
39357
40172
  *
39358
- * A `mode` outside the three approval modes, or a non-boolean where a switch
39359
- * belongs, is `Invalid` upstream dcode ignores it and falls back to its
39360
- * default. Importing it anyway would record a setting the tool is not applying
39361
- * and, because the canonical schema is stricter than TOML, would write a
39362
- * `.rulesync/permissions.jsonc` the next `rulesync generate` cannot even read.
40173
+ * `normalize` returns the value to keep, or `null` for one dcode would not
40174
+ * read a wrapper rather than the bare value, so a legitimately falsy setting
40175
+ * is not mistaken for a rejection.
39363
40176
  */
39364
- function liftStartupOverride({ startup, selfPath }) {
39365
- const startupOverride = {};
40177
+ function liftOverrideTable({ table, tableKey, keys, normalize, selfPath }) {
40178
+ const override = {};
39366
40179
  const rejected = [];
39367
- for (const key of DEEPAGENTS_STARTUP_KEYS) {
39368
- const value = startup[key];
40180
+ for (const key of keys) {
40181
+ const value = table[key];
39369
40182
  if (value === void 0) continue;
39370
- if (key === "mode" ? DEEPAGENTS_STARTUP_MODES.includes(value) : typeof value === "boolean") startupOverride[key] = value;
40183
+ const normalized = normalize({
40184
+ key,
40185
+ value
40186
+ });
40187
+ if (normalized) override[key] = normalized.value;
39371
40188
  else rejected.push(`${key} = ${JSON.stringify(value)}`);
39372
40189
  }
39373
- if (rejected.length > 0) warnWithFallback(void 0, `deepagents-cli falls back to its own default for a '[${STARTUP_TABLE_KEY}]' value it cannot read, so ${rejected.join(", ")} in ${selfPath} ${rejected.length === 1 ? "was" : "were"} not imported.`);
39374
- return startupOverride;
40190
+ if (rejected.length > 0) warnWithFallback(void 0, `deepagents-cli falls back to its own default for a '[${tableKey}]' value it cannot read, so ${rejected.join(", ")} in ${selfPath} ${rejected.length === 1 ? "was" : "were"} not imported.`);
40191
+ return override;
39375
40192
  }
39376
40193
  /**
39377
- * Merge the `deepagents.startup` override into `[startup]`, preserving every
39378
- * other key of that table. A `startup` that is not a table at all is something
39379
- * rulesync did not write and cannot merge into, so it is left exactly as the
39380
- * user has it rather than replaced.
40194
+ * Lift the `[startup]` keys rulesync models back into the `deepagents`
40195
+ * override. A `mode` outside the three approval modes, or a non-boolean where
40196
+ * a switch belongs, is one dcode ignores.
39381
40197
  */
39382
- function mergeStartupOverride({ settings, startupOverride, filePath, logger }) {
39383
- const existingStartup = settings[STARTUP_TABLE_KEY];
39384
- if (existingStartup !== void 0 && !isPlainObject$1(existingStartup)) {
39385
- warnWithFallback(logger, `deepagents-cli: '${STARTUP_TABLE_KEY}' in ${filePath} is not a table, so the deepagents startup override was skipped rather than overwriting it.`);
40198
+ function liftStartupOverride({ startup, selfPath }) {
40199
+ return liftOverrideTable({
40200
+ table: startup,
40201
+ tableKey: STARTUP_TABLE_KEY,
40202
+ keys: DEEPAGENTS_STARTUP_KEYS,
40203
+ normalize: ({ key, value }) => {
40204
+ if (key === "mode") return DEEPAGENTS_STARTUP_MODES.includes(value) ? { value } : null;
40205
+ return typeof value === "boolean" ? { value } : null;
40206
+ },
40207
+ selfPath
40208
+ });
40209
+ }
40210
+ /**
40211
+ * Lift the `[extensions]` keys rulesync models back into the `deepagents`
40212
+ * override.
40213
+ *
40214
+ * `trust` is read the way `parse_trust_policy` reads it — trimmed and
40215
+ * lowercased — so `"Always"` comes back as the policy dcode is actually
40216
+ * applying rather than being dropped, the same courtesy the allowlist
40217
+ * sentinels already get. The canonical enum holds only the lowercase
40218
+ * spellings, so keeping the value verbatim would write a permissions file the
40219
+ * next generate could not parse.
40220
+ */
40221
+ function liftExtensionsOverride({ extensions, selfPath }) {
40222
+ return liftOverrideTable({
40223
+ table: extensions,
40224
+ tableKey: EXTENSIONS_TABLE_KEY,
40225
+ keys: DEEPAGENTS_EXTENSIONS_KEYS,
40226
+ normalize: ({ key, value }) => {
40227
+ if (key === "trust") {
40228
+ if (typeof value !== "string") return null;
40229
+ const policy = value.trim().toLowerCase();
40230
+ return DEEPAGENTS_EXTENSION_TRUST_POLICIES.includes(policy) ? { value: policy } : null;
40231
+ }
40232
+ return typeof value === "boolean" ? { value } : null;
40233
+ },
40234
+ selfPath
40235
+ });
40236
+ }
40237
+ /**
40238
+ * Merge one block of the `deepagents` override into its `config.toml` table,
40239
+ * preserving every other key of that table. A table that is not a table at all
40240
+ * is something rulesync did not write and cannot merge into, so it is left
40241
+ * exactly as the user has it rather than replaced.
40242
+ */
40243
+ function mergeOverrideTable({ settings, tableKey, override, knownKeys, isDroppedKey, warnAboutRelaxations, filePath, logger }) {
40244
+ const existing = settings[tableKey];
40245
+ if (existing !== void 0 && !isPlainObject$1(existing)) {
40246
+ warnWithFallback(logger, `deepagents-cli: '${tableKey}' in ${filePath} is not a table, so the deepagents ${tableKey} override was skipped rather than overwriting it.`);
39386
40247
  return;
39387
40248
  }
39388
- const startup = isPlainObject$1(existingStartup) ? { ...existingStartup } : {};
39389
- const previousStartup = { ...startup };
40249
+ const table = isPlainObject$1(existing) ? { ...existing } : {};
40250
+ const previous = { ...table };
39390
40251
  const writtenKeys = [];
39391
- for (const [key, value] of Object.entries(startupOverride)) {
40252
+ for (const [key, value] of Object.entries(override)) {
39392
40253
  if (isPrototypePollutionKey(key)) continue;
39393
- if (key === STARTUP_RECENT_KEY) continue;
40254
+ if (isDroppedKey?.(key)) continue;
39394
40255
  if (value === null || value === void 0) continue;
39395
- startup[key] = value;
40256
+ table[key] = value;
39396
40257
  writtenKeys.push(key);
39397
40258
  }
39398
- warnAboutStartupRelaxations({
39399
- startupOverride,
39400
- previousStartup,
40259
+ warnAboutRelaxations({
40260
+ override,
40261
+ previous,
40262
+ filePath,
40263
+ logger
40264
+ });
40265
+ warnAboutUncheckedKeys({
40266
+ tableKey,
40267
+ knownKeys,
39401
40268
  writtenKeys,
39402
40269
  filePath,
39403
40270
  logger
39404
40271
  });
39405
- if (Object.keys(startup).length > 0) settings[STARTUP_TABLE_KEY] = startup;
40272
+ if (Object.keys(table).length > 0) settings[tableKey] = table;
40273
+ }
40274
+ /**
40275
+ * Name the keys the merge wrote that rulesync does not model. The override is a
40276
+ * loose object so a key added upstream still reaches the config, but this one
40277
+ * writes into the machine's global file: what rulesync cannot judge, it at
40278
+ * least names.
40279
+ */
40280
+ function warnAboutUncheckedKeys({ tableKey, knownKeys, writtenKeys, filePath, logger }) {
40281
+ const known = new Set(knownKeys);
40282
+ const unknownKeys = writtenKeys.filter((key) => !known.has(key));
40283
+ if (unknownKeys.length === 0) return;
40284
+ warnWithFallback(logger, `The deepagents ${tableKey} override wrote ${unknownKeys.join(", ")} into ${filePath} unchecked — rulesync does not know what those keys grant, and that file is your global deepagents-cli config.`);
40285
+ }
40286
+ /**
40287
+ * Merge the `deepagents.startup` override into `[startup]`.
40288
+ */
40289
+ function mergeStartupOverride({ settings, startupOverride, filePath, logger }) {
40290
+ mergeOverrideTable({
40291
+ settings,
40292
+ tableKey: STARTUP_TABLE_KEY,
40293
+ override: startupOverride,
40294
+ knownKeys: DEEPAGENTS_STARTUP_KEYS,
40295
+ isDroppedKey: (key) => key === STARTUP_RECENT_KEY,
40296
+ warnAboutRelaxations: warnAboutStartupRelaxations,
40297
+ filePath,
40298
+ logger
40299
+ });
40300
+ }
40301
+ /**
40302
+ * Merge the `deepagents.extensions` override into `[extensions]`. Nothing is
40303
+ * dropped here — `extra_paths` is not modeled, but it is a key the user may
40304
+ * legitimately want carried, so it passes through as any other unknown key
40305
+ * does, named by the unchecked-key warning.
40306
+ */
40307
+ function mergeExtensionsOverride({ settings, extensionsOverride, filePath, logger }) {
40308
+ mergeOverrideTable({
40309
+ settings,
40310
+ tableKey: EXTENSIONS_TABLE_KEY,
40311
+ override: extensionsOverride,
40312
+ knownKeys: DEEPAGENTS_EXTENSIONS_KEYS,
40313
+ warnAboutRelaxations: warnAboutExtensionsRelaxations,
40314
+ filePath,
40315
+ logger
40316
+ });
40317
+ }
40318
+ /**
40319
+ * Name the value an override key replaces, so a setting the user had turned
40320
+ * down is visible as such rather than reported as a bare new value.
40321
+ */
40322
+ function describeOverriddenValue({ key, value, previous }) {
40323
+ const previousValue = previous[key];
40324
+ return previousValue === void 0 || previousValue === value ? `${key} = ${JSON.stringify(value)}` : `${key} = ${JSON.stringify(value)} (was ${JSON.stringify(previousValue)})`;
40325
+ }
40326
+ /**
40327
+ * Warn when the `deepagents` extensions override widens what dcode loads on its
40328
+ * own, for the same reason the startup one does: this block is written into the
40329
+ * user's **global** config from a `.rulesync/permissions.jsonc` a repository
40330
+ * can carry.
40331
+ *
40332
+ * `trust` decides what happens to the Python in a checked-out project's
40333
+ * `.deepagents/extensions/` — `always` imports it into the agent process with
40334
+ * no prompt at all — and `enabled` is the switch above it. `never` and `false`
40335
+ * restrict, so neither is reported.
40336
+ */
40337
+ function warnAboutExtensionsRelaxations({ override, previous, filePath, logger }) {
40338
+ const relaxations = [];
40339
+ if (override.trust === "always") relaxations.push(describeOverriddenValue({
40340
+ key: "trust",
40341
+ value: "always",
40342
+ previous
40343
+ }));
40344
+ if (override.enabled === true && (previous.enabled ?? DEEPAGENTS_EXTENSIONS_ENABLED_DEFAULT) !== true) relaxations.push(describeOverriddenValue({
40345
+ key: "enabled",
40346
+ value: true,
40347
+ previous
40348
+ }));
40349
+ if (relaxations.length > 0) warnWithFallback(logger, `The deepagents extensions override wrote ${relaxations.join(", ")} into ${filePath}, which is your global deepagents-cli config: it decides whether the Python in a checked-out project's .deepagents/extensions/ is imported into dcode, for every project on this machine, not just this one.`);
39406
40350
  }
39407
40351
  /**
39408
40352
  * Warn when the `deepagents` startup override relaxes what dcode may do on its
@@ -39417,12 +40361,13 @@ function mergeStartupOverride({ settings, startupOverride, filePath, logger }) {
39417
40361
  * process environment. The value being replaced is named alongside, so a
39418
40362
  * setting the user had turned down is visible as such.
39419
40363
  */
39420
- function warnAboutStartupRelaxations({ startupOverride, previousStartup, writtenKeys, filePath, logger }) {
40364
+ function warnAboutStartupRelaxations({ override: startupOverride, previous: previousStartup, filePath, logger }) {
39421
40365
  const relaxations = [];
39422
- const describe = (key, value) => {
39423
- const previous = previousStartup[key];
39424
- return previous === void 0 || previous === value ? `${key} = ${JSON.stringify(value)}` : `${key} = ${JSON.stringify(value)} (was ${JSON.stringify(previous)})`;
39425
- };
40366
+ const describe = (key, value) => describeOverriddenValue({
40367
+ key,
40368
+ value,
40369
+ previous: previousStartup
40370
+ });
39426
40371
  const mode = startupOverride.mode;
39427
40372
  if (mode === "auto" || mode === "yolo") relaxations.push(describe("mode", mode));
39428
40373
  for (const [key, upstreamDefault] of Object.entries(DEEPAGENTS_STARTUP_BOOLEAN_DEFAULTS)) {
@@ -39432,9 +40377,6 @@ function warnAboutStartupRelaxations({ startupOverride, previousStartup, written
39432
40377
  }
39433
40378
  if (relaxations.length > 0) warnWithFallback(logger, `The deepagents startup override wrote ${relaxations.join(", ")} into ${filePath}, which is your global deepagents-cli config: it relaxes how much dcode does without asking, for every project on this machine, not just this one.`);
39434
40379
  if (startupOverride[STARTUP_RECENT_KEY] !== void 0) warnWithFallback(logger, `The deepagents startup override's '${STARTUP_RECENT_KEY}' was not written to ${filePath}: dcode manages that key itself, and with no explicit 'mode' beside it, it is what restores auto-approval at launch. Set 'mode' if switching approval modes is the intent.`);
39435
- const known = new Set(DEEPAGENTS_STARTUP_KEYS);
39436
- const unknownKeys = writtenKeys.filter((key) => !known.has(key));
39437
- if (unknownKeys.length > 0) warnWithFallback(logger, `The deepagents startup override wrote ${unknownKeys.join(", ")} into ${filePath} unchecked — rulesync does not know what those keys grant, and that file is your global deepagents-cli config.`);
39438
40380
  }
39439
40381
  /**
39440
40382
  * Read `[shell].allow_list` the way dcode does: a TOML array is taken element
@@ -54055,7 +54997,10 @@ var WarpSkill = class WarpSkill extends ToolSkill {
54055
54997
  //#region src/features/skills/zcode-skill.ts
54056
54998
  const ZcodeSkillFrontmatterSchema = zod_mini.z.looseObject({
54057
54999
  name: zod_mini.z.string(),
54058
- description: zod_mini.z.string()
55000
+ description: zod_mini.z.string(),
55001
+ when_to_use: zod_mini.z.optional(zod_mini.z.string()),
55002
+ license: zod_mini.z.optional(zod_mini.z.string()),
55003
+ metadata: zod_mini.z.optional(zod_mini.z.looseObject({}))
54059
55004
  });
54060
55005
  /**
54061
55006
  * Represents a ZCode skill directory.
@@ -54118,10 +55063,16 @@ var ZcodeSkill = class ZcodeSkill extends ToolSkill {
54118
55063
  }
54119
55064
  toRulesyncSkill() {
54120
55065
  const frontmatter = this.getFrontmatter();
55066
+ const zcodeSection = {
55067
+ ...frontmatter.when_to_use !== void 0 && { when_to_use: frontmatter.when_to_use },
55068
+ ...frontmatter.license !== void 0 && { license: frontmatter.license },
55069
+ ...frontmatter.metadata !== void 0 && { metadata: frontmatter.metadata }
55070
+ };
54121
55071
  const rulesyncFrontmatter = {
54122
55072
  name: frontmatter.name,
54123
55073
  description: frontmatter.description,
54124
- targets: ["*"]
55074
+ targets: ["*"],
55075
+ ...Object.keys(zcodeSection).length > 0 && { zcode: zcodeSection }
54125
55076
  };
54126
55077
  return new RulesyncSkill({
54127
55078
  outputRoot: this.outputRoot,
@@ -54136,9 +55087,21 @@ var ZcodeSkill = class ZcodeSkill extends ToolSkill {
54136
55087
  }
54137
55088
  static fromRulesyncSkill({ outputRoot = process.cwd(), rulesyncSkill, validate = true, global = false }) {
54138
55089
  const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
55090
+ const zcodeSection = rulesyncFrontmatter.zcode;
55091
+ const license = resolveLicense({
55092
+ rootFrontmatter: rulesyncFrontmatter,
55093
+ section: zcodeSection
55094
+ });
55095
+ const metadata = resolveMetadata({
55096
+ rootFrontmatter: rulesyncFrontmatter,
55097
+ section: zcodeSection
55098
+ });
54139
55099
  const zcodeFrontmatter = {
54140
55100
  name: rulesyncFrontmatter.name,
54141
- description: rulesyncFrontmatter.description
55101
+ description: rulesyncFrontmatter.description,
55102
+ ...zcodeSection?.when_to_use !== void 0 && { when_to_use: zcodeSection.when_to_use },
55103
+ ...license !== void 0 && { license },
55104
+ ...metadata !== void 0 && { metadata }
54142
55105
  };
54143
55106
  const settablePaths = ZcodeSkill.getSettablePaths({ global });
54144
55107
  return new ZcodeSkill({
@@ -62239,10 +63202,21 @@ var ClineRule = class ClineRule extends ToolRule {
62239
63202
  */
62240
63203
  const CodebuddyRuleFrontmatterSchema = zod_mini.z.object({
62241
63204
  description: zod_mini.z.optional(zod_mini.z.string()),
62242
- paths: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
62243
- alwaysApply: zod_mini.z.optional(zod_mini.z.boolean())
63205
+ paths: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())])),
63206
+ alwaysApply: zod_mini.z.optional(zod_mini.z.boolean()),
63207
+ enabled: zod_mini.z.optional(zod_mini.z.boolean())
62244
63208
  });
62245
63209
  /**
63210
+ * Normalizes the documented `string` / `string[]` shapes of `paths` to the
63211
+ * list form the rest of the adapter works with. An empty list and an empty
63212
+ * string both mean "no paths".
63213
+ */
63214
+ function normalizeCodebuddyPaths(paths) {
63215
+ if (paths === void 0) return;
63216
+ const list = (typeof paths === "string" ? [paths] : paths).filter((path) => path.trim() !== "");
63217
+ return list.length > 0 ? list : void 0;
63218
+ }
63219
+ /**
62246
63220
  * A universal glob (matching everything) is redundant on an Always Apply
62247
63221
  * rule and, paired with `alwaysApply: true`, is the same semantic conflict
62248
63222
  * `CursorRule.resolveCursorGlobs` avoids for Cursor: `alwaysApply` already
@@ -62260,7 +63234,7 @@ const UNIVERSAL_PATHS = /* @__PURE__ */ new Set(["**/*", "*"]);
62260
63234
  * Rules format:
62261
63235
  * - {project}/CODEBUDDY.md (root: true), also read from {project}/.codebuddy/CODEBUDDY.md
62262
63236
  * - {project}/.codebuddy/rules/*.md (root: false, with optional
62263
- * `description` / `paths` / `alwaysApply` frontmatter)
63237
+ * `description` / `paths` / `alwaysApply` / `enabled` frontmatter)
62264
63238
  * - Global: ~/.codebuddy/CODEBUDDY.md and ~/.codebuddy/rules/*.md
62265
63239
  *
62266
63240
  * @see https://www.codebuddy.ai/docs/cli/memory
@@ -62302,9 +63276,10 @@ var CodebuddyRule = class CodebuddyRule extends ToolRule {
62302
63276
  this.body = body;
62303
63277
  }
62304
63278
  static generateFileContent(body, frontmatter) {
62305
- if (frontmatter.description === void 0 && frontmatter.paths === void 0 && frontmatter.alwaysApply === void 0) return body;
63279
+ if (frontmatter.description === void 0 && frontmatter.paths === void 0 && frontmatter.alwaysApply === void 0 && frontmatter.enabled === void 0) return body;
62306
63280
  return stringifyFrontmatter(body, {
62307
63281
  description: frontmatter.description,
63282
+ enabled: frontmatter.enabled,
62308
63283
  alwaysApply: frontmatter.alwaysApply,
62309
63284
  paths: frontmatter.paths
62310
63285
  });
@@ -62352,10 +63327,36 @@ var CodebuddyRule = class CodebuddyRule extends ToolRule {
62352
63327
  root: isRoot
62353
63328
  });
62354
63329
  }
62355
- static resolveCodebuddyPaths({ paths, alwaysApply }) {
62356
- if (!paths || paths.length === 0) return;
62357
- if (alwaysApply && paths.every((path) => UNIVERSAL_PATHS.has(path.trim()))) return;
62358
- return paths;
63330
+ /**
63331
+ * Resolves the `paths` / `alwaysApply` pair against CodeBuddy's Rule Type
63332
+ * Determination table, which reads:
63333
+ *
63334
+ * | `alwaysApply` | `paths` | Rule type |
63335
+ * | ---------------- | --------- | ---------------------------------- |
63336
+ * | `true` (default) | any | ALWAYS — always injected |
63337
+ * | `false` | has value | MANUAL — triggered on matching file |
63338
+ * | `false` | none | not supported; the rule is dropped |
63339
+ *
63340
+ * The default being `true` is the opposite of Cursor, which the adapter was
63341
+ * modeled on: `paths` alone scopes nothing, so a rule meant to be path
63342
+ * triggered has to carry an explicit `alwaysApply: false` alongside it.
63343
+ *
63344
+ * @see https://www.codebuddy.ai/docs/cli/memory
63345
+ */
63346
+ static resolveCodebuddyRuleType({ paths, alwaysApply }) {
63347
+ const scopedPaths = paths && paths.length > 0 && !paths.every((path) => UNIVERSAL_PATHS.has(path.trim())) ? paths : void 0;
63348
+ if (alwaysApply === true) return {
63349
+ paths: scopedPaths,
63350
+ alwaysApply: true
63351
+ };
63352
+ if (scopedPaths === void 0) return {
63353
+ paths: void 0,
63354
+ alwaysApply: void 0
63355
+ };
63356
+ return {
63357
+ paths: scopedPaths,
63358
+ alwaysApply: false
63359
+ };
62359
63360
  }
62360
63361
  static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
62361
63362
  const rulesyncFrontmatter = rulesyncRule.getFrontmatter();
@@ -62372,17 +63373,17 @@ var CodebuddyRule = class CodebuddyRule extends ToolRule {
62372
63373
  root
62373
63374
  });
62374
63375
  if (!paths.nonRoot) throw new Error(`nonRoot path is not set for ${rulesyncRule.getRelativeFilePath()}`);
62375
- const codebuddyPaths = rulesyncFrontmatter.codebuddy?.paths;
63376
+ const codebuddyPaths = normalizeCodebuddyPaths(rulesyncFrontmatter.codebuddy?.paths);
62376
63377
  const globs = rulesyncFrontmatter.globs;
62377
- const alwaysApply = rulesyncFrontmatter.codebuddy?.alwaysApply;
62378
- const pathsValue = CodebuddyRule.resolveCodebuddyPaths({
63378
+ const ruleType = CodebuddyRule.resolveCodebuddyRuleType({
62379
63379
  paths: codebuddyPaths ?? (globs?.length ? globs : void 0),
62380
- alwaysApply: alwaysApply === true
63380
+ alwaysApply: rulesyncFrontmatter.codebuddy?.alwaysApply
62381
63381
  });
62382
63382
  const codebuddyFrontmatter = {
62383
63383
  description: rulesyncFrontmatter.codebuddy?.description ?? rulesyncFrontmatter.description,
62384
- paths: pathsValue,
62385
- alwaysApply
63384
+ paths: ruleType.paths,
63385
+ alwaysApply: ruleType.alwaysApply,
63386
+ enabled: rulesyncFrontmatter.codebuddy?.enabled
62386
63387
  };
62387
63388
  return new CodebuddyRule({
62388
63389
  outputRoot,
@@ -62412,17 +63413,19 @@ var CodebuddyRule = class CodebuddyRule extends ToolRule {
62412
63413
  validate: true
62413
63414
  });
62414
63415
  }
62415
- const isAlways = this.frontmatter.alwaysApply === true;
62416
- const sourcePaths = this.frontmatter.paths ?? [];
63416
+ const sourcePaths = normalizeCodebuddyPaths(this.frontmatter.paths) ?? [];
63417
+ const isAlways = this.frontmatter.alwaysApply !== false;
62417
63418
  const globs = sourcePaths.length === 0 && isAlways ? ["**/*"] : sourcePaths;
63419
+ const alwaysApply = this.frontmatter.alwaysApply ?? (sourcePaths.length > 0 ? true : void 0);
62418
63420
  const rulesyncFrontmatter = {
62419
63421
  targets,
62420
63422
  root: false,
62421
63423
  description: this.frontmatter.description,
62422
63424
  globs,
62423
- ...(this.frontmatter.paths !== void 0 || this.frontmatter.alwaysApply !== void 0 || this.frontmatter.description !== void 0) && { codebuddy: {
62424
- paths: this.frontmatter.paths,
62425
- alwaysApply: this.frontmatter.alwaysApply,
63425
+ ...(this.frontmatter.paths !== void 0 || this.frontmatter.alwaysApply !== void 0 || this.frontmatter.enabled !== void 0 || this.frontmatter.description !== void 0) && { codebuddy: {
63426
+ paths: sourcePaths.length > 0 ? sourcePaths : void 0,
63427
+ alwaysApply,
63428
+ enabled: this.frontmatter.enabled,
62426
63429
  description: this.frontmatter.description
62427
63430
  } }
62428
63431
  };
@@ -65205,33 +66208,49 @@ var ReplitRule = class ReplitRule extends ToolRule {
65205
66208
  * Supports plain Markdown without frontmatter, mode-specific rules,
65206
66209
  * and both directory-based and single-file configurations.
65207
66210
  *
65208
- * - Project scope writes the non-root directory `.roo/rules/`.
66211
+ * - Project scope writes the non-root directory `.roo/rules/`, and the root
66212
+ * rule to the workspace-root `AGENTS.md` the extension reads from `cwd`.
65209
66213
  * - Global scope writes the same non-root directory resolved under the home
65210
- * directory (`~/.roo/rules/`), which Roo loads before workspace rules.
66214
+ * directory (`~/.roo/rules/`), which Roo loads before workspace rules. The
66215
+ * root rule joins it as `~/.roo/rules/AGENTS.md`: agent-rules discovery is
66216
+ * workspace-only, so the default project-scope root would resolve to a
66217
+ * `~/AGENTS.md` nothing ever reads.
65211
66218
  * @see https://roocodeinc.github.io/Roo-Code/features/custom-instructions
65212
66219
  */
65213
66220
  var RooRule = class RooRule extends ToolRule {
65214
- static getSettablePaths(_options = {}) {
65215
- return { nonRoot: { relativeDirPath: buildToolPath(ROO_DIR, "rules", _options.excludeToolDir) } };
66221
+ static getSettablePaths({ global, excludeToolDir } = {}) {
66222
+ const rulesDirPath = buildToolPath(ROO_DIR, "rules", excludeToolDir);
66223
+ if (global) return {
66224
+ root: {
66225
+ relativeDirPath: rulesDirPath,
66226
+ relativeFilePath: ROO_GLOBAL_ROOT_RULE_FILE_NAME
66227
+ },
66228
+ nonRoot: { relativeDirPath: rulesDirPath }
66229
+ };
66230
+ return { nonRoot: { relativeDirPath: rulesDirPath } };
65216
66231
  }
65217
- static async fromFile({ outputRoot = process.cwd(), relativeFilePath, relativeDirPath: overrideDirPath, validate = true }) {
65218
- const relativeDirPath = overrideDirPath !== void 0 && RooRule.extractModeFromDirPath(overrideDirPath) !== void 0 ? overrideDirPath : this.getSettablePaths().nonRoot.relativeDirPath;
66232
+ static async fromFile({ outputRoot = process.cwd(), relativeFilePath, relativeDirPath: overrideDirPath, validate = true, global = false }) {
66233
+ const paths = this.getSettablePaths({ global });
66234
+ const relativeDirPath = overrideDirPath !== void 0 && RooRule.extractModeFromDirPath(overrideDirPath) !== void 0 ? overrideDirPath : paths.nonRoot?.relativeDirPath ?? buildToolPath(".roo", "rules");
65219
66235
  const fileContent = await readFileContent((0, node_path.join)(outputRoot, relativeDirPath, relativeFilePath));
66236
+ const isRoot = "root" in paths && relativeDirPath === paths.root.relativeDirPath && relativeFilePath === paths.root.relativeFilePath;
65220
66237
  return new RooRule({
65221
66238
  outputRoot,
65222
66239
  relativeDirPath,
65223
66240
  relativeFilePath,
65224
66241
  fileContent,
65225
66242
  validate,
65226
- root: false
66243
+ root: isRoot
65227
66244
  });
65228
66245
  }
65229
- static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true }) {
66246
+ static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
66247
+ const paths = this.getSettablePaths({ global });
65230
66248
  const params = this.buildToolRuleParamsDefault({
65231
66249
  outputRoot,
65232
66250
  rulesyncRule,
65233
66251
  validate,
65234
- nonRootPath: this.getSettablePaths().nonRoot
66252
+ ..."root" in paths && { rootPath: paths.root },
66253
+ nonRootPath: paths.nonRoot
65235
66254
  });
65236
66255
  const mode = rulesyncRule.getFrontmatter().roo?.mode;
65237
66256
  if (!params.root && mode !== void 0 && mode !== "") {
@@ -66154,7 +67173,9 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
66154
67173
  extension: "md",
66155
67174
  supportsGlobal: true,
66156
67175
  ruleDiscoveryMode: "auto",
66157
- collisionPolicy: "fold"
67176
+ collisionPolicy: "fold",
67177
+ localRootMode: "separate-local-file",
67178
+ localRootFileName: CRUSH_LOCAL_RULE_FILE_NAME
66158
67179
  }
66159
67180
  }],
66160
67181
  ["cursor", {
@@ -66874,6 +67895,34 @@ var RulesProcessor = class extends FeatureProcessor {
66874
67895
  root: true,
66875
67896
  localRoot
66876
67897
  });
67898
+ if (isClassOrSubclassOf({
67899
+ candidate: factory.class,
67900
+ base: CodebuddyRule
67901
+ })) {
67902
+ const paths = CodebuddyRule.getSettablePaths({ global: this.global });
67903
+ return new CodebuddyRule({
67904
+ outputRoot: this.outputRoot,
67905
+ relativeDirPath: relativeDirPath ?? paths.root.relativeDirPath,
67906
+ relativeFilePath: fileName,
67907
+ frontmatter: {},
67908
+ body,
67909
+ validate: true,
67910
+ root: true,
67911
+ localRoot
67912
+ });
67913
+ }
67914
+ if (isClassOrSubclassOf({
67915
+ candidate: factory.class,
67916
+ base: CrushRule
67917
+ })) return new CrushRule({
67918
+ outputRoot: this.outputRoot,
67919
+ relativeDirPath: relativeDirPath ?? ".",
67920
+ relativeFilePath: fileName,
67921
+ fileContent: body,
67922
+ validate: true,
67923
+ root: true,
67924
+ localRoot
67925
+ });
66877
67926
  if (isClassOrSubclassOf({
66878
67927
  candidate: factory.class,
66879
67928
  base: QwencodeRule
@@ -69002,6 +70051,7 @@ async function generateHooksCore(params) {
69002
70051
  toolTarget,
69003
70052
  global: config.getGlobal(),
69004
70053
  dryRun: config.isPreviewMode(),
70054
+ preserveUnownedHooks: config.getPreserveUnownedHooks(),
69005
70055
  logger
69006
70056
  });
69007
70057
  const result = await processFeatureWithRulesyncFiles({
@@ -69573,6 +70623,12 @@ Object.defineProperty(exports, "CONFLICTING_TARGET_PAIRS", {
69573
70623
  return CONFLICTING_TARGET_PAIRS;
69574
70624
  }
69575
70625
  });
70626
+ Object.defineProperty(exports, "CRUSH_LOCAL_RULE_FILE_NAME", {
70627
+ enumerable: true,
70628
+ get: function() {
70629
+ return CRUSH_LOCAL_RULE_FILE_NAME;
70630
+ }
70631
+ });
69576
70632
  Object.defineProperty(exports, "CURATED_RULES_FEATURE_SUBDIR", {
69577
70633
  enumerable: true,
69578
70634
  get: function() {