rulesync 21.0.0 → 23.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.
@@ -2728,6 +2728,7 @@ var Config = class Config {
2728
2728
  */
2729
2729
  objectFormTargetKeys;
2730
2730
  configFileTargets;
2731
+ configFileSelection;
2731
2732
  verbose;
2732
2733
  delete;
2733
2734
  global;
@@ -2762,7 +2763,7 @@ var Config = class Config {
2762
2763
  inputRoots;
2763
2764
  configFilePath;
2764
2765
  sources;
2765
- constructor({ outputRoots, targets, features, verbose, delete: isDelete, global, silent, simulateCommands, simulateSubagents, simulateSkills, preserveUnownedHooks, deriveSubprojectPathFromGlobs, flattenedCommandNaming, language, gitignoreTargetsOnly, gitignoreDestination, dryRun, check, inputRoot, inputRoots, configFilePath, sources, configFileTargets }) {
2766
+ constructor({ outputRoots, targets, features, verbose, delete: isDelete, global, silent, simulateCommands, simulateSubagents, simulateSkills, preserveUnownedHooks, deriveSubprojectPathFromGlobs, flattenedCommandNaming, language, gitignoreTargetsOnly, gitignoreDestination, dryRun, check, inputRoot, inputRoots, configFilePath, sources, configFileTargets, configFileSelection }) {
2766
2767
  assertTargetsFeaturesExclusive({
2767
2768
  targets,
2768
2769
  features
@@ -2787,6 +2788,7 @@ var Config = class Config {
2787
2788
  this.features = resolvedFeatures;
2788
2789
  this.objectFormTargetKeys = isRulesyncConfigTargetsObject(resolvedTargets) ? Config.filterValidToolTargets(Object.keys(resolvedTargets)) : void 0;
2789
2790
  this.configFileTargets = configFileTargets;
2791
+ this.configFileSelection = configFileSelection;
2790
2792
  this.verbose = verbose;
2791
2793
  this.delete = isDelete;
2792
2794
  this.global = global ?? false;
@@ -2877,17 +2879,54 @@ var Config = class Config {
2877
2879
  getConfigFileTargets() {
2878
2880
  return this.configFileTargets ?? this.getTargets();
2879
2881
  }
2882
+ /**
2883
+ * The features `target` has for a run over the configuration file's
2884
+ * targets, i.e. what `rulesync generate` without `--targets` would produce
2885
+ * for it (a CLI `--features` still applies to array-form targets). Falls
2886
+ * back to {@link getFeatures} when no configuration file declared `targets`.
2887
+ */
2888
+ getConfigFileFeatures(target) {
2889
+ const selection = this.configFileSelection;
2890
+ if (selection === void 0) return this.getFeatures(target);
2891
+ return Config.resolveTargetFeatures({
2892
+ targets: selection.targets,
2893
+ features: selection.features ?? [],
2894
+ target
2895
+ });
2896
+ }
2897
+ /**
2898
+ * The options `target` sets for `feature` in the configuration file, for
2899
+ * the same run as {@link getConfigFileFeatures}. Falls back to
2900
+ * {@link getFeatureOptions} when no configuration file declared `targets`.
2901
+ */
2902
+ getConfigFileFeatureOptions(target, feature) {
2903
+ const selection = this.configFileSelection;
2904
+ if (selection === void 0) return this.getFeatureOptions(target, feature);
2905
+ return Config.resolveFeatureOptions({
2906
+ targets: selection.targets,
2907
+ target,
2908
+ feature
2909
+ });
2910
+ }
2880
2911
  getFeatures(target) {
2881
- if (isRulesyncConfigTargetsObject(this.targets)) {
2882
- if (target) {
2883
- const value = this.targets[target];
2884
- if (!value) return [];
2885
- return Config.normalizeTargetFeatures(value);
2886
- }
2887
- return Config.collectAllFeatures(Object.values(this.targets));
2912
+ if (target) return Config.resolveTargetFeatures({
2913
+ targets: this.targets,
2914
+ features: this.features,
2915
+ target
2916
+ });
2917
+ if (isRulesyncConfigTargetsObject(this.targets)) return Config.collectAllFeatures(Object.values(this.targets));
2918
+ return Config.normalizeFeatureList(this.features);
2919
+ }
2920
+ static resolveTargetFeatures({ targets, features, target }) {
2921
+ if (isRulesyncConfigTargetsObject(targets)) {
2922
+ const value = targets[target];
2923
+ return value ? Config.normalizeTargetFeatures(value) : [];
2888
2924
  }
2889
- if (this.features.includes("*")) return [...ALL_FEATURES];
2890
- return this.features.filter((feature) => feature !== "*");
2925
+ return Config.normalizeFeatureList(features);
2926
+ }
2927
+ static normalizeFeatureList(features) {
2928
+ if (features.includes("*")) return [...ALL_FEATURES];
2929
+ return features.filter((feature) => feature !== "*");
2891
2930
  }
2892
2931
  /**
2893
2932
  * Normalize a per-target features value (array or per-feature object) into
@@ -2928,7 +2967,14 @@ var Config = class Config {
2928
2967
  * feature is not enabled for the given target.
2929
2968
  */
2930
2969
  getFeatureOptions(target, feature) {
2931
- const value = isRulesyncConfigTargetsObject(this.targets) ? this.targets[target] : void 0;
2970
+ return Config.resolveFeatureOptions({
2971
+ targets: this.targets,
2972
+ target,
2973
+ feature
2974
+ });
2975
+ }
2976
+ static resolveFeatureOptions({ targets, target, feature }) {
2977
+ const value = isRulesyncConfigTargetsObject(targets) ? targets[target] : void 0;
2932
2978
  if (!value || Array.isArray(value)) return;
2933
2979
  const featureValue = value[feature];
2934
2980
  if (featureValue && typeof featureValue === "object" && isFeatureValueEnabled(featureValue)) return featureValue;
@@ -3363,7 +3409,15 @@ var ConfigResolver = class {
3363
3409
  sources: configByFile.sources ?? getDefaults().sources,
3364
3410
  flattenedCommandNaming: configByFile.flattenedCommandNaming ?? getDefaults().flattenedCommandNaming,
3365
3411
  language: configByFile.language,
3366
- configFileTargets: extractConfigFileTargets(configByFile.targets)
3412
+ configFileTargets: extractConfigFileTargets(configByFile.targets),
3413
+ configFileSelection: configByFile.targets === void 0 ? void 0 : {
3414
+ targets: configByFile.targets,
3415
+ features: resolveFeaturesAndTargets({
3416
+ features,
3417
+ targets: void 0,
3418
+ configByFile
3419
+ }).resolvedFeatures
3420
+ }
3367
3421
  });
3368
3422
  }
3369
3423
  };
@@ -12295,7 +12349,7 @@ const CODEXCLI_AGENTS_DIR_PATH = (0, node_path.join)(CODEXCLI_DIR, "agents");
12295
12349
  const CODEXCLI_SKILLS_DIR_PATH = (0, node_path.join)(".agents", "skills");
12296
12350
  const CODEXCLI_HOOKS_FILE_NAME = "hooks.json";
12297
12351
  const CODEXCLI_MCP_FILE_NAME = "config.toml";
12298
- const CODEXCLI_RULE_FILE_NAME = "AGENTS.md";
12352
+ const CODEXCLI_PROJECT_DOC_MAX_BYTES = 32768;
12299
12353
  const CODEXCLI_BASH_RULES_FILE_NAME = "rulesync.rules";
12300
12354
  const CODEXCLI_OPENAI_YAML_RELATIVE_PATH = (0, node_path.join)("agents", "openai.yaml");
12301
12355
  const CODEXCLI_OVERRIDE_KEYS = [
@@ -14965,6 +15019,7 @@ const ANTIGRAVITY_GLOBAL_AGENTS_DIR_PATH = (0, node_path.join)(ANTIGRAVITY_GLOBA
14965
15019
  const ANTIGRAVITY_AGENTS_DIR = ANTIGRAVITY_DIR;
14966
15020
  const ANTIGRAVITY_RULE_FILE_NAME = "AGENTS.md";
14967
15021
  const ANTIGRAVITY_GLOBAL_RULE_FILE_NAME = "GEMINI.md";
15022
+ const ANTIGRAVITY_GLOBAL_RULES_SUBDIR = (0, node_path.join)(ANTIGRAVITY_GLOBAL_CONFIG_SUBDIR, "rules");
14968
15023
  //#endregion
14969
15024
  //#region src/features/commands/antigravity-command.ts
14970
15025
  const AntigravityWorkflowFrontmatterSchema = zod_mini.z.looseObject({
@@ -15480,12 +15535,50 @@ var BobCommand = class BobCommand extends ToolCommand {
15480
15535
  }
15481
15536
  };
15482
15537
  //#endregion
15538
+ //#region src/features/shared/claudecode-argument-hint.ts
15539
+ /**
15540
+ * Turns a YAML-list `argument-hint` back into the bracketed string form Claude
15541
+ * Code documents (`[issue-number]`, `[filename] [format]`), one bracketed
15542
+ * placeholder per list entry. The per-entry form is chosen over re-joining with
15543
+ * commas because it is the shape every Claude Code example uses. An empty list
15544
+ * carries no hint, so it yields `undefined` and the key is dropped.
15545
+ */
15546
+ function joinArgumentHint(value) {
15547
+ if (typeof value === "string") return value;
15548
+ if (value.length === 0) return;
15549
+ return value.map((entry) => `[${String(entry)}]`).join(" ");
15550
+ }
15551
+ /**
15552
+ * `argument-hint` as Claude Code reads it from skill and command frontmatter.
15553
+ *
15554
+ * The documented examples (`[issue-number]`) are written unquoted, and YAML
15555
+ * parses an unquoted `[issue-number]` as a one-element list. Claude Code
15556
+ * accepts that form, so it is accepted here too and normalized to the
15557
+ * bracketed string, keeping the parsed value (and everything downstream of it)
15558
+ * a plain string. A scalar entry YAML did not read as a string (`[1]`,
15559
+ * `[true]`, `[~]`) is stringified rather than rejected.
15560
+ * https://code.claude.com/docs/en/skills
15561
+ *
15562
+ * A list entry is stringified from the value YAML produced, so its original
15563
+ * spelling is not kept (`[1.0]` becomes `[1]`), and an entry YAML reads as a
15564
+ * date (`[2026-01-01]`) is rejected.
15565
+ *
15566
+ * The unquoted multi-placeholder form (`argument-hint: [filename] [format]`)
15567
+ * is not valid YAML at all, so it fails before this schema is reached.
15568
+ */
15569
+ const ClaudecodeArgumentHintSchema = zod_mini.z.pipe(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.union([
15570
+ zod_mini.z.string(),
15571
+ zod_mini.z.number(),
15572
+ zod_mini.z.boolean(),
15573
+ zod_mini.z.null()
15574
+ ]))]), zod_mini.z.transform(joinArgumentHint));
15575
+ //#endregion
15483
15576
  //#region src/features/commands/claudecode-command.ts
15484
15577
  const ClaudecodeCommandFrontmatterSchema = zod_mini.z.looseObject({
15485
15578
  description: zod_mini.z.optional(zod_mini.z.string()),
15486
15579
  "allowed-tools": zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())])),
15487
15580
  "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()),
15581
+ "argument-hint": zod_mini.z.optional(ClaudecodeArgumentHintSchema),
15489
15582
  model: zod_mini.z.optional(zod_mini.z.string()),
15490
15583
  "disable-model-invocation": zod_mini.z.optional(zod_mini.z.boolean())
15491
15584
  });
@@ -15514,7 +15607,8 @@ var ClaudecodeCommand = class ClaudecodeCommand extends ToolCommand {
15514
15607
  return this.frontmatter;
15515
15608
  }
15516
15609
  toRulesyncCommand() {
15517
- const { description, ...restFields } = this.frontmatter;
15610
+ const { description, ...extraFields } = this.frontmatter;
15611
+ const restFields = Object.fromEntries(Object.entries(extraFields).filter(([, value]) => value !== void 0));
15518
15612
  const rulesyncFrontmatter = {
15519
15613
  targets: ["*"],
15520
15614
  description,
@@ -33147,6 +33241,74 @@ function translateCanonicalKeyToCodex({ key, value, isStdio, serverName }) {
33147
33241
  }
33148
33242
  }
33149
33243
  /**
33244
+ * A header value that is exactly one canonical `${VAR}` reference. Anchored and
33245
+ * without the `g` flag, so `.exec()` carries no `lastIndex` state between
33246
+ * calls. `${VAR:-default}` does not match (the name excludes `:`), because
33247
+ * Codex has no way to express the fallback.
33248
+ */
33249
+ const WHOLE_ENV_VAR_REF_PATTERN = /^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$/;
33250
+ /**
33251
+ * An `Authorization` value that is exactly `Bearer ${VAR}`. The scheme is
33252
+ * matched case-insensitively, as HTTP authentication schemes are.
33253
+ */
33254
+ const BEARER_ENV_VAR_REF_PATTERN = /^Bearer \$\{([A-Za-z_][A-Za-z0-9_]*)\}$/i;
33255
+ const AUTHORIZATION_HEADER_NAME = "authorization";
33256
+ /**
33257
+ * Whether a string still contains an unexpanded `${…}` reference. Codex writes
33258
+ * `url` and `http_headers` values as-is and never expands them, so such a value
33259
+ * reaches the server literally.
33260
+ */
33261
+ function hasUnexpandedEnvVarRef(value) {
33262
+ return value.includes("${");
33263
+ }
33264
+ /**
33265
+ * Rewrite canonical `${VAR}` header references into the keys Codex reads them
33266
+ * from, since Codex sends `http_headers` values verbatim:
33267
+ * - `Authorization: Bearer ${VAR}` → `bearer_token_env_var = "VAR"`
33268
+ * - a header whose whole value is `${VAR}` → `env_http_headers.<name> = "VAR"`
33269
+ *
33270
+ * Runs after every key is translated, so a `bearer_token_env_var` or
33271
+ * `env_http_headers` the source already carries (they pass through under their
33272
+ * own names) is seen regardless of key order. Those explicit values win: a
33273
+ * derived bearer is not written over an existing one, and an existing
33274
+ * `env_http_headers` entry for the same header is kept. Any value the rewrite
33275
+ * cannot express — a partial reference, a `${VAR:-default}` fallback — stays in
33276
+ * `http_headers` verbatim with a warning, as does a `${…}` in `url`, for which
33277
+ * Codex has no environment-sourced counterpart at all.
33278
+ * @see https://learn.chatgpt.com/docs/config-file/config-reference
33279
+ */
33280
+ function convertEnvVarRefsForCodex({ converted, serverName }) {
33281
+ const url = converted["url"];
33282
+ 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.`);
33283
+ const httpHeaders = converted["http_headers"];
33284
+ if (!isHeadersRecord(httpHeaders)) return;
33285
+ const existingEnvHeaders = converted["env_http_headers"];
33286
+ const canWriteEnvHeaders = existingEnvHeaders === void 0 || isHeadersRecord(existingEnvHeaders);
33287
+ const envHeaders = {};
33288
+ const staticHeaders = {};
33289
+ let bearerTokenEnvVar;
33290
+ for (const [name, value] of Object.entries(httpHeaders)) {
33291
+ const bearerMatch = BEARER_ENV_VAR_REF_PATTERN.exec(value);
33292
+ if (bearerMatch?.[1] && name.toLowerCase() === AUTHORIZATION_HEADER_NAME) {
33293
+ if (converted["bearer_token_env_var"] === void 0 && bearerTokenEnvVar === void 0) bearerTokenEnvVar = bearerMatch[1];
33294
+ continue;
33295
+ }
33296
+ const wholeMatch = WHOLE_ENV_VAR_REF_PATTERN.exec(value);
33297
+ if (wholeMatch?.[1] && canWriteEnvHeaders) {
33298
+ envHeaders[name] = wholeMatch[1];
33299
+ continue;
33300
+ }
33301
+ 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.`);
33302
+ staticHeaders[name] = value;
33303
+ }
33304
+ converted["http_headers"] = staticHeaders;
33305
+ if (bearerTokenEnvVar !== void 0) converted["bearer_token_env_var"] = bearerTokenEnvVar;
33306
+ if (Object.keys(envHeaders).length > 0) converted["env_http_headers"] = {
33307
+ ...envHeaders,
33308
+ ...isHeadersRecord(existingEnvHeaders) ? existingEnvHeaders : {}
33309
+ };
33310
+ }
33311
+ /**
33150
33312
  * Codex states no transport of its own — it infers one from `command` versus
33151
33313
  * `url`, which is why generate drops the canonical `type`. Restate it for a url
33152
33314
  * server on the way back, so a config imported from Codex reaches the adapters
@@ -33233,6 +33395,10 @@ function convertToCodexFormat(mcpServers) {
33233
33395
  }
33234
33396
  } else converted[key] = value;
33235
33397
  }
33398
+ convertEnvVarRefsForCodex({
33399
+ converted,
33400
+ serverName: name
33401
+ });
33236
33402
  const previousName = originalNames.get(codexName);
33237
33403
  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
33404
  originalNames.set(codexName, name);
@@ -59232,7 +59398,7 @@ const ClaudecodeSkillFrontmatterSchema = zod_mini.z.looseObject({
59232
59398
  "disallowed-tools": zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())])),
59233
59399
  model: zod_mini.z.optional(zod_mini.z.string()),
59234
59400
  effort: zod_mini.z.optional(zod_mini.z.string()),
59235
- "argument-hint": zod_mini.z.optional(zod_mini.z.string()),
59401
+ "argument-hint": zod_mini.z.optional(ClaudecodeArgumentHintSchema),
59236
59402
  arguments: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())])),
59237
59403
  context: zod_mini.z.optional(zod_mini.z.string()),
59238
59404
  agent: zod_mini.z.optional(zod_mini.z.string()),
@@ -65388,7 +65554,8 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
65388
65554
  meta: {
65389
65555
  supportsProject: true,
65390
65556
  supportsSimulated: false,
65391
- supportsGlobal: true
65557
+ supportsGlobal: true,
65558
+ lenientImport: true
65392
65559
  }
65393
65560
  }],
65394
65561
  ["claudecode-plugin", {
@@ -72337,7 +72504,7 @@ var ToolRule = class extends ToolFile {
72337
72504
  static buildToolRuleParamsAgentsmd({ outputRoot = process.cwd(), rulesyncRule, validate = true, rootPath = {
72338
72505
  relativeDirPath: ".",
72339
72506
  relativeFilePath: "AGENTS.md"
72340
- }, nonRootPath = { relativeDirPath: AGENTSMD_MEMORIES_DIR_PATH } }) {
72507
+ }, nonRootPath = { relativeDirPath: AGENTSMD_MEMORIES_DIR_PATH }, global = false }) {
72341
72508
  const params = this.buildToolRuleParamsDefault({
72342
72509
  outputRoot,
72343
72510
  rulesyncRule,
@@ -72346,7 +72513,7 @@ var ToolRule = class extends ToolFile {
72346
72513
  nonRootPath
72347
72514
  });
72348
72515
  const rulesyncFrontmatter = rulesyncRule.getFrontmatter();
72349
- if (!rulesyncFrontmatter.root && rulesyncFrontmatter.agentsmd?.subprojectPath) {
72516
+ if (!global && !rulesyncFrontmatter.root && rulesyncFrontmatter.agentsmd?.subprojectPath) {
72350
72517
  params.relativeDirPath = (0, node_path.join)(rulesyncFrontmatter.agentsmd.subprojectPath);
72351
72518
  params.relativeFilePath = "AGENTS.md";
72352
72519
  }
@@ -73003,7 +73170,8 @@ var AmpRule = class AmpRule extends ToolRule {
73003
73170
  rulesyncRule,
73004
73171
  validate,
73005
73172
  rootPath: paths.root,
73006
- nonRootPath: paths.nonRoot
73173
+ nonRootPath: paths.nonRoot,
73174
+ global
73007
73175
  });
73008
73176
  if (!params.root && params.globs !== void 0 && params.globs.length > 0) params.fileContent = stringifyFrontmatter(params.fileContent, { globs: params.globs });
73009
73177
  return new AmpRule(params);
@@ -73049,97 +73217,6 @@ var AmpRule = class AmpRule extends ToolRule {
73049
73217
  }
73050
73218
  };
73051
73219
  //#endregion
73052
- //#region src/features/rules/antigravity-cli-rule.ts
73053
- /**
73054
- * Rule generator for the Google Antigravity CLI (`agy`, the Gemini-CLI
73055
- * successor in Antigravity 2.0).
73056
- *
73057
- * The CLI reads the same plain-markdown context files as Gemini CLI — a root
73058
- * context file plus non-root memory files in `.agents/rules/` — so this class
73059
- * follows that same plain-markdown approach but points at the new `.agents/` tree.
73060
- *
73061
- * - Project scope: root `AGENTS.md` (the cross-tool standard, matching
73062
- * `antigravity-ide`); non-root `.agents/rules/*.md`.
73063
- * - Global scope: a single plain `~/.gemini/GEMINI.md` (shared with the IDE).
73064
- */
73065
- var AntigravityCliRule = class AntigravityCliRule extends ToolRule {
73066
- static getSettablePaths({ global, excludeToolDir } = {}) {
73067
- if (global) return { root: {
73068
- relativeDirPath: buildToolPath(ANTIGRAVITY_GEMINI_DIR, ".", excludeToolDir),
73069
- relativeFilePath: ANTIGRAVITY_GLOBAL_RULE_FILE_NAME
73070
- } };
73071
- return {
73072
- root: {
73073
- relativeDirPath: ".",
73074
- relativeFilePath: ANTIGRAVITY_RULE_FILE_NAME
73075
- },
73076
- nonRoot: { relativeDirPath: buildToolPath(ANTIGRAVITY_AGENTS_DIR, "rules", excludeToolDir) }
73077
- };
73078
- }
73079
- static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
73080
- const paths = this.getSettablePaths({ global });
73081
- if (relativeFilePath === paths.root.relativeFilePath) {
73082
- const relativePath = paths.root.relativeFilePath;
73083
- const fileContent = await readFileContent((0, node_path.join)(outputRoot, paths.root.relativeDirPath, relativePath));
73084
- return new AntigravityCliRule({
73085
- outputRoot,
73086
- relativeDirPath: paths.root.relativeDirPath,
73087
- relativeFilePath: paths.root.relativeFilePath,
73088
- fileContent,
73089
- validate,
73090
- root: true
73091
- });
73092
- }
73093
- if (!paths.nonRoot) throw new Error(`nonRoot path is not set for ${relativeFilePath}`);
73094
- const relativePath = (0, node_path.join)(paths.nonRoot.relativeDirPath, relativeFilePath);
73095
- const fileContent = await readFileContent((0, node_path.join)(outputRoot, relativePath));
73096
- return new AntigravityCliRule({
73097
- outputRoot,
73098
- relativeDirPath: paths.nonRoot.relativeDirPath,
73099
- relativeFilePath,
73100
- fileContent,
73101
- validate,
73102
- root: false
73103
- });
73104
- }
73105
- static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
73106
- const paths = this.getSettablePaths({ global });
73107
- return new AntigravityCliRule(this.buildToolRuleParamsDefault({
73108
- outputRoot,
73109
- rulesyncRule,
73110
- validate,
73111
- rootPath: paths.root,
73112
- nonRootPath: paths.nonRoot
73113
- }));
73114
- }
73115
- toRulesyncRule() {
73116
- return this.toRulesyncRuleDefault();
73117
- }
73118
- validate() {
73119
- return {
73120
- success: true,
73121
- error: null
73122
- };
73123
- }
73124
- static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
73125
- const isRoot = relativeFilePath === this.getSettablePaths({ global }).root.relativeFilePath;
73126
- return new AntigravityCliRule({
73127
- outputRoot,
73128
- relativeDirPath,
73129
- relativeFilePath,
73130
- fileContent: "",
73131
- validate: false,
73132
- root: isRoot
73133
- });
73134
- }
73135
- static isTargetedByRulesyncRule(rulesyncRule) {
73136
- return this.isTargetedByRulesyncRuleDefault({
73137
- rulesyncRule,
73138
- toolTarget: "antigravity-cli"
73139
- });
73140
- }
73141
- };
73142
- //#endregion
73143
73220
  //#region src/features/rules/antigravity-rule.ts
73144
73221
  const AntigravityRuleFrontmatterSchema = zod_mini.z.looseObject({
73145
73222
  trigger: zod_mini.z.optional(zod_mini.z.union([
@@ -73286,6 +73363,185 @@ const STRATEGIES$1 = [
73286
73363
  }
73287
73364
  ];
73288
73365
  //#endregion
73366
+ //#region src/features/rules/antigravity-cli-rule.ts
73367
+ /**
73368
+ * Rule generator for the Google Antigravity CLI (`agy`, the Gemini-CLI
73369
+ * successor in Antigravity 2.0).
73370
+ *
73371
+ * The CLI reads the same plain-markdown context files as Gemini CLI — a root
73372
+ * context file plus non-root memory files in `.agents/rules/` — so this class
73373
+ * follows that same plain-markdown approach but points at the new `.agents/` tree.
73374
+ *
73375
+ * - Project scope: root `AGENTS.md` (the cross-tool standard, matching
73376
+ * `antigravity-ide`); non-root `.agents/rules/*.md`.
73377
+ * - Global scope: root plain `~/.gemini/GEMINI.md` (shared with the IDE);
73378
+ * non-root `~/.gemini/config/rules/*.md`. The CLI truncates each rule file at
73379
+ * 24,000 bytes, so keeping non-root rules as separate files stops a large rule
73380
+ * set from being cut off. Every file in that directory must carry a valid
73381
+ * `trigger` frontmatter or the CLI discards it, so these files get the same
73382
+ * trigger-strategy frontmatter as the IDE's non-root rules.
73383
+ *
73384
+ * @see https://antigravity.google/docs/rules
73385
+ */
73386
+ var AntigravityCliRule = class AntigravityCliRule extends ToolRule {
73387
+ static getSettablePaths({ global, excludeToolDir } = {}) {
73388
+ if (global) return {
73389
+ root: {
73390
+ relativeDirPath: buildToolPath(ANTIGRAVITY_GEMINI_DIR, ".", excludeToolDir),
73391
+ relativeFilePath: ANTIGRAVITY_GLOBAL_RULE_FILE_NAME
73392
+ },
73393
+ nonRoot: { relativeDirPath: buildToolPath(ANTIGRAVITY_GEMINI_DIR, ANTIGRAVITY_GLOBAL_RULES_SUBDIR, excludeToolDir) }
73394
+ };
73395
+ return {
73396
+ root: {
73397
+ relativeDirPath: ".",
73398
+ relativeFilePath: ANTIGRAVITY_RULE_FILE_NAME
73399
+ },
73400
+ nonRoot: { relativeDirPath: buildToolPath(ANTIGRAVITY_AGENTS_DIR, "rules", excludeToolDir) }
73401
+ };
73402
+ }
73403
+ static async fromFile({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, validate = true, global = false }) {
73404
+ const paths = this.getSettablePaths({ global });
73405
+ if (relativeFilePath === paths.root.relativeFilePath && (relativeDirPath === void 0 || relativeDirPath === paths.root.relativeDirPath)) {
73406
+ const relativePath = paths.root.relativeFilePath;
73407
+ const fileContent = await readFileContent((0, node_path.join)(outputRoot, paths.root.relativeDirPath, relativePath));
73408
+ return new AntigravityCliRule({
73409
+ outputRoot,
73410
+ relativeDirPath: paths.root.relativeDirPath,
73411
+ relativeFilePath: paths.root.relativeFilePath,
73412
+ fileContent,
73413
+ validate,
73414
+ root: true,
73415
+ global
73416
+ });
73417
+ }
73418
+ if (!paths.nonRoot) throw new Error(`nonRoot path is not set for ${relativeFilePath}`);
73419
+ const relativePath = (0, node_path.join)(paths.nonRoot.relativeDirPath, relativeFilePath);
73420
+ const fileContent = await readFileContent((0, node_path.join)(outputRoot, relativePath));
73421
+ const rule = new AntigravityCliRule({
73422
+ outputRoot,
73423
+ relativeDirPath: paths.nonRoot.relativeDirPath,
73424
+ relativeFilePath,
73425
+ fileContent,
73426
+ validate,
73427
+ root: false,
73428
+ global
73429
+ });
73430
+ if (validate) {
73431
+ const result = rule.validate();
73432
+ if (!result.success) throw new Error(`Invalid frontmatter in ${relativePath}: ${result.error.message}`);
73433
+ }
73434
+ return rule;
73435
+ }
73436
+ static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
73437
+ const paths = this.getSettablePaths({ global });
73438
+ const params = this.buildToolRuleParamsDefault({
73439
+ outputRoot,
73440
+ rulesyncRule,
73441
+ validate,
73442
+ rootPath: paths.root,
73443
+ nonRootPath: paths.nonRoot
73444
+ });
73445
+ if (!global || params.root) return new AntigravityCliRule({
73446
+ ...params,
73447
+ global
73448
+ });
73449
+ const rulesyncFrontmatter = rulesyncRule.getFrontmatter();
73450
+ const storedAntigravity = rulesyncFrontmatter.antigravity;
73451
+ const storedTrigger = storedAntigravity?.trigger;
73452
+ const strategy = STRATEGIES$1.find((s) => s.canHandle(storedTrigger));
73453
+ if (!strategy) throw new Error(`No strategy found for trigger: ${storedTrigger}`);
73454
+ const frontmatter = strategy.generateFrontmatter(normalizeStoredAntigravity(storedAntigravity), rulesyncFrontmatter);
73455
+ return new AntigravityCliRule({
73456
+ ...params,
73457
+ relativeFilePath: params.relativeFilePath.split(/[\\/]/).join("-"),
73458
+ fileContent: stringifyFrontmatter(rulesyncRule.getBody(), frontmatter),
73459
+ global
73460
+ });
73461
+ }
73462
+ toRulesyncRule() {
73463
+ if (!this.global || this.root) return this.toRulesyncRuleDefault();
73464
+ const { frontmatter, body } = this.parseGlobalNonRootContent();
73465
+ const strategy = STRATEGIES$1.find((s) => s.canHandle(frontmatter.trigger));
73466
+ const rulesyncData = strategy ? strategy.exportRulesyncData(frontmatter) : {
73467
+ globs: [],
73468
+ antigravity: frontmatter
73469
+ };
73470
+ return new RulesyncRule({
73471
+ outputRoot: process.cwd(),
73472
+ relativeDirPath: RulesyncRule.getSettablePaths().recommended.relativeDirPath,
73473
+ relativeFilePath: this.getRelativeFilePath(),
73474
+ frontmatter: {
73475
+ root: false,
73476
+ targets: ["*"],
73477
+ ...rulesyncData,
73478
+ antigravity: {
73479
+ ...rulesyncData.antigravity,
73480
+ globs: frontmatter.globs ? parseGlobsString$1(frontmatter.globs) : void 0
73481
+ }
73482
+ },
73483
+ body
73484
+ });
73485
+ }
73486
+ validate() {
73487
+ if (!this.global || this.root) return {
73488
+ success: true,
73489
+ error: null
73490
+ };
73491
+ try {
73492
+ const { frontmatter } = this.parseGlobalNonRootContent();
73493
+ const result = AntigravityRuleFrontmatterSchema.safeParse(frontmatter);
73494
+ if (!result.success) return {
73495
+ success: false,
73496
+ error: new Error(formatError(result.error))
73497
+ };
73498
+ } catch (error) {
73499
+ return {
73500
+ success: false,
73501
+ error: error instanceof Error ? error : new Error(String(error))
73502
+ };
73503
+ }
73504
+ return {
73505
+ success: true,
73506
+ error: null
73507
+ };
73508
+ }
73509
+ /**
73510
+ * `~/.gemini/config/rules/` is shared with the Antigravity IDE and holds
73511
+ * global rules the user created outside rulesync, which cannot be told apart
73512
+ * from generated ones, so the orphan sweep never removes files there.
73513
+ */
73514
+ isDeletable() {
73515
+ return !this.global || this.root;
73516
+ }
73517
+ parseGlobalNonRootContent() {
73518
+ const { frontmatter, body } = parseFrontmatter(this.getFileContent(), this.getFilePath());
73519
+ return {
73520
+ frontmatter,
73521
+ body
73522
+ };
73523
+ }
73524
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
73525
+ const paths = this.getSettablePaths({ global });
73526
+ const isRoot = relativeFilePath === paths.root.relativeFilePath && relativeDirPath === paths.root.relativeDirPath;
73527
+ return new AntigravityCliRule({
73528
+ outputRoot,
73529
+ relativeDirPath,
73530
+ relativeFilePath,
73531
+ fileContent: "",
73532
+ validate: false,
73533
+ root: isRoot,
73534
+ global
73535
+ });
73536
+ }
73537
+ static isTargetedByRulesyncRule(rulesyncRule) {
73538
+ return this.isTargetedByRulesyncRuleDefault({
73539
+ rulesyncRule,
73540
+ toolTarget: "antigravity-cli"
73541
+ });
73542
+ }
73543
+ };
73544
+ //#endregion
73289
73545
  //#region src/features/rules/antigravity-ide-rule.ts
73290
73546
  /**
73291
73547
  * Rule generator for the Google Antigravity IDE (Antigravity 2.0).
@@ -74298,7 +74554,8 @@ var ClineRule = class ClineRule extends ToolRule {
74298
74554
  outputRoot,
74299
74555
  rulesyncRule,
74300
74556
  validate,
74301
- rootPath: paths.root
74557
+ rootPath: paths.root,
74558
+ global
74302
74559
  }));
74303
74560
  }
74304
74561
  if (isRoot) return new ClineRule({
@@ -74718,26 +74975,62 @@ var CodebuddyRule = class CodebuddyRule extends ToolRule {
74718
74975
  }
74719
74976
  };
74720
74977
  //#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
- });
74978
+ //#region src/features/rules/nested-agentsmd-rule.ts
74979
+ /**
74980
+ * Shared adapter for the targets that read instructions exactly the way the
74981
+ * AGENTS.md standard describes them and nothing more: a root `AGENTS.md` at the
74982
+ * project root, nested per-directory `AGENTS.md` files loaded only while working
74983
+ * under that directory, and a single personal root file under a tool-specific
74984
+ * home directory. Such a target has no modular non-root instruction directory,
74985
+ * so every other non-root rule folds into the root file.
74986
+ *
74987
+ * Concrete targets (Codex CLI, Pool, Vibe, DeepSeek Harness) only supply the global
74988
+ * directory and their tool target through {@link getFamily}; the vendor docs
74989
+ * describing each one's discovery walk live on the subclass.
74990
+ *
74991
+ * The nested-file scan mirrors the AGENTS.md standard's nested discovery —
74992
+ * same file name, same exclusions, import-only, project scope — because it
74993
+ * discovers literally the same files.
74994
+ */
74995
+ var NestedAgentsmdRule = class extends ToolRule {
74996
+ static getFamily() {
74997
+ throw new Error("Please implement this method in the subclass.");
74729
74998
  }
74730
74999
  static getSettablePaths({ global = false } = {}) {
75000
+ if (global) return { root: {
75001
+ relativeDirPath: this.getFamily().globalDir,
75002
+ relativeFilePath: AGENTSMD_RULE_FILE_NAME
75003
+ } };
74731
75004
  return { root: {
74732
- relativeDirPath: global ? CODEXCLI_DIR : ".",
74733
- relativeFilePath: CODEXCLI_RULE_FILE_NAME
75005
+ relativeDirPath: ".",
75006
+ relativeFilePath: AGENTSMD_RULE_FILE_NAME
74734
75007
  } };
74735
75008
  }
74736
- static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, validate = true, global = false }) {
75009
+ static getNestedFilePatterns() {
75010
+ return this.buildNestedFilePatterns({ fileName: AGENTSMD_RULE_FILE_NAME });
75011
+ }
75012
+ /**
75013
+ * The subproject directory this rule scopes, or `undefined` for the root file
75014
+ * (project or global).
75015
+ */
75016
+ getSubprojectPath() {
75017
+ return this.getNestedSubprojectPath({ fileName: AGENTSMD_RULE_FILE_NAME });
75018
+ }
75019
+ static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, relativeDirPath: overrideDirPath, validate = true, global = false }) {
74737
75020
  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({
75021
+ if (overrideDirPath !== void 0 && overrideDirPath !== root.relativeDirPath && overrideDirPath !== ".") {
75022
+ const fileContent = await readFileContent((0, node_path.join)(outputRoot, overrideDirPath, AGENTSMD_RULE_FILE_NAME));
75023
+ return new this({
75024
+ outputRoot,
75025
+ relativeDirPath: overrideDirPath,
75026
+ relativeFilePath: AGENTSMD_RULE_FILE_NAME,
75027
+ fileContent,
75028
+ validate,
75029
+ root: false
75030
+ });
75031
+ }
75032
+ const fileContent = await readFileContent((0, node_path.join)(outputRoot, root.relativeDirPath, root.relativeFilePath));
75033
+ return new this({
74741
75034
  outputRoot,
74742
75035
  relativeDirPath: root.relativeDirPath,
74743
75036
  relativeFilePath: root.relativeFilePath,
@@ -74748,8 +75041,18 @@ var CodexcliRule = class CodexcliRule extends ToolRule {
74748
75041
  }
74749
75042
  static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
74750
75043
  const { root } = this.getSettablePaths({ global });
74751
- const isRoot = rulesyncRule.getFrontmatter().root ?? false;
74752
- return new CodexcliRule({
75044
+ const frontmatter = rulesyncRule.getFrontmatter();
75045
+ const isRoot = frontmatter.root ?? false;
75046
+ const subprojectPath = frontmatter.agentsmd?.subprojectPath;
75047
+ if (!global && !isRoot && subprojectPath) return new this({
75048
+ outputRoot,
75049
+ relativeDirPath: (0, node_path.join)(subprojectPath),
75050
+ relativeFilePath: AGENTSMD_RULE_FILE_NAME,
75051
+ fileContent: rulesyncRule.getBody(),
75052
+ validate,
75053
+ root: false
75054
+ });
75055
+ return new this({
74753
75056
  outputRoot,
74754
75057
  relativeDirPath: root.relativeDirPath,
74755
75058
  relativeFilePath: root.relativeFilePath,
@@ -74759,7 +75062,9 @@ var CodexcliRule = class CodexcliRule extends ToolRule {
74759
75062
  });
74760
75063
  }
74761
75064
  toRulesyncRule() {
74762
- return this.toRulesyncRuleDefault();
75065
+ const subprojectPath = this.getSubprojectPath();
75066
+ if (subprojectPath === void 0) return this.toRulesyncRuleDefault();
75067
+ return this.toRulesyncRuleNestedAgentsmd({ subprojectPath });
74763
75068
  }
74764
75069
  validate() {
74765
75070
  return {
@@ -74768,23 +75073,54 @@ var CodexcliRule = class CodexcliRule extends ToolRule {
74768
75073
  };
74769
75074
  }
74770
75075
  static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
74771
- return new CodexcliRule({
75076
+ const isRoot = relativeFilePath === "AGENTS.md" && (relativeDirPath === "." || relativeDirPath === this.getFamily().globalDir);
75077
+ return new this({
74772
75078
  outputRoot,
74773
75079
  relativeDirPath,
74774
75080
  relativeFilePath,
74775
75081
  fileContent: "",
74776
75082
  validate: false,
74777
- root: relativeFilePath === "AGENTS.md" && (relativeDirPath === "." || relativeDirPath === ".codex")
75083
+ root: isRoot
74778
75084
  });
74779
75085
  }
74780
75086
  static isTargetedByRulesyncRule(rulesyncRule) {
74781
75087
  return this.isTargetedByRulesyncRuleDefault({
74782
75088
  rulesyncRule,
74783
- toolTarget: "codexcli"
75089
+ toolTarget: this.getFamily().toolTarget
74784
75090
  });
74785
75091
  }
74786
75092
  };
74787
75093
  //#endregion
75094
+ //#region src/features/rules/codexcli-rule.ts
75095
+ /**
75096
+ * Rule generator for OpenAI Codex CLI.
75097
+ *
75098
+ * Codex CLI loads instructions only from the `AGENTS.md` family: the global
75099
+ * `~/.codex/AGENTS.md` (or `AGENTS.override.md`), then one file per directory
75100
+ * level from the project root down to the current working directory, joined
75101
+ * root-first so deeper files take precedence. Nested per-directory files are
75102
+ * therefore a real scoping surface, and Codex's docs recommend splitting large
75103
+ * instruction sets across nested directories. It does NOT scan a
75104
+ * `.codex/memories/` directory for instruction files — that directory belongs
75105
+ * to Codex's separate SQLite-backed auto-memory system.
75106
+ *
75107
+ * A non-root rule carrying `agentsmd.subprojectPath` is emitted as a nested
75108
+ * `<subprojectPath>/AGENTS.md` (project scope only); every other non-root rule
75109
+ * folds into the root `AGENTS.md`, since Codex has no modular rules directory.
75110
+ * The project files on the root-to-cwd chain share one `project_doc_max_bytes`
75111
+ * budget (32 KiB by default), which the RulesProcessor warns about.
75112
+ *
75113
+ * @see https://learn.chatgpt.com/docs/agent-configuration/agents-md
75114
+ */
75115
+ var CodexcliRule = class extends NestedAgentsmdRule {
75116
+ static getFamily() {
75117
+ return {
75118
+ globalDir: CODEXCLI_DIR,
75119
+ toolTarget: "codexcli"
75120
+ };
75121
+ }
75122
+ };
75123
+ //#endregion
74788
75124
  //#region src/features/rules/commandcode-rule.ts
74789
75125
  var CommandcodeRule = class CommandcodeRule extends ToolRule {
74790
75126
  constructor({ fileContent, root, ...rest }) {
@@ -76089,122 +76425,6 @@ var DevinRule = class DevinRule extends ToolRule {
76089
76425
  }
76090
76426
  };
76091
76427
  //#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
76428
  //#region src/features/rules/dsh-rule.ts
76209
76429
  /**
76210
76430
  * DeepSeek Harness (`dsh`) rules.
@@ -76416,7 +76636,8 @@ var FactorydroidRule = class FactorydroidRule extends ToolRule {
76416
76636
  rulesyncRule,
76417
76637
  validate,
76418
76638
  rootPath: paths.root,
76419
- nonRootPath: paths.nonRoot
76639
+ nonRootPath: paths.nonRoot,
76640
+ global
76420
76641
  }));
76421
76642
  }
76422
76643
  toRulesyncRule() {
@@ -76883,7 +77104,8 @@ var KiloRule = class KiloRule extends ToolRule {
76883
77104
  rulesyncRule,
76884
77105
  validate,
76885
77106
  rootPath: paths.root,
76886
- nonRootPath: paths.nonRoot
77107
+ nonRootPath: paths.nonRoot,
77108
+ global
76887
77109
  }));
76888
77110
  }
76889
77111
  toRulesyncRule() {
@@ -77387,7 +77609,8 @@ var OpenCodeRule = class OpenCodeRule extends ToolRule {
77387
77609
  rulesyncRule,
77388
77610
  validate,
77389
77611
  rootPath: paths.root,
77390
- nonRootPath: paths.nonRoot
77612
+ nonRootPath: paths.nonRoot,
77613
+ global
77391
77614
  }));
77392
77615
  }
77393
77616
  toRulesyncRule() {
@@ -78856,6 +79079,14 @@ var ZoocodeRule = class extends RooRule {
78856
79079
  //#endregion
78857
79080
  //#region src/features/rules/rules-processor.ts
78858
79081
  const RulesProcessorToolTargetSchema = zod_mini.z.enum(rulesProcessorToolTargetTuple);
79082
+ /**
79083
+ * Normalizes a relative directory path for comparison: POSIX separators, no
79084
+ * `./` segments and no trailing slash, with the current directory as `"."`
79085
+ * (so `"./"`, `"a/"` and `"a"` compare the way `join()` groups them).
79086
+ */
79087
+ const normalizeRelativeDir = (dir) => node_path.posix.normalize(toPosixPath(dir)).replace(/\/+$/, "") || ".";
79088
+ /** A tool rule's output path relative to its output root, for comparison. */
79089
+ const toolOutputPath = (toolRule) => node_path.posix.join(normalizeRelativeDir(toolRule.getRelativeDirPath()), toolRule.getRelativeFilePath());
78859
79090
  const formatRulePaths = (rules) => rules.map((r) => (0, node_path.join)(r.getRelativeDirPath(), r.getRelativeFilePath())).join(", ");
78860
79091
  const RulesFeatureOptionsSchema = zod_mini.z.looseObject({
78861
79092
  ruleDiscoveryMode: zod_mini.z.optional(zod_mini.z.enum(["none", "explicit"])),
@@ -78937,7 +79168,9 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
78937
79168
  meta: {
78938
79169
  extension: "md",
78939
79170
  supportsGlobal: true,
78940
- ruleDiscoveryMode: "toon"
79171
+ ruleDiscoveryMode: "toon",
79172
+ ruleDiscoveryModeGlobal: "auto",
79173
+ sharedGlobalNonRootDir: true
78941
79174
  }
78942
79175
  }],
78943
79176
  ["antigravity-ide", {
@@ -79024,7 +79257,11 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
79024
79257
  extension: "md",
79025
79258
  supportsGlobal: true,
79026
79259
  ruleDiscoveryMode: "auto",
79027
- collisionPolicy: "fold"
79260
+ collisionPolicy: "fold",
79261
+ projectInstructionBudget: {
79262
+ bytes: CODEXCLI_PROJECT_DOC_MAX_BYTES,
79263
+ 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)."
79264
+ }
79028
79265
  }
79029
79266
  }],
79030
79267
  ["commandcode", {
@@ -79482,9 +79719,61 @@ var RulesProcessor = class extends FeatureProcessor {
79482
79719
  toolRules,
79483
79720
  factory
79484
79721
  });
79722
+ this.warnForInstructionBudget({
79723
+ toolRules,
79724
+ meta
79725
+ });
79485
79726
  return outputFiles;
79486
79727
  }
79487
79728
  /**
79729
+ * Warn when a generated project instruction chain exceeds the tool's byte
79730
+ * budget (`meta.projectInstructionBudget`).
79731
+ *
79732
+ * Codex CLI concatenates the project `AGENTS.md` files from the project root
79733
+ * down to the cwd and stops once their combined size reaches
79734
+ * `project_doc_max_bytes`, truncating the file that crosses it — silently, so
79735
+ * without this warning the rules past the limit simply never reach the model.
79736
+ * Each generated file is measured together with the generated files in its
79737
+ * ancestor directories, since that is the chain Codex loads while working
79738
+ * there. When the root file alone is over the budget every chain is, so only
79739
+ * the root is reported; likewise each over-budget nested chain is reported
79740
+ * once, at its shallowest directory. With several output roots the warning
79741
+ * names the root it refers to. An output root below the git root is measured
79742
+ * without the parent directories' files Codex would also load, so the
79743
+ * estimate can come out low there. The budget checked is the tool's default: a value
79744
+ * raised by hand in the tool's own config is not read. Global scope is not
79745
+ * checked because Codex loads the personal `~/.codex/AGENTS.md` outside the
79746
+ * project budget.
79747
+ */
79748
+ warnForInstructionBudget({ toolRules, meta }) {
79749
+ const budget = meta.projectInstructionBudget;
79750
+ if (budget === void 0 || this.global) return;
79751
+ const files = toolRules.map((rule) => ({
79752
+ dir: normalizeRelativeDir(rule.getRelativeDirPath()),
79753
+ path: toPosixPath((0, node_path.join)(rule.getRelativeDirPath(), rule.getRelativeFilePath())),
79754
+ bytes: Buffer.byteLength(addTrailingNewline(rule.getFileContent()), "utf8")
79755
+ }));
79756
+ const isAncestorOrSelf = (ancestor, dir) => ancestor === "." || ancestor === dir || dir.startsWith(`${ancestor}/`);
79757
+ const location = this.outputRoot === process.cwd() ? "" : ` under '${this.outputRoot}'`;
79758
+ const rootFiles = files.filter(({ dir }) => dir === ".");
79759
+ const rootBytes = rootFiles.reduce((sum, { bytes }) => sum + bytes, 0);
79760
+ if (rootBytes > budget.bytes) {
79761
+ 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}`);
79762
+ return;
79763
+ }
79764
+ const reportedDirs = [];
79765
+ const nestedFiles = files.filter(({ dir }) => dir !== ".").toSorted((a, b) => a.dir.split("/").length - b.dir.split("/").length);
79766
+ for (const file of nestedFiles) {
79767
+ if (reportedDirs.some((dir) => isAncestorOrSelf(dir, file.dir))) continue;
79768
+ const chain = files.filter(({ dir }) => isAncestorOrSelf(dir, file.dir));
79769
+ const chainBytes = chain.reduce((sum, { bytes }) => sum + bytes, 0);
79770
+ if (chainBytes > budget.bytes) {
79771
+ reportedDirs.push(file.dir);
79772
+ 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}`);
79773
+ }
79774
+ }
79775
+ }
79776
+ /**
79488
79777
  * Handle localRoot rules (only in non-global mode and when enabled). Mutates
79489
79778
  * `toolRules` in place.
79490
79779
  */
@@ -79960,7 +80249,7 @@ var RulesProcessor = class extends FeatureProcessor {
79960
80249
  */
79961
80250
  generateReferenceSectionFromMeta(meta, toolRules) {
79962
80251
  switch (resolveRuleDiscoveryMode({
79963
- defaultMode: meta.ruleDiscoveryMode,
80252
+ defaultMode: this.global && meta.ruleDiscoveryModeGlobal ? meta.ruleDiscoveryModeGlobal : meta.ruleDiscoveryMode,
79964
80253
  options: this.featureOptions
79965
80254
  })) {
79966
80255
  case "toon": return this.generateToonReferencesSection(toolRules);
@@ -80156,14 +80445,14 @@ As this project's AI coding tool, you must follow the additional conventions bel
80156
80445
  * decides for all of them: its `pi.contextFile` is copied onto the non-root
80157
80446
  * rules, and the flag set only on a non-root rule is dropped with a warning.
80158
80447
  */
80159
- alignPiContextFile(rules) {
80448
+ alignPiContextFile(rules, { warn = true } = {}) {
80160
80449
  if (this.toolTarget !== "pi") return rules;
80161
80450
  const factory = this.getFactory(this.toolTarget);
80162
80451
  const targeted = rules.filter((rule) => factory.class.isTargetedByRulesyncRule(rule));
80163
80452
  const rootContextFile = targeted.some((rule) => rule.getFrontmatter().root === true && rule.getFrontmatter().pi?.contextFile === "override") ? "override" : void 0;
80164
80453
  const mismatched = targeted.filter((rule) => rule.getFrontmatter().pi?.contextFile !== rootContextFile);
80165
80454
  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)}`);
80455
+ 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
80456
  const mismatchedSet = new Set(mismatched);
80168
80457
  return rules.map((rule) => {
80169
80458
  if (!mismatchedSet.has(rule)) return rule;
@@ -80217,14 +80506,72 @@ As this project's AI coding tool, you must follow the additional conventions bel
80217
80506
  * duplication check the same way `loadRulesyncFiles`'s global-mode branch
80218
80507
  * excludes it from `nonRootRules`: `generate` ignores `localRoot` entirely
80219
80508
  * 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.
80509
+ * root output and warning about it here would be inaccurate. The same goes
80510
+ * for a tool that writes `localRoot` to a dedicated local file.
80511
+ *
80512
+ * Non-root rules written to a file other than the root (nested
80513
+ * per-directory files, Pi's `APPEND_SYSTEM.md`) get a separate warning when
80514
+ * that file already exists and would import under a different name.
80221
80515
  */
80222
80516
  async warnForFoldImportDuplicationRisk() {
80223
80517
  const factory = this.getFactory(this.toolTarget);
80224
80518
  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.`);
80519
+ const targetedRules = this.alignPiContextFile(await this.loadMergedRulesyncRules(), { warn: false }).filter((rule) => factory.class.isTargetedByRulesyncRule(rule));
80520
+ let includeLocalRoot = !this.global;
80521
+ try {
80522
+ includeLocalRoot &&= resolveIncludeLocalRoot(this.featureOptions);
80523
+ } catch {}
80524
+ const targetedNonRootRules = targetedRules.filter((rule) => !rule.getFrontmatter().root && !(rule.getFrontmatter().localRoot && (!includeLocalRoot || factory.meta.localRootMode === "separate-local-file")));
80525
+ const rootPaths = /* @__PURE__ */ new Set();
80526
+ const { root } = factory.class.getSettablePaths({ global: this.global });
80527
+ if (root) rootPaths.add(node_path.posix.join(normalizeRelativeDir(root.relativeDirPath), root.relativeFilePath));
80528
+ for (const rulesyncRule of targetedRules.filter((rule) => rule.getFrontmatter().root)) {
80529
+ const toolRule = this.toToolRuleForImportCheck({
80530
+ factory,
80531
+ rulesyncRule
80532
+ });
80533
+ rootPaths.add(toolOutputPath(toolRule));
80534
+ }
80535
+ const separateRules = [];
80536
+ const foldedRules = [];
80537
+ let localRootCounted = false;
80538
+ for (const rulesyncRule of targetedNonRootRules) {
80539
+ if (rulesyncRule.getFrontmatter().localRoot) {
80540
+ if (!localRootCounted) {
80541
+ foldedRules.push(rulesyncRule);
80542
+ localRootCounted = true;
80543
+ }
80544
+ continue;
80545
+ }
80546
+ const toolRule = this.toToolRuleForImportCheck({
80547
+ factory,
80548
+ rulesyncRule
80549
+ });
80550
+ if (rootPaths.size === 0 || rootPaths.has(toolOutputPath(toolRule))) foldedRules.push(rulesyncRule);
80551
+ else separateRules.push({
80552
+ toolRule,
80553
+ rulesyncRule
80554
+ });
80555
+ }
80556
+ 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.`);
80557
+ const duplicated = [];
80558
+ for (const conversion of separateRules) {
80559
+ const { toolRule, rulesyncRule } = conversion;
80560
+ if (toolRule.toRulesyncRule().getRelativeFilePath().toLowerCase() === rulesyncRule.getRelativeFilePath().toLowerCase()) continue;
80561
+ if (await fileExists(toolRule.getFilePath())) duplicated.push(conversion);
80562
+ }
80563
+ if (duplicated.length > 0) {
80564
+ const fileNames = [...new Set(duplicated.map(({ toolRule }) => toolRule.getRelativeFilePath()))].join(", ");
80565
+ 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.`);
80566
+ }
80567
+ }
80568
+ toToolRuleForImportCheck({ factory, rulesyncRule }) {
80569
+ return factory.class.fromRulesyncRule({
80570
+ outputRoot: this.outputRoot,
80571
+ rulesyncRule,
80572
+ validate: false,
80573
+ global: this.global
80574
+ });
80228
80575
  }
80229
80576
  /**
80230
80577
  * Implementation of abstract method from FeatureProcessor
@@ -80421,7 +80768,8 @@ As this project's AI coding tool, you must follow the additional conventions bel
80421
80768
  const nonRootToolRules = await (async () => {
80422
80769
  if (!settablePaths.nonRoot) return [];
80423
80770
  const nonRootOutputRoot = (0, node_path.join)(this.outputRoot, settablePaths.nonRoot.relativeDirPath);
80424
- const nonRootFilePaths = await findFilesByGlobs(`**/*.${factory.meta.extension}`, { cwd: nonRootOutputRoot });
80771
+ const isSharedGlobalNonRootDir = this.global && factory.meta.sharedGlobalNonRootDir === true;
80772
+ const nonRootFilePaths = await findFilesByGlobs(`${isSharedGlobalNonRootDir ? "" : "**/"}*.${factory.meta.extension}`, { cwd: nonRootOutputRoot });
80425
80773
  if (forDeletion) return buildDeletionRulesFromPaths(nonRootFilePaths, {
80426
80774
  outputRootOverride: nonRootOutputRoot,
80427
80775
  relativeDirPathOverride: settablePaths.nonRoot.relativeDirPath
@@ -80434,19 +80782,25 @@ As this project's AI coding tool, you must follow the additional conventions bel
80434
80782
  if (!ok) this.logger.warn(`Skipping reserved Rovodev path under modular-rules (import): ${(0, node_path.join)(modularRootRelative, relativeFilePath)}`);
80435
80783
  return ok;
80436
80784
  }) : nonRootFilePaths).filter((filePath) => rootFileNameInSameDir === void 0 || (0, node_path.relative)(nonRootOutputRoot, filePath) !== rootFileNameInSameDir);
80437
- return await Promise.all(nonRootPathsForImport.map((filePath) => {
80785
+ return (await Promise.all(nonRootPathsForImport.map(async (filePath) => {
80438
80786
  const relativeFilePath = (0, node_path.relative)(nonRootOutputRoot, filePath);
80439
80787
  checkPathTraversal({
80440
80788
  relativePath: relativeFilePath,
80441
80789
  intendedRootDir: nonRootOutputRoot
80442
80790
  });
80443
- return factory.class.fromFile({
80444
- outputRoot: this.outputRoot,
80445
- relativeDirPath: modularRootRelative,
80446
- relativeFilePath,
80447
- global: this.global
80448
- });
80449
- }));
80791
+ try {
80792
+ return await factory.class.fromFile({
80793
+ outputRoot: this.outputRoot,
80794
+ relativeDirPath: modularRootRelative,
80795
+ relativeFilePath,
80796
+ global: this.global
80797
+ });
80798
+ } catch (error) {
80799
+ if (!isSharedGlobalNonRootDir) throw error;
80800
+ this.logger.warn(`Skipping ${stripControlCharacters((0, node_path.join)(modularRootRelative, relativeFilePath))} for ${this.toolTarget}: ${formatError(error)}`);
80801
+ return;
80802
+ }
80803
+ }))).filter((rule) => rule !== void 0);
80450
80804
  })();
80451
80805
  this.logger.debug(`Found ${nonRootToolRules.length} non-root tool rule files`);
80452
80806
  return [
@@ -80518,6 +80872,14 @@ As this project's AI coding tool, you must follow the additional conventions bel
80518
80872
  function isPackagingToolTarget(toolTarget) {
80519
80873
  return PACKAGING_TOOL_TARGETS.includes(toolTarget);
80520
80874
  }
80875
+ /**
80876
+ * Packaging targets write into a plugin directory and support project scope
80877
+ * only. In global mode their output root would be the home directory, so
80878
+ * callers reject or skip them up front instead of checking all of $HOME.
80879
+ */
80880
+ function formatPackagingTargetGlobalMessage(toolTarget) {
80881
+ return `Target '${toolTarget}' is a plugin packaging target and supports only project scope. Re-run without '--global'.`;
80882
+ }
80521
80883
  async function assertPluginRootSafe(params) {
80522
80884
  if (!isPackagingToolTarget(params.toolTarget)) return;
80523
80885
  await assertDirectoryIfExists(params.outputRoot);
@@ -81437,14 +81799,17 @@ function warnUnsupportedTargets(params) {
81437
81799
  } catch {
81438
81800
  oppositeScopeTargets = [];
81439
81801
  }
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.`);
81802
+ for (const target of config.getTargets()) {
81803
+ if (config.getGlobal() && isPackagingToolTarget(target)) continue;
81804
+ if (!supportedTargets.includes(target) && config.getFeatures(target).includes(featureName)) {
81805
+ const simulateOption = SIMULATE_OPTION_MAP[featureName];
81806
+ if (simulateOption && simulatedTargets.includes(target)) logger.warn(`Target '${target}' only supports simulated '${featureName}'. Use '${simulateOption}' to enable it. Skipping.`);
81807
+ else if (oppositeScopeTargets.includes(target)) {
81808
+ const supportedScope = config.getGlobal() ? "project" : "global";
81809
+ const retry = config.getGlobal() ? "without '--global'" : "with '--global'";
81810
+ logger.warn(`Target '${target}' supports the feature '${featureName}' only in ${supportedScope} scope. Re-run ${retry}. Skipping.`);
81811
+ } else logger.warn(`Target '${target}' does not support the feature '${featureName}'. Skipping.`);
81812
+ }
81448
81813
  }
81449
81814
  }
81450
81815
  /**
@@ -81697,10 +82062,16 @@ async function generate(params) {
81697
82062
  const { config, logger } = params;
81698
82063
  resetRootShadowingWarnings({ logger });
81699
82064
  resetRunWarningState();
81700
- for (const toolTarget of config.getTargets()) for (const outputRoot of config.getOutputRoots(toolTarget)) await assertPluginRootSafe({
81701
- toolTarget,
81702
- outputRoot
81703
- });
82065
+ for (const toolTarget of config.getTargets()) {
82066
+ if (config.getGlobal() && isPackagingToolTarget(toolTarget)) {
82067
+ logger.warn(`${formatPackagingTargetGlobalMessage(toolTarget)} Skipping.`);
82068
+ continue;
82069
+ }
82070
+ for (const outputRoot of config.getOutputRoots(toolTarget)) await assertPluginRootSafe({
82071
+ toolTarget,
82072
+ outputRoot
82073
+ });
82074
+ }
81704
82075
  await warnSkillSubagentNameCollisions({
81705
82076
  config,
81706
82077
  logger
@@ -81823,6 +82194,115 @@ function computeRootFileOwnership(params) {
81823
82194
  }
81824
82195
  return ownerByPath;
81825
82196
  }
82197
+ function createSkippedRootFileCheck({ rootFileOwner }) {
82198
+ const skippedByRoot = /* @__PURE__ */ new Map();
82199
+ const emittedByRoot = /* @__PURE__ */ new Map();
82200
+ const emittedKey = (toolTarget, relativePath) => `${toolTarget}\0${relativePath}`;
82201
+ const loadOwnerToolFiles = async (processor) => {
82202
+ const rulesyncFiles = await processor.loadRulesyncFiles();
82203
+ return {
82204
+ toolFiles: rulesyncFiles.length > 0 ? await processor.convertRulesyncFilesToToolFiles(rulesyncFiles) : [],
82205
+ sourceLoadFailed: processor.hasRulesyncSourceLoadFailure()
82206
+ };
82207
+ };
82208
+ return {
82209
+ observe: ({ toolTarget, outputRoot, processor, toolFiles, skipFilePaths }) => {
82210
+ if (rootFileOwner.size === 0) return;
82211
+ for (const toolFile of toolFiles) {
82212
+ const relativePath = toolFile.getRelativePathFromCwd();
82213
+ if (!rootFileOwner.has(relativePath)) continue;
82214
+ const emitted = emittedByRoot.get(outputRoot) ?? /* @__PURE__ */ new Set();
82215
+ emittedByRoot.set(outputRoot, emitted);
82216
+ emitted.add(emittedKey(toolTarget, relativePath));
82217
+ if (!skipFilePaths.has(relativePath)) continue;
82218
+ const skipped = skippedByRoot.get(outputRoot) ?? /* @__PURE__ */ new Map();
82219
+ skippedByRoot.set(outputRoot, skipped);
82220
+ skipped.set(relativePath, {
82221
+ processor,
82222
+ toolFile
82223
+ });
82224
+ }
82225
+ },
82226
+ verify: async ({ isOwnerProcessed, createOwnerProcessor }) => {
82227
+ let count = 0;
82228
+ const paths = [];
82229
+ let sourceLoadFailed = false;
82230
+ for (const [outputRoot, skipped] of skippedByRoot) {
82231
+ const ownerOutputs = /* @__PURE__ */ new Map();
82232
+ for (const [relativePath, fallback] of skipped) {
82233
+ const owner = rootFileOwner.get(relativePath);
82234
+ if (owner === void 0) continue;
82235
+ if (emittedByRoot.get(outputRoot)?.has(emittedKey(owner, relativePath))) continue;
82236
+ let expected = fallback;
82237
+ if (!isOwnerProcessed(owner)) {
82238
+ if (!ownerOutputs.has(owner)) {
82239
+ const processor = await createOwnerProcessor({
82240
+ toolTarget: owner,
82241
+ outputRoot
82242
+ });
82243
+ if (processor === void 0) ownerOutputs.set(owner, void 0);
82244
+ else {
82245
+ const loaded = await loadOwnerToolFiles(processor);
82246
+ if (loaded.sourceLoadFailed) sourceLoadFailed = true;
82247
+ ownerOutputs.set(owner, {
82248
+ processor,
82249
+ toolFiles: loaded.toolFiles
82250
+ });
82251
+ }
82252
+ }
82253
+ const ownerOutput = ownerOutputs.get(owner);
82254
+ const ownerFile = ownerOutput?.toolFiles.find((file) => file.getRelativePathFromCwd() === relativePath && file.getFilePath() === fallback.toolFile.getFilePath());
82255
+ if (ownerOutput !== void 0 && ownerFile !== void 0) expected = {
82256
+ processor: ownerOutput.processor,
82257
+ toolFile: ownerFile
82258
+ };
82259
+ }
82260
+ const result = await expected.processor.writeAiFiles([expected.toolFile]);
82261
+ count += result.count;
82262
+ paths.push(...result.paths);
82263
+ }
82264
+ }
82265
+ return {
82266
+ count,
82267
+ paths,
82268
+ hasDiff: count > 0,
82269
+ sourceLoadFailed
82270
+ };
82271
+ }
82272
+ };
82273
+ }
82274
+ /**
82275
+ * The skills a full `rulesync generate` hands to the rules step: what the
82276
+ * skills step collects for every config-file target that generates skills, in
82277
+ * the same order. Simulated skill sections in a root file list these, so a
82278
+ * narrowed check needs them to rebuild another target's root file.
82279
+ */
82280
+ async function loadConfigFileSkills({ config, logger }) {
82281
+ const supportedSkillsTargets = SkillsProcessor.getToolTargets({
82282
+ global: config.getGlobal(),
82283
+ includeSimulated: config.getSimulateSkills()
82284
+ });
82285
+ const skills = [];
82286
+ for (const toolTarget of (0, es_toolkit.intersection)(config.getConfigFileTargets(), supportedSkillsTargets)) {
82287
+ if (!config.getConfigFileFeatures(toolTarget).includes("skills")) continue;
82288
+ for (const outputRoot of config.getOutputRoots(toolTarget)) {
82289
+ const processor = new SkillsProcessor({
82290
+ outputRoot: resolveToolOutputRoot({
82291
+ outputRoot,
82292
+ toolTarget,
82293
+ global: config.getGlobal()
82294
+ }),
82295
+ inputRoots: config.getInputRoots(),
82296
+ toolTarget,
82297
+ global: config.getGlobal(),
82298
+ dryRun: true,
82299
+ logger
82300
+ });
82301
+ for (const rulesyncDir of await processor.loadRulesyncDirs()) if (rulesyncDir instanceof RulesyncSkill) skills.push(rulesyncDir);
82302
+ }
82303
+ }
82304
+ return skills;
82305
+ }
81826
82306
  async function generateRulesCore(params) {
81827
82307
  const { config, logger, sweepPlan, skills } = params;
81828
82308
  let totalCount = 0;
@@ -81838,32 +82318,47 @@ async function generateRulesCore(params) {
81838
82318
  logger
81839
82319
  });
81840
82320
  const foldRootOverwriteWatch = createFoldRootOverwriteWatch({ logger });
82321
+ let fullRunSkills;
82322
+ const loadFullRunSkills = () => {
82323
+ fullRunSkills ??= loadConfigFileSkills({
82324
+ config,
82325
+ logger
82326
+ });
82327
+ return fullRunSkills;
82328
+ };
82329
+ const createProcessor = ({ toolTarget, outputRoot, featureOptions, skills: processorSkills }) => new RulesProcessor({
82330
+ outputRoot: resolveToolOutputRoot({
82331
+ outputRoot,
82332
+ toolTarget,
82333
+ global: config.getGlobal()
82334
+ }),
82335
+ inputRoots: config.getInputRoots(),
82336
+ toolTarget,
82337
+ global: config.getGlobal(),
82338
+ simulateCommands: config.getSimulateCommands(),
82339
+ simulateSubagents: config.getSimulateSubagents(),
82340
+ simulateSkills: config.getSimulateSkills(),
82341
+ language: config.getLanguage(),
82342
+ deriveSubprojectPathFromGlobs: config.getDeriveSubprojectPathFromGlobs(),
82343
+ skills: processorSkills,
82344
+ featureOptions,
82345
+ delete: config.getDelete(),
82346
+ dryRun: config.isPreviewMode(),
82347
+ logger
82348
+ });
81841
82349
  const isCheck = config.getCheck();
81842
82350
  const rootFileOwner = isCheck ? computeRootFileOwnership({
81843
- targets: config.getConfigFileTargets(),
82351
+ targets: config.getConfigFileTargets().filter((target) => (toolTargets.includes(target) ? config.getFeatures(target) : config.getConfigFileFeatures(target)).includes("rules")),
81844
82352
  global: config.getGlobal()
81845
82353
  }) : /* @__PURE__ */ new Map();
82354
+ const skippedRootFileCheck = createSkippedRootFileCheck({ rootFileOwner });
81846
82355
  for (const toolTarget of toolTargets) for (const outputRoot of config.getOutputRoots(toolTarget)) {
81847
82356
  if (!config.getFeatures(toolTarget).includes("rules")) continue;
81848
- const processor = new RulesProcessor({
81849
- outputRoot: resolveToolOutputRoot({
81850
- outputRoot,
81851
- toolTarget,
81852
- global: config.getGlobal()
81853
- }),
81854
- inputRoots: config.getInputRoots(),
82357
+ const processor = createProcessor({
81855
82358
  toolTarget,
81856
- global: config.getGlobal(),
81857
- simulateCommands: config.getSimulateCommands(),
81858
- simulateSubagents: config.getSimulateSubagents(),
81859
- simulateSkills: config.getSimulateSkills(),
81860
- language: config.getLanguage(),
81861
- deriveSubprojectPathFromGlobs: config.getDeriveSubprojectPathFromGlobs(),
81862
- skills,
82359
+ outputRoot,
81863
82360
  featureOptions: config.getFeatureOptions(toolTarget, "rules"),
81864
- delete: config.getDelete(),
81865
- dryRun: config.isPreviewMode(),
81866
- logger
82361
+ skills
81867
82362
  });
81868
82363
  const rulesyncFiles = await processor.loadRulesyncFiles();
81869
82364
  const skipFilePaths = /* @__PURE__ */ new Set();
@@ -81876,16 +82371,39 @@ async function generateRulesCore(params) {
81876
82371
  rulesyncFiles,
81877
82372
  sweepPlan,
81878
82373
  skipFilePaths: skipFilePaths.size > 0 ? skipFilePaths : void 0,
81879
- onToolFiles: (toolFiles) => foldRootOverwriteWatch.observe({
81880
- toolTarget,
81881
- toolFiles
81882
- })
82374
+ onToolFiles: (toolFiles) => {
82375
+ foldRootOverwriteWatch.observe({
82376
+ toolTarget,
82377
+ toolFiles
82378
+ });
82379
+ skippedRootFileCheck.observe({
82380
+ toolTarget,
82381
+ outputRoot,
82382
+ processor,
82383
+ toolFiles,
82384
+ skipFilePaths
82385
+ });
82386
+ }
81883
82387
  });
81884
82388
  totalCount += result.count;
81885
82389
  allPaths.push(...result.paths);
81886
82390
  if (result.hasDiff) hasDiff = true;
81887
82391
  if (result.sourceLoadFailed) sourceLoadFailed = true;
81888
82392
  }
82393
+ if (isCheck) {
82394
+ const skippedResult = await skippedRootFileCheck.verify({
82395
+ isOwnerProcessed: (owner) => toolTargets.includes(owner) && config.getFeatures(owner).includes("rules"),
82396
+ createOwnerProcessor: async (ownerParams) => supportedTargets.includes(ownerParams.toolTarget) && config.getOutputRoots(ownerParams.toolTarget).includes(ownerParams.outputRoot) ? createProcessor({
82397
+ ...ownerParams,
82398
+ featureOptions: config.getConfigFileFeatureOptions(ownerParams.toolTarget, "rules"),
82399
+ skills: config.getSimulateSkills() && SkillsProcessor.getToolTargetsSimulated().includes(ownerParams.toolTarget) ? await loadFullRunSkills() : void 0
82400
+ }) : void 0
82401
+ });
82402
+ totalCount += skippedResult.count;
82403
+ allPaths.push(...skippedResult.paths);
82404
+ if (skippedResult.hasDiff) hasDiff = true;
82405
+ if (skippedResult.sourceLoadFailed) sourceLoadFailed = true;
82406
+ }
81889
82407
  foldRootOverwriteWatch.report();
81890
82408
  return {
81891
82409
  count: totalCount,
@@ -82317,6 +82835,7 @@ function getToolOutputRoot({ config, tool }) {
82317
82835
  async function importFromTool(params) {
82318
82836
  const { config, tool, logger } = params;
82319
82837
  resetRunWarningState();
82838
+ if (config.getGlobal() && isPackagingToolTarget(tool)) throw new Error(formatPackagingTargetGlobalMessage(tool));
82320
82839
  await assertPluginRootSafe({
82321
82840
  toolTarget: tool,
82322
82841
  outputRoot: getToolOutputRoot({
@@ -82377,7 +82896,7 @@ async function importRulesCore(params) {
82377
82896
  if (!config.getFeatures(tool).includes("rules")) return 0;
82378
82897
  const global = config.getGlobal();
82379
82898
  if (!RulesProcessor.getToolTargets({ global }).includes(tool)) return 0;
82380
- const rulesProcessor = new RulesProcessor({
82899
+ const baseParams = {
82381
82900
  outputRoot: getToolOutputRoot({
82382
82901
  config,
82383
82902
  tool
@@ -82385,13 +82904,23 @@ async function importRulesCore(params) {
82385
82904
  toolTarget: tool,
82386
82905
  global,
82387
82906
  logger
82388
- });
82907
+ };
82908
+ const rulesProcessor = new RulesProcessor(baseParams);
82389
82909
  const toolFiles = await rulesProcessor.loadToolFiles();
82390
82910
  if (toolFiles.length === 0) {
82391
82911
  logger.warn(`No rule files found for ${tool}. Skipping import.`);
82392
82912
  return 0;
82393
82913
  }
82394
- await rulesProcessor.warnForFoldImportDuplicationRisk();
82914
+ try {
82915
+ await new RulesProcessor({
82916
+ ...baseParams,
82917
+ inputRoots: config.getInputRoots(),
82918
+ deriveSubprojectPathFromGlobs: config.getDeriveSubprojectPathFromGlobs(),
82919
+ featureOptions: config.getFeatureOptions(tool, "rules")
82920
+ }).warnForFoldImportDuplicationRisk();
82921
+ } catch (error) {
82922
+ logger.warn(`Skipped the ${tool} import duplication check: ${formatError(error)}`);
82923
+ }
82395
82924
  const rulesyncFiles = await rulesProcessor.convertToolFilesToRulesyncFiles(toolFiles);
82396
82925
  const { count: writtenCount } = await rulesProcessor.writeAiFiles(rulesyncFiles);
82397
82926
  if (config.getVerbose() && writtenCount > 0) logger.success(`Created ${writtenCount} rule files`);