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.
@@ -713,7 +713,8 @@ const hooksProcessorToolTargetTuple = [
713
713
  "vibe",
714
714
  "qwencode",
715
715
  "reasonix",
716
- "grokcli"
716
+ "grokcli",
717
+ "zcode"
717
718
  ];
718
719
  const permissionsProcessorToolTargetTuple = [
719
720
  "amp",
@@ -2393,6 +2394,7 @@ const ConfigParamsSchema = z.object({
2393
2394
  simulateCommands: optional(z.boolean()),
2394
2395
  simulateSubagents: optional(z.boolean()),
2395
2396
  simulateSkills: optional(z.boolean()),
2397
+ preserveUnownedHooks: optional(z.boolean()),
2396
2398
  deriveSubprojectPathFromGlobs: optional(z.boolean()),
2397
2399
  flattenedCommandNaming: optional(FlattenedCommandNamingSchema),
2398
2400
  /**
@@ -2541,6 +2543,7 @@ var Config = class Config {
2541
2543
  simulateCommands;
2542
2544
  simulateSubagents;
2543
2545
  simulateSkills;
2546
+ preserveUnownedHooks;
2544
2547
  deriveSubprojectPathFromGlobs;
2545
2548
  flattenedCommandNaming;
2546
2549
  language;
@@ -2567,7 +2570,7 @@ var Config = class Config {
2567
2570
  inputRoots;
2568
2571
  configFilePath;
2569
2572
  sources;
2570
- constructor({ outputRoots, targets, features, verbose, delete: isDelete, global, silent, simulateCommands, simulateSubagents, simulateSkills, deriveSubprojectPathFromGlobs, flattenedCommandNaming, language, gitignoreTargetsOnly, gitignoreDestination, dryRun, check, inputRoot, inputRoots, configFilePath, sources, configFileTargets }) {
2573
+ 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 }) {
2571
2574
  assertTargetsFeaturesExclusive({
2572
2575
  targets,
2573
2576
  features
@@ -2599,6 +2602,7 @@ var Config = class Config {
2599
2602
  this.simulateCommands = simulateCommands ?? false;
2600
2603
  this.simulateSubagents = simulateSubagents ?? false;
2601
2604
  this.simulateSkills = simulateSkills ?? false;
2605
+ this.preserveUnownedHooks = preserveUnownedHooks ?? false;
2602
2606
  this.deriveSubprojectPathFromGlobs = deriveSubprojectPathFromGlobs ?? false;
2603
2607
  this.flattenedCommandNaming = flattenedCommandNaming ?? "basename";
2604
2608
  this.language = language;
@@ -2777,6 +2781,9 @@ var Config = class Config {
2777
2781
  getSimulateCommands() {
2778
2782
  return this.simulateCommands;
2779
2783
  }
2784
+ getPreserveUnownedHooks() {
2785
+ return this.preserveUnownedHooks;
2786
+ }
2780
2787
  getFlattenedCommandNaming() {
2781
2788
  return this.flattenedCommandNaming;
2782
2789
  }
@@ -2856,6 +2863,7 @@ const getDefaults = () => ({
2856
2863
  simulateCommands: false,
2857
2864
  simulateSubagents: false,
2858
2865
  simulateSkills: false,
2866
+ preserveUnownedHooks: false,
2859
2867
  deriveSubprojectPathFromGlobs: false,
2860
2868
  flattenedCommandNaming: "basename",
2861
2869
  gitignoreTargetsOnly: true,
@@ -2904,6 +2912,7 @@ const mergeConfigs = (baseConfig, localConfig) => {
2904
2912
  simulateCommands: localConfig.simulateCommands ?? baseConfig.simulateCommands,
2905
2913
  simulateSubagents: localConfig.simulateSubagents ?? baseConfig.simulateSubagents,
2906
2914
  simulateSkills: localConfig.simulateSkills ?? baseConfig.simulateSkills,
2915
+ preserveUnownedHooks: localConfig.preserveUnownedHooks ?? baseConfig.preserveUnownedHooks,
2907
2916
  deriveSubprojectPathFromGlobs: localConfig.deriveSubprojectPathFromGlobs ?? baseConfig.deriveSubprojectPathFromGlobs,
2908
2917
  flattenedCommandNaming: localConfig.flattenedCommandNaming ?? baseConfig.flattenedCommandNaming,
2909
2918
  language: localConfig.language ?? baseConfig.language,
@@ -3127,6 +3136,11 @@ var ConfigResolver = class {
3127
3136
  file: configByFile.simulateSkills,
3128
3137
  fallback: getDefaults().simulateSkills
3129
3138
  }),
3139
+ preserveUnownedHooks: pick({
3140
+ cli: void 0,
3141
+ file: configByFile.preserveUnownedHooks,
3142
+ fallback: getDefaults().preserveUnownedHooks
3143
+ }),
3130
3144
  deriveSubprojectPathFromGlobs: pick({
3131
3145
  cli: deriveSubprojectPathFromGlobs,
3132
3146
  file: configByFile.deriveSubprojectPathFromGlobs,
@@ -4686,6 +4700,48 @@ const KIMI_CODE_NATIVE_HOOK_EVENTS = [
4686
4700
  ];
4687
4701
  const KIMI_CODE_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_KIMI_CODE_EVENT_NAMES).map(([canonical, kimiCode]) => [kimiCode, canonical]));
4688
4702
  /**
4703
+ * Hook events supported by ZCode.
4704
+ *
4705
+ * ZCode's configuration-file hooks expose exactly seven PascalCase events, all
4706
+ * of which have a clean canonical equivalent: `SessionStart`, `PreToolUse`,
4707
+ * `PostToolUse`, `PostToolUseFailure`, `PermissionRequest`, `Stop`, and
4708
+ * `UserPromptSubmit` ← `beforeSubmitPrompt`. An optional matcher is honored on
4709
+ * all of them except `UserPromptSubmit` and `Stop`, which expose no value to
4710
+ * match against. ZCode reads the matcher in one of two ways: a value made up
4711
+ * solely of letters, digits, underscores and `|` is an exact name list
4712
+ * (`Write|Edit` matches those two tool names, not the regex alternation), and
4713
+ * any other character makes the whole value a case-sensitive JavaScript regex.
4714
+ * Rulesync passes matchers through verbatim, so an authored value keeps
4715
+ * whichever reading ZCode gives it — worth knowing before escaping one.
4716
+ * Configuration hooks are read only from the user config
4717
+ * `~/.zcode/cli/config.json` (workspace config hooks are never executed) and
4718
+ * additionally require `hooks.enabled: true` to run.
4719
+ *
4720
+ * ZCode also accepts a native `process` hook type (an argv run without a
4721
+ * shell) which has no canonical equivalent; see ZcodeHooks.
4722
+ *
4723
+ * @see https://zcode.z.ai/en/docs
4724
+ */
4725
+ const ZCODE_HOOK_EVENTS = [
4726
+ "sessionStart",
4727
+ "beforeSubmitPrompt",
4728
+ "preToolUse",
4729
+ "postToolUse",
4730
+ "postToolUseFailure",
4731
+ "permissionRequest",
4732
+ "stop"
4733
+ ];
4734
+ const CANONICAL_TO_ZCODE_EVENT_NAMES = {
4735
+ sessionStart: "SessionStart",
4736
+ beforeSubmitPrompt: "UserPromptSubmit",
4737
+ preToolUse: "PreToolUse",
4738
+ postToolUse: "PostToolUse",
4739
+ postToolUseFailure: "PostToolUseFailure",
4740
+ permissionRequest: "PermissionRequest",
4741
+ stop: "Stop"
4742
+ };
4743
+ const ZCODE_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_ZCODE_EVENT_NAMES).map(([k, v]) => [v, k]));
4744
+ /**
4689
4745
  * Hook events supported by Hermes Agent's native Shell Hooks system.
4690
4746
  *
4691
4747
  * Hermes validates hook events against a fixed `VALID_HOOKS` set — 37 entries as
@@ -4837,6 +4893,7 @@ const HooksConfigSchema = z.looseObject({
4837
4893
  reasonix: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
4838
4894
  grokcli: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
4839
4895
  "kimi-code": z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
4896
+ zcode: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
4840
4897
  qwencode: z.optional(z.looseObject({
4841
4898
  hooks: z.optional(hooksRecordSchema),
4842
4899
  disableAllHooks: z.optional(z.boolean())
@@ -6827,12 +6884,37 @@ const DeepagentsStartupOverrideSchema = z.looseObject({
6827
6884
  read_project_dotenv: z.optional(z.boolean())
6828
6885
  });
6829
6886
  /**
6887
+ * deepagents-cli's Python extension gate under `[extensions]` in
6888
+ * `~/.deepagents/config.toml`. `discover_extensions` auto-loads `*.py` from the
6889
+ * user's `~/.deepagents/extensions/` and from the checked-out project's
6890
+ * `<root>/.deepagents/extensions/`, so these two keys decide whether
6891
+ * repository-authored Python is imported into the agent process at all —
6892
+ * the same class of knob as `[startup].mode`, and one that ships defaulting to
6893
+ * a prompt a user can accept. Loose so a key added upstream passes through
6894
+ * verbatim.
6895
+ *
6896
+ * `extensions.extra_paths` is deliberately absent: it names machine-local files
6897
+ * and directories, which do not belong in a committed
6898
+ * `.rulesync/permissions.jsonc`, and it only ever widens what loads.
6899
+ *
6900
+ * @see https://docs.langchain.com/oss/deepagents/code/configuration
6901
+ */
6902
+ const DeepagentsExtensionsOverrideSchema = z.looseObject({
6903
+ enabled: z.optional(z.boolean()),
6904
+ trust: z.optional(z.enum([
6905
+ "ask",
6906
+ "always",
6907
+ "never"
6908
+ ]))
6909
+ });
6910
+ /**
6830
6911
  * The `[shell].allow_list` array itself is rulesync-owned, driven by the
6831
6912
  * shared `permission.bash` block, so it has no key here.
6832
6913
  */
6833
6914
  const DeepagentsPermissionsOverrideSchema = z.looseObject({
6834
6915
  permission: z.optional(ToolScopedPermissionSchema),
6835
- startup: z.optional(DeepagentsStartupOverrideSchema)
6916
+ startup: z.optional(DeepagentsStartupOverrideSchema),
6917
+ extensions: z.optional(DeepagentsExtensionsOverrideSchema)
6836
6918
  });
6837
6919
  /**
6838
6920
  * The actions Junie's allowlist accepts. Verified against the shipped Junie
@@ -7843,8 +7925,9 @@ const RulesyncRuleFrontmatterSchema = z.object({
7843
7925
  agentsmd: z.optional(z.looseObject({ subprojectPath: z.optional(z.string()) })),
7844
7926
  claudecode: z.optional(z.looseObject({ paths: z.optional(z.array(z.string())) })),
7845
7927
  codebuddy: z.optional(z.looseObject({
7846
- paths: z.optional(z.array(z.string())),
7928
+ paths: z.optional(z.union([z.string(), z.array(z.string())])),
7847
7929
  alwaysApply: z.optional(z.boolean()),
7930
+ enabled: z.optional(z.boolean()),
7848
7931
  description: z.optional(z.string())
7849
7932
  })),
7850
7933
  cursor: z.optional(z.looseObject({
@@ -9137,6 +9220,11 @@ const RulesyncSkillFrontmatterSchema = z.looseObject({
9137
9220
  license: z.optional(z.string()),
9138
9221
  compatibility: z.optional(z.union([z.string(), z.looseObject({})])),
9139
9222
  metadata: z.optional(z.looseObject({}))
9223
+ })),
9224
+ zcode: z.optional(z.looseObject({
9225
+ when_to_use: z.optional(z.string()),
9226
+ license: z.optional(z.string()),
9227
+ metadata: z.optional(z.looseObject({}))
9140
9228
  }))
9141
9229
  });
9142
9230
  /**
@@ -12387,14 +12475,15 @@ const KIMI_CODE_CONFIG_DECLARATION = {
12387
12475
  };
12388
12476
  /**
12389
12477
  * ZCode's settings file, which also carries model/theme/permission keys
12390
- * rulesync does not own. The workspace copy (`<project>/.zcode/config.json`)
12391
- * and the user copy (`~/.zcode/cli/config.json`) are the same file with the
12392
- * same owners, so the declaration is written once and shared a policy edit
12393
- * cannot land on one scope only. `mcp` is owned as a whole key because the
12394
- * writer recomputes it from the existing file (non-`servers` siblings carried
12395
- * over) before applying the patch.
12396
- */
12397
- const ZCODE_CONFIG_DECLARATION = {
12478
+ * rulesync does not own. Both copies are the user's primary ZCode config, so
12479
+ * every writer refuses to read-modify-write a file it could not parse rather
12480
+ * than replacing it with generated output. `mcp` and `hooks` are owned as
12481
+ * whole keys because their writers recompute each from the existing file
12482
+ * (non-owned siblings carried over) before applying the patch. ZCode never
12483
+ * executes workspace config hooks, so the workspace copy is declared with
12484
+ * `mcp` alone and the user copy adds `hooks`.
12485
+ */
12486
+ const ZCODE_WORKSPACE_CONFIG_DECLARATION = {
12398
12487
  format: "json",
12399
12488
  invalidRootPolicy: "error",
12400
12489
  features: { mcp: {
@@ -12402,6 +12491,16 @@ const ZCODE_CONFIG_DECLARATION = {
12402
12491
  ownedKeys: ["mcp"]
12403
12492
  } }
12404
12493
  };
12494
+ const ZCODE_USER_CONFIG_DECLARATION = {
12495
+ ...ZCODE_WORKSPACE_CONFIG_DECLARATION,
12496
+ features: {
12497
+ ...ZCODE_WORKSPACE_CONFIG_DECLARATION.features,
12498
+ hooks: {
12499
+ kind: "replace-owned-keys",
12500
+ ownedKeys: ["hooks"]
12501
+ }
12502
+ }
12503
+ };
12405
12504
  /**
12406
12505
  * What the two Claude Code settings files have in common: both are plain JSON,
12407
12506
  * and both validate against the one published schema, which the gateway
@@ -12659,8 +12758,8 @@ const SHARED_CONFIG_OWNERSHIP = {
12659
12758
  ownedKeys: ["mcp_servers", "schema_version"]
12660
12759
  } }
12661
12760
  },
12662
- ".zcode/config.json": ZCODE_CONFIG_DECLARATION,
12663
- ".zcode/cli/config.json": ZCODE_CONFIG_DECLARATION,
12761
+ ".zcode/config.json": ZCODE_WORKSPACE_CONFIG_DECLARATION,
12762
+ ".zcode/cli/config.json": ZCODE_USER_CONFIG_DECLARATION,
12664
12763
  ".kiro/agents/default.json": {
12665
12764
  format: "json",
12666
12765
  features: {
@@ -17708,6 +17807,20 @@ const ROO_MCP_FILE_NAME = "mcp.json";
17708
17807
  */
17709
17808
  const ROO_MODE_SLUG_PATTERN = /^[a-zA-Z0-9-]+$/;
17710
17809
  /**
17810
+ * `~/.roo/rules/AGENTS.md` — where the root rule goes in global scope.
17811
+ *
17812
+ * Roo/Zoo Code discover the agent-rules files (`AGENTS.md`, `AGENT.md`,
17813
+ * `AGENTS.local.md`) in the **workspace only**: `loadAllAgentRulesFiles` reads
17814
+ * `cwd`, plus subdirectories holding a `.roo` folder when `enableSubfolderRules`
17815
+ * is on. There is no home-directory branch, so a `~/AGENTS.md` is never read.
17816
+ * The global rules directory `~/.roo/rules/` *is* loaded, so the root rule is
17817
+ * written there alongside the non-root files rather than to an unread path.
17818
+ * The basename is kept so the file still reads as the root overview.
17819
+ * @see https://github.com/Zoo-Code-Org/Zoo-Code/blob/main/src/core/prompts/sections/custom-instructions.ts
17820
+ * @see https://github.com/Zoo-Code-Org/Zoo-Code/blob/main/src/services/roo-config/index.ts
17821
+ */
17822
+ const ROO_GLOBAL_ROOT_RULE_FILE_NAME = "AGENTS.md";
17823
+ /**
17711
17824
  * `.roo/rules-{mode}/` — the mode-specific rule directory Roo/Zoo Code load
17712
17825
  * INSTEAD of `.roo/rules/` while that mode is active. The relative path is the
17713
17826
  * same in global scope, where it resolves under `~/.roo/`.
@@ -18324,6 +18437,8 @@ const ZCODE_SKILLS_DIR_PATH = join(ZCODE_DIR, "skills");
18324
18437
  const ZCODE_CONFIG_FILE_NAME = "config.json";
18325
18438
  const ZCODE_GLOBAL_CONFIG_DIR_PATH = join(ZCODE_DIR, "cli");
18326
18439
  const ZCODE_MCP_SERVERS_KEY = "servers";
18440
+ const ZCODE_HOOKS_CONFIG_KEY = "hooks";
18441
+ const ZCODE_HOOKS_EVENTS_KEY = "events";
18327
18442
  const ZCODE_AGENTS_DIR_PATH = join(ZCODE_DIR, "agents");
18328
18443
  //#endregion
18329
18444
  //#region src/features/commands/zcode-command.ts
@@ -19196,19 +19311,124 @@ function generateAmpPluginCode({ config, supportedEvents, eventMap }) {
19196
19311
  lines.push("}", "");
19197
19312
  return lines.join("\n");
19198
19313
  }
19314
+ /**
19315
+ * Name of the ownership record written next to the hooks destination it
19316
+ * describes (e.g. `.claude/.rulesync-hooks-lock.json`). Only written when hook
19317
+ * preservation is enabled, so a project that never opts in never sees it.
19318
+ */
19319
+ const HOOKS_OWNERSHIP_LOCK_FILE_NAME = ".rulesync-hooks-lock.json";
19320
+ const OwnedHookRefSchema = z.object({
19321
+ event: z.string(),
19322
+ matcher: z.optional(z.string()),
19323
+ identity: z.string()
19324
+ });
19325
+ const HooksOwnershipLockSchema = z.object({
19326
+ lockfileVersion: z.number(),
19327
+ owned: z.array(OwnedHookRefSchema)
19328
+ });
19329
+ /**
19330
+ * The comparable form of a hook entry: identical keys mean "the same hook, in
19331
+ * the same place". Encoded as JSON so no separator can collide with a matcher
19332
+ * or an identity that happens to contain one.
19333
+ */
19334
+ function ownedHookKey({ event, matcher, identity }) {
19335
+ return JSON.stringify([
19336
+ event,
19337
+ matcher ?? null,
19338
+ identity
19339
+ ]);
19340
+ }
19341
+ /**
19342
+ * Read the previous run's owned set. Anything unreadable — missing, empty,
19343
+ * malformed, or written by a different lock version — yields an empty set,
19344
+ * which makes every existing handler look third-party and therefore preserved.
19345
+ */
19346
+ function parseHooksOwnershipLock(content) {
19347
+ if (content === null || content.trim() === "") return /* @__PURE__ */ new Set();
19348
+ let parsed;
19349
+ try {
19350
+ parsed = JSON.parse(content);
19351
+ } catch {
19352
+ return /* @__PURE__ */ new Set();
19353
+ }
19354
+ const result = HooksOwnershipLockSchema.safeParse(parsed);
19355
+ if (!result.success || result.data.lockfileVersion !== 1) return /* @__PURE__ */ new Set();
19356
+ return new Set(result.data.owned.map((ref) => ownedHookKey(ref)));
19357
+ }
19358
+ function compareOwnedHookRefs(a, b) {
19359
+ const left = ownedHookKey(a);
19360
+ const right = ownedHookKey(b);
19361
+ if (left === right) return 0;
19362
+ return left < right ? -1 : 1;
19363
+ }
19364
+ /** Serialize the owned set in a stable order so regenerating produces no diff. */
19365
+ function serializeHooksOwnershipLock(owned) {
19366
+ return `${JSON.stringify({
19367
+ lockfileVersion: 1,
19368
+ owned: [...owned].toSorted(compareOwnedHookRefs)
19369
+ }, null, 2)}\n`;
19370
+ }
19371
+ /**
19372
+ * The ownership record itself. It is a rulesync by-product rather than tool
19373
+ * configuration, so it is deletable: dropping the target should take it along.
19374
+ */
19375
+ var HooksOwnershipLockFile = class extends ToolFile {
19376
+ isDeletable() {
19377
+ return true;
19378
+ }
19379
+ validate() {
19380
+ return {
19381
+ success: true,
19382
+ error: null
19383
+ };
19384
+ }
19385
+ };
19386
+ /** Build the lock file that records what this run generated. */
19387
+ function buildHooksOwnershipLockFile({ outputRoot, relativeDirPath, owned }) {
19388
+ return new HooksOwnershipLockFile({
19389
+ outputRoot,
19390
+ relativeDirPath,
19391
+ relativeFilePath: HOOKS_OWNERSHIP_LOCK_FILE_NAME,
19392
+ fileContent: serializeHooksOwnershipLock(owned),
19393
+ validate: false
19394
+ });
19395
+ }
19199
19396
  //#endregion
19200
19397
  //#region src/features/hooks/tool-hooks.ts
19201
19398
  var ToolHooks = class extends ToolFile {
19399
+ ownedHookRefs;
19202
19400
  constructor(params) {
19203
19401
  super({
19204
19402
  ...params,
19205
19403
  validate: true
19206
19404
  });
19405
+ this.ownedHookRefs = params.ownedHookRefs;
19207
19406
  if (params.validate) {
19208
19407
  const result = this.validate();
19209
19408
  if (!result.success) throw result.error;
19210
19409
  }
19211
19410
  }
19411
+ /**
19412
+ * The ownership record for this destination, or nothing when preservation is
19413
+ * off — in which case rulesync owns the whole list and needs no record.
19414
+ */
19415
+ getOwnershipLockFiles() {
19416
+ if (this.ownedHookRefs === void 0) return [];
19417
+ return [buildHooksOwnershipLockFile({
19418
+ outputRoot: this.getOutputRoot(),
19419
+ relativeDirPath: this.getRelativeDirPath(),
19420
+ owned: this.ownedHookRefs
19421
+ })];
19422
+ }
19423
+ /**
19424
+ * Whether the adapter can keep handlers it did not generate. Destinations
19425
+ * rulesync owns outright (plugin bundles) must answer `false`: there is no
19426
+ * third party writing into them, and preserving there would only make
19427
+ * removals impossible.
19428
+ */
19429
+ static supportsPreserveUnowned() {
19430
+ return false;
19431
+ }
19212
19432
  static getSettablePaths(_options) {
19213
19433
  throw new Error("Please implement this method in the subclass.");
19214
19434
  }
@@ -20645,6 +20865,292 @@ var AugmentcodeHooks = class AugmentcodeHooks extends ToolHooks {
20645
20865
  }
20646
20866
  };
20647
20867
  //#endregion
20868
+ //#region src/features/hooks/preserve-unowned-hook-commands.ts
20869
+ /**
20870
+ * Read the `hooks` value from a destination JSON file.
20871
+ *
20872
+ * `malformed` separates "the file has no hooks" from "the file could not be
20873
+ * read", so a caller that is about to preserve content can say out loud that
20874
+ * it is replacing a file it failed to parse.
20875
+ */
20876
+ function parseExistingHooksValue(existingContent) {
20877
+ if (existingContent.trim() === "") return {
20878
+ hooks: void 0,
20879
+ malformed: false
20880
+ };
20881
+ try {
20882
+ const parsed = JSON.parse(existingContent);
20883
+ return {
20884
+ hooks: isPlainObject$1(parsed) ? parsed.hooks : void 0,
20885
+ malformed: false
20886
+ };
20887
+ } catch {
20888
+ return {
20889
+ hooks: void 0,
20890
+ malformed: true
20891
+ };
20892
+ }
20893
+ }
20894
+ /**
20895
+ * Produce the destination hooks list, optionally keeping handlers rulesync does
20896
+ * not own.
20897
+ *
20898
+ * Ownership is decided by `previouslyOwned` — the identities recorded in the
20899
+ * destination's ownership lock on the previous run — and never by guessing from
20900
+ * the command text. That is what makes removal work: a handler rulesync wrote
20901
+ * before and no longer generates is retracted, while a handler rulesync has
20902
+ * never written is left alone. Without a lock (the first run after opting in)
20903
+ * nothing is retracted, which is the non-destructive direction.
20904
+ *
20905
+ * Callers must pass the destination's shape. Plugin destinations are fully
20906
+ * owned by rulesync and must not call this.
20907
+ */
20908
+ function mergeGeneratedHookLists({ existingContent, generatedHooks, shape, preserveUnowned, previouslyOwned, logger }) {
20909
+ const owned = collectOwnedRefs({
20910
+ generatedHooks,
20911
+ shape
20912
+ });
20913
+ if (!preserveUnowned) return {
20914
+ hooks: generatedHooks,
20915
+ owned
20916
+ };
20917
+ const { hooks: existingHooks, malformed } = parseExistingHooksValue(existingContent);
20918
+ if (malformed) logger?.warn("Replacing hooks wholesale: the existing file is not valid JSON, so no third-party hook in it can be preserved.");
20919
+ return {
20920
+ hooks: preserveUnownedHookCommands({
20921
+ existingHooks,
20922
+ generatedHooks,
20923
+ shape,
20924
+ owned,
20925
+ previouslyOwned: previouslyOwned ?? /* @__PURE__ */ new Set(),
20926
+ logger
20927
+ }),
20928
+ owned
20929
+ };
20930
+ }
20931
+ /**
20932
+ * Merge the destination's existing handlers into the generated set.
20933
+ *
20934
+ * Each existing handler falls into exactly one of three cases:
20935
+ * - this run generates it → the generated copy stands, the existing one is dropped;
20936
+ * - a previous run generated it and this one does not → it is retracted, with a warning;
20937
+ * - neither → it is unowned, and kept.
20938
+ */
20939
+ function preserveUnownedHookCommands({ existingHooks, generatedHooks, shape, owned, previouslyOwned, logger }) {
20940
+ const result = cloneGeneratedHooks({
20941
+ generatedHooks,
20942
+ shape
20943
+ });
20944
+ if (!isPlainObject$1(existingHooks)) return result;
20945
+ const seen = new Set(owned.map((ref) => ownedHookKey(ref)));
20946
+ for (const [event, existingValue] of Object.entries(existingHooks)) {
20947
+ if (isPrototypePollutionKey(event)) continue;
20948
+ if (!Array.isArray(existingValue)) {
20949
+ warnSkip({
20950
+ logger,
20951
+ event,
20952
+ expected: "an array of hook entries"
20953
+ });
20954
+ continue;
20955
+ }
20956
+ const merged = shape === "matcher-groups" ? mergeMatcherGroups({
20957
+ existing: existingValue,
20958
+ generated: result[event] ?? [],
20959
+ event,
20960
+ seen,
20961
+ previouslyOwned,
20962
+ logger
20963
+ }) : mergeFlatHandlers({
20964
+ existing: existingValue,
20965
+ generated: result[event] ?? [],
20966
+ event,
20967
+ seen,
20968
+ previouslyOwned,
20969
+ logger
20970
+ });
20971
+ if (merged.length === 0) delete result[event];
20972
+ else result[event] = merged;
20973
+ }
20974
+ return result;
20975
+ }
20976
+ function cloneGeneratedHooks({ generatedHooks, shape }) {
20977
+ const result = Object.create(null);
20978
+ for (const [event, value] of Object.entries(generatedHooks)) {
20979
+ if (isPrototypePollutionKey(event) || !Array.isArray(value)) continue;
20980
+ result[event] = shape === "matcher-groups" ? value.map((group) => cloneMatcherGroup(group)) : value.map((handler) => cloneHandler(handler));
20981
+ }
20982
+ return result;
20983
+ }
20984
+ /** The identities of every handler in the generated set, in destination order. */
20985
+ function collectOwnedRefs({ generatedHooks, shape }) {
20986
+ const owned = [];
20987
+ for (const [event, value] of Object.entries(generatedHooks)) {
20988
+ if (isPrototypePollutionKey(event) || !Array.isArray(value)) continue;
20989
+ if (shape === "flat") {
20990
+ for (const handler of value) if (isPlainObject$1(handler)) owned.push({
20991
+ event,
20992
+ identity: handlerIdentity(handler)
20993
+ });
20994
+ continue;
20995
+ }
20996
+ for (const group of value) {
20997
+ if (!isMatcherGroup(group)) continue;
20998
+ const matcher = matcherKey(group.matcher);
20999
+ for (const handler of group.hooks) if (isPlainObject$1(handler)) owned.push({
21000
+ event,
21001
+ matcher,
21002
+ identity: handlerIdentity(handler)
21003
+ });
21004
+ }
21005
+ }
21006
+ return owned;
21007
+ }
21008
+ function mergeMatcherGroups({ existing, generated, event, seen, previouslyOwned, logger }) {
21009
+ const merged = [...generated];
21010
+ for (const group of existing) {
21011
+ if (!isMatcherGroup(group)) {
21012
+ warnSkip({
21013
+ logger,
21014
+ event,
21015
+ expected: "a matcher group"
21016
+ });
21017
+ continue;
21018
+ }
21019
+ const matcher = matcherKey(group.matcher);
21020
+ const preserved = group.hooks.filter((handler) => shouldPreserve({
21021
+ handler,
21022
+ event,
21023
+ matcher,
21024
+ seen,
21025
+ previouslyOwned,
21026
+ logger
21027
+ })).map((handler) => cloneHandler(handler));
21028
+ if (preserved.length === 0) continue;
21029
+ const target = merged.find((candidate) => isMatcherGroup(candidate) && matcherKey(candidate.matcher) === matcher);
21030
+ if (target !== void 0 && isMatcherGroup(target)) target.hooks.push(...preserved);
21031
+ else merged.push({
21032
+ ...omitPrototypePollutionKeys(group),
21033
+ hooks: preserved
21034
+ });
21035
+ }
21036
+ return merged;
21037
+ }
21038
+ function mergeFlatHandlers({ existing, generated, event, seen, previouslyOwned, logger }) {
21039
+ const preserved = [];
21040
+ for (const handler of existing) {
21041
+ if (isMatcherGroup(handler)) {
21042
+ warnSkip({
21043
+ logger,
21044
+ event,
21045
+ expected: "a flat handler"
21046
+ });
21047
+ continue;
21048
+ }
21049
+ if (!shouldPreserve({
21050
+ handler,
21051
+ event,
21052
+ matcher: void 0,
21053
+ seen,
21054
+ previouslyOwned,
21055
+ logger
21056
+ })) continue;
21057
+ preserved.push(cloneHandler(handler));
21058
+ }
21059
+ return [...generated, ...preserved];
21060
+ }
21061
+ function isMatcherGroup(value) {
21062
+ return isPlainObject$1(value) && Array.isArray(value.hooks);
21063
+ }
21064
+ function cloneMatcherGroup(group) {
21065
+ if (!isMatcherGroup(group)) return cloneHandler(group);
21066
+ return {
21067
+ ...group,
21068
+ hooks: group.hooks.map((handler) => cloneHandler(handler))
21069
+ };
21070
+ }
21071
+ function cloneHandler(handler) {
21072
+ return isPlainObject$1(handler) ? omitPrototypePollutionKeys(handler) : handler;
21073
+ }
21074
+ /**
21075
+ * Decide one existing handler, and remember it so an exact duplicate later in
21076
+ * the same event is not appended twice.
21077
+ */
21078
+ function shouldPreserve({ handler, event, matcher, seen, previouslyOwned, logger }) {
21079
+ if (!isPlainObject$1(handler)) {
21080
+ warnSkip({
21081
+ logger,
21082
+ event,
21083
+ expected: "a hook object"
21084
+ });
21085
+ return false;
21086
+ }
21087
+ const identity = handlerIdentity(handler);
21088
+ const key = ownedHookKey({
21089
+ event,
21090
+ matcher,
21091
+ identity
21092
+ });
21093
+ if (seen.has(key)) return false;
21094
+ if (previouslyOwned.has(key)) {
21095
+ logger?.warn(`Removing hook rulesync no longer generates on ${event}: ${identity}`);
21096
+ return false;
21097
+ }
21098
+ seen.add(key);
21099
+ logger?.warn(`Preserving unowned hook on ${event}: ${identity}`);
21100
+ return true;
21101
+ }
21102
+ /**
21103
+ * A handler's identity is its action, not its whole shape: two entries running
21104
+ * the same command are the same hook even if their timeouts differ, so the
21105
+ * generated one replaces the existing one instead of doubling it. Shapes with
21106
+ * no recognizable action fall back to their full structure, which keeps them
21107
+ * comparable across runs — the alternative, no identity at all, made them
21108
+ * accumulate on every generate.
21109
+ */
21110
+ function handlerIdentity(handler) {
21111
+ const type = typeof handler.type === "string" ? handler.type : inferredType(handler);
21112
+ switch (type) {
21113
+ case "http": return taggedIdentity("http", handler.url) ?? structuralIdentity(handler);
21114
+ case "mcp_tool": return mcpToolIdentity(handler) ?? structuralIdentity(handler);
21115
+ case "prompt":
21116
+ case "agent": return taggedIdentity(type, handler.prompt) ?? structuralIdentity(handler);
21117
+ case "function": return taggedIdentity("function", handler.name) ?? structuralIdentity(handler);
21118
+ default: return taggedIdentity("command", handler.command) ?? taggedIdentity("prompt", handler.prompt) ?? structuralIdentity(handler);
21119
+ }
21120
+ }
21121
+ function inferredType(handler) {
21122
+ return typeof handler.url === "string" ? "http" : "command";
21123
+ }
21124
+ function taggedIdentity(kind, value) {
21125
+ return typeof value === "string" && value !== "" ? `${kind}:${value}` : void 0;
21126
+ }
21127
+ function mcpToolIdentity(handler) {
21128
+ const server = typeof handler.server === "string" ? handler.server : "";
21129
+ const tool = typeof handler.tool === "string" ? handler.tool : "";
21130
+ if (server === "" && tool === "") return;
21131
+ return `mcp_tool:${server}:${tool}:${stableStringify$1(handler.input)}`;
21132
+ }
21133
+ function structuralIdentity(handler) {
21134
+ return `json:${stableStringify$1(handler)}`;
21135
+ }
21136
+ /** Key-order-independent JSON, so an identity survives a rewritten file. */
21137
+ function stableStringify$1(value) {
21138
+ if (Array.isArray(value)) return `[${value.map((entry) => stableStringify$1(entry)).join(",")}]`;
21139
+ 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(",")}}`;
21140
+ return JSON.stringify(value) ?? "null";
21141
+ }
21142
+ /**
21143
+ * Matcher groups are matched by the whole matcher value, not just strings: a
21144
+ * non-string matcher is a shape rulesync does not emit, and collapsing every
21145
+ * such group onto one key would merge unrelated third-party groups together.
21146
+ */
21147
+ function matcherKey(matcher) {
21148
+ return matcher === void 0 ? "" : stableStringify$1(matcher);
21149
+ }
21150
+ function warnSkip({ logger, event, expected }) {
21151
+ logger?.warn(`Skipping existing hook entry on ${event}: expected ${expected}`);
21152
+ }
21153
+ //#endregion
20648
21154
  //#region src/features/hooks/claudecode-hooks.ts
20649
21155
  const CLAUDE_CONVERTER_CONFIG = {
20650
21156
  supportedEvents: CLAUDE_HOOK_EVENTS,
@@ -20713,7 +21219,7 @@ const CLAUDE_CONVERTER_CONFIG = {
20713
21219
  commandOnly: true
20714
21220
  }]
20715
21221
  };
20716
- var ClaudecodeHooks = class extends ToolHooks {
21222
+ var ClaudecodeHooks = class ClaudecodeHooks extends ToolHooks {
20717
21223
  constructor(params) {
20718
21224
  super({
20719
21225
  ...params,
@@ -20748,7 +21254,13 @@ var ClaudecodeHooks = class extends ToolHooks {
20748
21254
  validate
20749
21255
  });
20750
21256
  }
20751
- static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
21257
+ static supportsPreserveUnowned() {
21258
+ return true;
21259
+ }
21260
+ static async getAuxiliaryFiles({ toolHooks } = {}) {
21261
+ return toolHooks instanceof ClaudecodeHooks ? toolHooks.getOwnershipLockFiles() : [];
21262
+ }
21263
+ static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, preserveUnowned = false, logger }) {
20752
21264
  const paths = this.getSettablePaths({ global });
20753
21265
  const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
20754
21266
  const existingContent = await readFileContentOrNull(filePath) ?? JSON.stringify({}, null, 2);
@@ -20759,11 +21271,20 @@ var ClaudecodeHooks = class extends ToolHooks {
20759
21271
  converterConfig: this.getConverterConfig(),
20760
21272
  logger
20761
21273
  });
21274
+ const preserving = preserveUnowned && this.supportsPreserveUnowned();
21275
+ const merged = mergeGeneratedHookLists({
21276
+ existingContent,
21277
+ generatedHooks: claudeHooks,
21278
+ shape: "matcher-groups",
21279
+ preserveUnowned: preserving,
21280
+ previouslyOwned: preserving ? parseHooksOwnershipLock(await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, HOOKS_OWNERSHIP_LOCK_FILE_NAME))) : void 0,
21281
+ logger
21282
+ });
20762
21283
  const fileContent = applySharedConfigPatch({
20763
21284
  fileKey: CLAUDE_SETTINGS_SHARED_FILE_KEY,
20764
21285
  feature: "hooks",
20765
21286
  existingContent,
20766
- patch: { hooks: claudeHooks },
21287
+ patch: { hooks: merged.hooks },
20767
21288
  filePath
20768
21289
  });
20769
21290
  return new this({
@@ -20771,6 +21292,7 @@ var ClaudecodeHooks = class extends ToolHooks {
20771
21292
  relativeDirPath: paths.relativeDirPath,
20772
21293
  relativeFilePath: paths.relativeFilePath,
20773
21294
  fileContent,
21295
+ ownedHookRefs: preserving ? merged.owned : void 0,
20774
21296
  validate
20775
21297
  });
20776
21298
  }
@@ -20828,6 +21350,14 @@ var ClaudecodePluginHooks = class extends ClaudecodeHooks {
20828
21350
  projectDirVar: "$CLAUDE_PLUGIN_ROOT"
20829
21351
  };
20830
21352
  }
21353
+ /**
21354
+ * The plugin bundle is generated in full by rulesync — nothing else writes
21355
+ * into it — so preserving there would buy nothing and cost the ability to
21356
+ * remove a hook.
21357
+ */
21358
+ static supportsPreserveUnowned() {
21359
+ return false;
21360
+ }
20831
21361
  static getSettablePaths() {
20832
21362
  return {
20833
21363
  relativeDirPath: CLAUDECODE_PLUGIN_HOOKS_DIR,
@@ -21258,21 +21788,30 @@ var CodexcliHooks = class CodexcliHooks extends ToolHooks {
21258
21788
  validate
21259
21789
  });
21260
21790
  }
21261
- static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
21791
+ static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, preserveUnowned = false, logger }) {
21262
21792
  const paths = CodexcliHooks.getSettablePaths({ global });
21793
+ const existingContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
21263
21794
  const config = rulesyncHooks.getJson();
21264
- const codexHooks = canonicalToToolHooks({
21265
- config,
21266
- toolOverrideHooks: config.codexcli?.hooks,
21267
- converterConfig: CODEXCLI_CONVERTER_CONFIG,
21795
+ const merged = mergeGeneratedHookLists({
21796
+ existingContent,
21797
+ generatedHooks: canonicalToToolHooks({
21798
+ config,
21799
+ toolOverrideHooks: config.codexcli?.hooks,
21800
+ converterConfig: CODEXCLI_CONVERTER_CONFIG,
21801
+ logger
21802
+ }),
21803
+ shape: "matcher-groups",
21804
+ preserveUnowned,
21805
+ previouslyOwned: preserveUnowned ? parseHooksOwnershipLock(await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, HOOKS_OWNERSHIP_LOCK_FILE_NAME))) : void 0,
21268
21806
  logger
21269
21807
  });
21270
- const fileContent = JSON.stringify({ hooks: codexHooks }, null, 2);
21808
+ const fileContent = JSON.stringify({ hooks: merged.hooks }, null, 2);
21271
21809
  return new CodexcliHooks({
21272
21810
  outputRoot,
21273
21811
  relativeDirPath: paths.relativeDirPath,
21274
21812
  relativeFilePath: paths.relativeFilePath,
21275
21813
  fileContent,
21814
+ ownedHookRefs: preserveUnowned ? merged.owned : void 0,
21276
21815
  validate
21277
21816
  });
21278
21817
  }
@@ -21308,8 +21847,11 @@ var CodexcliHooks = class CodexcliHooks extends ToolHooks {
21308
21847
  validate: false
21309
21848
  });
21310
21849
  }
21311
- static async getAuxiliaryFiles({ outputRoot = process.cwd() } = {}) {
21312
- return [await CodexcliConfigToml.fromOutputRoot({ outputRoot })];
21850
+ static supportsPreserveUnowned() {
21851
+ return true;
21852
+ }
21853
+ static async getAuxiliaryFiles({ outputRoot = process.cwd(), toolHooks } = {}) {
21854
+ return [await CodexcliConfigToml.fromOutputRoot({ outputRoot }), ...toolHooks instanceof CodexcliHooks ? toolHooks.getOwnershipLockFiles() : []];
21313
21855
  }
21314
21856
  };
21315
21857
  //#endregion
@@ -21914,7 +22456,7 @@ var CursorHooks = class CursorHooks extends ToolHooks {
21914
22456
  validate
21915
22457
  });
21916
22458
  }
21917
- static fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false }) {
22459
+ static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, preserveUnowned = false, logger }) {
21918
22460
  const config = rulesyncHooks.getJson();
21919
22461
  const cursorSupported = new Set(CURSOR_HOOK_EVENTS);
21920
22462
  const sharedHooks = {};
@@ -21941,21 +22483,36 @@ var CursorHooks = class CursorHooks extends ToolHooks {
21941
22483
  }));
21942
22484
  if (mappedDefs.length > 0) mappedHooks[cursorEventName] = mappedDefs;
21943
22485
  }
22486
+ const paths = CursorHooks.getSettablePaths({ global });
22487
+ const merged = mergeGeneratedHookLists({
22488
+ existingContent: await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "",
22489
+ generatedHooks: mappedHooks,
22490
+ shape: "flat",
22491
+ preserveUnowned,
22492
+ previouslyOwned: preserveUnowned ? parseHooksOwnershipLock(await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, HOOKS_OWNERSHIP_LOCK_FILE_NAME))) : void 0,
22493
+ logger
22494
+ });
21944
22495
  const cursorConfig = {
21945
22496
  version: config.version ?? 1,
21946
- hooks: mappedHooks
22497
+ hooks: merged.hooks
21947
22498
  };
21948
22499
  const fileContent = JSON.stringify(cursorConfig, null, 2);
21949
- const paths = CursorHooks.getSettablePaths({ global });
21950
22500
  return new CursorHooks({
21951
22501
  outputRoot,
21952
22502
  relativeDirPath: paths.relativeDirPath,
21953
22503
  relativeFilePath: paths.relativeFilePath,
21954
22504
  fileContent,
22505
+ ownedHookRefs: preserveUnowned ? merged.owned : void 0,
21955
22506
  validate,
21956
22507
  rulesyncHooks
21957
22508
  });
21958
22509
  }
22510
+ static supportsPreserveUnowned() {
22511
+ return true;
22512
+ }
22513
+ static async getAuxiliaryFiles({ toolHooks } = {}) {
22514
+ return toolHooks instanceof CursorHooks ? toolHooks.getOwnershipLockFiles() : [];
22515
+ }
21959
22516
  toRulesyncHooks() {
21960
22517
  const content = this.getFileContent();
21961
22518
  const parsed = JSON.parse(content);
@@ -22323,7 +22880,7 @@ var DevinHooks = class DevinHooks extends ToolHooks {
22323
22880
  * adapter so the settings reader in `src/utils/` can name the same list without
22324
22881
  * importing a feature.
22325
22882
  *
22326
- * @see https://docs.factory.ai/cli/configuration/settings
22883
+ * @see https://docs.factory.ai/droid-cli/settings
22327
22884
  */
22328
22885
  const FACTORYDROID_OVERRIDE_KEYS = [
22329
22886
  "commandBlocklist",
@@ -22331,6 +22888,8 @@ const FACTORYDROID_OVERRIDE_KEYS = [
22331
22888
  "sandbox",
22332
22889
  "mcpPolicy",
22333
22890
  "mcpAutonomyOverrides",
22891
+ "mcpAutonomyUrlOverrides",
22892
+ "builtInToolAutonomyOverrides",
22334
22893
  "enableDroidShield",
22335
22894
  "sessionDefaultSettings",
22336
22895
  "maxAutonomyLevel",
@@ -22338,7 +22897,10 @@ const FACTORYDROID_OVERRIDE_KEYS = [
22338
22897
  "interactionMode",
22339
22898
  "extraKnownMarketplaces",
22340
22899
  "enabledPlugins",
22900
+ "strictEnabledPlugins",
22901
+ "strictKnownMarketplaces",
22341
22902
  "hooksDisabled",
22903
+ "allowManagedHooksOnly",
22342
22904
  "disabledSkills",
22343
22905
  "modelPolicy",
22344
22906
  "missionPolicy"
@@ -25195,6 +25757,186 @@ var VibeHooks = class VibeHooks extends ToolHooks {
25195
25757
  }
25196
25758
  };
25197
25759
  //#endregion
25760
+ //#region src/features/hooks/zcode-hooks.ts
25761
+ /**
25762
+ * Single spelling of the config.json codec/policy, matching the one in
25763
+ * ZcodeMcp: fail closed on an unparseable root rather than replacing the
25764
+ * user's primary ZCode config with generated output.
25765
+ */
25766
+ function parseZcodeConfig$1(fileContent, filePath) {
25767
+ return parseSharedConfig({
25768
+ format: "json",
25769
+ fileContent,
25770
+ filePath,
25771
+ invalidRootPolicy: "error"
25772
+ });
25773
+ }
25774
+ /**
25775
+ * Drop ZCode's native `process` hooks from an `events` map before the shared
25776
+ * import converter runs. The converter would coerce the unknown type to
25777
+ * `command` and import the executable alone — losing the `args` vector that
25778
+ * *is* the process hook's command line — so the imported hook would run
25779
+ * something else than the file states. Matcher groups left without hooks are
25780
+ * dropped with them.
25781
+ */
25782
+ function stripProcessHooks({ events, logger }) {
25783
+ if (!isRecord$1(events)) return events;
25784
+ const result = Object.create(null);
25785
+ for (const [eventName, entries] of Object.entries(events)) {
25786
+ if (!Array.isArray(entries)) {
25787
+ result[eventName] = entries;
25788
+ continue;
25789
+ }
25790
+ const kept = [];
25791
+ for (const entry of entries) {
25792
+ if (!isRecord$1(entry) || !Array.isArray(entry.hooks)) {
25793
+ kept.push(entry);
25794
+ continue;
25795
+ }
25796
+ const hooks = entry.hooks.filter((hook) => {
25797
+ if (isRecord$1(hook) && hook.type === "process") {
25798
+ 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.`);
25799
+ return false;
25800
+ }
25801
+ return true;
25802
+ });
25803
+ if (hooks.length > 0) kept.push({
25804
+ ...entry,
25805
+ hooks
25806
+ });
25807
+ }
25808
+ result[eventName] = kept;
25809
+ }
25810
+ return result;
25811
+ }
25812
+ const ZCODE_CONVERTER_CONFIG = {
25813
+ supportedEvents: ZCODE_HOOK_EVENTS,
25814
+ canonicalToToolEventNames: CANONICAL_TO_ZCODE_EVENT_NAMES,
25815
+ toolToCanonicalEventNames: ZCODE_TO_CANONICAL_EVENT_NAMES,
25816
+ projectDirVar: "",
25817
+ noMatcherEvents: /* @__PURE__ */ new Set(["beforeSubmitPrompt", "stop"]),
25818
+ supportedHookTypes: /* @__PURE__ */ new Set(["command"]),
25819
+ booleanPassthroughFields: [{
25820
+ canonical: "async",
25821
+ tool: "async"
25822
+ }, {
25823
+ canonical: "enabled",
25824
+ tool: "enabled"
25825
+ }],
25826
+ stringPassthroughFields: [{
25827
+ canonical: "statusMessage",
25828
+ tool: "statusMessage"
25829
+ }, {
25830
+ canonical: "shell",
25831
+ tool: "shell",
25832
+ commandOnly: true
25833
+ }],
25834
+ wildcardMatcherMeansAll: true
25835
+ };
25836
+ /**
25837
+ * ZCode hooks.
25838
+ *
25839
+ * ZCode reads configuration-file hooks from the `hooks` block of its user
25840
+ * config file, `~/.zcode/cli/config.json`. Workspace config hooks are never
25841
+ * executed — the workspace file is ignored regardless of `hooks.enabled` — so
25842
+ * rulesync treats ZCode hooks as global-only. The event map is nested under
25843
+ * `hooks.events` beside the user-tunable `enabled` and `timeoutMs` siblings,
25844
+ * which are carried over while `events` is replaced.
25845
+ *
25846
+ * @see https://zcode.z.ai/en/docs
25847
+ */
25848
+ var ZcodeHooks = class ZcodeHooks extends ToolHooks {
25849
+ json;
25850
+ constructor(params) {
25851
+ super(params);
25852
+ this.json = parseZcodeConfig$1(this.fileContent ?? "{}", join(this.relativeDirPath, this.relativeFilePath));
25853
+ }
25854
+ isDeletable() {
25855
+ return false;
25856
+ }
25857
+ static getSettablePaths(_options = {}) {
25858
+ return {
25859
+ relativeDirPath: ZCODE_GLOBAL_CONFIG_DIR_PATH,
25860
+ relativeFilePath: ZCODE_CONFIG_FILE_NAME
25861
+ };
25862
+ }
25863
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
25864
+ const paths = this.getSettablePaths({ global });
25865
+ const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{}";
25866
+ return new ZcodeHooks({
25867
+ outputRoot,
25868
+ relativeDirPath: paths.relativeDirPath,
25869
+ relativeFilePath: paths.relativeFilePath,
25870
+ fileContent,
25871
+ validate,
25872
+ global
25873
+ });
25874
+ }
25875
+ static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
25876
+ const paths = this.getSettablePaths({ global });
25877
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
25878
+ const existingContent = await readFileContentOrNull(filePath) ?? "{}";
25879
+ const existing = parseZcodeConfig$1(existingContent, filePath);
25880
+ const config = rulesyncHooks.getJson();
25881
+ const events = canonicalToToolHooks({
25882
+ config,
25883
+ toolOverrideHooks: config.zcode?.hooks,
25884
+ converterConfig: ZCODE_CONVERTER_CONFIG,
25885
+ logger
25886
+ });
25887
+ const existingHooks = isRecord$1(existing["hooks"]) ? existing[ZCODE_HOOKS_CONFIG_KEY] : {};
25888
+ const shouldStateEnabled = Object.keys(events).length > 0 && existingHooks.enabled === void 0;
25889
+ return new ZcodeHooks({
25890
+ outputRoot,
25891
+ relativeDirPath: paths.relativeDirPath,
25892
+ relativeFilePath: paths.relativeFilePath,
25893
+ fileContent: applySharedConfigPatch({
25894
+ fileKey: sharedConfigFileKey(paths),
25895
+ feature: "hooks",
25896
+ existingContent,
25897
+ patch: { [ZCODE_HOOKS_CONFIG_KEY]: {
25898
+ ...existingHooks,
25899
+ ...shouldStateEnabled ? { enabled: true } : {},
25900
+ [ZCODE_HOOKS_EVENTS_KEY]: events
25901
+ } },
25902
+ filePath
25903
+ }),
25904
+ validate,
25905
+ global
25906
+ });
25907
+ }
25908
+ toRulesyncHooks({ logger } = {}) {
25909
+ const hooks = toolHooksToCanonical({
25910
+ hooks: stripProcessHooks({
25911
+ events: (isRecord$1(this.json["hooks"]) ? this.json[ZCODE_HOOKS_CONFIG_KEY] : {})[ZCODE_HOOKS_EVENTS_KEY],
25912
+ logger
25913
+ }),
25914
+ converterConfig: ZCODE_CONVERTER_CONFIG,
25915
+ logger
25916
+ });
25917
+ return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
25918
+ hooks,
25919
+ overrideKey: "zcode"
25920
+ }), null, 2) });
25921
+ }
25922
+ validate() {
25923
+ return {
25924
+ success: true,
25925
+ error: null
25926
+ };
25927
+ }
25928
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
25929
+ return new ZcodeHooks({
25930
+ outputRoot,
25931
+ relativeDirPath,
25932
+ relativeFilePath,
25933
+ fileContent: JSON.stringify({ [ZCODE_HOOKS_CONFIG_KEY]: { [ZCODE_HOOKS_EVENTS_KEY]: {} } }, null, 2),
25934
+ validate: false,
25935
+ global
25936
+ });
25937
+ }
25938
+ };
25939
+ //#endregion
25198
25940
  //#region src/features/hooks/hooks-processor.ts
25199
25941
  const HooksProcessorToolTargetSchema = z.enum(hooksProcessorToolTargetTuple);
25200
25942
  /**
@@ -25266,8 +26008,12 @@ const HOOKS_OVERRIDE_KEY_ALIASES = {
25266
26008
  "kiro-cli": KIRO_HOOKS_OVERRIDE_KEY,
25267
26009
  "kiro-ide": KIRO_HOOKS_OVERRIDE_KEY
25268
26010
  };
25269
- /** The targets writing the standalone `.kiro/hooks/*.json` v1 format. */
25270
- const KIRO_STANDALONE_HOOKS_TARGETS = /* @__PURE__ */ new Set(["kiro-cli", "kiro-ide"]);
26011
+ /** The targets whose hooks format carries a per-hook on-disk enable flag. */
26012
+ const PER_HOOK_ENABLED_TARGETS = /* @__PURE__ */ new Set([
26013
+ "kiro-cli",
26014
+ "kiro-ide",
26015
+ "zcode"
26016
+ ]);
25271
26017
  const toolHooksFactories = /* @__PURE__ */ new Map([
25272
26018
  ["amp", {
25273
26019
  class: AmpHooks,
@@ -25616,6 +26362,17 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
25616
26362
  supportedEvents: GROKCLI_HOOK_EVENTS,
25617
26363
  supportedHookTypes: ["command", "http"],
25618
26364
  supportsMatcher: true
26365
+ }],
26366
+ ["zcode", {
26367
+ class: ZcodeHooks,
26368
+ meta: {
26369
+ supportsProject: false,
26370
+ supportsGlobal: true,
26371
+ supportsImport: true
26372
+ },
26373
+ supportedEvents: ZCODE_HOOK_EVENTS,
26374
+ supportedHookTypes: ["command"],
26375
+ supportsMatcher: true
25619
26376
  }]
25620
26377
  ]);
25621
26378
  const hooksProcessorToolTargets = [...toolHooksFactories.entries()].filter(([, f]) => f.meta.supportsProject).map(([t]) => t);
@@ -25625,7 +26382,8 @@ const hooksProcessorToolTargetsGlobalImportable = [...toolHooksFactories.entries
25625
26382
  var HooksProcessor = class extends FeatureProcessor {
25626
26383
  toolTarget;
25627
26384
  global;
25628
- constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, global = false, dryRun = false, logger }) {
26385
+ preserveUnownedHooks;
26386
+ constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, global = false, dryRun = false, preserveUnownedHooks = false, logger }) {
25629
26387
  super({
25630
26388
  outputRoot,
25631
26389
  inputRoots,
@@ -25636,6 +26394,7 @@ var HooksProcessor = class extends FeatureProcessor {
25636
26394
  if (!result.success) throw new Error(`Invalid tool target for HooksProcessor: ${toolTarget}. ${formatError(result.error)}`);
25637
26395
  this.toolTarget = result.data;
25638
26396
  this.global = global;
26397
+ this.preserveUnownedHooks = preserveUnownedHooks;
25639
26398
  }
25640
26399
  async loadRulesyncFiles() {
25641
26400
  const relativePaths = getRulesyncSourceCandidates({ paths: RulesyncHooks.getSettablePaths() }).map((candidate) => candidate.relativeFilePath);
@@ -25727,14 +26486,14 @@ var HooksProcessor = class extends FeatureProcessor {
25727
26486
  }
25728
26487
  for (const [hookType, events] of unsupportedTypeToEvents) this.logger.warn(`Skipped ${hookType}-type hook(s) for ${this.toolTarget} (not supported): ${Array.from(events).join(", ")}`);
25729
26488
  }
25730
- if (!KIRO_STANDALONE_HOOKS_TARGETS.has(this.toolTarget)) {
26489
+ if (!PER_HOOK_ENABLED_TARGETS.has(this.toolTarget)) {
25731
26490
  const skippedEvents = new Set(unsupportedEventNames({
25732
26491
  factory,
25733
26492
  sharedHooks,
25734
26493
  effectiveHooks
25735
26494
  }));
25736
26495
  const eventsWithDisabledHooks = Object.entries(sharedHooks).filter(([event, defs]) => !skippedEvents.has(event) && defs.some((def) => def.enabled === false)).map(([event]) => event);
25737
- 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(", ")}`);
26496
+ 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(", ")}`);
25738
26497
  }
25739
26498
  const eventsWithUnsupportedMatcher = unsupportedMatcherEventNames({
25740
26499
  factory,
@@ -25746,6 +26505,7 @@ var HooksProcessor = class extends FeatureProcessor {
25746
26505
  rulesyncHooks,
25747
26506
  validate: true,
25748
26507
  global: this.global,
26508
+ preserveUnowned: this.preserveUnownedHooks,
25749
26509
  logger: withToolTargetPrefix({
25750
26510
  logger: this.logger,
25751
26511
  toolTarget: this.toolTarget
@@ -26181,6 +26941,7 @@ var ClineIgnore = class ClineIgnore extends ToolIgnore {
26181
26941
  //#region src/constants/crush-paths.ts
26182
26942
  const CRUSH_RULE_FILE_NAME = "CRUSH.md";
26183
26943
  const CRUSH_GLOBAL_DIR = join(".config", "crush");
26944
+ const CRUSH_LOCAL_RULE_FILE_NAME = "CRUSH.local.md";
26184
26945
  const CRUSH_IGNORE_FILE_NAME = ".crushignore";
26185
26946
  const CRUSH_SKILLS_PROJECT_DIR = join(".crush", "skills");
26186
26947
  const CRUSH_SKILLS_GLOBAL_DIR = join(CRUSH_GLOBAL_DIR, "skills");
@@ -32401,9 +33162,16 @@ function lookupTransport(map, key) {
32401
33162
  }
32402
33163
  /**
32403
33164
  * Read the "put this server's own instructions into the agent's prompt" flag
32404
- * off an unfiltered canonical entry, under either spelling. Anything other
32405
- * than a literal `true` reads as not enabled, matching Rovo Dev, where the key
32406
- * is absent by default.
33165
+ * off an unfiltered canonical entry, under either spelling. `undefined` means
33166
+ * the entry says nothing, which is Rovo Dev's own default.
33167
+ *
33168
+ * The flag is a **tri-state**, not a switch that only exists when on. Atlassian
33169
+ * inverted the default on 2026-09-02: a server's instructions are now surfaced
33170
+ * when the key is absent or `true`, and `false` is what suppresses them — the
33171
+ * opposite of the opt-in wording this adapter was first written against, where
33172
+ * absent and `false` meant the same thing. So `false` has to travel end to end;
33173
+ * collapsing it away is what makes a suppression impossible to author, and
33174
+ * erases one on import.
32407
33175
  *
32408
33176
  * The canonical key decides whenever it is present, the way codex resolves the
32409
33177
  * same two-spelling conflict for `experimental_environment`. OR-ing the two
@@ -32411,28 +33179,25 @@ function lookupTransport(map, key) {
32411
33179
  * `enable_instructions: true` copied out of Atlassian's docs — fail-open, on
32412
33180
  * the one key whose whole purpose is a trust decision.
32413
33181
  *
33182
+ * A non-boolean under either spelling reads as `undefined` rather than as a
33183
+ * suppression: Rovo Dev surfaces the instructions for anything that is not
33184
+ * `false`, so writing `false` there would invent a restriction the tool is not
33185
+ * applying. (The canonical key is a strict boolean in the schema, so only the
33186
+ * raw spelling can carry one.)
33187
+ *
32414
33188
  * `isPlainObject` rather than `isRecord`: this walks a user-supplied key set,
32415
33189
  * so a `constructor` entry must not resolve up the prototype chain.
32416
33190
  */
32417
33191
  function readEnableInstructions(rawServer) {
32418
- if (!isPlainObject$1(rawServer)) return false;
32419
- if (rawServer.rovodevEnableInstructions !== void 0) return rawServer.rovodevEnableInstructions === true;
32420
- return rawServer.enable_instructions === true;
32421
- }
32422
- /**
32423
- * Names that were emitted with `enable_instructions: true` during one
32424
- * `fromRulesyncMcp` call, so the run can say so once rather than per server.
32425
- * Atlassian gates this key on trust, and it is the only thing generate writes
32426
- * that widens what steers the model — the quietest possible write is the wrong
32427
- * one for it.
32428
- */
32429
- function warnEnabledInstructions(names, logger) {
32430
- if (names.length === 0) return;
32431
- 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.`);
33192
+ if (!isPlainObject$1(rawServer)) return;
33193
+ const canonical = rawServer.rovodevEnableInstructions;
33194
+ if (canonical !== void 0) return typeof canonical === "boolean" ? canonical : void 0;
33195
+ const raw = rawServer.enable_instructions;
33196
+ return typeof raw === "boolean" ? raw : void 0;
32432
33197
  }
32433
33198
  function toRovodevServer(name, server, logger) {
32434
33199
  const { type, transport, disabled: _disabled, rovodevEnableInstructions, ...rest } = server;
32435
- if (rovodevEnableInstructions === true) rest.enable_instructions = true;
33200
+ if (typeof rovodevEnableInstructions === "boolean") rest.enable_instructions = rovodevEnableInstructions;
32436
33201
  const declared = typeof transport === "string" ? transport : typeof type === "string" ? type : void 0;
32437
33202
  if (declared === void 0) return rest;
32438
33203
  const mapped = lookupTransport(CANONICAL_TO_ROVODEV_TRANSPORT, declared);
@@ -32447,7 +33212,7 @@ function toRovodevServer(name, server, logger) {
32447
33212
  }
32448
33213
  function fromRovodevServer(server) {
32449
33214
  const { transport, enable_instructions: enableInstructions, ...rest } = server;
32450
- if (enableInstructions === true) rest.rovodevEnableInstructions = true;
33215
+ if (typeof enableInstructions === "boolean") rest.rovodevEnableInstructions = enableInstructions;
32451
33216
  if (typeof transport !== "string") return rest;
32452
33217
  const mapped = lookupTransport(ROVODEV_TO_CANONICAL_TRANSPORT, transport);
32453
33218
  return mapped === void 0 ? rest : {
@@ -32888,10 +33653,10 @@ var RovodevMcp = class RovodevMcp extends ToolMcp {
32888
33653
  canWriteDisableToggle = false;
32889
33654
  }
32890
33655
  const mcpServers = Object.fromEntries(Object.entries(rulesyncMcp.getMcpServers()).map(([name, server]) => {
32891
- const rawServer = rulesyncMcp.getRawMcpServer(name);
33656
+ const enableInstructions = readEnableInstructions(rulesyncMcp.getRawMcpServer(name));
32892
33657
  const record = {
32893
33658
  ...server,
32894
- ...readEnableInstructions(rawServer) && { rovodevEnableInstructions: true }
33659
+ ...enableInstructions !== void 0 && { rovodevEnableInstructions: enableInstructions }
32895
33660
  };
32896
33661
  if (record.disabled === true && !canWriteDisableToggle) {
32897
33662
  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.`);
@@ -32900,7 +33665,6 @@ var RovodevMcp = class RovodevMcp extends ToolMcp {
32900
33665
  const converted = toRovodevServer(name, record, logger);
32901
33666
  return converted === null ? null : [name, converted];
32902
33667
  }).filter((entry) => entry !== null));
32903
- warnEnabledInstructions(Object.entries(mcpServers).filter(([, server]) => server.enable_instructions === true).map(([name]) => name), logger);
32904
33668
  const rovodevConfig = {
32905
33669
  ...json,
32906
33670
  mcpServers
@@ -39044,6 +39808,34 @@ const DEEPAGENTS_STARTUP_BOOLEAN_DEFAULTS = {
39044
39808
  yolo_switcher: true,
39045
39809
  read_project_dotenv: true
39046
39810
  };
39811
+ /**
39812
+ * `[extensions]` gates dcode's Python extension system. `discover_extensions`
39813
+ * auto-loads `*.py` from the user's `~/.deepagents/extensions/` and from the
39814
+ * checked-out project's `<root>/.deepagents/extensions/`, so this table decides
39815
+ * whether a cloned repository's Python is imported into the agent process.
39816
+ */
39817
+ const EXTENSIONS_TABLE_KEY = "extensions";
39818
+ /**
39819
+ * Keys lifted back into the override on import. `extensions.extra_paths` is
39820
+ * deliberately absent: it names machine-local files and directories, which do
39821
+ * not belong in a committed `.rulesync/permissions.jsonc`, and it only ever
39822
+ * widens what loads.
39823
+ */
39824
+ const DEEPAGENTS_EXTENSIONS_KEYS = ["enabled", "trust"];
39825
+ /** `TrustPolicy` in `extensions/settings.py`; dcode reads nothing else. */
39826
+ const DEEPAGENTS_EXTENSION_TRUST_POLICIES = [
39827
+ "ask",
39828
+ "always",
39829
+ "never"
39830
+ ];
39831
+ /**
39832
+ * `[extensions].enabled` defaults to `true` upstream, so writing `true` on a
39833
+ * machine that has not set it changes nothing — the same reasoning as
39834
+ * `DEEPAGENTS_STARTUP_BOOLEAN_DEFAULTS`.
39835
+ *
39836
+ * @see https://github.com/langchain-ai/deepagents `config_manifest.py`
39837
+ */
39838
+ const DEEPAGENTS_EXTENSIONS_ENABLED_DEFAULT = true;
39047
39839
  const ALLOW_ALL_SENTINEL = "all";
39048
39840
  const RECOMMENDED_SENTINEL = "recommended";
39049
39841
  const GLOB_CHARACTERS_PATTERN = /[*?[\]]/;
@@ -39113,7 +39905,10 @@ const TRAILING_ARGUMENT_WILDCARD_PATTERN = /:\*$/;
39113
39905
  * `DeepagentsPermissionsOverrideSchema`): `[startup].mode`
39114
39906
  * (`manual` / `auto` / `yolo`), `[startup].yolo_switcher` and
39115
39907
  * `[startup].read_project_dotenv` are merged into the file on generate and
39116
- * lifted back into the override on import.
39908
+ * lifted back into the override on import. The same override carries
39909
+ * `[extensions].enabled` and `[extensions].trust` (`ask` / `always` / `never`),
39910
+ * which decide whether the Python in a checked-out project's
39911
+ * `.deepagents/extensions/` is imported into the agent process.
39117
39912
  *
39118
39913
  * On **import** the allowlist comes back as `bash` `allow` rules named by the
39119
39914
  * executable — skipping any entry dcode could not match in the first place, so
@@ -39215,6 +40010,13 @@ var DeepagentsPermissions = class DeepagentsPermissions extends ToolPermissions
39215
40010
  filePath,
39216
40011
  logger
39217
40012
  });
40013
+ const extensionsOverride = config.deepagents?.extensions;
40014
+ if (isPlainObject$1(extensionsOverride) && Object.keys(extensionsOverride).length > 0) mergeExtensionsOverride({
40015
+ settings,
40016
+ extensionsOverride,
40017
+ filePath,
40018
+ logger
40019
+ });
39218
40020
  return new DeepagentsPermissions({
39219
40021
  outputRoot,
39220
40022
  relativeDirPath: paths.relativeDirPath,
@@ -39239,8 +40041,15 @@ var DeepagentsPermissions = class DeepagentsPermissions extends ToolPermissions
39239
40041
  startup: isPlainObject$1(settings[STARTUP_TABLE_KEY]) ? settings[STARTUP_TABLE_KEY] : {},
39240
40042
  selfPath
39241
40043
  });
40044
+ const extensionsOverride = liftExtensionsOverride({
40045
+ extensions: isPlainObject$1(settings[EXTENSIONS_TABLE_KEY]) ? settings[EXTENSIONS_TABLE_KEY] : {},
40046
+ selfPath
40047
+ });
39242
40048
  const result = { ...config };
39243
- if (Object.keys(startupOverride).length > 0) result.deepagents = { startup: startupOverride };
40049
+ const deepagents = {};
40050
+ if (Object.keys(startupOverride).length > 0) deepagents.startup = startupOverride;
40051
+ if (Object.keys(extensionsOverride).length > 0) deepagents.extensions = extensionsOverride;
40052
+ if (Object.keys(deepagents).length > 0) result.deepagents = deepagents;
39244
40053
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
39245
40054
  }
39246
40055
  validate() {
@@ -39327,57 +40136,192 @@ function warnAboutUnwrittenBashRules({ allowAll, requestedAllowAll, askPatterns,
39327
40136
  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.");
39328
40137
  }
39329
40138
  /**
39330
- * Lift the `[startup]` keys rulesync models back into the `deepagents`
39331
- * override, keeping only values dcode itself accepts.
40139
+ * Lift the keys rulesync models from one `config.toml` table back into the
40140
+ * `deepagents` override, keeping only values dcode itself accepts.
39332
40141
  *
39333
- * A `mode` outside the three approval modes, or a non-boolean where a switch
39334
- * belongs, is `Invalid` upstream — dcode ignores it and falls back to its
39335
- * default. Importing it anyway would record a setting the tool is not applying
39336
- * and, because the canonical schema is stricter than TOML, would write a
39337
- * `.rulesync/permissions.jsonc` the next `rulesync generate` cannot even read.
40142
+ * A value outside what the option's own parser reads is `Invalid` upstream
40143
+ * dcode ignores it and falls back to its default. Importing it anyway would
40144
+ * record a setting the tool is not applying and, because the canonical schema
40145
+ * is stricter than TOML, would write a `.rulesync/permissions.jsonc` the next
40146
+ * `rulesync generate` cannot even read.
40147
+ *
40148
+ * `normalize` returns the value to keep, or `null` for one dcode would not
40149
+ * read — a wrapper rather than the bare value, so a legitimately falsy setting
40150
+ * is not mistaken for a rejection.
39338
40151
  */
39339
- function liftStartupOverride({ startup, selfPath }) {
39340
- const startupOverride = {};
40152
+ function liftOverrideTable({ table, tableKey, keys, normalize, selfPath }) {
40153
+ const override = {};
39341
40154
  const rejected = [];
39342
- for (const key of DEEPAGENTS_STARTUP_KEYS) {
39343
- const value = startup[key];
40155
+ for (const key of keys) {
40156
+ const value = table[key];
39344
40157
  if (value === void 0) continue;
39345
- if (key === "mode" ? DEEPAGENTS_STARTUP_MODES.includes(value) : typeof value === "boolean") startupOverride[key] = value;
40158
+ const normalized = normalize({
40159
+ key,
40160
+ value
40161
+ });
40162
+ if (normalized) override[key] = normalized.value;
39346
40163
  else rejected.push(`${key} = ${JSON.stringify(value)}`);
39347
40164
  }
39348
- 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.`);
39349
- return startupOverride;
40165
+ 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.`);
40166
+ return override;
39350
40167
  }
39351
40168
  /**
39352
- * Merge the `deepagents.startup` override into `[startup]`, preserving every
39353
- * other key of that table. A `startup` that is not a table at all is something
39354
- * rulesync did not write and cannot merge into, so it is left exactly as the
39355
- * user has it rather than replaced.
40169
+ * Lift the `[startup]` keys rulesync models back into the `deepagents`
40170
+ * override. A `mode` outside the three approval modes, or a non-boolean where
40171
+ * a switch belongs, is one dcode ignores.
39356
40172
  */
39357
- function mergeStartupOverride({ settings, startupOverride, filePath, logger }) {
39358
- const existingStartup = settings[STARTUP_TABLE_KEY];
39359
- if (existingStartup !== void 0 && !isPlainObject$1(existingStartup)) {
39360
- warnWithFallback(logger, `deepagents-cli: '${STARTUP_TABLE_KEY}' in ${filePath} is not a table, so the deepagents startup override was skipped rather than overwriting it.`);
40173
+ function liftStartupOverride({ startup, selfPath }) {
40174
+ return liftOverrideTable({
40175
+ table: startup,
40176
+ tableKey: STARTUP_TABLE_KEY,
40177
+ keys: DEEPAGENTS_STARTUP_KEYS,
40178
+ normalize: ({ key, value }) => {
40179
+ if (key === "mode") return DEEPAGENTS_STARTUP_MODES.includes(value) ? { value } : null;
40180
+ return typeof value === "boolean" ? { value } : null;
40181
+ },
40182
+ selfPath
40183
+ });
40184
+ }
40185
+ /**
40186
+ * Lift the `[extensions]` keys rulesync models back into the `deepagents`
40187
+ * override.
40188
+ *
40189
+ * `trust` is read the way `parse_trust_policy` reads it — trimmed and
40190
+ * lowercased — so `"Always"` comes back as the policy dcode is actually
40191
+ * applying rather than being dropped, the same courtesy the allowlist
40192
+ * sentinels already get. The canonical enum holds only the lowercase
40193
+ * spellings, so keeping the value verbatim would write a permissions file the
40194
+ * next generate could not parse.
40195
+ */
40196
+ function liftExtensionsOverride({ extensions, selfPath }) {
40197
+ return liftOverrideTable({
40198
+ table: extensions,
40199
+ tableKey: EXTENSIONS_TABLE_KEY,
40200
+ keys: DEEPAGENTS_EXTENSIONS_KEYS,
40201
+ normalize: ({ key, value }) => {
40202
+ if (key === "trust") {
40203
+ if (typeof value !== "string") return null;
40204
+ const policy = value.trim().toLowerCase();
40205
+ return DEEPAGENTS_EXTENSION_TRUST_POLICIES.includes(policy) ? { value: policy } : null;
40206
+ }
40207
+ return typeof value === "boolean" ? { value } : null;
40208
+ },
40209
+ selfPath
40210
+ });
40211
+ }
40212
+ /**
40213
+ * Merge one block of the `deepagents` override into its `config.toml` table,
40214
+ * preserving every other key of that table. A table that is not a table at all
40215
+ * is something rulesync did not write and cannot merge into, so it is left
40216
+ * exactly as the user has it rather than replaced.
40217
+ */
40218
+ function mergeOverrideTable({ settings, tableKey, override, knownKeys, isDroppedKey, warnAboutRelaxations, filePath, logger }) {
40219
+ const existing = settings[tableKey];
40220
+ if (existing !== void 0 && !isPlainObject$1(existing)) {
40221
+ warnWithFallback(logger, `deepagents-cli: '${tableKey}' in ${filePath} is not a table, so the deepagents ${tableKey} override was skipped rather than overwriting it.`);
39361
40222
  return;
39362
40223
  }
39363
- const startup = isPlainObject$1(existingStartup) ? { ...existingStartup } : {};
39364
- const previousStartup = { ...startup };
40224
+ const table = isPlainObject$1(existing) ? { ...existing } : {};
40225
+ const previous = { ...table };
39365
40226
  const writtenKeys = [];
39366
- for (const [key, value] of Object.entries(startupOverride)) {
40227
+ for (const [key, value] of Object.entries(override)) {
39367
40228
  if (isPrototypePollutionKey(key)) continue;
39368
- if (key === STARTUP_RECENT_KEY) continue;
40229
+ if (isDroppedKey?.(key)) continue;
39369
40230
  if (value === null || value === void 0) continue;
39370
- startup[key] = value;
40231
+ table[key] = value;
39371
40232
  writtenKeys.push(key);
39372
40233
  }
39373
- warnAboutStartupRelaxations({
39374
- startupOverride,
39375
- previousStartup,
40234
+ warnAboutRelaxations({
40235
+ override,
40236
+ previous,
40237
+ filePath,
40238
+ logger
40239
+ });
40240
+ warnAboutUncheckedKeys({
40241
+ tableKey,
40242
+ knownKeys,
39376
40243
  writtenKeys,
39377
40244
  filePath,
39378
40245
  logger
39379
40246
  });
39380
- if (Object.keys(startup).length > 0) settings[STARTUP_TABLE_KEY] = startup;
40247
+ if (Object.keys(table).length > 0) settings[tableKey] = table;
40248
+ }
40249
+ /**
40250
+ * Name the keys the merge wrote that rulesync does not model. The override is a
40251
+ * loose object so a key added upstream still reaches the config, but this one
40252
+ * writes into the machine's global file: what rulesync cannot judge, it at
40253
+ * least names.
40254
+ */
40255
+ function warnAboutUncheckedKeys({ tableKey, knownKeys, writtenKeys, filePath, logger }) {
40256
+ const known = new Set(knownKeys);
40257
+ const unknownKeys = writtenKeys.filter((key) => !known.has(key));
40258
+ if (unknownKeys.length === 0) return;
40259
+ 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.`);
40260
+ }
40261
+ /**
40262
+ * Merge the `deepagents.startup` override into `[startup]`.
40263
+ */
40264
+ function mergeStartupOverride({ settings, startupOverride, filePath, logger }) {
40265
+ mergeOverrideTable({
40266
+ settings,
40267
+ tableKey: STARTUP_TABLE_KEY,
40268
+ override: startupOverride,
40269
+ knownKeys: DEEPAGENTS_STARTUP_KEYS,
40270
+ isDroppedKey: (key) => key === STARTUP_RECENT_KEY,
40271
+ warnAboutRelaxations: warnAboutStartupRelaxations,
40272
+ filePath,
40273
+ logger
40274
+ });
40275
+ }
40276
+ /**
40277
+ * Merge the `deepagents.extensions` override into `[extensions]`. Nothing is
40278
+ * dropped here — `extra_paths` is not modeled, but it is a key the user may
40279
+ * legitimately want carried, so it passes through as any other unknown key
40280
+ * does, named by the unchecked-key warning.
40281
+ */
40282
+ function mergeExtensionsOverride({ settings, extensionsOverride, filePath, logger }) {
40283
+ mergeOverrideTable({
40284
+ settings,
40285
+ tableKey: EXTENSIONS_TABLE_KEY,
40286
+ override: extensionsOverride,
40287
+ knownKeys: DEEPAGENTS_EXTENSIONS_KEYS,
40288
+ warnAboutRelaxations: warnAboutExtensionsRelaxations,
40289
+ filePath,
40290
+ logger
40291
+ });
40292
+ }
40293
+ /**
40294
+ * Name the value an override key replaces, so a setting the user had turned
40295
+ * down is visible as such rather than reported as a bare new value.
40296
+ */
40297
+ function describeOverriddenValue({ key, value, previous }) {
40298
+ const previousValue = previous[key];
40299
+ return previousValue === void 0 || previousValue === value ? `${key} = ${JSON.stringify(value)}` : `${key} = ${JSON.stringify(value)} (was ${JSON.stringify(previousValue)})`;
40300
+ }
40301
+ /**
40302
+ * Warn when the `deepagents` extensions override widens what dcode loads on its
40303
+ * own, for the same reason the startup one does: this block is written into the
40304
+ * user's **global** config from a `.rulesync/permissions.jsonc` a repository
40305
+ * can carry.
40306
+ *
40307
+ * `trust` decides what happens to the Python in a checked-out project's
40308
+ * `.deepagents/extensions/` — `always` imports it into the agent process with
40309
+ * no prompt at all — and `enabled` is the switch above it. `never` and `false`
40310
+ * restrict, so neither is reported.
40311
+ */
40312
+ function warnAboutExtensionsRelaxations({ override, previous, filePath, logger }) {
40313
+ const relaxations = [];
40314
+ if (override.trust === "always") relaxations.push(describeOverriddenValue({
40315
+ key: "trust",
40316
+ value: "always",
40317
+ previous
40318
+ }));
40319
+ if (override.enabled === true && (previous.enabled ?? DEEPAGENTS_EXTENSIONS_ENABLED_DEFAULT) !== true) relaxations.push(describeOverriddenValue({
40320
+ key: "enabled",
40321
+ value: true,
40322
+ previous
40323
+ }));
40324
+ 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.`);
39381
40325
  }
39382
40326
  /**
39383
40327
  * Warn when the `deepagents` startup override relaxes what dcode may do on its
@@ -39392,12 +40336,13 @@ function mergeStartupOverride({ settings, startupOverride, filePath, logger }) {
39392
40336
  * process environment. The value being replaced is named alongside, so a
39393
40337
  * setting the user had turned down is visible as such.
39394
40338
  */
39395
- function warnAboutStartupRelaxations({ startupOverride, previousStartup, writtenKeys, filePath, logger }) {
40339
+ function warnAboutStartupRelaxations({ override: startupOverride, previous: previousStartup, filePath, logger }) {
39396
40340
  const relaxations = [];
39397
- const describe = (key, value) => {
39398
- const previous = previousStartup[key];
39399
- return previous === void 0 || previous === value ? `${key} = ${JSON.stringify(value)}` : `${key} = ${JSON.stringify(value)} (was ${JSON.stringify(previous)})`;
39400
- };
40341
+ const describe = (key, value) => describeOverriddenValue({
40342
+ key,
40343
+ value,
40344
+ previous: previousStartup
40345
+ });
39401
40346
  const mode = startupOverride.mode;
39402
40347
  if (mode === "auto" || mode === "yolo") relaxations.push(describe("mode", mode));
39403
40348
  for (const [key, upstreamDefault] of Object.entries(DEEPAGENTS_STARTUP_BOOLEAN_DEFAULTS)) {
@@ -39407,9 +40352,6 @@ function warnAboutStartupRelaxations({ startupOverride, previousStartup, written
39407
40352
  }
39408
40353
  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.`);
39409
40354
  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.`);
39410
- const known = new Set(DEEPAGENTS_STARTUP_KEYS);
39411
- const unknownKeys = writtenKeys.filter((key) => !known.has(key));
39412
- 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.`);
39413
40355
  }
39414
40356
  /**
39415
40357
  * Read `[shell].allow_list` the way dcode does: a TOML array is taken element
@@ -54030,7 +54972,10 @@ var WarpSkill = class WarpSkill extends ToolSkill {
54030
54972
  //#region src/features/skills/zcode-skill.ts
54031
54973
  const ZcodeSkillFrontmatterSchema = z.looseObject({
54032
54974
  name: z.string(),
54033
- description: z.string()
54975
+ description: z.string(),
54976
+ when_to_use: z.optional(z.string()),
54977
+ license: z.optional(z.string()),
54978
+ metadata: z.optional(z.looseObject({}))
54034
54979
  });
54035
54980
  /**
54036
54981
  * Represents a ZCode skill directory.
@@ -54093,10 +55038,16 @@ var ZcodeSkill = class ZcodeSkill extends ToolSkill {
54093
55038
  }
54094
55039
  toRulesyncSkill() {
54095
55040
  const frontmatter = this.getFrontmatter();
55041
+ const zcodeSection = {
55042
+ ...frontmatter.when_to_use !== void 0 && { when_to_use: frontmatter.when_to_use },
55043
+ ...frontmatter.license !== void 0 && { license: frontmatter.license },
55044
+ ...frontmatter.metadata !== void 0 && { metadata: frontmatter.metadata }
55045
+ };
54096
55046
  const rulesyncFrontmatter = {
54097
55047
  name: frontmatter.name,
54098
55048
  description: frontmatter.description,
54099
- targets: ["*"]
55049
+ targets: ["*"],
55050
+ ...Object.keys(zcodeSection).length > 0 && { zcode: zcodeSection }
54100
55051
  };
54101
55052
  return new RulesyncSkill({
54102
55053
  outputRoot: this.outputRoot,
@@ -54111,9 +55062,21 @@ var ZcodeSkill = class ZcodeSkill extends ToolSkill {
54111
55062
  }
54112
55063
  static fromRulesyncSkill({ outputRoot = process.cwd(), rulesyncSkill, validate = true, global = false }) {
54113
55064
  const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
55065
+ const zcodeSection = rulesyncFrontmatter.zcode;
55066
+ const license = resolveLicense({
55067
+ rootFrontmatter: rulesyncFrontmatter,
55068
+ section: zcodeSection
55069
+ });
55070
+ const metadata = resolveMetadata({
55071
+ rootFrontmatter: rulesyncFrontmatter,
55072
+ section: zcodeSection
55073
+ });
54114
55074
  const zcodeFrontmatter = {
54115
55075
  name: rulesyncFrontmatter.name,
54116
- description: rulesyncFrontmatter.description
55076
+ description: rulesyncFrontmatter.description,
55077
+ ...zcodeSection?.when_to_use !== void 0 && { when_to_use: zcodeSection.when_to_use },
55078
+ ...license !== void 0 && { license },
55079
+ ...metadata !== void 0 && { metadata }
54117
55080
  };
54118
55081
  const settablePaths = ZcodeSkill.getSettablePaths({ global });
54119
55082
  return new ZcodeSkill({
@@ -62214,10 +63177,21 @@ var ClineRule = class ClineRule extends ToolRule {
62214
63177
  */
62215
63178
  const CodebuddyRuleFrontmatterSchema = z.object({
62216
63179
  description: z.optional(z.string()),
62217
- paths: z.optional(z.array(z.string())),
62218
- alwaysApply: z.optional(z.boolean())
63180
+ paths: z.optional(z.union([z.string(), z.array(z.string())])),
63181
+ alwaysApply: z.optional(z.boolean()),
63182
+ enabled: z.optional(z.boolean())
62219
63183
  });
62220
63184
  /**
63185
+ * Normalizes the documented `string` / `string[]` shapes of `paths` to the
63186
+ * list form the rest of the adapter works with. An empty list and an empty
63187
+ * string both mean "no paths".
63188
+ */
63189
+ function normalizeCodebuddyPaths(paths) {
63190
+ if (paths === void 0) return;
63191
+ const list = (typeof paths === "string" ? [paths] : paths).filter((path) => path.trim() !== "");
63192
+ return list.length > 0 ? list : void 0;
63193
+ }
63194
+ /**
62221
63195
  * A universal glob (matching everything) is redundant on an Always Apply
62222
63196
  * rule and, paired with `alwaysApply: true`, is the same semantic conflict
62223
63197
  * `CursorRule.resolveCursorGlobs` avoids for Cursor: `alwaysApply` already
@@ -62235,7 +63209,7 @@ const UNIVERSAL_PATHS = /* @__PURE__ */ new Set(["**/*", "*"]);
62235
63209
  * Rules format:
62236
63210
  * - {project}/CODEBUDDY.md (root: true), also read from {project}/.codebuddy/CODEBUDDY.md
62237
63211
  * - {project}/.codebuddy/rules/*.md (root: false, with optional
62238
- * `description` / `paths` / `alwaysApply` frontmatter)
63212
+ * `description` / `paths` / `alwaysApply` / `enabled` frontmatter)
62239
63213
  * - Global: ~/.codebuddy/CODEBUDDY.md and ~/.codebuddy/rules/*.md
62240
63214
  *
62241
63215
  * @see https://www.codebuddy.ai/docs/cli/memory
@@ -62277,9 +63251,10 @@ var CodebuddyRule = class CodebuddyRule extends ToolRule {
62277
63251
  this.body = body;
62278
63252
  }
62279
63253
  static generateFileContent(body, frontmatter) {
62280
- if (frontmatter.description === void 0 && frontmatter.paths === void 0 && frontmatter.alwaysApply === void 0) return body;
63254
+ if (frontmatter.description === void 0 && frontmatter.paths === void 0 && frontmatter.alwaysApply === void 0 && frontmatter.enabled === void 0) return body;
62281
63255
  return stringifyFrontmatter(body, {
62282
63256
  description: frontmatter.description,
63257
+ enabled: frontmatter.enabled,
62283
63258
  alwaysApply: frontmatter.alwaysApply,
62284
63259
  paths: frontmatter.paths
62285
63260
  });
@@ -62327,10 +63302,36 @@ var CodebuddyRule = class CodebuddyRule extends ToolRule {
62327
63302
  root: isRoot
62328
63303
  });
62329
63304
  }
62330
- static resolveCodebuddyPaths({ paths, alwaysApply }) {
62331
- if (!paths || paths.length === 0) return;
62332
- if (alwaysApply && paths.every((path) => UNIVERSAL_PATHS.has(path.trim()))) return;
62333
- return paths;
63305
+ /**
63306
+ * Resolves the `paths` / `alwaysApply` pair against CodeBuddy's Rule Type
63307
+ * Determination table, which reads:
63308
+ *
63309
+ * | `alwaysApply` | `paths` | Rule type |
63310
+ * | ---------------- | --------- | ---------------------------------- |
63311
+ * | `true` (default) | any | ALWAYS — always injected |
63312
+ * | `false` | has value | MANUAL — triggered on matching file |
63313
+ * | `false` | none | not supported; the rule is dropped |
63314
+ *
63315
+ * The default being `true` is the opposite of Cursor, which the adapter was
63316
+ * modeled on: `paths` alone scopes nothing, so a rule meant to be path
63317
+ * triggered has to carry an explicit `alwaysApply: false` alongside it.
63318
+ *
63319
+ * @see https://www.codebuddy.ai/docs/cli/memory
63320
+ */
63321
+ static resolveCodebuddyRuleType({ paths, alwaysApply }) {
63322
+ const scopedPaths = paths && paths.length > 0 && !paths.every((path) => UNIVERSAL_PATHS.has(path.trim())) ? paths : void 0;
63323
+ if (alwaysApply === true) return {
63324
+ paths: scopedPaths,
63325
+ alwaysApply: true
63326
+ };
63327
+ if (scopedPaths === void 0) return {
63328
+ paths: void 0,
63329
+ alwaysApply: void 0
63330
+ };
63331
+ return {
63332
+ paths: scopedPaths,
63333
+ alwaysApply: false
63334
+ };
62334
63335
  }
62335
63336
  static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
62336
63337
  const rulesyncFrontmatter = rulesyncRule.getFrontmatter();
@@ -62347,17 +63348,17 @@ var CodebuddyRule = class CodebuddyRule extends ToolRule {
62347
63348
  root
62348
63349
  });
62349
63350
  if (!paths.nonRoot) throw new Error(`nonRoot path is not set for ${rulesyncRule.getRelativeFilePath()}`);
62350
- const codebuddyPaths = rulesyncFrontmatter.codebuddy?.paths;
63351
+ const codebuddyPaths = normalizeCodebuddyPaths(rulesyncFrontmatter.codebuddy?.paths);
62351
63352
  const globs = rulesyncFrontmatter.globs;
62352
- const alwaysApply = rulesyncFrontmatter.codebuddy?.alwaysApply;
62353
- const pathsValue = CodebuddyRule.resolveCodebuddyPaths({
63353
+ const ruleType = CodebuddyRule.resolveCodebuddyRuleType({
62354
63354
  paths: codebuddyPaths ?? (globs?.length ? globs : void 0),
62355
- alwaysApply: alwaysApply === true
63355
+ alwaysApply: rulesyncFrontmatter.codebuddy?.alwaysApply
62356
63356
  });
62357
63357
  const codebuddyFrontmatter = {
62358
63358
  description: rulesyncFrontmatter.codebuddy?.description ?? rulesyncFrontmatter.description,
62359
- paths: pathsValue,
62360
- alwaysApply
63359
+ paths: ruleType.paths,
63360
+ alwaysApply: ruleType.alwaysApply,
63361
+ enabled: rulesyncFrontmatter.codebuddy?.enabled
62361
63362
  };
62362
63363
  return new CodebuddyRule({
62363
63364
  outputRoot,
@@ -62387,17 +63388,19 @@ var CodebuddyRule = class CodebuddyRule extends ToolRule {
62387
63388
  validate: true
62388
63389
  });
62389
63390
  }
62390
- const isAlways = this.frontmatter.alwaysApply === true;
62391
- const sourcePaths = this.frontmatter.paths ?? [];
63391
+ const sourcePaths = normalizeCodebuddyPaths(this.frontmatter.paths) ?? [];
63392
+ const isAlways = this.frontmatter.alwaysApply !== false;
62392
63393
  const globs = sourcePaths.length === 0 && isAlways ? ["**/*"] : sourcePaths;
63394
+ const alwaysApply = this.frontmatter.alwaysApply ?? (sourcePaths.length > 0 ? true : void 0);
62393
63395
  const rulesyncFrontmatter = {
62394
63396
  targets,
62395
63397
  root: false,
62396
63398
  description: this.frontmatter.description,
62397
63399
  globs,
62398
- ...(this.frontmatter.paths !== void 0 || this.frontmatter.alwaysApply !== void 0 || this.frontmatter.description !== void 0) && { codebuddy: {
62399
- paths: this.frontmatter.paths,
62400
- alwaysApply: this.frontmatter.alwaysApply,
63400
+ ...(this.frontmatter.paths !== void 0 || this.frontmatter.alwaysApply !== void 0 || this.frontmatter.enabled !== void 0 || this.frontmatter.description !== void 0) && { codebuddy: {
63401
+ paths: sourcePaths.length > 0 ? sourcePaths : void 0,
63402
+ alwaysApply,
63403
+ enabled: this.frontmatter.enabled,
62401
63404
  description: this.frontmatter.description
62402
63405
  } }
62403
63406
  };
@@ -65180,33 +66183,49 @@ var ReplitRule = class ReplitRule extends ToolRule {
65180
66183
  * Supports plain Markdown without frontmatter, mode-specific rules,
65181
66184
  * and both directory-based and single-file configurations.
65182
66185
  *
65183
- * - Project scope writes the non-root directory `.roo/rules/`.
66186
+ * - Project scope writes the non-root directory `.roo/rules/`, and the root
66187
+ * rule to the workspace-root `AGENTS.md` the extension reads from `cwd`.
65184
66188
  * - Global scope writes the same non-root directory resolved under the home
65185
- * directory (`~/.roo/rules/`), which Roo loads before workspace rules.
66189
+ * directory (`~/.roo/rules/`), which Roo loads before workspace rules. The
66190
+ * root rule joins it as `~/.roo/rules/AGENTS.md`: agent-rules discovery is
66191
+ * workspace-only, so the default project-scope root would resolve to a
66192
+ * `~/AGENTS.md` nothing ever reads.
65186
66193
  * @see https://roocodeinc.github.io/Roo-Code/features/custom-instructions
65187
66194
  */
65188
66195
  var RooRule = class RooRule extends ToolRule {
65189
- static getSettablePaths(_options = {}) {
65190
- return { nonRoot: { relativeDirPath: buildToolPath(ROO_DIR, "rules", _options.excludeToolDir) } };
66196
+ static getSettablePaths({ global, excludeToolDir } = {}) {
66197
+ const rulesDirPath = buildToolPath(ROO_DIR, "rules", excludeToolDir);
66198
+ if (global) return {
66199
+ root: {
66200
+ relativeDirPath: rulesDirPath,
66201
+ relativeFilePath: ROO_GLOBAL_ROOT_RULE_FILE_NAME
66202
+ },
66203
+ nonRoot: { relativeDirPath: rulesDirPath }
66204
+ };
66205
+ return { nonRoot: { relativeDirPath: rulesDirPath } };
65191
66206
  }
65192
- static async fromFile({ outputRoot = process.cwd(), relativeFilePath, relativeDirPath: overrideDirPath, validate = true }) {
65193
- const relativeDirPath = overrideDirPath !== void 0 && RooRule.extractModeFromDirPath(overrideDirPath) !== void 0 ? overrideDirPath : this.getSettablePaths().nonRoot.relativeDirPath;
66207
+ static async fromFile({ outputRoot = process.cwd(), relativeFilePath, relativeDirPath: overrideDirPath, validate = true, global = false }) {
66208
+ const paths = this.getSettablePaths({ global });
66209
+ const relativeDirPath = overrideDirPath !== void 0 && RooRule.extractModeFromDirPath(overrideDirPath) !== void 0 ? overrideDirPath : paths.nonRoot?.relativeDirPath ?? buildToolPath(".roo", "rules");
65194
66210
  const fileContent = await readFileContent(join(outputRoot, relativeDirPath, relativeFilePath));
66211
+ const isRoot = "root" in paths && relativeDirPath === paths.root.relativeDirPath && relativeFilePath === paths.root.relativeFilePath;
65195
66212
  return new RooRule({
65196
66213
  outputRoot,
65197
66214
  relativeDirPath,
65198
66215
  relativeFilePath,
65199
66216
  fileContent,
65200
66217
  validate,
65201
- root: false
66218
+ root: isRoot
65202
66219
  });
65203
66220
  }
65204
- static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true }) {
66221
+ static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
66222
+ const paths = this.getSettablePaths({ global });
65205
66223
  const params = this.buildToolRuleParamsDefault({
65206
66224
  outputRoot,
65207
66225
  rulesyncRule,
65208
66226
  validate,
65209
- nonRootPath: this.getSettablePaths().nonRoot
66227
+ ..."root" in paths && { rootPath: paths.root },
66228
+ nonRootPath: paths.nonRoot
65210
66229
  });
65211
66230
  const mode = rulesyncRule.getFrontmatter().roo?.mode;
65212
66231
  if (!params.root && mode !== void 0 && mode !== "") {
@@ -66129,7 +67148,9 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
66129
67148
  extension: "md",
66130
67149
  supportsGlobal: true,
66131
67150
  ruleDiscoveryMode: "auto",
66132
- collisionPolicy: "fold"
67151
+ collisionPolicy: "fold",
67152
+ localRootMode: "separate-local-file",
67153
+ localRootFileName: CRUSH_LOCAL_RULE_FILE_NAME
66133
67154
  }
66134
67155
  }],
66135
67156
  ["cursor", {
@@ -66849,6 +67870,34 @@ var RulesProcessor = class extends FeatureProcessor {
66849
67870
  root: true,
66850
67871
  localRoot
66851
67872
  });
67873
+ if (isClassOrSubclassOf({
67874
+ candidate: factory.class,
67875
+ base: CodebuddyRule
67876
+ })) {
67877
+ const paths = CodebuddyRule.getSettablePaths({ global: this.global });
67878
+ return new CodebuddyRule({
67879
+ outputRoot: this.outputRoot,
67880
+ relativeDirPath: relativeDirPath ?? paths.root.relativeDirPath,
67881
+ relativeFilePath: fileName,
67882
+ frontmatter: {},
67883
+ body,
67884
+ validate: true,
67885
+ root: true,
67886
+ localRoot
67887
+ });
67888
+ }
67889
+ if (isClassOrSubclassOf({
67890
+ candidate: factory.class,
67891
+ base: CrushRule
67892
+ })) return new CrushRule({
67893
+ outputRoot: this.outputRoot,
67894
+ relativeDirPath: relativeDirPath ?? ".",
67895
+ relativeFilePath: fileName,
67896
+ fileContent: body,
67897
+ validate: true,
67898
+ root: true,
67899
+ localRoot
67900
+ });
66852
67901
  if (isClassOrSubclassOf({
66853
67902
  candidate: factory.class,
66854
67903
  base: QwencodeRule
@@ -68977,6 +70026,7 @@ async function generateHooksCore(params) {
68977
70026
  toolTarget,
68978
70027
  global: config.getGlobal(),
68979
70028
  dryRun: config.isPreviewMode(),
70029
+ preserveUnownedHooks: config.getPreserveUnownedHooks(),
68980
70030
  logger
68981
70031
  });
68982
70032
  const result = await processFeatureWithRulesyncFiles({
@@ -69446,6 +70496,6 @@ async function importChecksCore(params) {
69446
70496
  return writtenCount;
69447
70497
  }
69448
70498
  //#endregion
69449
- export { RulesyncCheckFrontmatterSchema as $, ALL_TOOL_TARGETS_WITH_WILDCARD as $t, FACTORYDROID_DIR as A, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as An, fileExists as At, RulesyncSkillFrontmatterSchema as B, stripControlCharacters as Bn, readFileContent as Bt, CODEXCLI_BASH_RULES_FILE_NAME as C, RULESYNC_PERMISSIONS_LEGACY_FILE_NAME as Cn, assertDirectoryIfExists as Ct, CLAUDECODE_MEMORIES_DIR_NAME as D, RULESYNC_RULES_RELATIVE_DIR_PATH as Dn, createTempDirectory as Dt, CLAUDECODE_LOCAL_RULE_FILE_NAME as E, RULESYNC_RELATIVE_DIR_PATH as En, checkPathTraversal as Et, AUGMENTCODE_SETTINGS_LOCAL_FILE_NAME as F, formatError as Fn, isSymlink as Ft, RulesyncIgnore as G, removeFileStrict as Gt, RulesyncRuleFrontmatterSchema as H, stripHiddenCharacters as Hn, removeDirectory as Ht, getLocalSkillDirNames as I, truncateText as In, listDirectoryEntryNames as It, resolveRulesyncSourceWritePath as J, runWithDirectoryRollback as Jt, RulesyncHooks as K, removeTempDirectory as Kt, RulesyncSubagent as L, hasDeceptiveHiddenCharacters as Ln, listFilePathsRecursively as Lt, caseFoldIdentity as M, ALL_FEATURES as Mn, getHomeDirectory as Mt, groupSpellingsByCaseFoldedIdentity as N, ALL_FEATURES_WITH_WILDCARD as Nn, isFileNotFoundError as Nt, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as O, RULESYNC_SKILLS_RELATIVE_DIR_PATH as On, directoryExists as Ot, AUGMENTCODE_DIR as P, DEPRECATED_FEATURE_REPLACEMENTS as Pn, isFileSystemError as Pt, RulesyncCheck as Q, ALL_TOOL_TARGETS as Qt, RulesyncSubagentFrontmatterSchema as R, hasEnclosingMarkOutsideKeycap as Rn, listSubdirectoryNames as Rt, ChecksProcessor as S, RULESYNC_PERMISSIONS_FILE_NAME as Sn, applyFileMode as St, CLAUDECODE_DIR as T, RULESYNC_PERMISSIONS_SCHEMA_URL as Tn, assertWritablePathInsideRoot as Tt, RulesyncPermissions as U, removeDirectoryStrict as Ut, RulesyncRule as V, stripControlCharactersKeepingLineFeeds as Vn, readFileContentOrNull as Vt, RulesyncMcp as W, removeFile as Wt, RulesyncCommand as X, writeFileBuffer as Xt, parseJsonc as Y, toPosixPath as Yt, RulesyncCommandFrontmatterSchema as Z, writeFileContent as Zt, IgnoreProcessor as _, RULESYNC_MCP_FILE_NAME as _n, withFallbackLoggerTarget as _t, getProcessorRegistryEntry as a, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as an, mergeInputRootConfigs as at, QWENCODE_DIR as b, RULESYNC_MCP_SCHEMA_URL as bn, CLIError as bt, RulesProcessor as c, RULESYNC_CONFIG_RELATIVE_FILE_PATH as cn, ConfigFileSchema as ct, CODEBUDDY_DIR as d, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as dn, findControlCharacter as dt, PACKAGING_TOOL_TARGETS as en, stringifyFrontmatter as et, CODEBUDDY_LOCAL_RULE_FILE_NAME as f, RULESYNC_HOOKS_FILE_NAME as fn, ConsoleLogger as ft, McpProcessor as g, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as gn, warnOnConflictingFlags as gt, shortenToWidth as h, RULESYNC_IGNORE_RELATIVE_FILE_PATH as hn, fallbackLogger as ht, inspectInputRoots as i, RULESYNC_AIIGNORE_FILE_NAME as in, ConfigResolver as it, FACTORYDROID_SETTINGS_LOCAL_FILE_NAME as j, parseCommaSeparatedList as jn, getFileSize as jt, CLAUDECODE_SKILLS_DIR_PATH as k, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as kn, ensureDir as kt, SubagentsProcessor as l, RULESYNC_CONFIG_SCHEMA_URL as ln, GITIGNORE_DESTINATION_KEY as lt, displayWidthOf as m, RULESYNC_HOOKS_RELATIVE_FILE_PATH as mn, WarningCollectingLogger as mt, formatSourceLoadFailure as n, CURATED_RULES_FEATURE_SUBDIR as nn, SHARED_USER_MANAGED_CONFIG_PATHS as nt, convertFromTool as o, RULESYNC_CHECKS_RELATIVE_DIR_PATH as on, resolveEffectiveInputRoots as ot, ELLIPSIS_WIDTH as p, RULESYNC_HOOKS_LEGACY_FILE_NAME as pn, JsonLogger as pt, getRulesyncSourceCandidates as q, resolvePath as qt, generate as r, MAX_FILE_SIZE as rn, SKILL_FILE_NAME as rt, isPackagingToolTarget as s, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as sn, CONFLICTING_TARGET_PAIRS as st, importFromTool as t, ToolTargetSchema as tn, loadYaml as tt, SkillsProcessor as u, RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH as un, SourceEntrySchema as ut, HooksProcessor as v, RULESYNC_MCP_LEGACY_FILE_NAME as vn, resetRunWarningState as vt, CODEXCLI_DIR as w, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as wn, assertTreeContainsNoSymlinks as wt, QWENCODE_LOCAL_RULE_FILE_NAME as x, RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH as xn, ErrorCodes as xt, CommandsProcessor as y, RULESYNC_MCP_RELATIVE_FILE_PATH as yn, withWarnOnceScope as yt, RulesyncSkill as z, quoteForLog as zn, pathEscapesRoot as zt };
70499
+ export { RulesyncCheck as $, ALL_TOOL_TARGETS as $t, CLAUDECODE_SKILLS_DIR_PATH as A, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as An, ensureDir as At, RulesyncSkill as B, quoteForLog as Bn, pathEscapesRoot as Bt, ChecksProcessor as C, RULESYNC_PERMISSIONS_FILE_NAME as Cn, applyFileMode as Ct, CLAUDECODE_LOCAL_RULE_FILE_NAME as D, RULESYNC_RELATIVE_DIR_PATH as Dn, checkPathTraversal as Dt, CLAUDECODE_DIR as E, RULESYNC_PERMISSIONS_SCHEMA_URL as En, assertWritablePathInsideRoot as Et, AUGMENTCODE_DIR as F, DEPRECATED_FEATURE_REPLACEMENTS as Fn, isFileSystemError as Ft, RulesyncMcp as G, removeFile as Gt, RulesyncRule as H, stripControlCharactersKeepingLineFeeds as Hn, readFileContentOrNull as Ht, AUGMENTCODE_SETTINGS_LOCAL_FILE_NAME as I, formatError as In, isSymlink as It, getRulesyncSourceCandidates as J, resolvePath as Jt, RulesyncIgnore as K, removeFileStrict as Kt, getLocalSkillDirNames as L, truncateText as Ln, listDirectoryEntryNames as Lt, FACTORYDROID_SETTINGS_LOCAL_FILE_NAME as M, parseCommaSeparatedList as Mn, getFileSize as Mt, caseFoldIdentity as N, ALL_FEATURES as Nn, getHomeDirectory as Nt, CLAUDECODE_MEMORIES_DIR_NAME as O, RULESYNC_RULES_RELATIVE_DIR_PATH as On, createTempDirectory as Ot, groupSpellingsByCaseFoldedIdentity as P, ALL_FEATURES_WITH_WILDCARD as Pn, isFileNotFoundError as Pt, RulesyncCommandFrontmatterSchema as Q, writeFileContent as Qt, RulesyncSubagent as R, hasDeceptiveHiddenCharacters as Rn, listFilePathsRecursively as Rt, QWENCODE_LOCAL_RULE_FILE_NAME as S, RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH as Sn, ErrorCodes as St, CODEXCLI_DIR as T, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as Tn, assertTreeContainsNoSymlinks as Tt, RulesyncRuleFrontmatterSchema as U, stripHiddenCharacters as Un, removeDirectory as Ut, RulesyncSkillFrontmatterSchema as V, stripControlCharacters as Vn, readFileContent as Vt, RulesyncPermissions as W, removeDirectoryStrict as Wt, parseJsonc as X, toPosixPath as Xt, resolveRulesyncSourceWritePath as Y, runWithDirectoryRollback as Yt, RulesyncCommand as Z, writeFileBuffer as Zt, IgnoreProcessor as _, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as _n, warnOnConflictingFlags as _t, getProcessorRegistryEntry as a, RULESYNC_AIIGNORE_FILE_NAME as an, ConfigResolver as at, CommandsProcessor as b, RULESYNC_MCP_RELATIVE_FILE_PATH as bn, withWarnOnceScope as bt, RulesProcessor as c, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as cn, CONFLICTING_TARGET_PAIRS as ct, CODEBUDDY_DIR as d, RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH as dn, SourceEntrySchema as dt, ALL_TOOL_TARGETS_WITH_WILDCARD as en, RulesyncCheckFrontmatterSchema as et, CODEBUDDY_LOCAL_RULE_FILE_NAME as f, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as fn, findControlCharacter as ft, McpProcessor as g, RULESYNC_IGNORE_RELATIVE_FILE_PATH as gn, fallbackLogger as gt, shortenToWidth as h, RULESYNC_HOOKS_RELATIVE_FILE_PATH as hn, WarningCollectingLogger as ht, inspectInputRoots as i, MAX_FILE_SIZE as in, SKILL_FILE_NAME as it, FACTORYDROID_DIR as j, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as jn, fileExists as jt, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as k, RULESYNC_SKILLS_RELATIVE_DIR_PATH as kn, directoryExists as kt, SubagentsProcessor as l, RULESYNC_CONFIG_RELATIVE_FILE_PATH as ln, ConfigFileSchema as lt, displayWidthOf as m, RULESYNC_HOOKS_LEGACY_FILE_NAME as mn, JsonLogger as mt, formatSourceLoadFailure as n, ToolTargetSchema as nn, loadYaml as nt, convertFromTool as o, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as on, mergeInputRootConfigs as ot, ELLIPSIS_WIDTH as p, RULESYNC_HOOKS_FILE_NAME as pn, ConsoleLogger as pt, RulesyncHooks as q, removeTempDirectory as qt, generate as r, CURATED_RULES_FEATURE_SUBDIR as rn, SHARED_USER_MANAGED_CONFIG_PATHS as rt, isPackagingToolTarget as s, RULESYNC_CHECKS_RELATIVE_DIR_PATH as sn, resolveEffectiveInputRoots as st, importFromTool as t, PACKAGING_TOOL_TARGETS as tn, stringifyFrontmatter as tt, SkillsProcessor as u, RULESYNC_CONFIG_SCHEMA_URL as un, GITIGNORE_DESTINATION_KEY as ut, CRUSH_LOCAL_RULE_FILE_NAME as v, RULESYNC_MCP_FILE_NAME as vn, withFallbackLoggerTarget as vt, CODEXCLI_BASH_RULES_FILE_NAME as w, RULESYNC_PERMISSIONS_LEGACY_FILE_NAME as wn, assertDirectoryIfExists as wt, QWENCODE_DIR as x, RULESYNC_MCP_SCHEMA_URL as xn, CLIError as xt, HooksProcessor as y, RULESYNC_MCP_LEGACY_FILE_NAME as yn, resetRunWarningState as yt, RulesyncSubagentFrontmatterSchema as z, hasEnclosingMarkOutsideKeycap as zn, listSubdirectoryNames as zt };
69450
70500
 
69451
- //# sourceMappingURL=import-DL8paEMP.js.map
70501
+ //# sourceMappingURL=import-BlHibyDS.js.map