rulesync 11.0.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
  /**
@@ -17521,6 +17525,14 @@ const CodexBasePermissionProfileSchema = zod_mini.z.enum(CODEX_BASE_PERMISSION_P
17521
17525
  * | `guardian_subagent`), or a table for the richer reviewer config.
17522
17526
  * Defaults to `auto_review` when neither the override nor the existing
17523
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.
17524
17536
  *
17525
17537
  * Two surfaces are deliberately NOT authorable here so the override can never
17526
17538
  * clobber a feature-owned key: `mcp_servers.*` per-MCP gating is owned by the
@@ -17546,7 +17558,8 @@ const CodexcliPermissionsOverrideSchema = zod_mini.z.looseObject({
17546
17558
  /** @deprecated Superseded by `base_permission_profile` (permission profiles). */
17547
17559
  sandbox_workspace_write: zod_mini.z.optional(zod_mini.z.looseObject({})),
17548
17560
  apps: zod_mini.z.optional(zod_mini.z.looseObject({})),
17549
- 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())
17550
17563
  });
17551
17564
  /**
17552
17565
  * Permissions configuration.
@@ -19515,11 +19528,16 @@ var ClinePermissions = class ClinePermissions extends ToolPermissions {
19515
19528
  const RULESYNC_PROFILE_NAME = "rulesync";
19516
19529
  const CODEX_WORKSPACE_ROOTS_KEY = ":workspace_roots";
19517
19530
  const CODEX_WORKSPACE_BASELINE = ":workspace";
19531
+ const CODEX_READ_ONLY_BASELINE = ":read-only";
19518
19532
  const CODEX_EXTENDABLE_BASELINES = new Set(CODEX_BASE_PERMISSION_PROFILES);
19519
19533
  const CODEX_DEFAULT_APPROVAL_POLICY = "on-request";
19520
19534
  const CODEX_DEFAULT_APPROVALS_REVIEWER = "auto_review";
19521
19535
  const CODEX_GLOB_SCAN_MAX_DEPTH = 8;
19522
19536
  const CODEX_MINIMAL_KEY = ":minimal";
19537
+ const CODEX_GIT_WRITE_RULES = {
19538
+ ".git/**": "write",
19539
+ ".git/config": "read"
19540
+ };
19523
19541
  const GLOBAL_WILDCARD_DOMAIN = "*";
19524
19542
  var CodexcliPermissions = class CodexcliPermissions extends ToolPermissions {
19525
19543
  static getSettablePaths(_options = {}) {
@@ -19686,6 +19704,11 @@ function convertRulesyncToCodexProfile({ config, logger }) {
19686
19704
  }
19687
19705
  logger?.warn(`Codex CLI permissions support only read/edit/write/webfetch categories. Skipping: ${toolName}`);
19688
19706
  }
19707
+ applyDefaultGitWriteRules({
19708
+ config,
19709
+ filesystem,
19710
+ workspaceRootFilesystem
19711
+ });
19689
19712
  if (Object.keys(workspaceRootFilesystem).length > 0) {
19690
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.`);
19691
19714
  if (Object.keys(workspaceRootFilesystem).some((pattern) => pattern.includes("**"))) filesystem.glob_scan_max_depth = CODEX_GLOB_SCAN_MAX_DEPTH;
@@ -19703,6 +19726,12 @@ function convertRulesyncToCodexProfile({ config, logger }) {
19703
19726
  ...network ? { network } : {}
19704
19727
  };
19705
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
+ }
19706
19735
  function convertCodexProfileToRulesync({ profile, domainsHadUnknown }) {
19707
19736
  const permission = {};
19708
19737
  if (profile?.filesystem) {
@@ -19714,7 +19743,10 @@ function convertCodexProfileToRulesync({ profile, domainsHadUnknown }) {
19714
19743
  addRulesyncFilesystemRule(permission, pattern, access);
19715
19744
  continue;
19716
19745
  }
19717
- 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
+ }
19718
19750
  }
19719
19751
  }
19720
19752
  if (profile?.network && profile.network.enabled !== false) {
@@ -19756,9 +19788,28 @@ function toCodexProfile(value) {
19756
19788
  }
19757
19789
  function warnAboutPreservedProfileState({ existingProfile, newProfile, existingDomainsHadUnknown, logger }) {
19758
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 }) {
19759
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
+ });
19760
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.`);
19761
- if (existingDomainsHadUnknown) logger?.warn(`Existing "network.domains" contained unrecognized values. These entries were skipped and will not be imported.`);
19762
19813
  }
19763
19814
  const MANAGED_PROFILE_KEYS = /* @__PURE__ */ new Set([
19764
19815
  "description",
@@ -19800,6 +19851,7 @@ function preserveUnmanagedProfileKeys({ rawExistingProfile, profile, logger }) {
19800
19851
  function mergeWithExistingProfile({ newProfile, existingProfile }) {
19801
19852
  if (!existingProfile) return newProfile;
19802
19853
  const mergedNetwork = { ...newProfile.network };
19854
+ if (existingProfile.network?.enabled !== void 0 && mergedNetwork.enabled === void 0 && !networkHasAllowDomain(existingProfile.network)) mergedNetwork.enabled = existingProfile.network.enabled;
19803
19855
  if (existingProfile.network?.mode !== void 0 && mergedNetwork.mode === void 0) mergedNetwork.mode = existingProfile.network.mode;
19804
19856
  if (existingProfile.network?.unix_sockets !== void 0 && mergedNetwork.unix_sockets === void 0) mergedNetwork.unix_sockets = existingProfile.network.unix_sockets;
19805
19857
  const hasNetwork = Object.keys(mergedNetwork).length > 0;
@@ -19851,7 +19903,7 @@ function computeCodexcliOverridePatch({ existing, override, logger }) {
19851
19903
  const patch = {};
19852
19904
  const allowed = new Set(CODEXCLI_OVERRIDE_KEYS);
19853
19905
  for (const [key, value] of Object.entries(override ?? {})) {
19854
- if (key === "base_permission_profile") continue;
19906
+ if (key === "base_permission_profile" || key === "git_write_rules") continue;
19855
19907
  if (!allowed.has(key)) {
19856
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.`);
19857
19909
  continue;
@@ -28491,6 +28543,24 @@ var ReasonixSkill = class ReasonixSkill extends ToolSkill {
28491
28543
  global: loaded.global
28492
28544
  });
28493
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
+ }
28494
28564
  static forDeletion({ outputRoot = process.cwd(), relativeDirPath, dirName, global = false }) {
28495
28565
  return new ReasonixSkill({
28496
28566
  outputRoot,
@@ -29789,6 +29859,11 @@ var SkillsProcessor = class extends DirFeatureProcessor {
29789
29859
  for (const dirPath of dirPaths) {
29790
29860
  const dirName = (0, node_path.basename)(dirPath);
29791
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;
29792
29867
  seenDirNames.add(dirName);
29793
29868
  loadEntries.push({
29794
29869
  root,
@@ -29815,6 +29890,11 @@ var SkillsProcessor = class extends DirFeatureProcessor {
29815
29890
  const dirPaths = await findFilesByGlobs((0, node_path.join)(skillsDirPath, "*"), { type: "dir" });
29816
29891
  for (const dirPath of dirPaths) {
29817
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;
29818
29898
  const toolSkill = factory.class.forDeletion({
29819
29899
  outputRoot: this.outputRoot,
29820
29900
  relativeDirPath: root,
@@ -33114,6 +33194,188 @@ var OpenCodeSubagent = class OpenCodeSubagent extends OpenCodeStyleSubagent {
33114
33194
  }
33115
33195
  };
33116
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
33117
33379
  //#region src/features/subagents/roo-subagent.ts
33118
33380
  /**
33119
33381
  * Default tool groups assigned to a generated mode when the rulesync source
@@ -33748,6 +34010,14 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
33748
34010
  filePattern: "*.md"
33749
34011
  }
33750
34012
  }],
34013
+ ["reasonix", {
34014
+ class: ReasonixSubagent,
34015
+ meta: {
34016
+ supportsSimulated: false,
34017
+ supportsGlobal: true,
34018
+ filePattern: (0, node_path.join)("*", "SKILL.md")
34019
+ }
34020
+ }],
33751
34021
  ["roo", {
33752
34022
  class: RooSubagent,
33753
34023
  meta: {
@@ -33900,8 +34170,17 @@ var SubagentsProcessor = class extends FeatureProcessor {
33900
34170
  const baseDir = (0, node_path.join)(this.outputRoot, dirPath);
33901
34171
  const subagentFilePaths = await findFilesByGlobs((0, node_path.join)(baseDir, factory.meta.filePattern));
33902
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
+ }
33903
34182
  if (forDeletion) {
33904
- toolSubagents.push(...subagentFilePaths.map((path) => factory.class.forDeletion({
34183
+ toolSubagents.push(...ownedFilePaths.map((path) => factory.class.forDeletion({
33905
34184
  outputRoot: this.outputRoot,
33906
34185
  relativeDirPath: dirPath,
33907
34186
  relativeFilePath: toRelativeFilePath(path),
@@ -33909,7 +34188,7 @@ var SubagentsProcessor = class extends FeatureProcessor {
33909
34188
  })).filter((subagent) => subagent.isDeletable()));
33910
34189
  continue;
33911
34190
  }
33912
- const loaded = await Promise.all(subagentFilePaths.map((path) => factory.class.fromFile({
34191
+ const loaded = await Promise.all(ownedFilePaths.map((path) => factory.class.fromFile({
33913
34192
  outputRoot: this.outputRoot,
33914
34193
  relativeDirPath: dirPath,
33915
34194
  relativeFilePath: toRelativeFilePath(path),
@@ -39728,11 +40007,51 @@ const GENERATION_STEP_GRAPH = [
39728
40007
  }
39729
40008
  ];
39730
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
+ /**
39731
40046
  * Generate configuration files for AI tools.
39732
40047
  * @throws Error if generation fails
39733
40048
  */
39734
40049
  async function generate(params) {
39735
40050
  const { config, logger } = params;
40051
+ await warnSkillSubagentNameCollisions({
40052
+ config,
40053
+ logger
40054
+ });
39736
40055
  let skillsResult;
39737
40056
  const runners = {
39738
40057
  ignore: () => generateIgnoreCore({