rulesync 10.1.0 → 12.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -114,25 +114,25 @@ function formatError(error) {
114
114
  }
115
115
  //#endregion
116
116
  //#region src/constants/rulesync-paths.ts
117
- const { join: join$251 } = node_path.posix;
117
+ const { join: join$252 } = node_path.posix;
118
118
  const RULESYNC_CONFIG_RELATIVE_FILE_PATH = "rulesync.jsonc";
119
119
  const RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH = "rulesync.local.jsonc";
120
120
  const RULESYNC_RELATIVE_DIR_PATH = ".rulesync";
121
- const RULESYNC_RULES_RELATIVE_DIR_PATH = join$251(RULESYNC_RELATIVE_DIR_PATH, "rules");
122
- const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$251(RULESYNC_RELATIVE_DIR_PATH, "commands");
123
- const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$251(RULESYNC_RELATIVE_DIR_PATH, "subagents");
124
- const RULESYNC_MCP_RELATIVE_FILE_PATH = join$251(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
125
- const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$251(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
126
- const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$251(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
127
- const RULESYNC_MCP_JSONC_RELATIVE_FILE_PATH = join$251(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
128
- const RULESYNC_HOOKS_JSONC_RELATIVE_FILE_PATH = join$251(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
129
- const RULESYNC_PERMISSIONS_JSONC_RELATIVE_FILE_PATH = join$251(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
121
+ const RULESYNC_RULES_RELATIVE_DIR_PATH = join$252(RULESYNC_RELATIVE_DIR_PATH, "rules");
122
+ const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$252(RULESYNC_RELATIVE_DIR_PATH, "commands");
123
+ const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$252(RULESYNC_RELATIVE_DIR_PATH, "subagents");
124
+ const RULESYNC_MCP_RELATIVE_FILE_PATH = join$252(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
125
+ const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$252(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
126
+ const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$252(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
127
+ const RULESYNC_MCP_JSONC_RELATIVE_FILE_PATH = join$252(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
128
+ const RULESYNC_HOOKS_JSONC_RELATIVE_FILE_PATH = join$252(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
129
+ const RULESYNC_PERMISSIONS_JSONC_RELATIVE_FILE_PATH = join$252(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
130
130
  const RULESYNC_AIIGNORE_FILE_NAME = ".aiignore";
131
- const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$251(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
131
+ const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$252(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
132
132
  const RULESYNC_IGNORE_RELATIVE_FILE_PATH = ".rulesyncignore";
133
133
  const RULESYNC_OVERVIEW_FILE_NAME = "overview.md";
134
- const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$251(RULESYNC_RELATIVE_DIR_PATH, "skills");
135
- const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$251(RULESYNC_SKILLS_RELATIVE_DIR_PATH, ".curated");
134
+ const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$252(RULESYNC_RELATIVE_DIR_PATH, "skills");
135
+ const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$252(RULESYNC_SKILLS_RELATIVE_DIR_PATH, ".curated");
136
136
  const RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync.lock";
137
137
  const RULESYNC_MCP_FILE_NAME = "mcp.json";
138
138
  const RULESYNC_HOOKS_FILE_NAME = "hooks.json";
@@ -289,6 +289,7 @@ const subagentsProcessorToolTargetTuple = [
289
289
  "kiro-ide",
290
290
  "opencode",
291
291
  "qwencode",
292
+ "reasonix",
292
293
  "roo",
293
294
  "rovodev",
294
295
  "takt",
@@ -5118,6 +5119,9 @@ const REASONIX_SETTINGS_FILE_NAME = "settings.json";
5118
5119
  const REASONIX_COMMANDS_DIR_PATH = (0, node_path.join)(REASONIX_DIR, "commands");
5119
5120
  const REASONIX_RULE_FILE_NAME = "REASONIX.md";
5120
5121
  const REASONIX_SKILLS_DIR_PATH = (0, node_path.join)(REASONIX_DIR, "skills");
5122
+ const REASONIX_SUBAGENTS_DIR_PATH = REASONIX_SKILLS_DIR_PATH;
5123
+ const REASONIX_SUBAGENT_INVOCATION = "manual";
5124
+ const REASONIX_SUBAGENT_RUN_AS = "subagent";
5121
5125
  //#endregion
5122
5126
  //#region src/features/commands/reasonix-command.ts
5123
5127
  /**
@@ -17454,7 +17458,11 @@ const CodexApprovalPolicySchema = zod_mini.z.enum([
17454
17458
  ]);
17455
17459
  /**
17456
17460
  * Codex CLI's classic sandbox mode. Serialized as a kebab-case string in
17457
- * `.codex/config.toml`.
17461
+ * `.codex/config.toml`. Deprecated in favor of permission profiles
17462
+ * (`base_permission_profile`): Codex prioritizes these legacy sandbox keys
17463
+ * over permission profiles when both are present, so authoring one disables
17464
+ * the managed `[permissions.rulesync]` profile. Kept so existing configs
17465
+ * round-trip.
17458
17466
  * @see https://learn.chatgpt.com/docs/config-file/config-reference
17459
17467
  */
17460
17468
  const CodexSandboxModeSchema = zod_mini.z.enum([
@@ -17474,6 +17482,18 @@ const CodexApprovalsReviewerSchema = zod_mini.z.enum([
17474
17482
  "guardian_subagent"
17475
17483
  ]);
17476
17484
  /**
17485
+ * Codex CLI's built-in permission profiles that the managed
17486
+ * `[permissions.rulesync]` profile may extend. Codex ships three built-in
17487
+ * profiles (`:read-only`, `:workspace`, `:danger-full-access`; the leading
17488
+ * colon is reserved for built-ins), but `extends` rejects
17489
+ * `:danger-full-access` at config load time, so only the two extendable
17490
+ * baselines are accepted here. The value list is exported so the Codex CLI
17491
+ * translator derives its import-side baseline check from the same source.
17492
+ * @see https://learn.chatgpt.com/docs/permissions
17493
+ */
17494
+ const CODEX_BASE_PERMISSION_PROFILES = [":read-only", ":workspace"];
17495
+ const CodexBasePermissionProfileSchema = zod_mini.z.enum(CODEX_BASE_PERMISSION_PROFILES);
17496
+ /**
17477
17497
  * Codex CLI-scoped permission override.
17478
17498
  *
17479
17499
  * Codex CLI's permission surface is richer than the canonical allow/ask/deny
@@ -17482,16 +17502,37 @@ const CodexApprovalsReviewerSchema = zod_mini.z.enum([
17482
17502
  * override whose fields are written verbatim as top-level `.codex/config.toml`
17483
17503
  * keys (the override wins per key; existing sibling keys the user set directly
17484
17504
  * are preserved):
17505
+ * - `base_permission_profile` — the built-in profile the managed
17506
+ * `[permissions.rulesync]` profile extends (`:read-only` | `:workspace`).
17507
+ * Unlike the other keys it is not a top-level config key: it is emitted as
17508
+ * the profile's `extends` value. Defaults to `:workspace` when unspecified.
17485
17509
  * - `approval_policy` — `untrusted` | `on-request` (legacy alias `on-failure`) |
17486
17510
  * `never`, or a `{ granular = { … } }` table (kept verbatim; the granular
17487
17511
  * schema has required fields that are brittle to model as typed keys).
17488
- * - `sandbox_mode` `read-only` | `workspace-write` | `danger-full-access`,
17489
- * with the sibling `sandbox_workspace_write` table (`network_access`,
17490
- * `writable_roots`, …).
17512
+ * Defaults to `on-request` when neither the override nor the existing
17513
+ * config sets it.
17514
+ * - `sandbox_mode` — **deprecated.** `read-only` | `workspace-write` |
17515
+ * `danger-full-access`, with the sibling `sandbox_workspace_write` table
17516
+ * (`network_access`, `writable_roots`, …). Codex has superseded the classic
17517
+ * sandbox system with permission profiles and prioritizes these legacy keys
17518
+ * over profiles when both are present, so setting them disables the managed
17519
+ * `[permissions.rulesync]` profile. Use `base_permission_profile` and the
17520
+ * shared `permission` block instead. Still accepted so existing configs
17521
+ * round-trip.
17491
17522
  * - `apps` — per-app tool gating (`apps.<id>.tools.<tool>.approval_mode` /
17492
17523
  * `.enabled`, `apps.<id>.default_tools_approval_mode`).
17493
17524
  * - `approvals_reviewer` — the reviewer-approval surface (`user` | `auto_review`
17494
17525
  * | `guardian_subagent`), or a table for the richer reviewer config.
17526
+ * Defaults to `auto_review` when neither the override nor the existing
17527
+ * config sets it.
17528
+ * - `git_write_rules` — whether the managed profile's `:workspace_roots` table
17529
+ * emits the default `.git` carve-outs (`".git/**" = "write"` with
17530
+ * `".git/config" = "read"` kept read-only as a security guard). Codex's
17531
+ * `:workspace` baseline makes `.git` read-only, which denies basic git
17532
+ * workflows (commit/stage writes to `.git/index`, `.git/objects`, refs,
17533
+ * logs), so the carve-outs are emitted by default. Defaults to `true`; only
17534
+ * an explicit `false` suppresses them. Like `base_permission_profile` it is
17535
+ * consumed by the profile builder, not written as a top-level config key.
17495
17536
  *
17496
17537
  * Two surfaces are deliberately NOT authorable here so the override can never
17497
17538
  * clobber a feature-owned key: `mcp_servers.*` per-MCP gating is owned by the
@@ -17510,11 +17551,15 @@ const CodexApprovalsReviewerSchema = zod_mini.z.enum([
17510
17551
  */
17511
17552
  const CodexcliPermissionsOverrideSchema = zod_mini.z.looseObject({
17512
17553
  permission: zod_mini.z.optional(ToolScopedPermissionSchema),
17554
+ base_permission_profile: zod_mini.z.optional(CodexBasePermissionProfileSchema),
17513
17555
  approval_policy: zod_mini.z.optional(zod_mini.z.union([CodexApprovalPolicySchema, zod_mini.z.looseObject({})])),
17556
+ /** @deprecated Superseded by `base_permission_profile` (permission profiles). */
17514
17557
  sandbox_mode: zod_mini.z.optional(CodexSandboxModeSchema),
17558
+ /** @deprecated Superseded by `base_permission_profile` (permission profiles). */
17515
17559
  sandbox_workspace_write: zod_mini.z.optional(zod_mini.z.looseObject({})),
17516
17560
  apps: zod_mini.z.optional(zod_mini.z.looseObject({})),
17517
- approvals_reviewer: zod_mini.z.optional(zod_mini.z.union([CodexApprovalsReviewerSchema, zod_mini.z.looseObject({})]))
17561
+ approvals_reviewer: zod_mini.z.optional(zod_mini.z.union([CodexApprovalsReviewerSchema, zod_mini.z.looseObject({})])),
17562
+ git_write_rules: zod_mini.z.optional(zod_mini.z.boolean())
17518
17563
  });
17519
17564
  /**
17520
17565
  * Permissions configuration.
@@ -19483,14 +19528,16 @@ var ClinePermissions = class ClinePermissions extends ToolPermissions {
19483
19528
  const RULESYNC_PROFILE_NAME = "rulesync";
19484
19529
  const CODEX_WORKSPACE_ROOTS_KEY = ":workspace_roots";
19485
19530
  const CODEX_WORKSPACE_BASELINE = ":workspace";
19531
+ const CODEX_READ_ONLY_BASELINE = ":read-only";
19532
+ const CODEX_EXTENDABLE_BASELINES = new Set(CODEX_BASE_PERMISSION_PROFILES);
19533
+ const CODEX_DEFAULT_APPROVAL_POLICY = "on-request";
19534
+ const CODEX_DEFAULT_APPROVALS_REVIEWER = "auto_review";
19486
19535
  const CODEX_GLOB_SCAN_MAX_DEPTH = 8;
19487
- const WORKSPACE_WIDE_WRITE_PATTERNS = /* @__PURE__ */ new Set([
19488
- ".",
19489
- "./",
19490
- "**",
19491
- "./**"
19492
- ]);
19493
19536
  const CODEX_MINIMAL_KEY = ":minimal";
19537
+ const CODEX_GIT_WRITE_RULES = {
19538
+ ".git/**": "write",
19539
+ ".git/config": "read"
19540
+ };
19494
19541
  const GLOBAL_WILDCARD_DOMAIN = "*";
19495
19542
  var CodexcliPermissions = class CodexcliPermissions extends ToolPermissions {
19496
19543
  static getSettablePaths(_options = {}) {
@@ -19579,6 +19626,7 @@ var CodexcliPermissions = class CodexcliPermissions extends ToolPermissions {
19579
19626
  domainsHadUnknown
19580
19627
  });
19581
19628
  const override = extractCodexcliOverride(table);
19629
+ if (typeof profile?.extends === "string" && CODEX_EXTENDABLE_BASELINES.has(profile.extends)) override.base_permission_profile = profile.extends;
19582
19630
  const result = Object.keys(override).length > 0 ? {
19583
19631
  ...config,
19584
19632
  codexcli: override
@@ -19656,23 +19704,34 @@ function convertRulesyncToCodexProfile({ config, logger }) {
19656
19704
  }
19657
19705
  logger?.warn(`Codex CLI permissions support only read/edit/write/webfetch categories. Skipping: ${toolName}`);
19658
19706
  }
19707
+ applyDefaultGitWriteRules({
19708
+ config,
19709
+ filesystem,
19710
+ workspaceRootFilesystem
19711
+ });
19659
19712
  if (Object.keys(workspaceRootFilesystem).length > 0) {
19660
19713
  if (typeof filesystem[CODEX_WORKSPACE_ROOTS_KEY] === "string") logger?.warn(`"${CODEX_WORKSPACE_ROOTS_KEY}" is set as a direct filesystem access rule in the permissions, but it will be overwritten by workspace-root rules. Consider removing the direct "${CODEX_WORKSPACE_ROOTS_KEY}" entry.`);
19661
19714
  if (Object.keys(workspaceRootFilesystem).some((pattern) => pattern.includes("**"))) filesystem.glob_scan_max_depth = CODEX_GLOB_SCAN_MAX_DEPTH;
19662
19715
  filesystem[CODEX_WORKSPACE_ROOTS_KEY] = workspaceRootFilesystem;
19663
19716
  }
19664
- const hasWorkspaceWideWrite = Object.entries(workspaceRootFilesystem).some(([pattern, access]) => access === "write" && WORKSPACE_WIDE_WRITE_PATTERNS.has(pattern));
19717
+ const basePermissionProfile = config.codexcli?.base_permission_profile ?? CODEX_WORKSPACE_BASELINE;
19665
19718
  const hasAllowDomain = Object.values(domains).some((action) => action === "allow");
19666
19719
  const network = Object.keys(domains).length > 0 ? {
19667
19720
  ...hasAllowDomain ? { enabled: true } : {},
19668
19721
  domains
19669
19722
  } : void 0;
19670
19723
  return {
19671
- ...hasWorkspaceWideWrite ? { extends: CODEX_WORKSPACE_BASELINE } : {},
19724
+ extends: basePermissionProfile,
19672
19725
  filesystem,
19673
19726
  ...network ? { network } : {}
19674
19727
  };
19675
19728
  }
19729
+ function applyDefaultGitWriteRules({ config, filesystem, workspaceRootFilesystem }) {
19730
+ if (config.codexcli?.git_write_rules === false) return;
19731
+ if (config.codexcli?.base_permission_profile === CODEX_READ_ONLY_BASELINE) return;
19732
+ if (typeof filesystem[CODEX_WORKSPACE_ROOTS_KEY] === "string") return;
19733
+ for (const [pattern, access] of Object.entries(CODEX_GIT_WRITE_RULES)) workspaceRootFilesystem[pattern] ??= access;
19734
+ }
19676
19735
  function convertCodexProfileToRulesync({ profile, domainsHadUnknown }) {
19677
19736
  const permission = {};
19678
19737
  if (profile?.filesystem) {
@@ -19684,13 +19743,12 @@ function convertCodexProfileToRulesync({ profile, domainsHadUnknown }) {
19684
19743
  addRulesyncFilesystemRule(permission, pattern, access);
19685
19744
  continue;
19686
19745
  }
19687
- if (isCodexFilesystemRuleTable(access)) for (const [nestedPattern, nestedAccess] of Object.entries(access)) addRulesyncFilesystemRule(permission, nestedPattern, nestedAccess);
19746
+ if (isCodexFilesystemRuleTable(access)) for (const [nestedPattern, nestedAccess] of Object.entries(access)) {
19747
+ if (pattern === CODEX_WORKSPACE_ROOTS_KEY && CODEX_GIT_WRITE_RULES[nestedPattern] === nestedAccess) continue;
19748
+ addRulesyncFilesystemRule(permission, nestedPattern, nestedAccess);
19749
+ }
19688
19750
  }
19689
19751
  }
19690
- if (profile?.extends === CODEX_WORKSPACE_BASELINE) {
19691
- permission.edit ??= {};
19692
- permission.edit["."] ??= "allow";
19693
- }
19694
19752
  if (profile?.network && profile.network.enabled !== false) {
19695
19753
  const networkEnabled = profile.network.enabled === true;
19696
19754
  const importedEntries = (profile.network.domains ? Object.entries(profile.network.domains) : []).filter(([, value]) => value === "deny" || networkEnabled);
@@ -19729,10 +19787,29 @@ function toCodexProfile(value) {
19729
19787
  };
19730
19788
  }
19731
19789
  function warnAboutPreservedProfileState({ existingProfile, newProfile, existingDomainsHadUnknown, logger }) {
19732
- if (existingProfile?.extends !== void 0 && existingProfile.extends !== newProfile.extends) logger?.warn(`Existing "extends" value "${existingProfile.extends}" will be replaced by Rulesync-managed "${newProfile.extends ?? "(none)"}".`);
19790
+ if (existingProfile !== void 0 && existingProfile.extends !== newProfile.extends) logger?.warn(`Existing "extends" value "${existingProfile.extends ?? "(none)"}" will be replaced by Rulesync-managed "${newProfile.extends ?? "(none)"}".`);
19791
+ warnAboutPreservedNetworkState({
19792
+ existingProfile,
19793
+ newProfile,
19794
+ logger
19795
+ });
19796
+ if (existingDomainsHadUnknown) logger?.warn(`Existing "network.domains" contained unrecognized values. These entries were skipped and will not be imported.`);
19797
+ }
19798
+ function networkHasAllowDomain(network) {
19799
+ return Object.values(network?.domains ?? {}).some((action) => action === "allow");
19800
+ }
19801
+ function warnAboutNetworkEnabledState({ existingProfile, newProfile, logger }) {
19802
+ if (existingProfile?.network?.enabled !== void 0 && newProfile.network?.enabled === void 0 && !networkHasAllowDomain(existingProfile.network)) logger?.warn(`Preserving existing "network.enabled" from config. Review this value manually as it may enable network access beyond the Rulesync-managed domain rules.`);
19803
+ if (existingProfile?.network?.enabled === false && newProfile.network?.enabled === true) logger?.warn(`Existing "network.enabled = false" will be replaced by Rulesync-managed "enabled = true" because the canonical model contains an allow domain.`);
19804
+ }
19805
+ function warnAboutPreservedNetworkState({ existingProfile, newProfile, logger }) {
19733
19806
  if (existingProfile?.network?.unix_sockets !== void 0) logger?.warn(`Preserving existing "network.unix_sockets" from config. Review these entries manually as they may grant broad system access.`);
19807
+ warnAboutNetworkEnabledState({
19808
+ existingProfile,
19809
+ newProfile,
19810
+ logger
19811
+ });
19734
19812
  if (existingProfile?.network?.mode !== void 0) logger?.warn(`Preserving existing "network.mode" from config. Review this value manually as it may grant broader network access than the Rulesync-managed domain rules.`);
19735
- if (existingDomainsHadUnknown) logger?.warn(`Existing "network.domains" contained unrecognized values. These entries were skipped and will not be imported.`);
19736
19813
  }
19737
19814
  const MANAGED_PROFILE_KEYS = /* @__PURE__ */ new Set([
19738
19815
  "description",
@@ -19774,6 +19851,7 @@ function preserveUnmanagedProfileKeys({ rawExistingProfile, profile, logger }) {
19774
19851
  function mergeWithExistingProfile({ newProfile, existingProfile }) {
19775
19852
  if (!existingProfile) return newProfile;
19776
19853
  const mergedNetwork = { ...newProfile.network };
19854
+ if (existingProfile.network?.enabled !== void 0 && mergedNetwork.enabled === void 0 && !networkHasAllowDomain(existingProfile.network)) mergedNetwork.enabled = existingProfile.network.enabled;
19777
19855
  if (existingProfile.network?.mode !== void 0 && mergedNetwork.mode === void 0) mergedNetwork.mode = existingProfile.network.mode;
19778
19856
  if (existingProfile.network?.unix_sockets !== void 0 && mergedNetwork.unix_sockets === void 0) mergedNetwork.unix_sockets = existingProfile.network.unix_sockets;
19779
19857
  const hasNetwork = Object.keys(mergedNetwork).length > 0;
@@ -19823,20 +19901,26 @@ function isPlainObject(value) {
19823
19901
  }
19824
19902
  function computeCodexcliOverridePatch({ existing, override, logger }) {
19825
19903
  const patch = {};
19826
- if (!override) return patch;
19827
19904
  const allowed = new Set(CODEXCLI_OVERRIDE_KEYS);
19828
- for (const [key, value] of Object.entries(override)) {
19905
+ for (const [key, value] of Object.entries(override ?? {})) {
19906
+ if (key === "base_permission_profile" || key === "git_write_rules") continue;
19829
19907
  if (!allowed.has(key)) {
19830
19908
  logger?.warn(`Codex CLI permission override key "${key}" is not managed and was skipped. "permissions"/"default_permissions" are owned by the canonical permission model and "mcp_servers" gating by the MCP feature.`);
19831
19909
  continue;
19832
19910
  }
19833
19911
  if (value === void 0) continue;
19912
+ if (key === "sandbox_mode" || key === "sandbox_workspace_write") logger?.warn(`Codex CLI permission override key "${key}" is deprecated. Codex prioritizes the legacy sandbox settings over permission profiles when both are present, so it disables the generated "${RULESYNC_PROFILE_NAME}" permissions profile. Use "base_permission_profile" and the shared "permission" block instead.`);
19834
19913
  const existingValue = existing[key];
19835
19914
  patch[key] = isPlainObject(existingValue) && isPlainObject(value) ? {
19836
19915
  ...existingValue,
19837
19916
  ...value
19838
19917
  } : value;
19839
19918
  }
19919
+ const defaults = {
19920
+ approval_policy: CODEX_DEFAULT_APPROVAL_POLICY,
19921
+ approvals_reviewer: CODEX_DEFAULT_APPROVALS_REVIEWER
19922
+ };
19923
+ for (const [key, value] of Object.entries(defaults)) if (patch[key] === void 0 && existing[key] === void 0) patch[key] = value;
19840
19924
  return patch;
19841
19925
  }
19842
19926
  function extractCodexcliOverride(table) {
@@ -28459,6 +28543,24 @@ var ReasonixSkill = class ReasonixSkill extends ToolSkill {
28459
28543
  global: loaded.global
28460
28544
  });
28461
28545
  }
28546
+ /**
28547
+ * Whether the skill directory belongs to the skills feature.
28548
+ *
28549
+ * `.reasonix/skills/` is shared with the subagents feature: a directory whose
28550
+ * SKILL.md declares `runAs: subagent` is a subagent profile, not a regular
28551
+ * skill, so it must be neither imported as a skill nor deleted as an orphan
28552
+ * skill. Directories without a readable/parsable SKILL.md keep the default
28553
+ * skills-feature ownership, matching the previous behavior for such dirs.
28554
+ */
28555
+ static async isDirOwned({ outputRoot, relativeDirPath, dirName }) {
28556
+ const skillFilePath = (0, node_path.join)(outputRoot, relativeDirPath, dirName, SKILL_FILE_NAME);
28557
+ try {
28558
+ const { frontmatter } = parseFrontmatter(await readFileContent(skillFilePath), skillFilePath);
28559
+ return frontmatter["runAs"] !== REASONIX_SUBAGENT_RUN_AS;
28560
+ } catch {
28561
+ return true;
28562
+ }
28563
+ }
28462
28564
  static forDeletion({ outputRoot = process.cwd(), relativeDirPath, dirName, global = false }) {
28463
28565
  return new ReasonixSkill({
28464
28566
  outputRoot,
@@ -29757,6 +29859,11 @@ var SkillsProcessor = class extends DirFeatureProcessor {
29757
29859
  for (const dirPath of dirPaths) {
29758
29860
  const dirName = (0, node_path.basename)(dirPath);
29759
29861
  if (seenDirNames.has(dirName)) continue;
29862
+ if (factory.class.isDirOwned && !await factory.class.isDirOwned({
29863
+ outputRoot: this.outputRoot,
29864
+ relativeDirPath: root,
29865
+ dirName
29866
+ })) continue;
29760
29867
  seenDirNames.add(dirName);
29761
29868
  loadEntries.push({
29762
29869
  root,
@@ -29783,6 +29890,11 @@ var SkillsProcessor = class extends DirFeatureProcessor {
29783
29890
  const dirPaths = await findFilesByGlobs((0, node_path.join)(skillsDirPath, "*"), { type: "dir" });
29784
29891
  for (const dirPath of dirPaths) {
29785
29892
  const dirName = (0, node_path.basename)(dirPath);
29893
+ if (factory.class.isDirOwned && !await factory.class.isDirOwned({
29894
+ outputRoot: this.outputRoot,
29895
+ relativeDirPath: root,
29896
+ dirName
29897
+ })) continue;
29786
29898
  const toolSkill = factory.class.forDeletion({
29787
29899
  outputRoot: this.outputRoot,
29788
29900
  relativeDirPath: root,
@@ -33082,6 +33194,188 @@ var OpenCodeSubagent = class OpenCodeSubagent extends OpenCodeStyleSubagent {
33082
33194
  }
33083
33195
  };
33084
33196
  //#endregion
33197
+ //#region src/features/subagents/reasonix-subagent.ts
33198
+ const ReasonixSubagentFrontmatterSchema = zod_mini.z.looseObject({
33199
+ name: zod_mini.z.string(),
33200
+ description: zod_mini.z.optional(zod_mini.z.string()),
33201
+ invocation: zod_mini.z.optional(zod_mini.z.string()),
33202
+ runAs: zod_mini.z.optional(zod_mini.z.string()),
33203
+ model: zod_mini.z.optional(zod_mini.z.string()),
33204
+ effort: zod_mini.z.optional(zod_mini.z.string()),
33205
+ "allowed-tools": zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
33206
+ color: zod_mini.z.optional(zod_mini.z.string())
33207
+ });
33208
+ /**
33209
+ * Represents a DeepSeek-Reasonix subagent profile.
33210
+ *
33211
+ * Reasonix discovers subagents as directory-layout Skills (`<name>/SKILL.md`)
33212
+ * under `.reasonix/skills/` (project) and `~/.reasonix/skills/` (global); the
33213
+ * global scope is served by the processor supplying the home directory as
33214
+ * outputRoot, so the relative directory path is identical for both scopes. A
33215
+ * subagent is a Skill whose frontmatter declares `invocation: manual` and
33216
+ * `runAs: subagent`.
33217
+ *
33218
+ * @see https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/docs/SUBAGENT_PROFILES.md
33219
+ */
33220
+ var ReasonixSubagent = class ReasonixSubagent extends ToolSubagent {
33221
+ frontmatter;
33222
+ body;
33223
+ constructor({ frontmatter, body, fileContent, ...rest }) {
33224
+ if (rest.validate !== false) {
33225
+ const result = ReasonixSubagentFrontmatterSchema.safeParse(frontmatter);
33226
+ if (!result.success) throw new Error(`Invalid frontmatter in ${(0, node_path.join)(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}`);
33227
+ }
33228
+ super({
33229
+ ...rest,
33230
+ fileContent: fileContent ?? stringifyFrontmatter(body, frontmatter)
33231
+ });
33232
+ this.frontmatter = frontmatter;
33233
+ this.body = body;
33234
+ }
33235
+ static getSettablePaths({ global: _global = false } = {}) {
33236
+ return { relativeDirPath: REASONIX_SUBAGENTS_DIR_PATH };
33237
+ }
33238
+ getFrontmatter() {
33239
+ return this.frontmatter;
33240
+ }
33241
+ getBody() {
33242
+ return this.body;
33243
+ }
33244
+ toRulesyncSubagent() {
33245
+ const { name, description, invocation: _invocation, runAs: _runAs, ...restFields } = this.frontmatter;
33246
+ const reasonixSection = { ...restFields };
33247
+ const rulesyncFrontmatter = {
33248
+ targets: ["*"],
33249
+ name,
33250
+ description,
33251
+ ...Object.keys(reasonixSection).length > 0 && { reasonix: reasonixSection }
33252
+ };
33253
+ return new RulesyncSubagent({
33254
+ outputRoot: this.getOutputRoot(),
33255
+ frontmatter: rulesyncFrontmatter,
33256
+ body: this.body,
33257
+ relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH,
33258
+ relativeFilePath: `${this.getSubagentName()}.md`,
33259
+ validate: true
33260
+ });
33261
+ }
33262
+ /**
33263
+ * Derive the subagent name from this instance's relative file path.
33264
+ *
33265
+ * The tool-side layout is `<name>/SKILL.md`, so the name is the parent
33266
+ * directory of the file. If the path is unexpectedly flat (e.g. a legacy
33267
+ * `<name>.md`), fall back to the basename without extension.
33268
+ */
33269
+ getSubagentName() {
33270
+ const relativeFilePath = this.getRelativeFilePath();
33271
+ const dir = (0, node_path.dirname)(relativeFilePath);
33272
+ if (dir && dir !== ".") return (0, node_path.basename)(dir);
33273
+ return (0, node_path.basename)(relativeFilePath, (0, node_path.extname)(relativeFilePath));
33274
+ }
33275
+ static fromRulesyncSubagent({ outputRoot = process.cwd(), rulesyncSubagent, validate = true, global = false }) {
33276
+ const rulesyncFrontmatter = rulesyncSubagent.getFrontmatter();
33277
+ const reasonixSection = this.filterToolSpecificSection(rulesyncFrontmatter.reasonix ?? {}, ["name", "description"]);
33278
+ const rawReasonixFrontmatter = {
33279
+ name: rulesyncFrontmatter.name,
33280
+ description: rulesyncFrontmatter.description,
33281
+ ...reasonixSection,
33282
+ invocation: REASONIX_SUBAGENT_INVOCATION,
33283
+ runAs: REASONIX_SUBAGENT_RUN_AS
33284
+ };
33285
+ const result = ReasonixSubagentFrontmatterSchema.safeParse(rawReasonixFrontmatter);
33286
+ if (!result.success) throw new Error(`Invalid reasonix subagent frontmatter in ${rulesyncSubagent.getRelativeFilePath()}: ${formatError(result.error)}`);
33287
+ const reasonixFrontmatter = result.data;
33288
+ const body = rulesyncSubagent.getBody();
33289
+ const fileContent = stringifyFrontmatter(body, reasonixFrontmatter);
33290
+ const paths = this.getSettablePaths({ global });
33291
+ const subagentName = (0, node_path.basename)(rulesyncSubagent.getRelativeFilePath(), (0, node_path.extname)(rulesyncSubagent.getRelativeFilePath()));
33292
+ return new ReasonixSubagent({
33293
+ outputRoot,
33294
+ frontmatter: reasonixFrontmatter,
33295
+ body,
33296
+ relativeDirPath: paths.relativeDirPath,
33297
+ relativeFilePath: (0, node_path.join)(subagentName, SKILL_FILE_NAME),
33298
+ fileContent,
33299
+ validate,
33300
+ global
33301
+ });
33302
+ }
33303
+ validate() {
33304
+ if (!this.frontmatter) return {
33305
+ success: true,
33306
+ error: null
33307
+ };
33308
+ const result = ReasonixSubagentFrontmatterSchema.safeParse(this.frontmatter);
33309
+ if (result.success) return {
33310
+ success: true,
33311
+ error: null
33312
+ };
33313
+ else return {
33314
+ success: false,
33315
+ error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${(0, node_path.join)(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
33316
+ };
33317
+ }
33318
+ static isTargetedByRulesyncSubagent(rulesyncSubagent) {
33319
+ return this.isTargetedByRulesyncSubagentDefault({
33320
+ rulesyncSubagent,
33321
+ toolTarget: "reasonix"
33322
+ });
33323
+ }
33324
+ static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
33325
+ const paths = this.getSettablePaths({ global });
33326
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, relativeFilePath);
33327
+ const fileContent = await readFileContent(filePath);
33328
+ const { frontmatter, body: content } = parseFrontmatter(fileContent, filePath);
33329
+ const result = ReasonixSubagentFrontmatterSchema.safeParse(frontmatter);
33330
+ if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
33331
+ return new ReasonixSubagent({
33332
+ outputRoot,
33333
+ relativeDirPath: paths.relativeDirPath,
33334
+ relativeFilePath,
33335
+ frontmatter: result.data,
33336
+ body: content.trim(),
33337
+ fileContent,
33338
+ validate,
33339
+ global
33340
+ });
33341
+ }
33342
+ /**
33343
+ * Whether the SKILL.md at the given path is a subagent profile.
33344
+ *
33345
+ * `.reasonix/skills/` is shared with the skills feature: a regular skill and
33346
+ * a subagent profile differ only by their frontmatter markers. Only files
33347
+ * carrying `runAs: subagent` belong to this feature, so regular skills are
33348
+ * neither imported as subagents nor deleted as orphans by the subagents
33349
+ * feature. `runAs` alone is checked (not `invocation`) because it is the
33350
+ * marker that switches the execution mode; generation always emits both.
33351
+ * Unreadable or unparsable files are treated as not owned, erring on the
33352
+ * side of leaving foreign files untouched.
33353
+ */
33354
+ static async isFileOwned({ outputRoot, relativeDirPath, relativeFilePath }) {
33355
+ const filePath = (0, node_path.join)(outputRoot, relativeDirPath, relativeFilePath);
33356
+ try {
33357
+ const { frontmatter } = parseFrontmatter(await readFileContent(filePath), filePath);
33358
+ return frontmatter["runAs"] === REASONIX_SUBAGENT_RUN_AS;
33359
+ } catch {
33360
+ return false;
33361
+ }
33362
+ }
33363
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
33364
+ return new ReasonixSubagent({
33365
+ outputRoot,
33366
+ relativeDirPath,
33367
+ relativeFilePath,
33368
+ frontmatter: {
33369
+ name: "",
33370
+ description: ""
33371
+ },
33372
+ body: "",
33373
+ fileContent: "",
33374
+ validate: false
33375
+ });
33376
+ }
33377
+ };
33378
+ //#endregion
33085
33379
  //#region src/features/subagents/roo-subagent.ts
33086
33380
  /**
33087
33381
  * Default tool groups assigned to a generated mode when the rulesync source
@@ -33716,6 +34010,14 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
33716
34010
  filePattern: "*.md"
33717
34011
  }
33718
34012
  }],
34013
+ ["reasonix", {
34014
+ class: ReasonixSubagent,
34015
+ meta: {
34016
+ supportsSimulated: false,
34017
+ supportsGlobal: true,
34018
+ filePattern: (0, node_path.join)("*", "SKILL.md")
34019
+ }
34020
+ }],
33719
34021
  ["roo", {
33720
34022
  class: RooSubagent,
33721
34023
  meta: {
@@ -33868,8 +34170,17 @@ var SubagentsProcessor = class extends FeatureProcessor {
33868
34170
  const baseDir = (0, node_path.join)(this.outputRoot, dirPath);
33869
34171
  const subagentFilePaths = await findFilesByGlobs((0, node_path.join)(baseDir, factory.meta.filePattern));
33870
34172
  const toRelativeFilePath = (path) => (0, node_path.relative)(baseDir, path);
34173
+ let ownedFilePaths = subagentFilePaths;
34174
+ if (factory.class.isFileOwned) {
34175
+ const ownership = await Promise.all(subagentFilePaths.map((path) => factory.class.isFileOwned({
34176
+ outputRoot: this.outputRoot,
34177
+ relativeDirPath: dirPath,
34178
+ relativeFilePath: toRelativeFilePath(path)
34179
+ })));
34180
+ ownedFilePaths = subagentFilePaths.filter((_, index) => ownership[index]);
34181
+ }
33871
34182
  if (forDeletion) {
33872
- toolSubagents.push(...subagentFilePaths.map((path) => factory.class.forDeletion({
34183
+ toolSubagents.push(...ownedFilePaths.map((path) => factory.class.forDeletion({
33873
34184
  outputRoot: this.outputRoot,
33874
34185
  relativeDirPath: dirPath,
33875
34186
  relativeFilePath: toRelativeFilePath(path),
@@ -33877,7 +34188,7 @@ var SubagentsProcessor = class extends FeatureProcessor {
33877
34188
  })).filter((subagent) => subagent.isDeletable()));
33878
34189
  continue;
33879
34190
  }
33880
- const loaded = await Promise.all(subagentFilePaths.map((path) => factory.class.fromFile({
34191
+ const loaded = await Promise.all(ownedFilePaths.map((path) => factory.class.fromFile({
33881
34192
  outputRoot: this.outputRoot,
33882
34193
  relativeDirPath: dirPath,
33883
34194
  relativeFilePath: toRelativeFilePath(path),
@@ -39696,11 +40007,51 @@ const GENERATION_STEP_GRAPH = [
39696
40007
  }
39697
40008
  ];
39698
40009
  /**
40010
+ * Warn when a rulesync skill and a rulesync subagent share a name for a tool
40011
+ * that emits both features into the same directory (e.g. Reasonix, where both
40012
+ * write `<name>/SKILL.md` under `.reasonix/skills/`). The colliding outputs
40013
+ * target the same on-disk file, so whichever generation step runs later
40014
+ * silently overwrites the other's file.
40015
+ */
40016
+ async function warnSkillSubagentNameCollisions(params) {
40017
+ const { config, logger } = params;
40018
+ const global = config.getGlobal();
40019
+ for (const toolTarget of config.getTargets()) {
40020
+ const features = config.getFeatures(toolTarget);
40021
+ if (!features.includes("skills") || !features.includes("subagents")) continue;
40022
+ if (!SubagentsProcessor.getToolTargets({ global }).includes(toolTarget) || !SkillsProcessor.getToolTargets({ global }).includes(toolTarget)) continue;
40023
+ const subagentFactory = SubagentsProcessor.getFactory(toolTarget);
40024
+ const skillFactory = SkillsProcessor.getFactory(toolTarget);
40025
+ if (!subagentFactory || !skillFactory) continue;
40026
+ const subagentsDirPath = subagentFactory.class.getSettablePaths({ global }).relativeDirPath;
40027
+ if (subagentsDirPath !== skillFactory.class.getSettablePaths({ global }).relativeDirPath) continue;
40028
+ const subagentsProcessor = new SubagentsProcessor({
40029
+ inputRoot: config.getInputRoot(),
40030
+ toolTarget,
40031
+ global,
40032
+ logger
40033
+ });
40034
+ const subagentNames = new Set((await subagentsProcessor.loadRulesyncFiles()).filter((file) => file instanceof RulesyncSubagent).filter((file) => subagentFactory.class.isTargetedByRulesyncSubagent(file)).map((file) => (0, node_path.basename)(file.getRelativeFilePath(), (0, node_path.extname)(file.getRelativeFilePath()))));
40035
+ if (subagentNames.size === 0) continue;
40036
+ const skillNames = (await new SkillsProcessor({
40037
+ inputRoot: config.getInputRoot(),
40038
+ toolTarget,
40039
+ global,
40040
+ logger
40041
+ }).loadRulesyncDirs()).filter((dir) => dir instanceof RulesyncSkill).filter((skill) => skillFactory.class.isTargetedByRulesyncSkill(skill)).map((skill) => skill.getDirName());
40042
+ for (const name of skillNames) if (subagentNames.has(name)) logger.warn(`Skill "${name}" and subagent "${name}" both target '${toolTarget}' and write the same path '${(0, node_path.join)(subagentsDirPath, name)}'; the later generation step overwrites the other's output. Rename one of them or narrow their targets.`);
40043
+ }
40044
+ }
40045
+ /**
39699
40046
  * Generate configuration files for AI tools.
39700
40047
  * @throws Error if generation fails
39701
40048
  */
39702
40049
  async function generate(params) {
39703
40050
  const { config, logger } = params;
40051
+ await warnSkillSubagentNameCollisions({
40052
+ config,
40053
+ logger
40054
+ });
39704
40055
  let skillsResult;
39705
40056
  const runners = {
39706
40057
  ignore: () => generateIgnoreCore({