rulesync 21.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.
@@ -444,36 +444,36 @@ const isFeatureValueEnabled = (value) => {
444
444
  const parseCommaSeparatedList = (value) => value.split(",").map((s) => s.trim()).filter(Boolean);
445
445
  //#endregion
446
446
  //#region src/constants/rulesync-paths.ts
447
- const { join: join$352 } = node_path.posix;
447
+ const { join: join$351 } = node_path.posix;
448
448
  const RULESYNC_CONFIG_RELATIVE_FILE_PATH = "rulesync.jsonc";
449
449
  const RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH = "rulesync.local.jsonc";
450
450
  const RULESYNC_USER_CONFIG_DIR_NAME = "rulesync";
451
451
  const RULESYNC_XDG_CONFIG_HOME_DEFAULT_DIR_NAME = ".config";
452
452
  const RULESYNC_RELATIVE_DIR_PATH = ".rulesync";
453
453
  const RULES_FEATURE_SUBDIR = "rules";
454
- const CURATED_RULES_FEATURE_SUBDIR = join$352(RULES_FEATURE_SUBDIR, ".curated");
454
+ const CURATED_RULES_FEATURE_SUBDIR = join$351(RULES_FEATURE_SUBDIR, ".curated");
455
455
  const COMMANDS_FEATURE_SUBDIR = "commands";
456
456
  const SUBAGENTS_FEATURE_SUBDIR = "subagents";
457
457
  const CHECKS_FEATURE_SUBDIR = "checks";
458
458
  const SKILLS_FEATURE_SUBDIR = "skills";
459
- const CURATED_SKILLS_FEATURE_SUBDIR = join$352(SKILLS_FEATURE_SUBDIR, ".curated");
460
- const RULESYNC_RULES_RELATIVE_DIR_PATH = join$352(RULESYNC_RELATIVE_DIR_PATH, RULES_FEATURE_SUBDIR);
461
- const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$352(RULESYNC_RELATIVE_DIR_PATH, CURATED_RULES_FEATURE_SUBDIR);
462
- const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$352(RULESYNC_RELATIVE_DIR_PATH, COMMANDS_FEATURE_SUBDIR);
463
- const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$352(RULESYNC_RELATIVE_DIR_PATH, SUBAGENTS_FEATURE_SUBDIR);
464
- const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$352(RULESYNC_RELATIVE_DIR_PATH, CHECKS_FEATURE_SUBDIR);
465
- const RULESYNC_MCP_RELATIVE_FILE_PATH = join$352(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
466
- const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$352(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
467
- const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$352(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
468
- join$352(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
469
- const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$352(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
470
- const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$352(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
459
+ const CURATED_SKILLS_FEATURE_SUBDIR = join$351(SKILLS_FEATURE_SUBDIR, ".curated");
460
+ const RULESYNC_RULES_RELATIVE_DIR_PATH = join$351(RULESYNC_RELATIVE_DIR_PATH, RULES_FEATURE_SUBDIR);
461
+ const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$351(RULESYNC_RELATIVE_DIR_PATH, CURATED_RULES_FEATURE_SUBDIR);
462
+ const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$351(RULESYNC_RELATIVE_DIR_PATH, COMMANDS_FEATURE_SUBDIR);
463
+ const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$351(RULESYNC_RELATIVE_DIR_PATH, SUBAGENTS_FEATURE_SUBDIR);
464
+ const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$351(RULESYNC_RELATIVE_DIR_PATH, CHECKS_FEATURE_SUBDIR);
465
+ const RULESYNC_MCP_RELATIVE_FILE_PATH = join$351(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
466
+ const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$351(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
467
+ const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$351(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
468
+ join$351(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
469
+ const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$351(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
470
+ const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$351(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
471
471
  const RULESYNC_AIIGNORE_FILE_NAME = ".aiignore";
472
- const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$352(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
472
+ const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$351(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
473
473
  const RULESYNC_IGNORE_RELATIVE_FILE_PATH = ".rulesyncignore";
474
474
  const RULESYNC_OVERVIEW_FILE_NAME = "overview.md";
475
- const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$352(RULESYNC_RELATIVE_DIR_PATH, SKILLS_FEATURE_SUBDIR);
476
- const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$352(RULESYNC_RELATIVE_DIR_PATH, CURATED_SKILLS_FEATURE_SUBDIR);
475
+ const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$351(RULESYNC_RELATIVE_DIR_PATH, SKILLS_FEATURE_SUBDIR);
476
+ const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$351(RULESYNC_RELATIVE_DIR_PATH, CURATED_SKILLS_FEATURE_SUBDIR);
477
477
  const RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync.lock";
478
478
  const RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync-npm.lock.json";
479
479
  const RULESYNC_MCP_FILE_NAME = "mcp.jsonc";
@@ -12295,7 +12295,7 @@ const CODEXCLI_AGENTS_DIR_PATH = (0, node_path.join)(CODEXCLI_DIR, "agents");
12295
12295
  const CODEXCLI_SKILLS_DIR_PATH = (0, node_path.join)(".agents", "skills");
12296
12296
  const CODEXCLI_HOOKS_FILE_NAME = "hooks.json";
12297
12297
  const CODEXCLI_MCP_FILE_NAME = "config.toml";
12298
- const CODEXCLI_RULE_FILE_NAME = "AGENTS.md";
12298
+ const CODEXCLI_PROJECT_DOC_MAX_BYTES = 32768;
12299
12299
  const CODEXCLI_BASH_RULES_FILE_NAME = "rulesync.rules";
12300
12300
  const CODEXCLI_OPENAI_YAML_RELATIVE_PATH = (0, node_path.join)("agents", "openai.yaml");
12301
12301
  const CODEXCLI_OVERRIDE_KEYS = [
@@ -15480,12 +15480,50 @@ var BobCommand = class BobCommand extends ToolCommand {
15480
15480
  }
15481
15481
  };
15482
15482
  //#endregion
15483
+ //#region src/features/shared/claudecode-argument-hint.ts
15484
+ /**
15485
+ * Turns a YAML-list `argument-hint` back into the bracketed string form Claude
15486
+ * Code documents (`[issue-number]`, `[filename] [format]`), one bracketed
15487
+ * placeholder per list entry. The per-entry form is chosen over re-joining with
15488
+ * commas because it is the shape every Claude Code example uses. An empty list
15489
+ * carries no hint, so it yields `undefined` and the key is dropped.
15490
+ */
15491
+ function joinArgumentHint(value) {
15492
+ if (typeof value === "string") return value;
15493
+ if (value.length === 0) return;
15494
+ return value.map((entry) => `[${String(entry)}]`).join(" ");
15495
+ }
15496
+ /**
15497
+ * `argument-hint` as Claude Code reads it from skill and command frontmatter.
15498
+ *
15499
+ * The documented examples (`[issue-number]`) are written unquoted, and YAML
15500
+ * parses an unquoted `[issue-number]` as a one-element list. Claude Code
15501
+ * accepts that form, so it is accepted here too and normalized to the
15502
+ * bracketed string, keeping the parsed value (and everything downstream of it)
15503
+ * a plain string. A scalar entry YAML did not read as a string (`[1]`,
15504
+ * `[true]`, `[~]`) is stringified rather than rejected.
15505
+ * https://code.claude.com/docs/en/skills
15506
+ *
15507
+ * A list entry is stringified from the value YAML produced, so its original
15508
+ * spelling is not kept (`[1.0]` becomes `[1]`), and an entry YAML reads as a
15509
+ * date (`[2026-01-01]`) is rejected.
15510
+ *
15511
+ * The unquoted multi-placeholder form (`argument-hint: [filename] [format]`)
15512
+ * is not valid YAML at all, so it fails before this schema is reached.
15513
+ */
15514
+ const ClaudecodeArgumentHintSchema = zod_mini.z.pipe(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.union([
15515
+ zod_mini.z.string(),
15516
+ zod_mini.z.number(),
15517
+ zod_mini.z.boolean(),
15518
+ zod_mini.z.null()
15519
+ ]))]), zod_mini.z.transform(joinArgumentHint));
15520
+ //#endregion
15483
15521
  //#region src/features/commands/claudecode-command.ts
15484
15522
  const ClaudecodeCommandFrontmatterSchema = zod_mini.z.looseObject({
15485
15523
  description: zod_mini.z.optional(zod_mini.z.string()),
15486
15524
  "allowed-tools": zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())])),
15487
15525
  "disallowed-tools": zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())])),
15488
- "argument-hint": zod_mini.z.optional(zod_mini.z.string()),
15526
+ "argument-hint": zod_mini.z.optional(ClaudecodeArgumentHintSchema),
15489
15527
  model: zod_mini.z.optional(zod_mini.z.string()),
15490
15528
  "disable-model-invocation": zod_mini.z.optional(zod_mini.z.boolean())
15491
15529
  });
@@ -15514,7 +15552,8 @@ var ClaudecodeCommand = class ClaudecodeCommand extends ToolCommand {
15514
15552
  return this.frontmatter;
15515
15553
  }
15516
15554
  toRulesyncCommand() {
15517
- const { description, ...restFields } = this.frontmatter;
15555
+ const { description, ...extraFields } = this.frontmatter;
15556
+ const restFields = Object.fromEntries(Object.entries(extraFields).filter(([, value]) => value !== void 0));
15518
15557
  const rulesyncFrontmatter = {
15519
15558
  targets: ["*"],
15520
15559
  description,
@@ -33147,6 +33186,74 @@ function translateCanonicalKeyToCodex({ key, value, isStdio, serverName }) {
33147
33186
  }
33148
33187
  }
33149
33188
  /**
33189
+ * A header value that is exactly one canonical `${VAR}` reference. Anchored and
33190
+ * without the `g` flag, so `.exec()` carries no `lastIndex` state between
33191
+ * calls. `${VAR:-default}` does not match (the name excludes `:`), because
33192
+ * Codex has no way to express the fallback.
33193
+ */
33194
+ const WHOLE_ENV_VAR_REF_PATTERN = /^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$/;
33195
+ /**
33196
+ * An `Authorization` value that is exactly `Bearer ${VAR}`. The scheme is
33197
+ * matched case-insensitively, as HTTP authentication schemes are.
33198
+ */
33199
+ const BEARER_ENV_VAR_REF_PATTERN = /^Bearer \$\{([A-Za-z_][A-Za-z0-9_]*)\}$/i;
33200
+ const AUTHORIZATION_HEADER_NAME = "authorization";
33201
+ /**
33202
+ * Whether a string still contains an unexpanded `${…}` reference. Codex writes
33203
+ * `url` and `http_headers` values as-is and never expands them, so such a value
33204
+ * reaches the server literally.
33205
+ */
33206
+ function hasUnexpandedEnvVarRef(value) {
33207
+ return value.includes("${");
33208
+ }
33209
+ /**
33210
+ * Rewrite canonical `${VAR}` header references into the keys Codex reads them
33211
+ * from, since Codex sends `http_headers` values verbatim:
33212
+ * - `Authorization: Bearer ${VAR}` → `bearer_token_env_var = "VAR"`
33213
+ * - a header whose whole value is `${VAR}` → `env_http_headers.<name> = "VAR"`
33214
+ *
33215
+ * Runs after every key is translated, so a `bearer_token_env_var` or
33216
+ * `env_http_headers` the source already carries (they pass through under their
33217
+ * own names) is seen regardless of key order. Those explicit values win: a
33218
+ * derived bearer is not written over an existing one, and an existing
33219
+ * `env_http_headers` entry for the same header is kept. Any value the rewrite
33220
+ * cannot express — a partial reference, a `${VAR:-default}` fallback — stays in
33221
+ * `http_headers` verbatim with a warning, as does a `${…}` in `url`, for which
33222
+ * Codex has no environment-sourced counterpart at all.
33223
+ * @see https://learn.chatgpt.com/docs/config-file/config-reference
33224
+ */
33225
+ function convertEnvVarRefsForCodex({ converted, serverName }) {
33226
+ const url = converted["url"];
33227
+ 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.`);
33228
+ const httpHeaders = converted["http_headers"];
33229
+ if (!isHeadersRecord(httpHeaders)) return;
33230
+ const existingEnvHeaders = converted["env_http_headers"];
33231
+ const canWriteEnvHeaders = existingEnvHeaders === void 0 || isHeadersRecord(existingEnvHeaders);
33232
+ const envHeaders = {};
33233
+ const staticHeaders = {};
33234
+ let bearerTokenEnvVar;
33235
+ for (const [name, value] of Object.entries(httpHeaders)) {
33236
+ const bearerMatch = BEARER_ENV_VAR_REF_PATTERN.exec(value);
33237
+ if (bearerMatch?.[1] && name.toLowerCase() === AUTHORIZATION_HEADER_NAME) {
33238
+ if (converted["bearer_token_env_var"] === void 0 && bearerTokenEnvVar === void 0) bearerTokenEnvVar = bearerMatch[1];
33239
+ continue;
33240
+ }
33241
+ const wholeMatch = WHOLE_ENV_VAR_REF_PATTERN.exec(value);
33242
+ if (wholeMatch?.[1] && canWriteEnvHeaders) {
33243
+ envHeaders[name] = wholeMatch[1];
33244
+ continue;
33245
+ }
33246
+ 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.`);
33247
+ staticHeaders[name] = value;
33248
+ }
33249
+ converted["http_headers"] = staticHeaders;
33250
+ if (bearerTokenEnvVar !== void 0) converted["bearer_token_env_var"] = bearerTokenEnvVar;
33251
+ if (Object.keys(envHeaders).length > 0) converted["env_http_headers"] = {
33252
+ ...envHeaders,
33253
+ ...isHeadersRecord(existingEnvHeaders) ? existingEnvHeaders : {}
33254
+ };
33255
+ }
33256
+ /**
33150
33257
  * Codex states no transport of its own — it infers one from `command` versus
33151
33258
  * `url`, which is why generate drops the canonical `type`. Restate it for a url
33152
33259
  * server on the way back, so a config imported from Codex reaches the adapters
@@ -33233,6 +33340,10 @@ function convertToCodexFormat(mcpServers) {
33233
33340
  }
33234
33341
  } else converted[key] = value;
33235
33342
  }
33343
+ convertEnvVarRefsForCodex({
33344
+ converted,
33345
+ serverName: name
33346
+ });
33236
33347
  const previousName = originalNames.get(codexName);
33237
33348
  if (previousName !== void 0) warnWithFallback(void 0, `Codex MCP server name collision: "${previousName}" and "${name}" both normalize to "${codexName}"; "${name}" (processed last) overwrites "${previousName}".`);
33238
33349
  originalNames.set(codexName, name);
@@ -59232,7 +59343,7 @@ const ClaudecodeSkillFrontmatterSchema = zod_mini.z.looseObject({
59232
59343
  "disallowed-tools": zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())])),
59233
59344
  model: zod_mini.z.optional(zod_mini.z.string()),
59234
59345
  effort: zod_mini.z.optional(zod_mini.z.string()),
59235
- "argument-hint": zod_mini.z.optional(zod_mini.z.string()),
59346
+ "argument-hint": zod_mini.z.optional(ClaudecodeArgumentHintSchema),
59236
59347
  arguments: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())])),
59237
59348
  context: zod_mini.z.optional(zod_mini.z.string()),
59238
59349
  agent: zod_mini.z.optional(zod_mini.z.string()),
@@ -65388,7 +65499,8 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
65388
65499
  meta: {
65389
65500
  supportsProject: true,
65390
65501
  supportsSimulated: false,
65391
- supportsGlobal: true
65502
+ supportsGlobal: true,
65503
+ lenientImport: true
65392
65504
  }
65393
65505
  }],
65394
65506
  ["claudecode-plugin", {
@@ -72337,7 +72449,7 @@ var ToolRule = class extends ToolFile {
72337
72449
  static buildToolRuleParamsAgentsmd({ outputRoot = process.cwd(), rulesyncRule, validate = true, rootPath = {
72338
72450
  relativeDirPath: ".",
72339
72451
  relativeFilePath: "AGENTS.md"
72340
- }, nonRootPath = { relativeDirPath: AGENTSMD_MEMORIES_DIR_PATH } }) {
72452
+ }, nonRootPath = { relativeDirPath: AGENTSMD_MEMORIES_DIR_PATH }, global = false }) {
72341
72453
  const params = this.buildToolRuleParamsDefault({
72342
72454
  outputRoot,
72343
72455
  rulesyncRule,
@@ -72346,7 +72458,7 @@ var ToolRule = class extends ToolFile {
72346
72458
  nonRootPath
72347
72459
  });
72348
72460
  const rulesyncFrontmatter = rulesyncRule.getFrontmatter();
72349
- if (!rulesyncFrontmatter.root && rulesyncFrontmatter.agentsmd?.subprojectPath) {
72461
+ if (!global && !rulesyncFrontmatter.root && rulesyncFrontmatter.agentsmd?.subprojectPath) {
72350
72462
  params.relativeDirPath = (0, node_path.join)(rulesyncFrontmatter.agentsmd.subprojectPath);
72351
72463
  params.relativeFilePath = "AGENTS.md";
72352
72464
  }
@@ -73003,7 +73115,8 @@ var AmpRule = class AmpRule extends ToolRule {
73003
73115
  rulesyncRule,
73004
73116
  validate,
73005
73117
  rootPath: paths.root,
73006
- nonRootPath: paths.nonRoot
73118
+ nonRootPath: paths.nonRoot,
73119
+ global
73007
73120
  });
73008
73121
  if (!params.root && params.globs !== void 0 && params.globs.length > 0) params.fileContent = stringifyFrontmatter(params.fileContent, { globs: params.globs });
73009
73122
  return new AmpRule(params);
@@ -74298,7 +74411,8 @@ var ClineRule = class ClineRule extends ToolRule {
74298
74411
  outputRoot,
74299
74412
  rulesyncRule,
74300
74413
  validate,
74301
- rootPath: paths.root
74414
+ rootPath: paths.root,
74415
+ global
74302
74416
  }));
74303
74417
  }
74304
74418
  if (isRoot) return new ClineRule({
@@ -74718,26 +74832,62 @@ var CodebuddyRule = class CodebuddyRule extends ToolRule {
74718
74832
  }
74719
74833
  };
74720
74834
  //#endregion
74721
- //#region src/features/rules/codexcli-rule.ts
74722
- var CodexcliRule = class CodexcliRule extends ToolRule {
74723
- constructor({ fileContent, root, ...rest }) {
74724
- super({
74725
- ...rest,
74726
- fileContent,
74727
- root: root ?? false
74728
- });
74835
+ //#region src/features/rules/nested-agentsmd-rule.ts
74836
+ /**
74837
+ * Shared adapter for the targets that read instructions exactly the way the
74838
+ * AGENTS.md standard describes them and nothing more: a root `AGENTS.md` at the
74839
+ * project root, nested per-directory `AGENTS.md` files loaded only while working
74840
+ * under that directory, and a single personal root file under a tool-specific
74841
+ * home directory. Such a target has no modular non-root instruction directory,
74842
+ * so every other non-root rule folds into the root file.
74843
+ *
74844
+ * Concrete targets (Codex CLI, Pool, Vibe, DeepSeek Harness) only supply the global
74845
+ * directory and their tool target through {@link getFamily}; the vendor docs
74846
+ * describing each one's discovery walk live on the subclass.
74847
+ *
74848
+ * The nested-file scan mirrors the AGENTS.md standard's nested discovery —
74849
+ * same file name, same exclusions, import-only, project scope — because it
74850
+ * discovers literally the same files.
74851
+ */
74852
+ var NestedAgentsmdRule = class extends ToolRule {
74853
+ static getFamily() {
74854
+ throw new Error("Please implement this method in the subclass.");
74729
74855
  }
74730
74856
  static getSettablePaths({ global = false } = {}) {
74857
+ if (global) return { root: {
74858
+ relativeDirPath: this.getFamily().globalDir,
74859
+ relativeFilePath: AGENTSMD_RULE_FILE_NAME
74860
+ } };
74731
74861
  return { root: {
74732
- relativeDirPath: global ? CODEXCLI_DIR : ".",
74733
- relativeFilePath: CODEXCLI_RULE_FILE_NAME
74862
+ relativeDirPath: ".",
74863
+ relativeFilePath: AGENTSMD_RULE_FILE_NAME
74734
74864
  } };
74735
74865
  }
74736
- static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, validate = true, global = false }) {
74866
+ static getNestedFilePatterns() {
74867
+ return this.buildNestedFilePatterns({ fileName: AGENTSMD_RULE_FILE_NAME });
74868
+ }
74869
+ /**
74870
+ * The subproject directory this rule scopes, or `undefined` for the root file
74871
+ * (project or global).
74872
+ */
74873
+ getSubprojectPath() {
74874
+ return this.getNestedSubprojectPath({ fileName: AGENTSMD_RULE_FILE_NAME });
74875
+ }
74876
+ static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, relativeDirPath: overrideDirPath, validate = true, global = false }) {
74737
74877
  const { root } = this.getSettablePaths({ global });
74738
- const relativePath = (0, node_path.join)(root.relativeDirPath, root.relativeFilePath);
74739
- const fileContent = await readFileContent((0, node_path.join)(outputRoot, relativePath));
74740
- return new CodexcliRule({
74878
+ if (overrideDirPath !== void 0 && overrideDirPath !== root.relativeDirPath && overrideDirPath !== ".") {
74879
+ const fileContent = await readFileContent((0, node_path.join)(outputRoot, overrideDirPath, AGENTSMD_RULE_FILE_NAME));
74880
+ return new this({
74881
+ outputRoot,
74882
+ relativeDirPath: overrideDirPath,
74883
+ relativeFilePath: AGENTSMD_RULE_FILE_NAME,
74884
+ fileContent,
74885
+ validate,
74886
+ root: false
74887
+ });
74888
+ }
74889
+ const fileContent = await readFileContent((0, node_path.join)(outputRoot, root.relativeDirPath, root.relativeFilePath));
74890
+ return new this({
74741
74891
  outputRoot,
74742
74892
  relativeDirPath: root.relativeDirPath,
74743
74893
  relativeFilePath: root.relativeFilePath,
@@ -74748,8 +74898,18 @@ var CodexcliRule = class CodexcliRule extends ToolRule {
74748
74898
  }
74749
74899
  static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
74750
74900
  const { root } = this.getSettablePaths({ global });
74751
- const isRoot = rulesyncRule.getFrontmatter().root ?? false;
74752
- return new CodexcliRule({
74901
+ const frontmatter = rulesyncRule.getFrontmatter();
74902
+ const isRoot = frontmatter.root ?? false;
74903
+ const subprojectPath = frontmatter.agentsmd?.subprojectPath;
74904
+ if (!global && !isRoot && subprojectPath) return new this({
74905
+ outputRoot,
74906
+ relativeDirPath: (0, node_path.join)(subprojectPath),
74907
+ relativeFilePath: AGENTSMD_RULE_FILE_NAME,
74908
+ fileContent: rulesyncRule.getBody(),
74909
+ validate,
74910
+ root: false
74911
+ });
74912
+ return new this({
74753
74913
  outputRoot,
74754
74914
  relativeDirPath: root.relativeDirPath,
74755
74915
  relativeFilePath: root.relativeFilePath,
@@ -74759,7 +74919,9 @@ var CodexcliRule = class CodexcliRule extends ToolRule {
74759
74919
  });
74760
74920
  }
74761
74921
  toRulesyncRule() {
74762
- return this.toRulesyncRuleDefault();
74922
+ const subprojectPath = this.getSubprojectPath();
74923
+ if (subprojectPath === void 0) return this.toRulesyncRuleDefault();
74924
+ return this.toRulesyncRuleNestedAgentsmd({ subprojectPath });
74763
74925
  }
74764
74926
  validate() {
74765
74927
  return {
@@ -74768,23 +74930,54 @@ var CodexcliRule = class CodexcliRule extends ToolRule {
74768
74930
  };
74769
74931
  }
74770
74932
  static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
74771
- return new CodexcliRule({
74933
+ const isRoot = relativeFilePath === "AGENTS.md" && (relativeDirPath === "." || relativeDirPath === this.getFamily().globalDir);
74934
+ return new this({
74772
74935
  outputRoot,
74773
74936
  relativeDirPath,
74774
74937
  relativeFilePath,
74775
74938
  fileContent: "",
74776
74939
  validate: false,
74777
- root: relativeFilePath === "AGENTS.md" && (relativeDirPath === "." || relativeDirPath === ".codex")
74940
+ root: isRoot
74778
74941
  });
74779
74942
  }
74780
74943
  static isTargetedByRulesyncRule(rulesyncRule) {
74781
74944
  return this.isTargetedByRulesyncRuleDefault({
74782
74945
  rulesyncRule,
74783
- toolTarget: "codexcli"
74946
+ toolTarget: this.getFamily().toolTarget
74784
74947
  });
74785
74948
  }
74786
74949
  };
74787
74950
  //#endregion
74951
+ //#region src/features/rules/codexcli-rule.ts
74952
+ /**
74953
+ * Rule generator for OpenAI Codex CLI.
74954
+ *
74955
+ * Codex CLI loads instructions only from the `AGENTS.md` family: the global
74956
+ * `~/.codex/AGENTS.md` (or `AGENTS.override.md`), then one file per directory
74957
+ * level from the project root down to the current working directory, joined
74958
+ * root-first so deeper files take precedence. Nested per-directory files are
74959
+ * therefore a real scoping surface, and Codex's docs recommend splitting large
74960
+ * instruction sets across nested directories. It does NOT scan a
74961
+ * `.codex/memories/` directory for instruction files — that directory belongs
74962
+ * to Codex's separate SQLite-backed auto-memory system.
74963
+ *
74964
+ * A non-root rule carrying `agentsmd.subprojectPath` is emitted as a nested
74965
+ * `<subprojectPath>/AGENTS.md` (project scope only); every other non-root rule
74966
+ * folds into the root `AGENTS.md`, since Codex has no modular rules directory.
74967
+ * The project files on the root-to-cwd chain share one `project_doc_max_bytes`
74968
+ * budget (32 KiB by default), which the RulesProcessor warns about.
74969
+ *
74970
+ * @see https://learn.chatgpt.com/docs/agent-configuration/agents-md
74971
+ */
74972
+ var CodexcliRule = class extends NestedAgentsmdRule {
74973
+ static getFamily() {
74974
+ return {
74975
+ globalDir: CODEXCLI_DIR,
74976
+ toolTarget: "codexcli"
74977
+ };
74978
+ }
74979
+ };
74980
+ //#endregion
74788
74981
  //#region src/features/rules/commandcode-rule.ts
74789
74982
  var CommandcodeRule = class CommandcodeRule extends ToolRule {
74790
74983
  constructor({ fileContent, root, ...rest }) {
@@ -76089,122 +76282,6 @@ var DevinRule = class DevinRule extends ToolRule {
76089
76282
  }
76090
76283
  };
76091
76284
  //#endregion
76092
- //#region src/features/rules/nested-agentsmd-rule.ts
76093
- /**
76094
- * Shared adapter for the targets that read instructions exactly the way the
76095
- * AGENTS.md standard describes them and nothing more: a root `AGENTS.md` at the
76096
- * project root, nested per-directory `AGENTS.md` files loaded only while working
76097
- * under that directory, and a single personal root file under a tool-specific
76098
- * home directory. Such a target has no modular non-root instruction directory,
76099
- * so every other non-root rule folds into the root file.
76100
- *
76101
- * Concrete targets (Pool, Vibe, DeepSeek Harness) only supply the global
76102
- * directory and their tool target through {@link getFamily}; the vendor docs
76103
- * describing each one's discovery walk live on the subclass.
76104
- *
76105
- * The nested-file scan mirrors the AGENTS.md standard's nested discovery —
76106
- * same file name, same exclusions, import-only, project scope — because it
76107
- * discovers literally the same files.
76108
- */
76109
- var NestedAgentsmdRule = class extends ToolRule {
76110
- static getFamily() {
76111
- throw new Error("Please implement this method in the subclass.");
76112
- }
76113
- static getSettablePaths({ global = false } = {}) {
76114
- if (global) return { root: {
76115
- relativeDirPath: this.getFamily().globalDir,
76116
- relativeFilePath: AGENTSMD_RULE_FILE_NAME
76117
- } };
76118
- return { root: {
76119
- relativeDirPath: ".",
76120
- relativeFilePath: AGENTSMD_RULE_FILE_NAME
76121
- } };
76122
- }
76123
- static getNestedFilePatterns() {
76124
- return this.buildNestedFilePatterns({ fileName: AGENTSMD_RULE_FILE_NAME });
76125
- }
76126
- /**
76127
- * The subproject directory this rule scopes, or `undefined` for the root file
76128
- * (project or global).
76129
- */
76130
- getSubprojectPath() {
76131
- return this.getNestedSubprojectPath({ fileName: AGENTSMD_RULE_FILE_NAME });
76132
- }
76133
- static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, relativeDirPath: overrideDirPath, validate = true, global = false }) {
76134
- const { root } = this.getSettablePaths({ global });
76135
- if (overrideDirPath !== void 0 && overrideDirPath !== root.relativeDirPath && overrideDirPath !== ".") {
76136
- const fileContent = await readFileContent((0, node_path.join)(outputRoot, overrideDirPath, AGENTSMD_RULE_FILE_NAME));
76137
- return new this({
76138
- outputRoot,
76139
- relativeDirPath: overrideDirPath,
76140
- relativeFilePath: AGENTSMD_RULE_FILE_NAME,
76141
- fileContent,
76142
- validate,
76143
- root: false
76144
- });
76145
- }
76146
- const fileContent = await readFileContent((0, node_path.join)(outputRoot, root.relativeDirPath, root.relativeFilePath));
76147
- return new this({
76148
- outputRoot,
76149
- relativeDirPath: root.relativeDirPath,
76150
- relativeFilePath: root.relativeFilePath,
76151
- fileContent,
76152
- validate,
76153
- root: true
76154
- });
76155
- }
76156
- static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
76157
- const { root } = this.getSettablePaths({ global });
76158
- const frontmatter = rulesyncRule.getFrontmatter();
76159
- const isRoot = frontmatter.root ?? false;
76160
- const subprojectPath = frontmatter.agentsmd?.subprojectPath;
76161
- if (!global && !isRoot && subprojectPath) return new this({
76162
- outputRoot,
76163
- relativeDirPath: (0, node_path.join)(subprojectPath),
76164
- relativeFilePath: AGENTSMD_RULE_FILE_NAME,
76165
- fileContent: rulesyncRule.getBody(),
76166
- validate,
76167
- root: false
76168
- });
76169
- return new this({
76170
- outputRoot,
76171
- relativeDirPath: root.relativeDirPath,
76172
- relativeFilePath: root.relativeFilePath,
76173
- fileContent: rulesyncRule.getBody(),
76174
- validate,
76175
- root: isRoot
76176
- });
76177
- }
76178
- toRulesyncRule() {
76179
- const subprojectPath = this.getSubprojectPath();
76180
- if (subprojectPath === void 0) return this.toRulesyncRuleDefault();
76181
- return this.toRulesyncRuleNestedAgentsmd({ subprojectPath });
76182
- }
76183
- validate() {
76184
- return {
76185
- success: true,
76186
- error: null
76187
- };
76188
- }
76189
- static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
76190
- const isRoot = relativeFilePath === "AGENTS.md" && (relativeDirPath === "." || relativeDirPath === this.getFamily().globalDir);
76191
- return new this({
76192
- outputRoot,
76193
- relativeDirPath,
76194
- relativeFilePath,
76195
- fileContent: "",
76196
- validate: false,
76197
- root: isRoot
76198
- });
76199
- }
76200
- static isTargetedByRulesyncRule(rulesyncRule) {
76201
- return this.isTargetedByRulesyncRuleDefault({
76202
- rulesyncRule,
76203
- toolTarget: this.getFamily().toolTarget
76204
- });
76205
- }
76206
- };
76207
- //#endregion
76208
76285
  //#region src/features/rules/dsh-rule.ts
76209
76286
  /**
76210
76287
  * DeepSeek Harness (`dsh`) rules.
@@ -76416,7 +76493,8 @@ var FactorydroidRule = class FactorydroidRule extends ToolRule {
76416
76493
  rulesyncRule,
76417
76494
  validate,
76418
76495
  rootPath: paths.root,
76419
- nonRootPath: paths.nonRoot
76496
+ nonRootPath: paths.nonRoot,
76497
+ global
76420
76498
  }));
76421
76499
  }
76422
76500
  toRulesyncRule() {
@@ -76883,7 +76961,8 @@ var KiloRule = class KiloRule extends ToolRule {
76883
76961
  rulesyncRule,
76884
76962
  validate,
76885
76963
  rootPath: paths.root,
76886
- nonRootPath: paths.nonRoot
76964
+ nonRootPath: paths.nonRoot,
76965
+ global
76887
76966
  }));
76888
76967
  }
76889
76968
  toRulesyncRule() {
@@ -77387,7 +77466,8 @@ var OpenCodeRule = class OpenCodeRule extends ToolRule {
77387
77466
  rulesyncRule,
77388
77467
  validate,
77389
77468
  rootPath: paths.root,
77390
- nonRootPath: paths.nonRoot
77469
+ nonRootPath: paths.nonRoot,
77470
+ global
77391
77471
  }));
77392
77472
  }
77393
77473
  toRulesyncRule() {
@@ -78856,6 +78936,14 @@ var ZoocodeRule = class extends RooRule {
78856
78936
  //#endregion
78857
78937
  //#region src/features/rules/rules-processor.ts
78858
78938
  const RulesProcessorToolTargetSchema = zod_mini.z.enum(rulesProcessorToolTargetTuple);
78939
+ /**
78940
+ * Normalizes a relative directory path for comparison: POSIX separators, no
78941
+ * `./` segments and no trailing slash, with the current directory as `"."`
78942
+ * (so `"./"`, `"a/"` and `"a"` compare the way `join()` groups them).
78943
+ */
78944
+ const normalizeRelativeDir = (dir) => node_path.posix.normalize(toPosixPath(dir)).replace(/\/+$/, "") || ".";
78945
+ /** A tool rule's output path relative to its output root, for comparison. */
78946
+ const toolOutputPath = (toolRule) => node_path.posix.join(normalizeRelativeDir(toolRule.getRelativeDirPath()), toolRule.getRelativeFilePath());
78859
78947
  const formatRulePaths = (rules) => rules.map((r) => (0, node_path.join)(r.getRelativeDirPath(), r.getRelativeFilePath())).join(", ");
78860
78948
  const RulesFeatureOptionsSchema = zod_mini.z.looseObject({
78861
78949
  ruleDiscoveryMode: zod_mini.z.optional(zod_mini.z.enum(["none", "explicit"])),
@@ -79024,7 +79112,11 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
79024
79112
  extension: "md",
79025
79113
  supportsGlobal: true,
79026
79114
  ruleDiscoveryMode: "auto",
79027
- collisionPolicy: "fold"
79115
+ collisionPolicy: "fold",
79116
+ projectInstructionBudget: {
79117
+ bytes: CODEXCLI_PROJECT_DOC_MAX_BYTES,
79118
+ 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)."
79119
+ }
79028
79120
  }
79029
79121
  }],
79030
79122
  ["commandcode", {
@@ -79482,9 +79574,61 @@ var RulesProcessor = class extends FeatureProcessor {
79482
79574
  toolRules,
79483
79575
  factory
79484
79576
  });
79577
+ this.warnForInstructionBudget({
79578
+ toolRules,
79579
+ meta
79580
+ });
79485
79581
  return outputFiles;
79486
79582
  }
79487
79583
  /**
79584
+ * Warn when a generated project instruction chain exceeds the tool's byte
79585
+ * budget (`meta.projectInstructionBudget`).
79586
+ *
79587
+ * Codex CLI concatenates the project `AGENTS.md` files from the project root
79588
+ * down to the cwd and stops once their combined size reaches
79589
+ * `project_doc_max_bytes`, truncating the file that crosses it — silently, so
79590
+ * without this warning the rules past the limit simply never reach the model.
79591
+ * Each generated file is measured together with the generated files in its
79592
+ * ancestor directories, since that is the chain Codex loads while working
79593
+ * there. When the root file alone is over the budget every chain is, so only
79594
+ * the root is reported; likewise each over-budget nested chain is reported
79595
+ * once, at its shallowest directory. With several output roots the warning
79596
+ * names the root it refers to. An output root below the git root is measured
79597
+ * without the parent directories' files Codex would also load, so the
79598
+ * estimate can come out low there. The budget checked is the tool's default: a value
79599
+ * raised by hand in the tool's own config is not read. Global scope is not
79600
+ * checked because Codex loads the personal `~/.codex/AGENTS.md` outside the
79601
+ * project budget.
79602
+ */
79603
+ warnForInstructionBudget({ toolRules, meta }) {
79604
+ const budget = meta.projectInstructionBudget;
79605
+ if (budget === void 0 || this.global) return;
79606
+ const files = toolRules.map((rule) => ({
79607
+ dir: normalizeRelativeDir(rule.getRelativeDirPath()),
79608
+ path: toPosixPath((0, node_path.join)(rule.getRelativeDirPath(), rule.getRelativeFilePath())),
79609
+ bytes: Buffer.byteLength(addTrailingNewline(rule.getFileContent()), "utf8")
79610
+ }));
79611
+ const isAncestorOrSelf = (ancestor, dir) => ancestor === "." || ancestor === dir || dir.startsWith(`${ancestor}/`);
79612
+ const location = this.outputRoot === process.cwd() ? "" : ` under '${this.outputRoot}'`;
79613
+ const rootFiles = files.filter(({ dir }) => dir === ".");
79614
+ const rootBytes = rootFiles.reduce((sum, { bytes }) => sum + bytes, 0);
79615
+ if (rootBytes > budget.bytes) {
79616
+ 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}`);
79617
+ return;
79618
+ }
79619
+ const reportedDirs = [];
79620
+ const nestedFiles = files.filter(({ dir }) => dir !== ".").toSorted((a, b) => a.dir.split("/").length - b.dir.split("/").length);
79621
+ for (const file of nestedFiles) {
79622
+ if (reportedDirs.some((dir) => isAncestorOrSelf(dir, file.dir))) continue;
79623
+ const chain = files.filter(({ dir }) => isAncestorOrSelf(dir, file.dir));
79624
+ const chainBytes = chain.reduce((sum, { bytes }) => sum + bytes, 0);
79625
+ if (chainBytes > budget.bytes) {
79626
+ reportedDirs.push(file.dir);
79627
+ 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}`);
79628
+ }
79629
+ }
79630
+ }
79631
+ /**
79488
79632
  * Handle localRoot rules (only in non-global mode and when enabled). Mutates
79489
79633
  * `toolRules` in place.
79490
79634
  */
@@ -80156,14 +80300,14 @@ As this project's AI coding tool, you must follow the additional conventions bel
80156
80300
  * decides for all of them: its `pi.contextFile` is copied onto the non-root
80157
80301
  * rules, and the flag set only on a non-root rule is dropped with a warning.
80158
80302
  */
80159
- alignPiContextFile(rules) {
80303
+ alignPiContextFile(rules, { warn = true } = {}) {
80160
80304
  if (this.toolTarget !== "pi") return rules;
80161
80305
  const factory = this.getFactory(this.toolTarget);
80162
80306
  const targeted = rules.filter((rule) => factory.class.isTargetedByRulesyncRule(rule));
80163
80307
  const rootContextFile = targeted.some((rule) => rule.getFrontmatter().root === true && rule.getFrontmatter().pi?.contextFile === "override") ? "override" : void 0;
80164
80308
  const mismatched = targeted.filter((rule) => rule.getFrontmatter().pi?.contextFile !== rootContextFile);
80165
80309
  if (mismatched.length === 0) return rules;
80166
- 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)}`);
80310
+ 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)}`);
80167
80311
  const mismatchedSet = new Set(mismatched);
80168
80312
  return rules.map((rule) => {
80169
80313
  if (!mismatchedSet.has(rule)) return rule;
@@ -80217,14 +80361,72 @@ As this project's AI coding tool, you must follow the additional conventions bel
80217
80361
  * duplication check the same way `loadRulesyncFiles`'s global-mode branch
80218
80362
  * excludes it from `nonRootRules`: `generate` ignores `localRoot` entirely
80219
80363
  * in global mode, so such a rule is never actually folded into the global
80220
- * root output and warning about it here would be inaccurate.
80364
+ * root output and warning about it here would be inaccurate. The same goes
80365
+ * for a tool that writes `localRoot` to a dedicated local file.
80366
+ *
80367
+ * Non-root rules written to a file other than the root (nested
80368
+ * per-directory files, Pi's `APPEND_SYSTEM.md`) get a separate warning when
80369
+ * that file already exists and would import under a different name.
80221
80370
  */
80222
80371
  async warnForFoldImportDuplicationRisk() {
80223
80372
  const factory = this.getFactory(this.toolTarget);
80224
80373
  if (factory.meta.collisionPolicy !== "fold") return;
80225
- const nonRootRules = (await this.loadMergedRulesyncRules()).filter((rule) => !rule.getFrontmatter().root && (!this.global || !rule.getFrontmatter().localRoot) && factory.class.isTargetedByRulesyncRule(rule));
80226
- if (nonRootRules.length === 0) return;
80227
- 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.`);
80374
+ const targetedRules = this.alignPiContextFile(await this.loadMergedRulesyncRules(), { warn: false }).filter((rule) => factory.class.isTargetedByRulesyncRule(rule));
80375
+ let includeLocalRoot = !this.global;
80376
+ try {
80377
+ includeLocalRoot &&= resolveIncludeLocalRoot(this.featureOptions);
80378
+ } catch {}
80379
+ const targetedNonRootRules = targetedRules.filter((rule) => !rule.getFrontmatter().root && !(rule.getFrontmatter().localRoot && (!includeLocalRoot || factory.meta.localRootMode === "separate-local-file")));
80380
+ const rootPaths = /* @__PURE__ */ new Set();
80381
+ const { root } = factory.class.getSettablePaths({ global: this.global });
80382
+ if (root) rootPaths.add(node_path.posix.join(normalizeRelativeDir(root.relativeDirPath), root.relativeFilePath));
80383
+ for (const rulesyncRule of targetedRules.filter((rule) => rule.getFrontmatter().root)) {
80384
+ const toolRule = this.toToolRuleForImportCheck({
80385
+ factory,
80386
+ rulesyncRule
80387
+ });
80388
+ rootPaths.add(toolOutputPath(toolRule));
80389
+ }
80390
+ const separateRules = [];
80391
+ const foldedRules = [];
80392
+ let localRootCounted = false;
80393
+ for (const rulesyncRule of targetedNonRootRules) {
80394
+ if (rulesyncRule.getFrontmatter().localRoot) {
80395
+ if (!localRootCounted) {
80396
+ foldedRules.push(rulesyncRule);
80397
+ localRootCounted = true;
80398
+ }
80399
+ continue;
80400
+ }
80401
+ const toolRule = this.toToolRuleForImportCheck({
80402
+ factory,
80403
+ rulesyncRule
80404
+ });
80405
+ if (rootPaths.size === 0 || rootPaths.has(toolOutputPath(toolRule))) foldedRules.push(rulesyncRule);
80406
+ else separateRules.push({
80407
+ toolRule,
80408
+ rulesyncRule
80409
+ });
80410
+ }
80411
+ 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.`);
80412
+ const duplicated = [];
80413
+ for (const conversion of separateRules) {
80414
+ const { toolRule, rulesyncRule } = conversion;
80415
+ if (toolRule.toRulesyncRule().getRelativeFilePath().toLowerCase() === rulesyncRule.getRelativeFilePath().toLowerCase()) continue;
80416
+ if (await fileExists(toolRule.getFilePath())) duplicated.push(conversion);
80417
+ }
80418
+ if (duplicated.length > 0) {
80419
+ const fileNames = [...new Set(duplicated.map(({ toolRule }) => toolRule.getRelativeFilePath()))].join(", ");
80420
+ 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.`);
80421
+ }
80422
+ }
80423
+ toToolRuleForImportCheck({ factory, rulesyncRule }) {
80424
+ return factory.class.fromRulesyncRule({
80425
+ outputRoot: this.outputRoot,
80426
+ rulesyncRule,
80427
+ validate: false,
80428
+ global: this.global
80429
+ });
80228
80430
  }
80229
80431
  /**
80230
80432
  * Implementation of abstract method from FeatureProcessor
@@ -80518,6 +80720,14 @@ As this project's AI coding tool, you must follow the additional conventions bel
80518
80720
  function isPackagingToolTarget(toolTarget) {
80519
80721
  return PACKAGING_TOOL_TARGETS.includes(toolTarget);
80520
80722
  }
80723
+ /**
80724
+ * Packaging targets write into a plugin directory and support project scope
80725
+ * only. In global mode their output root would be the home directory, so
80726
+ * callers reject or skip them up front instead of checking all of $HOME.
80727
+ */
80728
+ function formatPackagingTargetGlobalMessage(toolTarget) {
80729
+ return `Target '${toolTarget}' is a plugin packaging target and supports only project scope. Re-run without '--global'.`;
80730
+ }
80521
80731
  async function assertPluginRootSafe(params) {
80522
80732
  if (!isPackagingToolTarget(params.toolTarget)) return;
80523
80733
  await assertDirectoryIfExists(params.outputRoot);
@@ -81437,14 +81647,17 @@ function warnUnsupportedTargets(params) {
81437
81647
  } catch {
81438
81648
  oppositeScopeTargets = [];
81439
81649
  }
81440
- for (const target of config.getTargets()) if (!supportedTargets.includes(target) && config.getFeatures(target).includes(featureName)) {
81441
- const simulateOption = SIMULATE_OPTION_MAP[featureName];
81442
- if (simulateOption && simulatedTargets.includes(target)) logger.warn(`Target '${target}' only supports simulated '${featureName}'. Use '${simulateOption}' to enable it. Skipping.`);
81443
- else if (oppositeScopeTargets.includes(target)) {
81444
- const supportedScope = config.getGlobal() ? "project" : "global";
81445
- const retry = config.getGlobal() ? "without '--global'" : "with '--global'";
81446
- logger.warn(`Target '${target}' supports the feature '${featureName}' only in ${supportedScope} scope. Re-run ${retry}. Skipping.`);
81447
- } else logger.warn(`Target '${target}' does not support the feature '${featureName}'. Skipping.`);
81650
+ for (const target of config.getTargets()) {
81651
+ if (config.getGlobal() && isPackagingToolTarget(target)) continue;
81652
+ if (!supportedTargets.includes(target) && config.getFeatures(target).includes(featureName)) {
81653
+ const simulateOption = SIMULATE_OPTION_MAP[featureName];
81654
+ if (simulateOption && simulatedTargets.includes(target)) logger.warn(`Target '${target}' only supports simulated '${featureName}'. Use '${simulateOption}' to enable it. Skipping.`);
81655
+ else if (oppositeScopeTargets.includes(target)) {
81656
+ const supportedScope = config.getGlobal() ? "project" : "global";
81657
+ const retry = config.getGlobal() ? "without '--global'" : "with '--global'";
81658
+ logger.warn(`Target '${target}' supports the feature '${featureName}' only in ${supportedScope} scope. Re-run ${retry}. Skipping.`);
81659
+ } else logger.warn(`Target '${target}' does not support the feature '${featureName}'. Skipping.`);
81660
+ }
81448
81661
  }
81449
81662
  }
81450
81663
  /**
@@ -81697,10 +81910,16 @@ async function generate(params) {
81697
81910
  const { config, logger } = params;
81698
81911
  resetRootShadowingWarnings({ logger });
81699
81912
  resetRunWarningState();
81700
- for (const toolTarget of config.getTargets()) for (const outputRoot of config.getOutputRoots(toolTarget)) await assertPluginRootSafe({
81701
- toolTarget,
81702
- outputRoot
81703
- });
81913
+ for (const toolTarget of config.getTargets()) {
81914
+ if (config.getGlobal() && isPackagingToolTarget(toolTarget)) {
81915
+ logger.warn(`${formatPackagingTargetGlobalMessage(toolTarget)} Skipping.`);
81916
+ continue;
81917
+ }
81918
+ for (const outputRoot of config.getOutputRoots(toolTarget)) await assertPluginRootSafe({
81919
+ toolTarget,
81920
+ outputRoot
81921
+ });
81922
+ }
81704
81923
  await warnSkillSubagentNameCollisions({
81705
81924
  config,
81706
81925
  logger
@@ -82317,6 +82536,7 @@ function getToolOutputRoot({ config, tool }) {
82317
82536
  async function importFromTool(params) {
82318
82537
  const { config, tool, logger } = params;
82319
82538
  resetRunWarningState();
82539
+ if (config.getGlobal() && isPackagingToolTarget(tool)) throw new Error(formatPackagingTargetGlobalMessage(tool));
82320
82540
  await assertPluginRootSafe({
82321
82541
  toolTarget: tool,
82322
82542
  outputRoot: getToolOutputRoot({
@@ -82377,7 +82597,7 @@ async function importRulesCore(params) {
82377
82597
  if (!config.getFeatures(tool).includes("rules")) return 0;
82378
82598
  const global = config.getGlobal();
82379
82599
  if (!RulesProcessor.getToolTargets({ global }).includes(tool)) return 0;
82380
- const rulesProcessor = new RulesProcessor({
82600
+ const baseParams = {
82381
82601
  outputRoot: getToolOutputRoot({
82382
82602
  config,
82383
82603
  tool
@@ -82385,13 +82605,23 @@ async function importRulesCore(params) {
82385
82605
  toolTarget: tool,
82386
82606
  global,
82387
82607
  logger
82388
- });
82608
+ };
82609
+ const rulesProcessor = new RulesProcessor(baseParams);
82389
82610
  const toolFiles = await rulesProcessor.loadToolFiles();
82390
82611
  if (toolFiles.length === 0) {
82391
82612
  logger.warn(`No rule files found for ${tool}. Skipping import.`);
82392
82613
  return 0;
82393
82614
  }
82394
- await rulesProcessor.warnForFoldImportDuplicationRisk();
82615
+ try {
82616
+ await new RulesProcessor({
82617
+ ...baseParams,
82618
+ inputRoots: config.getInputRoots(),
82619
+ deriveSubprojectPathFromGlobs: config.getDeriveSubprojectPathFromGlobs(),
82620
+ featureOptions: config.getFeatureOptions(tool, "rules")
82621
+ }).warnForFoldImportDuplicationRisk();
82622
+ } catch (error) {
82623
+ logger.warn(`Skipped the ${tool} import duplication check: ${formatError(error)}`);
82624
+ }
82395
82625
  const rulesyncFiles = await rulesProcessor.convertToolFilesToRulesyncFiles(toolFiles);
82396
82626
  const { count: writtenCount } = await rulesProcessor.writeAiFiles(rulesyncFiles);
82397
82627
  if (config.getVerbose() && writtenCount > 0) logger.success(`Created ${writtenCount} rule files`);