rulesync 20.0.0 → 22.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.
@@ -12270,7 +12270,7 @@ const CODEXCLI_AGENTS_DIR_PATH = join(CODEXCLI_DIR, "agents");
12270
12270
  const CODEXCLI_SKILLS_DIR_PATH = join(".agents", "skills");
12271
12271
  const CODEXCLI_HOOKS_FILE_NAME = "hooks.json";
12272
12272
  const CODEXCLI_MCP_FILE_NAME = "config.toml";
12273
- const CODEXCLI_RULE_FILE_NAME = "AGENTS.md";
12273
+ const CODEXCLI_PROJECT_DOC_MAX_BYTES = 32768;
12274
12274
  const CODEXCLI_BASH_RULES_FILE_NAME = "rulesync.rules";
12275
12275
  const CODEXCLI_OPENAI_YAML_RELATIVE_PATH = join("agents", "openai.yaml");
12276
12276
  const CODEXCLI_OVERRIDE_KEYS = [
@@ -15455,12 +15455,50 @@ var BobCommand = class BobCommand extends ToolCommand {
15455
15455
  }
15456
15456
  };
15457
15457
  //#endregion
15458
+ //#region src/features/shared/claudecode-argument-hint.ts
15459
+ /**
15460
+ * Turns a YAML-list `argument-hint` back into the bracketed string form Claude
15461
+ * Code documents (`[issue-number]`, `[filename] [format]`), one bracketed
15462
+ * placeholder per list entry. The per-entry form is chosen over re-joining with
15463
+ * commas because it is the shape every Claude Code example uses. An empty list
15464
+ * carries no hint, so it yields `undefined` and the key is dropped.
15465
+ */
15466
+ function joinArgumentHint(value) {
15467
+ if (typeof value === "string") return value;
15468
+ if (value.length === 0) return;
15469
+ return value.map((entry) => `[${String(entry)}]`).join(" ");
15470
+ }
15471
+ /**
15472
+ * `argument-hint` as Claude Code reads it from skill and command frontmatter.
15473
+ *
15474
+ * The documented examples (`[issue-number]`) are written unquoted, and YAML
15475
+ * parses an unquoted `[issue-number]` as a one-element list. Claude Code
15476
+ * accepts that form, so it is accepted here too and normalized to the
15477
+ * bracketed string, keeping the parsed value (and everything downstream of it)
15478
+ * a plain string. A scalar entry YAML did not read as a string (`[1]`,
15479
+ * `[true]`, `[~]`) is stringified rather than rejected.
15480
+ * https://code.claude.com/docs/en/skills
15481
+ *
15482
+ * A list entry is stringified from the value YAML produced, so its original
15483
+ * spelling is not kept (`[1.0]` becomes `[1]`), and an entry YAML reads as a
15484
+ * date (`[2026-01-01]`) is rejected.
15485
+ *
15486
+ * The unquoted multi-placeholder form (`argument-hint: [filename] [format]`)
15487
+ * is not valid YAML at all, so it fails before this schema is reached.
15488
+ */
15489
+ const ClaudecodeArgumentHintSchema = z.pipe(z.union([z.string(), z.array(z.union([
15490
+ z.string(),
15491
+ z.number(),
15492
+ z.boolean(),
15493
+ z.null()
15494
+ ]))]), z.transform(joinArgumentHint));
15495
+ //#endregion
15458
15496
  //#region src/features/commands/claudecode-command.ts
15459
15497
  const ClaudecodeCommandFrontmatterSchema = z.looseObject({
15460
15498
  description: z.optional(z.string()),
15461
15499
  "allowed-tools": z.optional(z.union([z.string(), z.array(z.string())])),
15462
15500
  "disallowed-tools": z.optional(z.union([z.string(), z.array(z.string())])),
15463
- "argument-hint": z.optional(z.string()),
15501
+ "argument-hint": z.optional(ClaudecodeArgumentHintSchema),
15464
15502
  model: z.optional(z.string()),
15465
15503
  "disable-model-invocation": z.optional(z.boolean())
15466
15504
  });
@@ -15489,7 +15527,8 @@ var ClaudecodeCommand = class ClaudecodeCommand extends ToolCommand {
15489
15527
  return this.frontmatter;
15490
15528
  }
15491
15529
  toRulesyncCommand() {
15492
- const { description, ...restFields } = this.frontmatter;
15530
+ const { description, ...extraFields } = this.frontmatter;
15531
+ const restFields = Object.fromEntries(Object.entries(extraFields).filter(([, value]) => value !== void 0));
15493
15532
  const rulesyncFrontmatter = {
15494
15533
  targets: ["*"],
15495
15534
  description,
@@ -33122,6 +33161,74 @@ function translateCanonicalKeyToCodex({ key, value, isStdio, serverName }) {
33122
33161
  }
33123
33162
  }
33124
33163
  /**
33164
+ * A header value that is exactly one canonical `${VAR}` reference. Anchored and
33165
+ * without the `g` flag, so `.exec()` carries no `lastIndex` state between
33166
+ * calls. `${VAR:-default}` does not match (the name excludes `:`), because
33167
+ * Codex has no way to express the fallback.
33168
+ */
33169
+ const WHOLE_ENV_VAR_REF_PATTERN = /^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$/;
33170
+ /**
33171
+ * An `Authorization` value that is exactly `Bearer ${VAR}`. The scheme is
33172
+ * matched case-insensitively, as HTTP authentication schemes are.
33173
+ */
33174
+ const BEARER_ENV_VAR_REF_PATTERN = /^Bearer \$\{([A-Za-z_][A-Za-z0-9_]*)\}$/i;
33175
+ const AUTHORIZATION_HEADER_NAME = "authorization";
33176
+ /**
33177
+ * Whether a string still contains an unexpanded `${…}` reference. Codex writes
33178
+ * `url` and `http_headers` values as-is and never expands them, so such a value
33179
+ * reaches the server literally.
33180
+ */
33181
+ function hasUnexpandedEnvVarRef(value) {
33182
+ return value.includes("${");
33183
+ }
33184
+ /**
33185
+ * Rewrite canonical `${VAR}` header references into the keys Codex reads them
33186
+ * from, since Codex sends `http_headers` values verbatim:
33187
+ * - `Authorization: Bearer ${VAR}` → `bearer_token_env_var = "VAR"`
33188
+ * - a header whose whole value is `${VAR}` → `env_http_headers.<name> = "VAR"`
33189
+ *
33190
+ * Runs after every key is translated, so a `bearer_token_env_var` or
33191
+ * `env_http_headers` the source already carries (they pass through under their
33192
+ * own names) is seen regardless of key order. Those explicit values win: a
33193
+ * derived bearer is not written over an existing one, and an existing
33194
+ * `env_http_headers` entry for the same header is kept. Any value the rewrite
33195
+ * cannot express — a partial reference, a `${VAR:-default}` fallback — stays in
33196
+ * `http_headers` verbatim with a warning, as does a `${…}` in `url`, for which
33197
+ * Codex has no environment-sourced counterpart at all.
33198
+ * @see https://learn.chatgpt.com/docs/config-file/config-reference
33199
+ */
33200
+ function convertEnvVarRefsForCodex({ converted, serverName }) {
33201
+ const url = converted["url"];
33202
+ if (typeof url === "string" && hasUnexpandedEnvVarRef(url)) warnWithFallback(void 0, `[CodexCliMcp] MCP server "${serverName}": 'url' contains an environment variable reference that Codex does not expand, so it is written verbatim and the server will likely fail to start.`);
33203
+ const httpHeaders = converted["http_headers"];
33204
+ if (!isHeadersRecord(httpHeaders)) return;
33205
+ const existingEnvHeaders = converted["env_http_headers"];
33206
+ const canWriteEnvHeaders = existingEnvHeaders === void 0 || isHeadersRecord(existingEnvHeaders);
33207
+ const envHeaders = {};
33208
+ const staticHeaders = {};
33209
+ let bearerTokenEnvVar;
33210
+ for (const [name, value] of Object.entries(httpHeaders)) {
33211
+ const bearerMatch = BEARER_ENV_VAR_REF_PATTERN.exec(value);
33212
+ if (bearerMatch?.[1] && name.toLowerCase() === AUTHORIZATION_HEADER_NAME) {
33213
+ if (converted["bearer_token_env_var"] === void 0 && bearerTokenEnvVar === void 0) bearerTokenEnvVar = bearerMatch[1];
33214
+ continue;
33215
+ }
33216
+ const wholeMatch = WHOLE_ENV_VAR_REF_PATTERN.exec(value);
33217
+ if (wholeMatch?.[1] && canWriteEnvHeaders) {
33218
+ envHeaders[name] = wholeMatch[1];
33219
+ continue;
33220
+ }
33221
+ if (hasUnexpandedEnvVarRef(value)) warnWithFallback(void 0, `[CodexCliMcp] MCP server "${serverName}": header '${name}' contains an environment variable reference that Codex cannot source from the environment, so it is written verbatim to 'http_headers' and sent literally. Only 'Authorization: Bearer \${VAR}' and a whole-value '\${VAR}' are converted.`);
33222
+ staticHeaders[name] = value;
33223
+ }
33224
+ converted["http_headers"] = staticHeaders;
33225
+ if (bearerTokenEnvVar !== void 0) converted["bearer_token_env_var"] = bearerTokenEnvVar;
33226
+ if (Object.keys(envHeaders).length > 0) converted["env_http_headers"] = {
33227
+ ...envHeaders,
33228
+ ...isHeadersRecord(existingEnvHeaders) ? existingEnvHeaders : {}
33229
+ };
33230
+ }
33231
+ /**
33125
33232
  * Codex states no transport of its own — it infers one from `command` versus
33126
33233
  * `url`, which is why generate drops the canonical `type`. Restate it for a url
33127
33234
  * server on the way back, so a config imported from Codex reaches the adapters
@@ -33208,6 +33315,10 @@ function convertToCodexFormat(mcpServers) {
33208
33315
  }
33209
33316
  } else converted[key] = value;
33210
33317
  }
33318
+ convertEnvVarRefsForCodex({
33319
+ converted,
33320
+ serverName: name
33321
+ });
33211
33322
  const previousName = originalNames.get(codexName);
33212
33323
  if (previousName !== void 0) warnWithFallback(void 0, `Codex MCP server name collision: "${previousName}" and "${name}" both normalize to "${codexName}"; "${name}" (processed last) overwrites "${previousName}".`);
33213
33324
  originalNames.set(codexName, name);
@@ -44986,14 +45097,14 @@ const COMMANDCODE_TOOL_TO_CATEGORY = {
44986
45097
  web_fetch: "webfetch",
44987
45098
  web_search: "websearch"
44988
45099
  };
44989
- const ACTION_RANK$1 = {
45100
+ const ACTION_RANK$2 = {
44990
45101
  allow: 0,
44991
45102
  ask: 1,
44992
45103
  deny: 2
44993
45104
  };
44994
45105
  /** Whether `action` outranks `existing` (deny > ask > allow). */
44995
45106
  function isStricterAction({ action, existing }) {
44996
- return existing === void 0 || ACTION_RANK$1[action] > ACTION_RANK$1[existing];
45107
+ return existing === void 0 || ACTION_RANK$2[action] > ACTION_RANK$2[existing];
44997
45108
  }
44998
45109
  /**
44999
45110
  * Why a padded `Shell( * )` cannot stand in for the bare `Shell` in `allow`:
@@ -48341,20 +48452,22 @@ function convertFactorydroidToRulesyncPermissions({ allow, deny }) {
48341
48452
  const GOOSE_GLOBAL_ONLY_MESSAGE = "Goose permissions are global-only; use --global to sync ~/.config/goose/permission.yaml";
48342
48453
  const GOOSE_USER_KEY = "user";
48343
48454
  const CATCH_ALL_PATTERN$4 = "*";
48344
- const RULESYNC_TO_GOOSE_TOOL_NAME = {
48345
- bash: "developer__shell",
48346
- edit: "developer__text_editor",
48347
- write: "developer__text_editor"
48348
- };
48349
- const GOOSE_TO_RULESYNC_TOOL_NAME = {
48350
- developer__shell: "bash",
48351
- developer__text_editor: "edit"
48455
+ const RULESYNC_TO_GOOSE_TOOL_NAME = { bash: "shell" };
48456
+ const GOOSE_TO_RULESYNC_TOOL_NAMES = {
48457
+ shell: ["bash"],
48458
+ developer__shell: ["bash"],
48459
+ developer__text_editor: ["edit", "write"]
48352
48460
  };
48353
48461
  const ACTION_TO_GOOSE_LIST = {
48354
48462
  allow: "always_allow",
48355
48463
  ask: "ask_before",
48356
48464
  deny: "never_allow"
48357
48465
  };
48466
+ const ACTION_RANK$1 = {
48467
+ allow: 0,
48468
+ ask: 1,
48469
+ deny: 2
48470
+ };
48358
48471
  const GOOSE_LIST_TO_ACTION = {
48359
48472
  always_allow: "allow",
48360
48473
  ask_before: "ask",
@@ -48379,10 +48492,11 @@ const GOOSE_PERMISSION_LIST_KEYS = [
48379
48492
  *
48380
48493
  * Mapping (rulesync canonical -> Goose):
48381
48494
  * - Action: `allow` -> `always_allow`, `ask` -> `ask_before`, `deny` -> `never_allow`.
48382
- * - Tool name: `bash` -> `developer__shell`, `edit` -> `developer__text_editor`;
48383
- * any other category passes through verbatim as the Goose tool name (mirrors
48384
- * the Gemini CLI adapter). `write` collapses onto `developer__text_editor`
48385
- * too, so a conflicting `edit`/`write` catch-all is reported and `edit` wins.
48495
+ * - Tool name: `bash` -> `shell`; any other category (including `edit` and
48496
+ * `write`) passes through verbatim as the Goose tool name (mirrors the
48497
+ * Gemini CLI adapter). The legacy `developer__shell` /
48498
+ * `developer__text_editor` names are still accepted on import, the latter
48499
+ * as both `edit` and `write`.
48386
48500
  * - Granularity: Goose lists hold whole tool names, so only a category's
48387
48501
  * catch-all `*` pattern is representable. Non-catch-all patterns cannot be
48388
48502
  * expressed per-tool and are reported via `logger.warn` and skipped.
@@ -48498,8 +48612,7 @@ function convertRulesyncToGoosePermissionConfig({ config, logger }) {
48498
48612
  never_allow: []
48499
48613
  };
48500
48614
  const assigned = /* @__PURE__ */ new Map();
48501
- const orderedEntries = Object.entries(honorAllToolsOnBash(config.permission)).toSorted(([a], [b]) => (a === "edit" ? 1 : 0) - (b === "edit" ? 1 : 0));
48502
- for (const [category, rules] of orderedEntries) {
48615
+ for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) {
48503
48616
  const toolName = RULESYNC_TO_GOOSE_TOOL_NAME[category] ?? category;
48504
48617
  for (const [pattern, action] of Object.entries(rules)) {
48505
48618
  if (pattern !== CATCH_ALL_PATTERN$4) {
@@ -48507,12 +48620,15 @@ function convertRulesyncToGoosePermissionConfig({ config, logger }) {
48507
48620
  continue;
48508
48621
  }
48509
48622
  const previous = assigned.get(toolName);
48510
- if (previous !== void 0 && previous !== action) {
48511
- logger?.warn(`Goose maps "${category}" onto the "${toolName}" tool, which already has a conflicting permission ("${previous}"). The "${action}" value takes precedence.`);
48623
+ if (previous === action) continue;
48624
+ if (previous !== void 0) {
48625
+ const stricter = ACTION_RANK$1[action] > ACTION_RANK$1[previous] ? action : previous;
48626
+ logger?.warn(`Goose maps "${category}" onto the "${toolName}" tool, which already has a conflicting permission ("${previous}"). Keeping the stricter "${stricter}" (deny > ask > allow).`);
48627
+ if (stricter === previous) continue;
48512
48628
  const previousList = lists[ACTION_TO_GOOSE_LIST[previous]];
48513
48629
  const index = previousList.indexOf(toolName);
48514
48630
  if (index !== -1) previousList.splice(index, 1);
48515
- } else if (previous === action) continue;
48631
+ }
48516
48632
  lists[ACTION_TO_GOOSE_LIST[action]].push(toolName);
48517
48633
  assigned.set(toolName, action);
48518
48634
  }
@@ -48528,8 +48644,7 @@ function convertGoosePermissionConfigToRulesync(userPermission) {
48528
48644
  for (const key of GOOSE_PERMISSION_LIST_KEYS) {
48529
48645
  const toolNames = isStringArray$2(userPermission[key]) ? userPermission[key] : [];
48530
48646
  const action = GOOSE_LIST_TO_ACTION[key];
48531
- for (const toolName of toolNames) {
48532
- const category = GOOSE_TO_RULESYNC_TOOL_NAME[toolName] ?? toolName;
48647
+ for (const toolName of toolNames) for (const category of GOOSE_TO_RULESYNC_TOOL_NAMES[toolName] ?? [toolName]) {
48533
48648
  permission[category] ??= {};
48534
48649
  permission[category][CATCH_ALL_PATTERN$4] = action;
48535
48650
  }
@@ -59203,7 +59318,7 @@ const ClaudecodeSkillFrontmatterSchema = z.looseObject({
59203
59318
  "disallowed-tools": z.optional(z.union([z.string(), z.array(z.string())])),
59204
59319
  model: z.optional(z.string()),
59205
59320
  effort: z.optional(z.string()),
59206
- "argument-hint": z.optional(z.string()),
59321
+ "argument-hint": z.optional(ClaudecodeArgumentHintSchema),
59207
59322
  arguments: z.optional(z.union([z.string(), z.array(z.string())])),
59208
59323
  context: z.optional(z.string()),
59209
59324
  agent: z.optional(z.string()),
@@ -65359,7 +65474,8 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
65359
65474
  meta: {
65360
65475
  supportsProject: true,
65361
65476
  supportsSimulated: false,
65362
- supportsGlobal: true
65477
+ supportsGlobal: true,
65478
+ lenientImport: true
65363
65479
  }
65364
65480
  }],
65365
65481
  ["claudecode-plugin", {
@@ -72308,7 +72424,7 @@ var ToolRule = class extends ToolFile {
72308
72424
  static buildToolRuleParamsAgentsmd({ outputRoot = process.cwd(), rulesyncRule, validate = true, rootPath = {
72309
72425
  relativeDirPath: ".",
72310
72426
  relativeFilePath: "AGENTS.md"
72311
- }, nonRootPath = { relativeDirPath: AGENTSMD_MEMORIES_DIR_PATH } }) {
72427
+ }, nonRootPath = { relativeDirPath: AGENTSMD_MEMORIES_DIR_PATH }, global = false }) {
72312
72428
  const params = this.buildToolRuleParamsDefault({
72313
72429
  outputRoot,
72314
72430
  rulesyncRule,
@@ -72317,7 +72433,7 @@ var ToolRule = class extends ToolFile {
72317
72433
  nonRootPath
72318
72434
  });
72319
72435
  const rulesyncFrontmatter = rulesyncRule.getFrontmatter();
72320
- if (!rulesyncFrontmatter.root && rulesyncFrontmatter.agentsmd?.subprojectPath) {
72436
+ if (!global && !rulesyncFrontmatter.root && rulesyncFrontmatter.agentsmd?.subprojectPath) {
72321
72437
  params.relativeDirPath = join(rulesyncFrontmatter.agentsmd.subprojectPath);
72322
72438
  params.relativeFilePath = "AGENTS.md";
72323
72439
  }
@@ -72974,7 +73090,8 @@ var AmpRule = class AmpRule extends ToolRule {
72974
73090
  rulesyncRule,
72975
73091
  validate,
72976
73092
  rootPath: paths.root,
72977
- nonRootPath: paths.nonRoot
73093
+ nonRootPath: paths.nonRoot,
73094
+ global
72978
73095
  });
72979
73096
  if (!params.root && params.globs !== void 0 && params.globs.length > 0) params.fileContent = stringifyFrontmatter(params.fileContent, { globs: params.globs });
72980
73097
  return new AmpRule(params);
@@ -74269,7 +74386,8 @@ var ClineRule = class ClineRule extends ToolRule {
74269
74386
  outputRoot,
74270
74387
  rulesyncRule,
74271
74388
  validate,
74272
- rootPath: paths.root
74389
+ rootPath: paths.root,
74390
+ global
74273
74391
  }));
74274
74392
  }
74275
74393
  if (isRoot) return new ClineRule({
@@ -74689,26 +74807,62 @@ var CodebuddyRule = class CodebuddyRule extends ToolRule {
74689
74807
  }
74690
74808
  };
74691
74809
  //#endregion
74692
- //#region src/features/rules/codexcli-rule.ts
74693
- var CodexcliRule = class CodexcliRule extends ToolRule {
74694
- constructor({ fileContent, root, ...rest }) {
74695
- super({
74696
- ...rest,
74697
- fileContent,
74698
- root: root ?? false
74699
- });
74810
+ //#region src/features/rules/nested-agentsmd-rule.ts
74811
+ /**
74812
+ * Shared adapter for the targets that read instructions exactly the way the
74813
+ * AGENTS.md standard describes them and nothing more: a root `AGENTS.md` at the
74814
+ * project root, nested per-directory `AGENTS.md` files loaded only while working
74815
+ * under that directory, and a single personal root file under a tool-specific
74816
+ * home directory. Such a target has no modular non-root instruction directory,
74817
+ * so every other non-root rule folds into the root file.
74818
+ *
74819
+ * Concrete targets (Codex CLI, Pool, Vibe, DeepSeek Harness) only supply the global
74820
+ * directory and their tool target through {@link getFamily}; the vendor docs
74821
+ * describing each one's discovery walk live on the subclass.
74822
+ *
74823
+ * The nested-file scan mirrors the AGENTS.md standard's nested discovery —
74824
+ * same file name, same exclusions, import-only, project scope — because it
74825
+ * discovers literally the same files.
74826
+ */
74827
+ var NestedAgentsmdRule = class extends ToolRule {
74828
+ static getFamily() {
74829
+ throw new Error("Please implement this method in the subclass.");
74700
74830
  }
74701
74831
  static getSettablePaths({ global = false } = {}) {
74832
+ if (global) return { root: {
74833
+ relativeDirPath: this.getFamily().globalDir,
74834
+ relativeFilePath: AGENTSMD_RULE_FILE_NAME
74835
+ } };
74702
74836
  return { root: {
74703
- relativeDirPath: global ? CODEXCLI_DIR : ".",
74704
- relativeFilePath: CODEXCLI_RULE_FILE_NAME
74837
+ relativeDirPath: ".",
74838
+ relativeFilePath: AGENTSMD_RULE_FILE_NAME
74705
74839
  } };
74706
74840
  }
74707
- static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, validate = true, global = false }) {
74841
+ static getNestedFilePatterns() {
74842
+ return this.buildNestedFilePatterns({ fileName: AGENTSMD_RULE_FILE_NAME });
74843
+ }
74844
+ /**
74845
+ * The subproject directory this rule scopes, or `undefined` for the root file
74846
+ * (project or global).
74847
+ */
74848
+ getSubprojectPath() {
74849
+ return this.getNestedSubprojectPath({ fileName: AGENTSMD_RULE_FILE_NAME });
74850
+ }
74851
+ static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, relativeDirPath: overrideDirPath, validate = true, global = false }) {
74708
74852
  const { root } = this.getSettablePaths({ global });
74709
- const relativePath = join(root.relativeDirPath, root.relativeFilePath);
74710
- const fileContent = await readFileContent(join(outputRoot, relativePath));
74711
- return new CodexcliRule({
74853
+ if (overrideDirPath !== void 0 && overrideDirPath !== root.relativeDirPath && overrideDirPath !== ".") {
74854
+ const fileContent = await readFileContent(join(outputRoot, overrideDirPath, AGENTSMD_RULE_FILE_NAME));
74855
+ return new this({
74856
+ outputRoot,
74857
+ relativeDirPath: overrideDirPath,
74858
+ relativeFilePath: AGENTSMD_RULE_FILE_NAME,
74859
+ fileContent,
74860
+ validate,
74861
+ root: false
74862
+ });
74863
+ }
74864
+ const fileContent = await readFileContent(join(outputRoot, root.relativeDirPath, root.relativeFilePath));
74865
+ return new this({
74712
74866
  outputRoot,
74713
74867
  relativeDirPath: root.relativeDirPath,
74714
74868
  relativeFilePath: root.relativeFilePath,
@@ -74719,8 +74873,18 @@ var CodexcliRule = class CodexcliRule extends ToolRule {
74719
74873
  }
74720
74874
  static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
74721
74875
  const { root } = this.getSettablePaths({ global });
74722
- const isRoot = rulesyncRule.getFrontmatter().root ?? false;
74723
- return new CodexcliRule({
74876
+ const frontmatter = rulesyncRule.getFrontmatter();
74877
+ const isRoot = frontmatter.root ?? false;
74878
+ const subprojectPath = frontmatter.agentsmd?.subprojectPath;
74879
+ if (!global && !isRoot && subprojectPath) return new this({
74880
+ outputRoot,
74881
+ relativeDirPath: join(subprojectPath),
74882
+ relativeFilePath: AGENTSMD_RULE_FILE_NAME,
74883
+ fileContent: rulesyncRule.getBody(),
74884
+ validate,
74885
+ root: false
74886
+ });
74887
+ return new this({
74724
74888
  outputRoot,
74725
74889
  relativeDirPath: root.relativeDirPath,
74726
74890
  relativeFilePath: root.relativeFilePath,
@@ -74730,7 +74894,9 @@ var CodexcliRule = class CodexcliRule extends ToolRule {
74730
74894
  });
74731
74895
  }
74732
74896
  toRulesyncRule() {
74733
- return this.toRulesyncRuleDefault();
74897
+ const subprojectPath = this.getSubprojectPath();
74898
+ if (subprojectPath === void 0) return this.toRulesyncRuleDefault();
74899
+ return this.toRulesyncRuleNestedAgentsmd({ subprojectPath });
74734
74900
  }
74735
74901
  validate() {
74736
74902
  return {
@@ -74739,23 +74905,54 @@ var CodexcliRule = class CodexcliRule extends ToolRule {
74739
74905
  };
74740
74906
  }
74741
74907
  static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
74742
- return new CodexcliRule({
74908
+ const isRoot = relativeFilePath === "AGENTS.md" && (relativeDirPath === "." || relativeDirPath === this.getFamily().globalDir);
74909
+ return new this({
74743
74910
  outputRoot,
74744
74911
  relativeDirPath,
74745
74912
  relativeFilePath,
74746
74913
  fileContent: "",
74747
74914
  validate: false,
74748
- root: relativeFilePath === "AGENTS.md" && (relativeDirPath === "." || relativeDirPath === ".codex")
74915
+ root: isRoot
74749
74916
  });
74750
74917
  }
74751
74918
  static isTargetedByRulesyncRule(rulesyncRule) {
74752
74919
  return this.isTargetedByRulesyncRuleDefault({
74753
74920
  rulesyncRule,
74754
- toolTarget: "codexcli"
74921
+ toolTarget: this.getFamily().toolTarget
74755
74922
  });
74756
74923
  }
74757
74924
  };
74758
74925
  //#endregion
74926
+ //#region src/features/rules/codexcli-rule.ts
74927
+ /**
74928
+ * Rule generator for OpenAI Codex CLI.
74929
+ *
74930
+ * Codex CLI loads instructions only from the `AGENTS.md` family: the global
74931
+ * `~/.codex/AGENTS.md` (or `AGENTS.override.md`), then one file per directory
74932
+ * level from the project root down to the current working directory, joined
74933
+ * root-first so deeper files take precedence. Nested per-directory files are
74934
+ * therefore a real scoping surface, and Codex's docs recommend splitting large
74935
+ * instruction sets across nested directories. It does NOT scan a
74936
+ * `.codex/memories/` directory for instruction files — that directory belongs
74937
+ * to Codex's separate SQLite-backed auto-memory system.
74938
+ *
74939
+ * A non-root rule carrying `agentsmd.subprojectPath` is emitted as a nested
74940
+ * `<subprojectPath>/AGENTS.md` (project scope only); every other non-root rule
74941
+ * folds into the root `AGENTS.md`, since Codex has no modular rules directory.
74942
+ * The project files on the root-to-cwd chain share one `project_doc_max_bytes`
74943
+ * budget (32 KiB by default), which the RulesProcessor warns about.
74944
+ *
74945
+ * @see https://learn.chatgpt.com/docs/agent-configuration/agents-md
74946
+ */
74947
+ var CodexcliRule = class extends NestedAgentsmdRule {
74948
+ static getFamily() {
74949
+ return {
74950
+ globalDir: CODEXCLI_DIR,
74951
+ toolTarget: "codexcli"
74952
+ };
74953
+ }
74954
+ };
74955
+ //#endregion
74759
74956
  //#region src/features/rules/commandcode-rule.ts
74760
74957
  var CommandcodeRule = class CommandcodeRule extends ToolRule {
74761
74958
  constructor({ fileContent, root, ...rest }) {
@@ -76060,122 +76257,6 @@ var DevinRule = class DevinRule extends ToolRule {
76060
76257
  }
76061
76258
  };
76062
76259
  //#endregion
76063
- //#region src/features/rules/nested-agentsmd-rule.ts
76064
- /**
76065
- * Shared adapter for the targets that read instructions exactly the way the
76066
- * AGENTS.md standard describes them and nothing more: a root `AGENTS.md` at the
76067
- * project root, nested per-directory `AGENTS.md` files loaded only while working
76068
- * under that directory, and a single personal root file under a tool-specific
76069
- * home directory. Such a target has no modular non-root instruction directory,
76070
- * so every other non-root rule folds into the root file.
76071
- *
76072
- * Concrete targets (Pool, Vibe, DeepSeek Harness) only supply the global
76073
- * directory and their tool target through {@link getFamily}; the vendor docs
76074
- * describing each one's discovery walk live on the subclass.
76075
- *
76076
- * The nested-file scan mirrors the AGENTS.md standard's nested discovery —
76077
- * same file name, same exclusions, import-only, project scope — because it
76078
- * discovers literally the same files.
76079
- */
76080
- var NestedAgentsmdRule = class extends ToolRule {
76081
- static getFamily() {
76082
- throw new Error("Please implement this method in the subclass.");
76083
- }
76084
- static getSettablePaths({ global = false } = {}) {
76085
- if (global) return { root: {
76086
- relativeDirPath: this.getFamily().globalDir,
76087
- relativeFilePath: AGENTSMD_RULE_FILE_NAME
76088
- } };
76089
- return { root: {
76090
- relativeDirPath: ".",
76091
- relativeFilePath: AGENTSMD_RULE_FILE_NAME
76092
- } };
76093
- }
76094
- static getNestedFilePatterns() {
76095
- return this.buildNestedFilePatterns({ fileName: AGENTSMD_RULE_FILE_NAME });
76096
- }
76097
- /**
76098
- * The subproject directory this rule scopes, or `undefined` for the root file
76099
- * (project or global).
76100
- */
76101
- getSubprojectPath() {
76102
- return this.getNestedSubprojectPath({ fileName: AGENTSMD_RULE_FILE_NAME });
76103
- }
76104
- static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, relativeDirPath: overrideDirPath, validate = true, global = false }) {
76105
- const { root } = this.getSettablePaths({ global });
76106
- if (overrideDirPath !== void 0 && overrideDirPath !== root.relativeDirPath && overrideDirPath !== ".") {
76107
- const fileContent = await readFileContent(join(outputRoot, overrideDirPath, AGENTSMD_RULE_FILE_NAME));
76108
- return new this({
76109
- outputRoot,
76110
- relativeDirPath: overrideDirPath,
76111
- relativeFilePath: AGENTSMD_RULE_FILE_NAME,
76112
- fileContent,
76113
- validate,
76114
- root: false
76115
- });
76116
- }
76117
- const fileContent = await readFileContent(join(outputRoot, root.relativeDirPath, root.relativeFilePath));
76118
- return new this({
76119
- outputRoot,
76120
- relativeDirPath: root.relativeDirPath,
76121
- relativeFilePath: root.relativeFilePath,
76122
- fileContent,
76123
- validate,
76124
- root: true
76125
- });
76126
- }
76127
- static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
76128
- const { root } = this.getSettablePaths({ global });
76129
- const frontmatter = rulesyncRule.getFrontmatter();
76130
- const isRoot = frontmatter.root ?? false;
76131
- const subprojectPath = frontmatter.agentsmd?.subprojectPath;
76132
- if (!global && !isRoot && subprojectPath) return new this({
76133
- outputRoot,
76134
- relativeDirPath: join(subprojectPath),
76135
- relativeFilePath: AGENTSMD_RULE_FILE_NAME,
76136
- fileContent: rulesyncRule.getBody(),
76137
- validate,
76138
- root: false
76139
- });
76140
- return new this({
76141
- outputRoot,
76142
- relativeDirPath: root.relativeDirPath,
76143
- relativeFilePath: root.relativeFilePath,
76144
- fileContent: rulesyncRule.getBody(),
76145
- validate,
76146
- root: isRoot
76147
- });
76148
- }
76149
- toRulesyncRule() {
76150
- const subprojectPath = this.getSubprojectPath();
76151
- if (subprojectPath === void 0) return this.toRulesyncRuleDefault();
76152
- return this.toRulesyncRuleNestedAgentsmd({ subprojectPath });
76153
- }
76154
- validate() {
76155
- return {
76156
- success: true,
76157
- error: null
76158
- };
76159
- }
76160
- static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
76161
- const isRoot = relativeFilePath === "AGENTS.md" && (relativeDirPath === "." || relativeDirPath === this.getFamily().globalDir);
76162
- return new this({
76163
- outputRoot,
76164
- relativeDirPath,
76165
- relativeFilePath,
76166
- fileContent: "",
76167
- validate: false,
76168
- root: isRoot
76169
- });
76170
- }
76171
- static isTargetedByRulesyncRule(rulesyncRule) {
76172
- return this.isTargetedByRulesyncRuleDefault({
76173
- rulesyncRule,
76174
- toolTarget: this.getFamily().toolTarget
76175
- });
76176
- }
76177
- };
76178
- //#endregion
76179
76260
  //#region src/features/rules/dsh-rule.ts
76180
76261
  /**
76181
76262
  * DeepSeek Harness (`dsh`) rules.
@@ -76387,7 +76468,8 @@ var FactorydroidRule = class FactorydroidRule extends ToolRule {
76387
76468
  rulesyncRule,
76388
76469
  validate,
76389
76470
  rootPath: paths.root,
76390
- nonRootPath: paths.nonRoot
76471
+ nonRootPath: paths.nonRoot,
76472
+ global
76391
76473
  }));
76392
76474
  }
76393
76475
  toRulesyncRule() {
@@ -76854,7 +76936,8 @@ var KiloRule = class KiloRule extends ToolRule {
76854
76936
  rulesyncRule,
76855
76937
  validate,
76856
76938
  rootPath: paths.root,
76857
- nonRootPath: paths.nonRoot
76939
+ nonRootPath: paths.nonRoot,
76940
+ global
76858
76941
  }));
76859
76942
  }
76860
76943
  toRulesyncRule() {
@@ -77358,7 +77441,8 @@ var OpenCodeRule = class OpenCodeRule extends ToolRule {
77358
77441
  rulesyncRule,
77359
77442
  validate,
77360
77443
  rootPath: paths.root,
77361
- nonRootPath: paths.nonRoot
77444
+ nonRootPath: paths.nonRoot,
77445
+ global
77362
77446
  }));
77363
77447
  }
77364
77448
  toRulesyncRule() {
@@ -78827,6 +78911,14 @@ var ZoocodeRule = class extends RooRule {
78827
78911
  //#endregion
78828
78912
  //#region src/features/rules/rules-processor.ts
78829
78913
  const RulesProcessorToolTargetSchema = z.enum(rulesProcessorToolTargetTuple);
78914
+ /**
78915
+ * Normalizes a relative directory path for comparison: POSIX separators, no
78916
+ * `./` segments and no trailing slash, with the current directory as `"."`
78917
+ * (so `"./"`, `"a/"` and `"a"` compare the way `join()` groups them).
78918
+ */
78919
+ const normalizeRelativeDir = (dir) => posix.normalize(toPosixPath(dir)).replace(/\/+$/, "") || ".";
78920
+ /** A tool rule's output path relative to its output root, for comparison. */
78921
+ const toolOutputPath = (toolRule) => posix.join(normalizeRelativeDir(toolRule.getRelativeDirPath()), toolRule.getRelativeFilePath());
78830
78922
  const formatRulePaths = (rules) => rules.map((r) => join(r.getRelativeDirPath(), r.getRelativeFilePath())).join(", ");
78831
78923
  const RulesFeatureOptionsSchema = z.looseObject({
78832
78924
  ruleDiscoveryMode: z.optional(z.enum(["none", "explicit"])),
@@ -78995,7 +79087,11 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
78995
79087
  extension: "md",
78996
79088
  supportsGlobal: true,
78997
79089
  ruleDiscoveryMode: "auto",
78998
- collisionPolicy: "fold"
79090
+ collisionPolicy: "fold",
79091
+ projectInstructionBudget: {
79092
+ bytes: CODEXCLI_PROJECT_DOC_MAX_BYTES,
79093
+ remedy: "Codex CLI stops reading project `AGENTS.md` files once their combined size on the root-to-working-directory chain reaches `project_doc_max_bytes` (32 KiB by default) and silently drops the rest. Move directory-scoped rules into nested `AGENTS.md` files with `agentsmd.subprojectPath` (or `deriveSubprojectPathFromGlobs: true` in rulesync.jsonc), trim the rules, or raise `project_doc_max_bytes` in `.codex/config.toml` (ignore this warning if you already have)."
79094
+ }
78999
79095
  }
79000
79096
  }],
79001
79097
  ["commandcode", {
@@ -79453,9 +79549,61 @@ var RulesProcessor = class extends FeatureProcessor {
79453
79549
  toolRules,
79454
79550
  factory
79455
79551
  });
79552
+ this.warnForInstructionBudget({
79553
+ toolRules,
79554
+ meta
79555
+ });
79456
79556
  return outputFiles;
79457
79557
  }
79458
79558
  /**
79559
+ * Warn when a generated project instruction chain exceeds the tool's byte
79560
+ * budget (`meta.projectInstructionBudget`).
79561
+ *
79562
+ * Codex CLI concatenates the project `AGENTS.md` files from the project root
79563
+ * down to the cwd and stops once their combined size reaches
79564
+ * `project_doc_max_bytes`, truncating the file that crosses it — silently, so
79565
+ * without this warning the rules past the limit simply never reach the model.
79566
+ * Each generated file is measured together with the generated files in its
79567
+ * ancestor directories, since that is the chain Codex loads while working
79568
+ * there. When the root file alone is over the budget every chain is, so only
79569
+ * the root is reported; likewise each over-budget nested chain is reported
79570
+ * once, at its shallowest directory. With several output roots the warning
79571
+ * names the root it refers to. An output root below the git root is measured
79572
+ * without the parent directories' files Codex would also load, so the
79573
+ * estimate can come out low there. The budget checked is the tool's default: a value
79574
+ * raised by hand in the tool's own config is not read. Global scope is not
79575
+ * checked because Codex loads the personal `~/.codex/AGENTS.md` outside the
79576
+ * project budget.
79577
+ */
79578
+ warnForInstructionBudget({ toolRules, meta }) {
79579
+ const budget = meta.projectInstructionBudget;
79580
+ if (budget === void 0 || this.global) return;
79581
+ const files = toolRules.map((rule) => ({
79582
+ dir: normalizeRelativeDir(rule.getRelativeDirPath()),
79583
+ path: toPosixPath(join(rule.getRelativeDirPath(), rule.getRelativeFilePath())),
79584
+ bytes: Buffer.byteLength(addTrailingNewline(rule.getFileContent()), "utf8")
79585
+ }));
79586
+ const isAncestorOrSelf = (ancestor, dir) => ancestor === "." || ancestor === dir || dir.startsWith(`${ancestor}/`);
79587
+ const location = this.outputRoot === process.cwd() ? "" : ` under '${this.outputRoot}'`;
79588
+ const rootFiles = files.filter(({ dir }) => dir === ".");
79589
+ const rootBytes = rootFiles.reduce((sum, { bytes }) => sum + bytes, 0);
79590
+ if (rootBytes > budget.bytes) {
79591
+ this.logger.warn(`The generated ${this.toolTarget} root instruction file${location} (${rootFiles.map(({ path }) => path).join(", ")}) is ${rootBytes} bytes, over the ${budget.bytes}-byte limit. ${budget.remedy}`);
79592
+ return;
79593
+ }
79594
+ const reportedDirs = [];
79595
+ const nestedFiles = files.filter(({ dir }) => dir !== ".").toSorted((a, b) => a.dir.split("/").length - b.dir.split("/").length);
79596
+ for (const file of nestedFiles) {
79597
+ if (reportedDirs.some((dir) => isAncestorOrSelf(dir, file.dir))) continue;
79598
+ const chain = files.filter(({ dir }) => isAncestorOrSelf(dir, file.dir));
79599
+ const chainBytes = chain.reduce((sum, { bytes }) => sum + bytes, 0);
79600
+ if (chainBytes > budget.bytes) {
79601
+ reportedDirs.push(file.dir);
79602
+ this.logger.warn(`The generated ${this.toolTarget} instruction files${location} loaded while working in '${file.dir}' (${chain.map(({ path }) => path).join(", ")}) total ${chainBytes} bytes, over the ${budget.bytes}-byte limit (as is every directory below it). ${budget.remedy}`);
79603
+ }
79604
+ }
79605
+ }
79606
+ /**
79459
79607
  * Handle localRoot rules (only in non-global mode and when enabled). Mutates
79460
79608
  * `toolRules` in place.
79461
79609
  */
@@ -80127,14 +80275,14 @@ As this project's AI coding tool, you must follow the additional conventions bel
80127
80275
  * decides for all of them: its `pi.contextFile` is copied onto the non-root
80128
80276
  * rules, and the flag set only on a non-root rule is dropped with a warning.
80129
80277
  */
80130
- alignPiContextFile(rules) {
80278
+ alignPiContextFile(rules, { warn = true } = {}) {
80131
80279
  if (this.toolTarget !== "pi") return rules;
80132
80280
  const factory = this.getFactory(this.toolTarget);
80133
80281
  const targeted = rules.filter((rule) => factory.class.isTargetedByRulesyncRule(rule));
80134
80282
  const rootContextFile = targeted.some((rule) => rule.getFrontmatter().root === true && rule.getFrontmatter().pi?.contextFile === "override") ? "override" : void 0;
80135
80283
  const mismatched = targeted.filter((rule) => rule.getFrontmatter().pi?.contextFile !== rootContextFile);
80136
80284
  if (mismatched.length === 0) return rules;
80137
- if (rootContextFile === void 0) this.logger.warn(`pi.contextFile is set on ${mismatched.length} non-root rule(s) but not on the root rule, so it is ignored: Pi folds every rule body into the root context file, and emitting AGENTS.override.md for some of them would hide the rest. Set it on the root rule instead: ${formatRulePaths(mismatched)}`);
80285
+ if (warn && rootContextFile === void 0) this.logger.warn(`pi.contextFile is set on ${mismatched.length} non-root rule(s) but not on the root rule, so it is ignored: Pi folds every rule body into the root context file, and emitting AGENTS.override.md for some of them would hide the rest. Set it on the root rule instead: ${formatRulePaths(mismatched)}`);
80138
80286
  const mismatchedSet = new Set(mismatched);
80139
80287
  return rules.map((rule) => {
80140
80288
  if (!mismatchedSet.has(rule)) return rule;
@@ -80188,14 +80336,72 @@ As this project's AI coding tool, you must follow the additional conventions bel
80188
80336
  * duplication check the same way `loadRulesyncFiles`'s global-mode branch
80189
80337
  * excludes it from `nonRootRules`: `generate` ignores `localRoot` entirely
80190
80338
  * in global mode, so such a rule is never actually folded into the global
80191
- * root output and warning about it here would be inaccurate.
80339
+ * root output and warning about it here would be inaccurate. The same goes
80340
+ * for a tool that writes `localRoot` to a dedicated local file.
80341
+ *
80342
+ * Non-root rules written to a file other than the root (nested
80343
+ * per-directory files, Pi's `APPEND_SYSTEM.md`) get a separate warning when
80344
+ * that file already exists and would import under a different name.
80192
80345
  */
80193
80346
  async warnForFoldImportDuplicationRisk() {
80194
80347
  const factory = this.getFactory(this.toolTarget);
80195
80348
  if (factory.meta.collisionPolicy !== "fold") return;
80196
- const nonRootRules = (await this.loadMergedRulesyncRules()).filter((rule) => !rule.getFrontmatter().root && (!this.global || !rule.getFrontmatter().localRoot) && factory.class.isTargetedByRulesyncRule(rule));
80197
- if (nonRootRules.length === 0) return;
80198
- this.logger.warn(`Importing ${this.toolTarget}'s root file will re-add content already folded from ${formatRulePaths(nonRootRules)}: ${this.toolTarget} concatenates every non-root rule into its single root output file, so the imported copy duplicates them the next time you run \`rulesync generate --targets ${this.toolTarget}\`. Review the imported rule and remove the duplicated content, or remove the original non-root rule files, before generating again.`);
80349
+ const targetedRules = this.alignPiContextFile(await this.loadMergedRulesyncRules(), { warn: false }).filter((rule) => factory.class.isTargetedByRulesyncRule(rule));
80350
+ let includeLocalRoot = !this.global;
80351
+ try {
80352
+ includeLocalRoot &&= resolveIncludeLocalRoot(this.featureOptions);
80353
+ } catch {}
80354
+ const targetedNonRootRules = targetedRules.filter((rule) => !rule.getFrontmatter().root && !(rule.getFrontmatter().localRoot && (!includeLocalRoot || factory.meta.localRootMode === "separate-local-file")));
80355
+ const rootPaths = /* @__PURE__ */ new Set();
80356
+ const { root } = factory.class.getSettablePaths({ global: this.global });
80357
+ if (root) rootPaths.add(posix.join(normalizeRelativeDir(root.relativeDirPath), root.relativeFilePath));
80358
+ for (const rulesyncRule of targetedRules.filter((rule) => rule.getFrontmatter().root)) {
80359
+ const toolRule = this.toToolRuleForImportCheck({
80360
+ factory,
80361
+ rulesyncRule
80362
+ });
80363
+ rootPaths.add(toolOutputPath(toolRule));
80364
+ }
80365
+ const separateRules = [];
80366
+ const foldedRules = [];
80367
+ let localRootCounted = false;
80368
+ for (const rulesyncRule of targetedNonRootRules) {
80369
+ if (rulesyncRule.getFrontmatter().localRoot) {
80370
+ if (!localRootCounted) {
80371
+ foldedRules.push(rulesyncRule);
80372
+ localRootCounted = true;
80373
+ }
80374
+ continue;
80375
+ }
80376
+ const toolRule = this.toToolRuleForImportCheck({
80377
+ factory,
80378
+ rulesyncRule
80379
+ });
80380
+ if (rootPaths.size === 0 || rootPaths.has(toolOutputPath(toolRule))) foldedRules.push(rulesyncRule);
80381
+ else separateRules.push({
80382
+ toolRule,
80383
+ rulesyncRule
80384
+ });
80385
+ }
80386
+ if (foldedRules.length > 0) this.logger.warn(`Importing ${this.toolTarget}'s root file will re-add content already folded from ${formatRulePaths(foldedRules)}: ${this.toolTarget} concatenates every non-root rule into its single root output file, so the imported copy duplicates them the next time you run \`rulesync generate --targets ${this.toolTarget}\`. Review the imported rule and remove the duplicated content, or remove the original non-root rule files, before generating again.`);
80387
+ const duplicated = [];
80388
+ for (const conversion of separateRules) {
80389
+ const { toolRule, rulesyncRule } = conversion;
80390
+ if (toolRule.toRulesyncRule().getRelativeFilePath().toLowerCase() === rulesyncRule.getRelativeFilePath().toLowerCase()) continue;
80391
+ if (await fileExists(toolRule.getFilePath())) duplicated.push(conversion);
80392
+ }
80393
+ if (duplicated.length > 0) {
80394
+ const fileNames = [...new Set(duplicated.map(({ toolRule }) => toolRule.getRelativeFilePath()))].join(", ");
80395
+ this.logger.warn(`Importing ${this.toolTarget}'s ${fileNames} files outside the root file will re-add content already written from ${formatRulePaths(duplicated.map(({ rulesyncRule }) => rulesyncRule))}: each such file is imported as its own rule under a name derived from its path, so the imported copy and the original rule resolve to the same file and are concatenated the next time you run \`rulesync generate --targets ${this.toolTarget}\`. Remove one of the two copies before generating again.`);
80396
+ }
80397
+ }
80398
+ toToolRuleForImportCheck({ factory, rulesyncRule }) {
80399
+ return factory.class.fromRulesyncRule({
80400
+ outputRoot: this.outputRoot,
80401
+ rulesyncRule,
80402
+ validate: false,
80403
+ global: this.global
80404
+ });
80199
80405
  }
80200
80406
  /**
80201
80407
  * Implementation of abstract method from FeatureProcessor
@@ -80489,6 +80695,14 @@ As this project's AI coding tool, you must follow the additional conventions bel
80489
80695
  function isPackagingToolTarget(toolTarget) {
80490
80696
  return PACKAGING_TOOL_TARGETS.includes(toolTarget);
80491
80697
  }
80698
+ /**
80699
+ * Packaging targets write into a plugin directory and support project scope
80700
+ * only. In global mode their output root would be the home directory, so
80701
+ * callers reject or skip them up front instead of checking all of $HOME.
80702
+ */
80703
+ function formatPackagingTargetGlobalMessage(toolTarget) {
80704
+ return `Target '${toolTarget}' is a plugin packaging target and supports only project scope. Re-run without '--global'.`;
80705
+ }
80492
80706
  async function assertPluginRootSafe(params) {
80493
80707
  if (!isPackagingToolTarget(params.toolTarget)) return;
80494
80708
  await assertDirectoryIfExists(params.outputRoot);
@@ -81408,14 +81622,17 @@ function warnUnsupportedTargets(params) {
81408
81622
  } catch {
81409
81623
  oppositeScopeTargets = [];
81410
81624
  }
81411
- for (const target of config.getTargets()) if (!supportedTargets.includes(target) && config.getFeatures(target).includes(featureName)) {
81412
- const simulateOption = SIMULATE_OPTION_MAP[featureName];
81413
- if (simulateOption && simulatedTargets.includes(target)) logger.warn(`Target '${target}' only supports simulated '${featureName}'. Use '${simulateOption}' to enable it. Skipping.`);
81414
- else if (oppositeScopeTargets.includes(target)) {
81415
- const supportedScope = config.getGlobal() ? "project" : "global";
81416
- const retry = config.getGlobal() ? "without '--global'" : "with '--global'";
81417
- logger.warn(`Target '${target}' supports the feature '${featureName}' only in ${supportedScope} scope. Re-run ${retry}. Skipping.`);
81418
- } else logger.warn(`Target '${target}' does not support the feature '${featureName}'. Skipping.`);
81625
+ for (const target of config.getTargets()) {
81626
+ if (config.getGlobal() && isPackagingToolTarget(target)) continue;
81627
+ if (!supportedTargets.includes(target) && config.getFeatures(target).includes(featureName)) {
81628
+ const simulateOption = SIMULATE_OPTION_MAP[featureName];
81629
+ if (simulateOption && simulatedTargets.includes(target)) logger.warn(`Target '${target}' only supports simulated '${featureName}'. Use '${simulateOption}' to enable it. Skipping.`);
81630
+ else if (oppositeScopeTargets.includes(target)) {
81631
+ const supportedScope = config.getGlobal() ? "project" : "global";
81632
+ const retry = config.getGlobal() ? "without '--global'" : "with '--global'";
81633
+ logger.warn(`Target '${target}' supports the feature '${featureName}' only in ${supportedScope} scope. Re-run ${retry}. Skipping.`);
81634
+ } else logger.warn(`Target '${target}' does not support the feature '${featureName}'. Skipping.`);
81635
+ }
81419
81636
  }
81420
81637
  }
81421
81638
  /**
@@ -81668,10 +81885,16 @@ async function generate(params) {
81668
81885
  const { config, logger } = params;
81669
81886
  resetRootShadowingWarnings({ logger });
81670
81887
  resetRunWarningState();
81671
- for (const toolTarget of config.getTargets()) for (const outputRoot of config.getOutputRoots(toolTarget)) await assertPluginRootSafe({
81672
- toolTarget,
81673
- outputRoot
81674
- });
81888
+ for (const toolTarget of config.getTargets()) {
81889
+ if (config.getGlobal() && isPackagingToolTarget(toolTarget)) {
81890
+ logger.warn(`${formatPackagingTargetGlobalMessage(toolTarget)} Skipping.`);
81891
+ continue;
81892
+ }
81893
+ for (const outputRoot of config.getOutputRoots(toolTarget)) await assertPluginRootSafe({
81894
+ toolTarget,
81895
+ outputRoot
81896
+ });
81897
+ }
81675
81898
  await warnSkillSubagentNameCollisions({
81676
81899
  config,
81677
81900
  logger
@@ -82288,6 +82511,7 @@ function getToolOutputRoot({ config, tool }) {
82288
82511
  async function importFromTool(params) {
82289
82512
  const { config, tool, logger } = params;
82290
82513
  resetRunWarningState();
82514
+ if (config.getGlobal() && isPackagingToolTarget(tool)) throw new Error(formatPackagingTargetGlobalMessage(tool));
82291
82515
  await assertPluginRootSafe({
82292
82516
  toolTarget: tool,
82293
82517
  outputRoot: getToolOutputRoot({
@@ -82348,7 +82572,7 @@ async function importRulesCore(params) {
82348
82572
  if (!config.getFeatures(tool).includes("rules")) return 0;
82349
82573
  const global = config.getGlobal();
82350
82574
  if (!RulesProcessor.getToolTargets({ global }).includes(tool)) return 0;
82351
- const rulesProcessor = new RulesProcessor({
82575
+ const baseParams = {
82352
82576
  outputRoot: getToolOutputRoot({
82353
82577
  config,
82354
82578
  tool
@@ -82356,13 +82580,23 @@ async function importRulesCore(params) {
82356
82580
  toolTarget: tool,
82357
82581
  global,
82358
82582
  logger
82359
- });
82583
+ };
82584
+ const rulesProcessor = new RulesProcessor(baseParams);
82360
82585
  const toolFiles = await rulesProcessor.loadToolFiles();
82361
82586
  if (toolFiles.length === 0) {
82362
82587
  logger.warn(`No rule files found for ${tool}. Skipping import.`);
82363
82588
  return 0;
82364
82589
  }
82365
- await rulesProcessor.warnForFoldImportDuplicationRisk();
82590
+ try {
82591
+ await new RulesProcessor({
82592
+ ...baseParams,
82593
+ inputRoots: config.getInputRoots(),
82594
+ deriveSubprojectPathFromGlobs: config.getDeriveSubprojectPathFromGlobs(),
82595
+ featureOptions: config.getFeatureOptions(tool, "rules")
82596
+ }).warnForFoldImportDuplicationRisk();
82597
+ } catch (error) {
82598
+ logger.warn(`Skipped the ${tool} import duplication check: ${formatError(error)}`);
82599
+ }
82366
82600
  const rulesyncFiles = await rulesProcessor.convertToolFilesToRulesyncFiles(toolFiles);
82367
82601
  const { count: writtenCount } = await rulesProcessor.writeAiFiles(rulesyncFiles);
82368
82602
  if (config.getVerbose() && writtenCount > 0) logger.success(`Created ${writtenCount} rule files`);
@@ -82612,4 +82846,4 @@ async function importChecksCore(params) {
82612
82846
  //#endregion
82613
82847
  export { RulesyncCommand as $, toPosixPath as $t, CLAUDECODE_MEMORIES_DIR_NAME as A, RULESYNC_XDG_CONFIG_HOME_DEFAULT_DIR_NAME as An, checkPathTraversal as At, RulesyncSubagent as B, stripControlCharacters as Bn, listDirectoryEntryNames as Bt, COMMANDCODE_DIR as C, RULESYNC_PERMISSIONS_SCHEMA_URL as Cn, withWarnOnceScope as Ct, CODEXCLI_DIR as D, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as Dn, assertDirectoryIfExists as Dt, CODEXCLI_BASH_RULES_FILE_NAME as E, RULESYNC_SKILLS_RELATIVE_DIR_PATH as En, applyFileMode as Et, caseFoldIdentity as F, formatError as Fn, getFileSize as Ft, RulesyncRuleFrontmatterSchema as G, readFileContentOrNull as Gt, RulesyncSkill as H, stripHiddenCharacters as Hn, listSubdirectoryNames as Ht, groupSpellingsByCaseFoldedIdentity as I, truncateText as In, getHomeDirectory as It, RulesyncIgnore as J, removeFile as Jt, RulesyncPermissions as K, removeDirectory as Kt, AUGMENTCODE_DIR as L, hasDeceptiveHiddenCharacters as Ln, isFileNotFoundError as Lt, CLAUDECODE_SKILLS_DIR_PATH as M, ALL_FEATURES as Mn, directoryExists as Mt, FACTORYDROID_DIR as N, ALL_FEATURES_WITH_WILDCARD as Nn, ensureDir as Nt, CLAUDECODE_DIR as O, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as On, assertTreeContainsNoSymlinks as Ot, FACTORYDROID_SETTINGS_LOCAL_FILE_NAME as P, DEPRECATED_FEATURE_REPLACEMENTS as Pn, fileExists as Pt, parseJsonc as Q, runWithDirectoryRollback as Qt, AUGMENTCODE_SETTINGS_LOCAL_FILE_NAME as R, hasEnclosingMarkOutsideKeycap as Rn, isFileSystemError as Rt, QWENCODE_LOCAL_RULE_FILE_NAME as S, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as Sn, resetRunWarningState as St, ChecksProcessor as T, RULESYNC_RULES_RELATIVE_DIR_PATH as Tn, ErrorCodes as Tt, RulesyncSkillFrontmatterSchema as U, pathEscapesRoot as Ut, RulesyncSubagentFrontmatterSchema as V, stripControlCharactersKeepingLineFeeds as Vn, listFilePathsRecursively as Vt, RulesyncRule as W, readFileContent as Wt, getRulesyncSourceCandidates as X, removeTempDirectory as Xt, RulesyncHooks as Y, removeFileStrict as Yt, resolveRulesyncSourceWritePath as Z, resolvePath as Zt, IgnoreProcessor as _, RULESYNC_IGNORE_RELATIVE_FILE_PATH as _n, JsonLogger as _t, getProcessorRegistryEntry as a, ToolTargetSchema as an, SHARED_USER_MANAGED_CONFIG_PATHS as at, CommandsProcessor as b, RULESYNC_MCP_SCHEMA_URL as bn, warnOnConflictingFlags as bt, RulesProcessor as c, MAX_FILE_SIZE as cn, mergeInputRootConfigs as ct, CODEBUDDY_DIR as d, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as dn, ConfigFileSchema as dt, writeFileBuffer as en, RulesyncCommandFrontmatterSchema as et, CODEBUDDY_LOCAL_RULE_FILE_NAME as f, RULESYNC_CONFIG_RELATIVE_FILE_PATH as fn, GITIGNORE_DESTINATION_KEY as ft, McpProcessor as g, RULESYNC_HOOKS_RELATIVE_FILE_PATH as gn, ConsoleLogger as gt, shortenToWidth as h, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as hn, findControlCharacter as ht, inspectInputRoots as i, PACKAGING_TOOL_TARGETS as in, loadYaml as it, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as j, parseCommaSeparatedList as jn, createTempDirectory as jt, CLAUDECODE_LOCAL_RULE_FILE_NAME as k, RULESYNC_USER_CONFIG_DIR_NAME as kn, assertWritablePathInsideRoot as kt, SubagentsProcessor as l, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as ln, resolveEffectiveInputRoots as lt, displayWidthOf as m, RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH as mn, assertTargetsFeaturesExclusive as mt, formatSourceLoadFailure as n, ALL_TOOL_TARGETS as nn, RulesyncCheckFrontmatterSchema as nt, convertFromTool as o, CURATED_RULES_FEATURE_SUBDIR as on, SKILL_FILE_NAME as ot, ELLIPSIS_WIDTH as p, RULESYNC_CONFIG_SCHEMA_URL as pn, SourceEntrySchema as pt, RulesyncMcp as q, removeDirectoryStrict as qt, generate as r, ALL_TOOL_TARGETS_WITH_WILDCARD as rn, stringifyFrontmatter as rt, isPackagingToolTarget as s, FEATURE_SOURCE_TREE_ENTRIES as sn, ConfigResolver as st, importFromTool as t, writeFileContent as tn, RulesyncCheck as tt, SkillsProcessor as u, RULESYNC_CHECKS_RELATIVE_DIR_PATH as un, CONFLICTING_TARGET_PAIRS as ut, HooksProcessor as v, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as vn, WarningCollectingLogger as vt, COMMANDCODE_SETTINGS_LOCAL_FILE_NAME as w, RULESYNC_RELATIVE_DIR_PATH as wn, CLIError as wt, QWENCODE_DIR as x, RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH as xn, withFallbackLoggerTarget as xt, CRUSH_LOCAL_RULE_FILE_NAME as y, RULESYNC_MCP_RELATIVE_FILE_PATH as yn, fallbackLogger as yt, getLocalSkillDirNames as z, quoteForLog as zn, isSymlink as zt };
82614
82848
 
82615
- //# sourceMappingURL=import-_UbLpXUZ.js.map
82849
+ //# sourceMappingURL=import-DNDrEvS2.js.map