rulesync 20.0.0 → 22.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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);
@@ -45011,14 +45122,14 @@ const COMMANDCODE_TOOL_TO_CATEGORY = {
45011
45122
  web_fetch: "webfetch",
45012
45123
  web_search: "websearch"
45013
45124
  };
45014
- const ACTION_RANK$1 = {
45125
+ const ACTION_RANK$2 = {
45015
45126
  allow: 0,
45016
45127
  ask: 1,
45017
45128
  deny: 2
45018
45129
  };
45019
45130
  /** Whether `action` outranks `existing` (deny > ask > allow). */
45020
45131
  function isStricterAction({ action, existing }) {
45021
- return existing === void 0 || ACTION_RANK$1[action] > ACTION_RANK$1[existing];
45132
+ return existing === void 0 || ACTION_RANK$2[action] > ACTION_RANK$2[existing];
45022
45133
  }
45023
45134
  /**
45024
45135
  * Why a padded `Shell( * )` cannot stand in for the bare `Shell` in `allow`:
@@ -48366,20 +48477,22 @@ function convertFactorydroidToRulesyncPermissions({ allow, deny }) {
48366
48477
  const GOOSE_GLOBAL_ONLY_MESSAGE = "Goose permissions are global-only; use --global to sync ~/.config/goose/permission.yaml";
48367
48478
  const GOOSE_USER_KEY = "user";
48368
48479
  const CATCH_ALL_PATTERN$4 = "*";
48369
- const RULESYNC_TO_GOOSE_TOOL_NAME = {
48370
- bash: "developer__shell",
48371
- edit: "developer__text_editor",
48372
- write: "developer__text_editor"
48373
- };
48374
- const GOOSE_TO_RULESYNC_TOOL_NAME = {
48375
- developer__shell: "bash",
48376
- developer__text_editor: "edit"
48480
+ const RULESYNC_TO_GOOSE_TOOL_NAME = { bash: "shell" };
48481
+ const GOOSE_TO_RULESYNC_TOOL_NAMES = {
48482
+ shell: ["bash"],
48483
+ developer__shell: ["bash"],
48484
+ developer__text_editor: ["edit", "write"]
48377
48485
  };
48378
48486
  const ACTION_TO_GOOSE_LIST = {
48379
48487
  allow: "always_allow",
48380
48488
  ask: "ask_before",
48381
48489
  deny: "never_allow"
48382
48490
  };
48491
+ const ACTION_RANK$1 = {
48492
+ allow: 0,
48493
+ ask: 1,
48494
+ deny: 2
48495
+ };
48383
48496
  const GOOSE_LIST_TO_ACTION = {
48384
48497
  always_allow: "allow",
48385
48498
  ask_before: "ask",
@@ -48404,10 +48517,11 @@ const GOOSE_PERMISSION_LIST_KEYS = [
48404
48517
  *
48405
48518
  * Mapping (rulesync canonical -> Goose):
48406
48519
  * - Action: `allow` -> `always_allow`, `ask` -> `ask_before`, `deny` -> `never_allow`.
48407
- * - Tool name: `bash` -> `developer__shell`, `edit` -> `developer__text_editor`;
48408
- * any other category passes through verbatim as the Goose tool name (mirrors
48409
- * the Gemini CLI adapter). `write` collapses onto `developer__text_editor`
48410
- * too, so a conflicting `edit`/`write` catch-all is reported and `edit` wins.
48520
+ * - Tool name: `bash` -> `shell`; any other category (including `edit` and
48521
+ * `write`) passes through verbatim as the Goose tool name (mirrors the
48522
+ * Gemini CLI adapter). The legacy `developer__shell` /
48523
+ * `developer__text_editor` names are still accepted on import, the latter
48524
+ * as both `edit` and `write`.
48411
48525
  * - Granularity: Goose lists hold whole tool names, so only a category's
48412
48526
  * catch-all `*` pattern is representable. Non-catch-all patterns cannot be
48413
48527
  * expressed per-tool and are reported via `logger.warn` and skipped.
@@ -48523,8 +48637,7 @@ function convertRulesyncToGoosePermissionConfig({ config, logger }) {
48523
48637
  never_allow: []
48524
48638
  };
48525
48639
  const assigned = /* @__PURE__ */ new Map();
48526
- const orderedEntries = Object.entries(honorAllToolsOnBash(config.permission)).toSorted(([a], [b]) => (a === "edit" ? 1 : 0) - (b === "edit" ? 1 : 0));
48527
- for (const [category, rules] of orderedEntries) {
48640
+ for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) {
48528
48641
  const toolName = RULESYNC_TO_GOOSE_TOOL_NAME[category] ?? category;
48529
48642
  for (const [pattern, action] of Object.entries(rules)) {
48530
48643
  if (pattern !== CATCH_ALL_PATTERN$4) {
@@ -48532,12 +48645,15 @@ function convertRulesyncToGoosePermissionConfig({ config, logger }) {
48532
48645
  continue;
48533
48646
  }
48534
48647
  const previous = assigned.get(toolName);
48535
- if (previous !== void 0 && previous !== action) {
48536
- logger?.warn(`Goose maps "${category}" onto the "${toolName}" tool, which already has a conflicting permission ("${previous}"). The "${action}" value takes precedence.`);
48648
+ if (previous === action) continue;
48649
+ if (previous !== void 0) {
48650
+ const stricter = ACTION_RANK$1[action] > ACTION_RANK$1[previous] ? action : previous;
48651
+ logger?.warn(`Goose maps "${category}" onto the "${toolName}" tool, which already has a conflicting permission ("${previous}"). Keeping the stricter "${stricter}" (deny > ask > allow).`);
48652
+ if (stricter === previous) continue;
48537
48653
  const previousList = lists[ACTION_TO_GOOSE_LIST[previous]];
48538
48654
  const index = previousList.indexOf(toolName);
48539
48655
  if (index !== -1) previousList.splice(index, 1);
48540
- } else if (previous === action) continue;
48656
+ }
48541
48657
  lists[ACTION_TO_GOOSE_LIST[action]].push(toolName);
48542
48658
  assigned.set(toolName, action);
48543
48659
  }
@@ -48553,8 +48669,7 @@ function convertGoosePermissionConfigToRulesync(userPermission) {
48553
48669
  for (const key of GOOSE_PERMISSION_LIST_KEYS) {
48554
48670
  const toolNames = isStringArray$2(userPermission[key]) ? userPermission[key] : [];
48555
48671
  const action = GOOSE_LIST_TO_ACTION[key];
48556
- for (const toolName of toolNames) {
48557
- const category = GOOSE_TO_RULESYNC_TOOL_NAME[toolName] ?? toolName;
48672
+ for (const toolName of toolNames) for (const category of GOOSE_TO_RULESYNC_TOOL_NAMES[toolName] ?? [toolName]) {
48558
48673
  permission[category] ??= {};
48559
48674
  permission[category][CATCH_ALL_PATTERN$4] = action;
48560
48675
  }
@@ -59228,7 +59343,7 @@ const ClaudecodeSkillFrontmatterSchema = zod_mini.z.looseObject({
59228
59343
  "disallowed-tools": zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())])),
59229
59344
  model: zod_mini.z.optional(zod_mini.z.string()),
59230
59345
  effort: zod_mini.z.optional(zod_mini.z.string()),
59231
- "argument-hint": zod_mini.z.optional(zod_mini.z.string()),
59346
+ "argument-hint": zod_mini.z.optional(ClaudecodeArgumentHintSchema),
59232
59347
  arguments: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())])),
59233
59348
  context: zod_mini.z.optional(zod_mini.z.string()),
59234
59349
  agent: zod_mini.z.optional(zod_mini.z.string()),
@@ -65384,7 +65499,8 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
65384
65499
  meta: {
65385
65500
  supportsProject: true,
65386
65501
  supportsSimulated: false,
65387
- supportsGlobal: true
65502
+ supportsGlobal: true,
65503
+ lenientImport: true
65388
65504
  }
65389
65505
  }],
65390
65506
  ["claudecode-plugin", {
@@ -72333,7 +72449,7 @@ var ToolRule = class extends ToolFile {
72333
72449
  static buildToolRuleParamsAgentsmd({ outputRoot = process.cwd(), rulesyncRule, validate = true, rootPath = {
72334
72450
  relativeDirPath: ".",
72335
72451
  relativeFilePath: "AGENTS.md"
72336
- }, nonRootPath = { relativeDirPath: AGENTSMD_MEMORIES_DIR_PATH } }) {
72452
+ }, nonRootPath = { relativeDirPath: AGENTSMD_MEMORIES_DIR_PATH }, global = false }) {
72337
72453
  const params = this.buildToolRuleParamsDefault({
72338
72454
  outputRoot,
72339
72455
  rulesyncRule,
@@ -72342,7 +72458,7 @@ var ToolRule = class extends ToolFile {
72342
72458
  nonRootPath
72343
72459
  });
72344
72460
  const rulesyncFrontmatter = rulesyncRule.getFrontmatter();
72345
- if (!rulesyncFrontmatter.root && rulesyncFrontmatter.agentsmd?.subprojectPath) {
72461
+ if (!global && !rulesyncFrontmatter.root && rulesyncFrontmatter.agentsmd?.subprojectPath) {
72346
72462
  params.relativeDirPath = (0, node_path.join)(rulesyncFrontmatter.agentsmd.subprojectPath);
72347
72463
  params.relativeFilePath = "AGENTS.md";
72348
72464
  }
@@ -72999,7 +73115,8 @@ var AmpRule = class AmpRule extends ToolRule {
72999
73115
  rulesyncRule,
73000
73116
  validate,
73001
73117
  rootPath: paths.root,
73002
- nonRootPath: paths.nonRoot
73118
+ nonRootPath: paths.nonRoot,
73119
+ global
73003
73120
  });
73004
73121
  if (!params.root && params.globs !== void 0 && params.globs.length > 0) params.fileContent = stringifyFrontmatter(params.fileContent, { globs: params.globs });
73005
73122
  return new AmpRule(params);
@@ -74294,7 +74411,8 @@ var ClineRule = class ClineRule extends ToolRule {
74294
74411
  outputRoot,
74295
74412
  rulesyncRule,
74296
74413
  validate,
74297
- rootPath: paths.root
74414
+ rootPath: paths.root,
74415
+ global
74298
74416
  }));
74299
74417
  }
74300
74418
  if (isRoot) return new ClineRule({
@@ -74714,26 +74832,62 @@ var CodebuddyRule = class CodebuddyRule extends ToolRule {
74714
74832
  }
74715
74833
  };
74716
74834
  //#endregion
74717
- //#region src/features/rules/codexcli-rule.ts
74718
- var CodexcliRule = class CodexcliRule extends ToolRule {
74719
- constructor({ fileContent, root, ...rest }) {
74720
- super({
74721
- ...rest,
74722
- fileContent,
74723
- root: root ?? false
74724
- });
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.");
74725
74855
  }
74726
74856
  static getSettablePaths({ global = false } = {}) {
74857
+ if (global) return { root: {
74858
+ relativeDirPath: this.getFamily().globalDir,
74859
+ relativeFilePath: AGENTSMD_RULE_FILE_NAME
74860
+ } };
74727
74861
  return { root: {
74728
- relativeDirPath: global ? CODEXCLI_DIR : ".",
74729
- relativeFilePath: CODEXCLI_RULE_FILE_NAME
74862
+ relativeDirPath: ".",
74863
+ relativeFilePath: AGENTSMD_RULE_FILE_NAME
74730
74864
  } };
74731
74865
  }
74732
- 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 }) {
74733
74877
  const { root } = this.getSettablePaths({ global });
74734
- const relativePath = (0, node_path.join)(root.relativeDirPath, root.relativeFilePath);
74735
- const fileContent = await readFileContent((0, node_path.join)(outputRoot, relativePath));
74736
- 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({
74737
74891
  outputRoot,
74738
74892
  relativeDirPath: root.relativeDirPath,
74739
74893
  relativeFilePath: root.relativeFilePath,
@@ -74744,8 +74898,18 @@ var CodexcliRule = class CodexcliRule extends ToolRule {
74744
74898
  }
74745
74899
  static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
74746
74900
  const { root } = this.getSettablePaths({ global });
74747
- const isRoot = rulesyncRule.getFrontmatter().root ?? false;
74748
- 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({
74749
74913
  outputRoot,
74750
74914
  relativeDirPath: root.relativeDirPath,
74751
74915
  relativeFilePath: root.relativeFilePath,
@@ -74755,7 +74919,9 @@ var CodexcliRule = class CodexcliRule extends ToolRule {
74755
74919
  });
74756
74920
  }
74757
74921
  toRulesyncRule() {
74758
- return this.toRulesyncRuleDefault();
74922
+ const subprojectPath = this.getSubprojectPath();
74923
+ if (subprojectPath === void 0) return this.toRulesyncRuleDefault();
74924
+ return this.toRulesyncRuleNestedAgentsmd({ subprojectPath });
74759
74925
  }
74760
74926
  validate() {
74761
74927
  return {
@@ -74764,23 +74930,54 @@ var CodexcliRule = class CodexcliRule extends ToolRule {
74764
74930
  };
74765
74931
  }
74766
74932
  static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
74767
- return new CodexcliRule({
74933
+ const isRoot = relativeFilePath === "AGENTS.md" && (relativeDirPath === "." || relativeDirPath === this.getFamily().globalDir);
74934
+ return new this({
74768
74935
  outputRoot,
74769
74936
  relativeDirPath,
74770
74937
  relativeFilePath,
74771
74938
  fileContent: "",
74772
74939
  validate: false,
74773
- root: relativeFilePath === "AGENTS.md" && (relativeDirPath === "." || relativeDirPath === ".codex")
74940
+ root: isRoot
74774
74941
  });
74775
74942
  }
74776
74943
  static isTargetedByRulesyncRule(rulesyncRule) {
74777
74944
  return this.isTargetedByRulesyncRuleDefault({
74778
74945
  rulesyncRule,
74779
- toolTarget: "codexcli"
74946
+ toolTarget: this.getFamily().toolTarget
74780
74947
  });
74781
74948
  }
74782
74949
  };
74783
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
74784
74981
  //#region src/features/rules/commandcode-rule.ts
74785
74982
  var CommandcodeRule = class CommandcodeRule extends ToolRule {
74786
74983
  constructor({ fileContent, root, ...rest }) {
@@ -76085,122 +76282,6 @@ var DevinRule = class DevinRule extends ToolRule {
76085
76282
  }
76086
76283
  };
76087
76284
  //#endregion
76088
- //#region src/features/rules/nested-agentsmd-rule.ts
76089
- /**
76090
- * Shared adapter for the targets that read instructions exactly the way the
76091
- * AGENTS.md standard describes them and nothing more: a root `AGENTS.md` at the
76092
- * project root, nested per-directory `AGENTS.md` files loaded only while working
76093
- * under that directory, and a single personal root file under a tool-specific
76094
- * home directory. Such a target has no modular non-root instruction directory,
76095
- * so every other non-root rule folds into the root file.
76096
- *
76097
- * Concrete targets (Pool, Vibe, DeepSeek Harness) only supply the global
76098
- * directory and their tool target through {@link getFamily}; the vendor docs
76099
- * describing each one's discovery walk live on the subclass.
76100
- *
76101
- * The nested-file scan mirrors the AGENTS.md standard's nested discovery —
76102
- * same file name, same exclusions, import-only, project scope — because it
76103
- * discovers literally the same files.
76104
- */
76105
- var NestedAgentsmdRule = class extends ToolRule {
76106
- static getFamily() {
76107
- throw new Error("Please implement this method in the subclass.");
76108
- }
76109
- static getSettablePaths({ global = false } = {}) {
76110
- if (global) return { root: {
76111
- relativeDirPath: this.getFamily().globalDir,
76112
- relativeFilePath: AGENTSMD_RULE_FILE_NAME
76113
- } };
76114
- return { root: {
76115
- relativeDirPath: ".",
76116
- relativeFilePath: AGENTSMD_RULE_FILE_NAME
76117
- } };
76118
- }
76119
- static getNestedFilePatterns() {
76120
- return this.buildNestedFilePatterns({ fileName: AGENTSMD_RULE_FILE_NAME });
76121
- }
76122
- /**
76123
- * The subproject directory this rule scopes, or `undefined` for the root file
76124
- * (project or global).
76125
- */
76126
- getSubprojectPath() {
76127
- return this.getNestedSubprojectPath({ fileName: AGENTSMD_RULE_FILE_NAME });
76128
- }
76129
- static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, relativeDirPath: overrideDirPath, validate = true, global = false }) {
76130
- const { root } = this.getSettablePaths({ global });
76131
- if (overrideDirPath !== void 0 && overrideDirPath !== root.relativeDirPath && overrideDirPath !== ".") {
76132
- const fileContent = await readFileContent((0, node_path.join)(outputRoot, overrideDirPath, AGENTSMD_RULE_FILE_NAME));
76133
- return new this({
76134
- outputRoot,
76135
- relativeDirPath: overrideDirPath,
76136
- relativeFilePath: AGENTSMD_RULE_FILE_NAME,
76137
- fileContent,
76138
- validate,
76139
- root: false
76140
- });
76141
- }
76142
- const fileContent = await readFileContent((0, node_path.join)(outputRoot, root.relativeDirPath, root.relativeFilePath));
76143
- return new this({
76144
- outputRoot,
76145
- relativeDirPath: root.relativeDirPath,
76146
- relativeFilePath: root.relativeFilePath,
76147
- fileContent,
76148
- validate,
76149
- root: true
76150
- });
76151
- }
76152
- static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
76153
- const { root } = this.getSettablePaths({ global });
76154
- const frontmatter = rulesyncRule.getFrontmatter();
76155
- const isRoot = frontmatter.root ?? false;
76156
- const subprojectPath = frontmatter.agentsmd?.subprojectPath;
76157
- if (!global && !isRoot && subprojectPath) return new this({
76158
- outputRoot,
76159
- relativeDirPath: (0, node_path.join)(subprojectPath),
76160
- relativeFilePath: AGENTSMD_RULE_FILE_NAME,
76161
- fileContent: rulesyncRule.getBody(),
76162
- validate,
76163
- root: false
76164
- });
76165
- return new this({
76166
- outputRoot,
76167
- relativeDirPath: root.relativeDirPath,
76168
- relativeFilePath: root.relativeFilePath,
76169
- fileContent: rulesyncRule.getBody(),
76170
- validate,
76171
- root: isRoot
76172
- });
76173
- }
76174
- toRulesyncRule() {
76175
- const subprojectPath = this.getSubprojectPath();
76176
- if (subprojectPath === void 0) return this.toRulesyncRuleDefault();
76177
- return this.toRulesyncRuleNestedAgentsmd({ subprojectPath });
76178
- }
76179
- validate() {
76180
- return {
76181
- success: true,
76182
- error: null
76183
- };
76184
- }
76185
- static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
76186
- const isRoot = relativeFilePath === "AGENTS.md" && (relativeDirPath === "." || relativeDirPath === this.getFamily().globalDir);
76187
- return new this({
76188
- outputRoot,
76189
- relativeDirPath,
76190
- relativeFilePath,
76191
- fileContent: "",
76192
- validate: false,
76193
- root: isRoot
76194
- });
76195
- }
76196
- static isTargetedByRulesyncRule(rulesyncRule) {
76197
- return this.isTargetedByRulesyncRuleDefault({
76198
- rulesyncRule,
76199
- toolTarget: this.getFamily().toolTarget
76200
- });
76201
- }
76202
- };
76203
- //#endregion
76204
76285
  //#region src/features/rules/dsh-rule.ts
76205
76286
  /**
76206
76287
  * DeepSeek Harness (`dsh`) rules.
@@ -76412,7 +76493,8 @@ var FactorydroidRule = class FactorydroidRule extends ToolRule {
76412
76493
  rulesyncRule,
76413
76494
  validate,
76414
76495
  rootPath: paths.root,
76415
- nonRootPath: paths.nonRoot
76496
+ nonRootPath: paths.nonRoot,
76497
+ global
76416
76498
  }));
76417
76499
  }
76418
76500
  toRulesyncRule() {
@@ -76879,7 +76961,8 @@ var KiloRule = class KiloRule extends ToolRule {
76879
76961
  rulesyncRule,
76880
76962
  validate,
76881
76963
  rootPath: paths.root,
76882
- nonRootPath: paths.nonRoot
76964
+ nonRootPath: paths.nonRoot,
76965
+ global
76883
76966
  }));
76884
76967
  }
76885
76968
  toRulesyncRule() {
@@ -77383,7 +77466,8 @@ var OpenCodeRule = class OpenCodeRule extends ToolRule {
77383
77466
  rulesyncRule,
77384
77467
  validate,
77385
77468
  rootPath: paths.root,
77386
- nonRootPath: paths.nonRoot
77469
+ nonRootPath: paths.nonRoot,
77470
+ global
77387
77471
  }));
77388
77472
  }
77389
77473
  toRulesyncRule() {
@@ -78852,6 +78936,14 @@ var ZoocodeRule = class extends RooRule {
78852
78936
  //#endregion
78853
78937
  //#region src/features/rules/rules-processor.ts
78854
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());
78855
78947
  const formatRulePaths = (rules) => rules.map((r) => (0, node_path.join)(r.getRelativeDirPath(), r.getRelativeFilePath())).join(", ");
78856
78948
  const RulesFeatureOptionsSchema = zod_mini.z.looseObject({
78857
78949
  ruleDiscoveryMode: zod_mini.z.optional(zod_mini.z.enum(["none", "explicit"])),
@@ -79020,7 +79112,11 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
79020
79112
  extension: "md",
79021
79113
  supportsGlobal: true,
79022
79114
  ruleDiscoveryMode: "auto",
79023
- 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
+ }
79024
79120
  }
79025
79121
  }],
79026
79122
  ["commandcode", {
@@ -79478,9 +79574,61 @@ var RulesProcessor = class extends FeatureProcessor {
79478
79574
  toolRules,
79479
79575
  factory
79480
79576
  });
79577
+ this.warnForInstructionBudget({
79578
+ toolRules,
79579
+ meta
79580
+ });
79481
79581
  return outputFiles;
79482
79582
  }
79483
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
+ /**
79484
79632
  * Handle localRoot rules (only in non-global mode and when enabled). Mutates
79485
79633
  * `toolRules` in place.
79486
79634
  */
@@ -80152,14 +80300,14 @@ As this project's AI coding tool, you must follow the additional conventions bel
80152
80300
  * decides for all of them: its `pi.contextFile` is copied onto the non-root
80153
80301
  * rules, and the flag set only on a non-root rule is dropped with a warning.
80154
80302
  */
80155
- alignPiContextFile(rules) {
80303
+ alignPiContextFile(rules, { warn = true } = {}) {
80156
80304
  if (this.toolTarget !== "pi") return rules;
80157
80305
  const factory = this.getFactory(this.toolTarget);
80158
80306
  const targeted = rules.filter((rule) => factory.class.isTargetedByRulesyncRule(rule));
80159
80307
  const rootContextFile = targeted.some((rule) => rule.getFrontmatter().root === true && rule.getFrontmatter().pi?.contextFile === "override") ? "override" : void 0;
80160
80308
  const mismatched = targeted.filter((rule) => rule.getFrontmatter().pi?.contextFile !== rootContextFile);
80161
80309
  if (mismatched.length === 0) return rules;
80162
- 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)}`);
80163
80311
  const mismatchedSet = new Set(mismatched);
80164
80312
  return rules.map((rule) => {
80165
80313
  if (!mismatchedSet.has(rule)) return rule;
@@ -80213,14 +80361,72 @@ As this project's AI coding tool, you must follow the additional conventions bel
80213
80361
  * duplication check the same way `loadRulesyncFiles`'s global-mode branch
80214
80362
  * excludes it from `nonRootRules`: `generate` ignores `localRoot` entirely
80215
80363
  * in global mode, so such a rule is never actually folded into the global
80216
- * 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.
80217
80370
  */
80218
80371
  async warnForFoldImportDuplicationRisk() {
80219
80372
  const factory = this.getFactory(this.toolTarget);
80220
80373
  if (factory.meta.collisionPolicy !== "fold") return;
80221
- const nonRootRules = (await this.loadMergedRulesyncRules()).filter((rule) => !rule.getFrontmatter().root && (!this.global || !rule.getFrontmatter().localRoot) && factory.class.isTargetedByRulesyncRule(rule));
80222
- if (nonRootRules.length === 0) return;
80223
- 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
+ });
80224
80430
  }
80225
80431
  /**
80226
80432
  * Implementation of abstract method from FeatureProcessor
@@ -80514,6 +80720,14 @@ As this project's AI coding tool, you must follow the additional conventions bel
80514
80720
  function isPackagingToolTarget(toolTarget) {
80515
80721
  return PACKAGING_TOOL_TARGETS.includes(toolTarget);
80516
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
+ }
80517
80731
  async function assertPluginRootSafe(params) {
80518
80732
  if (!isPackagingToolTarget(params.toolTarget)) return;
80519
80733
  await assertDirectoryIfExists(params.outputRoot);
@@ -81433,14 +81647,17 @@ function warnUnsupportedTargets(params) {
81433
81647
  } catch {
81434
81648
  oppositeScopeTargets = [];
81435
81649
  }
81436
- for (const target of config.getTargets()) if (!supportedTargets.includes(target) && config.getFeatures(target).includes(featureName)) {
81437
- const simulateOption = SIMULATE_OPTION_MAP[featureName];
81438
- if (simulateOption && simulatedTargets.includes(target)) logger.warn(`Target '${target}' only supports simulated '${featureName}'. Use '${simulateOption}' to enable it. Skipping.`);
81439
- else if (oppositeScopeTargets.includes(target)) {
81440
- const supportedScope = config.getGlobal() ? "project" : "global";
81441
- const retry = config.getGlobal() ? "without '--global'" : "with '--global'";
81442
- logger.warn(`Target '${target}' supports the feature '${featureName}' only in ${supportedScope} scope. Re-run ${retry}. Skipping.`);
81443
- } 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
+ }
81444
81661
  }
81445
81662
  }
81446
81663
  /**
@@ -81693,10 +81910,16 @@ async function generate(params) {
81693
81910
  const { config, logger } = params;
81694
81911
  resetRootShadowingWarnings({ logger });
81695
81912
  resetRunWarningState();
81696
- for (const toolTarget of config.getTargets()) for (const outputRoot of config.getOutputRoots(toolTarget)) await assertPluginRootSafe({
81697
- toolTarget,
81698
- outputRoot
81699
- });
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
+ }
81700
81923
  await warnSkillSubagentNameCollisions({
81701
81924
  config,
81702
81925
  logger
@@ -82313,6 +82536,7 @@ function getToolOutputRoot({ config, tool }) {
82313
82536
  async function importFromTool(params) {
82314
82537
  const { config, tool, logger } = params;
82315
82538
  resetRunWarningState();
82539
+ if (config.getGlobal() && isPackagingToolTarget(tool)) throw new Error(formatPackagingTargetGlobalMessage(tool));
82316
82540
  await assertPluginRootSafe({
82317
82541
  toolTarget: tool,
82318
82542
  outputRoot: getToolOutputRoot({
@@ -82373,7 +82597,7 @@ async function importRulesCore(params) {
82373
82597
  if (!config.getFeatures(tool).includes("rules")) return 0;
82374
82598
  const global = config.getGlobal();
82375
82599
  if (!RulesProcessor.getToolTargets({ global }).includes(tool)) return 0;
82376
- const rulesProcessor = new RulesProcessor({
82600
+ const baseParams = {
82377
82601
  outputRoot: getToolOutputRoot({
82378
82602
  config,
82379
82603
  tool
@@ -82381,13 +82605,23 @@ async function importRulesCore(params) {
82381
82605
  toolTarget: tool,
82382
82606
  global,
82383
82607
  logger
82384
- });
82608
+ };
82609
+ const rulesProcessor = new RulesProcessor(baseParams);
82385
82610
  const toolFiles = await rulesProcessor.loadToolFiles();
82386
82611
  if (toolFiles.length === 0) {
82387
82612
  logger.warn(`No rule files found for ${tool}. Skipping import.`);
82388
82613
  return 0;
82389
82614
  }
82390
- 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
+ }
82391
82625
  const rulesyncFiles = await rulesProcessor.convertToolFilesToRulesyncFiles(toolFiles);
82392
82626
  const { count: writtenCount } = await rulesProcessor.writeAiFiles(rulesyncFiles);
82393
82627
  if (config.getVerbose() && writtenCount > 0) logger.success(`Created ${writtenCount} rule files`);