rulesync 16.9.1 → 16.10.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.
@@ -171,6 +171,7 @@ const rulesProcessorToolTargetTuple = [
171
171
  "kiro",
172
172
  "kiro-cli",
173
173
  "kiro-ide",
174
+ "musecode",
174
175
  "opencode",
175
176
  "pi",
176
177
  "qwencode",
@@ -234,6 +235,7 @@ const mcpProcessorToolTargetTuple = [
234
235
  "kiro-cli",
235
236
  "kiro-ide",
236
237
  "junie",
238
+ "musecode",
237
239
  "opencode",
238
240
  "qwencode",
239
241
  "reasonix",
@@ -341,6 +343,7 @@ const skillsProcessorToolTargetTuple = [
341
343
  "kiro",
342
344
  "kiro-cli",
343
345
  "kiro-ide",
346
+ "musecode",
344
347
  "opencode",
345
348
  "pi",
346
349
  "qwencode",
@@ -395,6 +398,7 @@ const permissionsProcessorToolTargetTuple = [
395
398
  "cline",
396
399
  "codexcli",
397
400
  "copilot",
401
+ "copilotcli",
398
402
  "cursor",
399
403
  "devin",
400
404
  "factorydroid",
@@ -418,6 +422,7 @@ const permissionsProcessorToolTargetTuple = [
418
422
  ];
419
423
  const checksProcessorToolTargetTuple = [
420
424
  "amp",
425
+ "augmentcode",
421
426
  "cursor",
422
427
  "hermesagent",
423
428
  "rovodev",
@@ -1730,6 +1735,8 @@ const SHARED_USER_MANAGED_CONFIG_PATHS = [
1730
1735
  ".claude/settings.json",
1731
1736
  ".claude/settings.local.json",
1732
1737
  ".codex/config.toml",
1738
+ ".copilot/settings.json",
1739
+ ".github/copilot/settings.json",
1733
1740
  ".devin/config.json",
1734
1741
  ".factory/settings.json",
1735
1742
  ".grok/config.toml",
@@ -2588,8 +2595,8 @@ const CODEXCLI_HOOK_EVENTS = [
2588
2595
  * has NO `SubagentStart`/`SubagentStop` arms — emitting them would write keys
2589
2596
  * Goose silently ignores, so `subagentStart`/`subagentStop` are intentionally
2590
2597
  * excluded here and from `CANONICAL_TO_GOOSE_EVENT_NAMES`.
2591
- * @see https://github.com/block/goose/blob/v1.41.0/crates/goose/src/hooks/mod.rs
2592
- * @see https://block.github.io/goose/docs/guides/context-engineering/hooks/
2598
+ * @see https://github.com/aaif-goose/goose/blob/v1.41.0/crates/goose/src/hooks/mod.rs
2599
+ * @see https://goose-docs.ai/docs/guides/context-engineering/hooks/
2593
2600
  */
2594
2601
  const GOOSE_HOOK_EVENTS = [
2595
2602
  "sessionStart",
@@ -2626,8 +2633,8 @@ const KIRO_HOOK_EVENTS = [
2626
2633
  * `UserPromptSubmit`, `PreToolUse`, and `PostToolUse`. Kiro also documents
2627
2634
  * file-event (`PostFileCreate`/`PostFileSave`/`PostFileDelete`) and spec-task
2628
2635
  * (`PreTaskExec`/`PostTaskExec`) triggers that have no canonical equivalent;
2629
- * those can still be emitted verbatim via a `kiro-ide` or `kiro-cli` override
2630
- * block (unknown event keys pass through unchanged). There is no `SessionEnd`
2636
+ * those can still be emitted verbatim via the shared `kiro` override block
2637
+ * (unknown event keys pass through unchanged). There is no `SessionEnd`
2631
2638
  * trigger, so the canonical `sessionEnd` has no home here.
2632
2639
  * @see https://kiro.dev/docs/hooks/types/
2633
2640
  */
@@ -3360,6 +3367,47 @@ const CANONICAL_TO_KIRO_EVENT_NAMES = {
3360
3367
  stop: "stop"
3361
3368
  };
3362
3369
  /**
3370
+ * Native event keys of the embedded agent-config hook format, as listed by the
3371
+ * Kiro CLI 3.0 migration guide's old-format column: `agentSpawn`,
3372
+ * `userPromptSubmit`, `preToolUse`, `postToolUse`, `fileEdited`, `fileCreated`
3373
+ * and `agentStop`/`stop`. Two of them (`fileEdited`, `fileCreated`) have no
3374
+ * canonical equivalent, so they exist only here.
3375
+ *
3376
+ * The `kiro` override block is shared with the standalone-format targets, whose
3377
+ * trigger vocabulary is different, so the alias writer uses this list to decide
3378
+ * what it can express.
3379
+ * @see https://kiro.dev/docs/cli/v3/hooks-migration/
3380
+ */
3381
+ const KIRO_AGENT_CONFIG_NATIVE_EVENT_NAMES = [
3382
+ "agentSpawn",
3383
+ "userPromptSubmit",
3384
+ "preToolUse",
3385
+ "postToolUse",
3386
+ "fileEdited",
3387
+ "fileCreated",
3388
+ "agentStop",
3389
+ "stop"
3390
+ ];
3391
+ /**
3392
+ * Old agent-config event keys mapped to their standalone v1 trigger, so a
3393
+ * `kiro.hooks` block authored in the deprecated spelling still emits a valid
3394
+ * trigger for the `kiro-cli` / `kiro-ide` targets that read the same block.
3395
+ * The migration guide states the old names "map directly to their newer
3396
+ * equivalents" and spells out `agentSpawn` → `SessionStart` and `fileEdited` →
3397
+ * `PostFileSave`; the rest are the same event under the v1 casing.
3398
+ * @see https://kiro.dev/docs/cli/v3/hooks-migration/
3399
+ */
3400
+ const KIRO_LEGACY_TO_KIRO_IDE_TRIGGER_NAMES = {
3401
+ agentSpawn: "SessionStart",
3402
+ userPromptSubmit: "UserPromptSubmit",
3403
+ preToolUse: "PreToolUse",
3404
+ postToolUse: "PostToolUse",
3405
+ fileEdited: "PostFileSave",
3406
+ fileCreated: "PostFileCreate",
3407
+ agentStop: "Stop",
3408
+ stop: "Stop"
3409
+ };
3410
+ /**
3363
3411
  * Map Kiro CLI camelCase event names to canonical camelCase.
3364
3412
  */
3365
3413
  const KIRO_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_KIRO_EVENT_NAMES).map(([k, v]) => [v, k]));
@@ -3367,8 +3415,8 @@ const KIRO_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICA
3367
3415
  * Map canonical camelCase event names to Kiro IDE PascalCase triggers.
3368
3416
  *
3369
3417
  * Only the canonical lifecycle events with a clean IDE equivalent are mapped.
3370
- * Unknown keys (e.g. IDE-only `PostFileSave`/`PreTaskExec` set via a `kiro-ide`
3371
- * override) pass through unchanged.
3418
+ * Unknown keys (e.g. IDE-only `PostFileSave`/`PreTaskExec` set via the shared
3419
+ * `kiro` override block) pass through unchanged.
3372
3420
  * @see https://kiro.dev/docs/hooks/types/
3373
3421
  */
3374
3422
  const CANONICAL_TO_KIRO_IDE_EVENT_NAMES = {
@@ -4869,13 +4917,57 @@ const CodexcliPermissionsOverrideSchema = z.looseObject({
4869
4917
  git_write_rules: z.optional(z.boolean())
4870
4918
  });
4871
4919
  /**
4920
+ * Tool-scoped override block for Zed. Two Zed surfaces sit outside the canonical
4921
+ * allow/ask/deny model that `agent.tool_permissions` implements, and both are
4922
+ * authored here verbatim:
4923
+ *
4924
+ * - `sandbox_permissions` — the OS-level agent sandbox, on by default since Zed
4925
+ * 1.14.2 for the `terminal` and `fetch` tools. Its defaults forbid network
4926
+ * access, writing outside the project directories, and writing to `.git`, so a
4927
+ * real setup usually needs to relax one of them (`network_hosts` with exact
4928
+ * hostnames or leading `*.` wildcards, `allow_all_hosts`, `write_paths`,
4929
+ * `allow_fs_write_all`, `allow_unsandboxed`). This is containment, not tool
4930
+ * gating: no canonical category expresses it, so the block is written straight
4931
+ * into `agent.sandbox_permissions`, mirroring the Kilo / Claude Code / Codex
4932
+ * CLI `sandbox` passthrough precedent. Kept a bare `looseObject` rather than
4933
+ * enumerating the keys, since upstream adds to them release over release.
4934
+ * - `profiles` — the tool-availability layer, a separate enforcement stage from
4935
+ * `tool_permissions`: a tool absent from the active profile cannot be used no
4936
+ * matter what the permission rules say. Per-profile keys are `name`, `tools`
4937
+ * (per-tool booleans), `enable_all_context_servers`, `context_servers` and
4938
+ * `default_model`. Like Kimi Code's `tools.enabled`/`disabled` block this is a
4939
+ * verbatim passthrough — no canonicalization is attempted.
4940
+ *
4941
+ * Neither surface can weaken a canonical deny, because `agent.tool_permissions`
4942
+ * is off-limits to this override: the translator consumes only the two keys
4943
+ * above by name and rejects a `tool_permissions` key with a warning.
4944
+ *
4945
+ * @see https://zed.dev/docs/ai/sandboxing
4946
+ * @see https://zed.dev/docs/ai/agent-profiles
4947
+ *
4948
+ * @example
4949
+ * { "sandbox_permissions": { "network_hosts": ["*.github.com"], "write_paths": ["/tmp"] },
4950
+ * "profiles": { "review": { "name": "Review", "tools": { "terminal": false } } } }
4951
+ */
4952
+ const ZedPermissionsOverrideSchema = z.looseObject({
4953
+ permission: z.optional(ToolScopedPermissionSchema),
4954
+ sandbox_permissions: z.optional(z.looseObject({})),
4955
+ profiles: z.optional(z.record(z.string(), z.looseObject({
4956
+ name: z.optional(z.string()),
4957
+ tools: z.optional(z.record(z.string(), z.boolean())),
4958
+ enable_all_context_servers: z.optional(z.boolean()),
4959
+ context_servers: z.optional(z.looseObject({})),
4960
+ default_model: z.optional(z.looseObject({}))
4961
+ })))
4962
+ });
4963
+ /**
4872
4964
  * Permissions configuration.
4873
4965
  * Keys are tool category names (e.g., "bash", "edit", "read", "webfetch").
4874
4966
  * Values are pattern-to-action mappings for that tool category.
4875
4967
  *
4876
4968
  * The optional `opencode`/`hermes`/`cline`/`kilo`/`claudecode`/`vibe`/`cursor`/
4877
4969
  * `qwencode`/`reasonix`/`factorydroid`/`warp`/`junie`/`takt`/`amp`/
4878
- * `antigravity-cli`/`augmentcode`/`kiro`/`codexcli` keys are tool-scoped
4970
+ * `antigravity-cli`/`augmentcode`/`kiro`/`codexcli`/`zed` keys are tool-scoped
4879
4971
  * overrides consumed only by their respective translator (see the matching
4880
4972
  * `*PermissionsOverrideSchema`); every other tool reads the shared `permission`
4881
4973
  * block and ignores them.
@@ -4915,14 +5007,15 @@ const PermissionsConfigSchema = z.looseObject({
4915
5007
  augmentcode: z.optional(AugmentcodePermissionsOverrideSchema),
4916
5008
  kiro: z.optional(KiroPermissionsOverrideSchema),
4917
5009
  codexcli: z.optional(CodexcliPermissionsOverrideSchema),
5010
+ zed: z.optional(ZedPermissionsOverrideSchema),
4918
5011
  "antigravity-ide": z.optional(CanonicalPermissionsOverrideSchema),
4919
5012
  copilot: z.optional(CanonicalPermissionsOverrideSchema),
5013
+ copilotcli: z.optional(CanonicalPermissionsOverrideSchema),
4920
5014
  devin: z.optional(CanonicalPermissionsOverrideSchema),
4921
5015
  goose: z.optional(CanonicalPermissionsOverrideSchema),
4922
5016
  grokcli: z.optional(CanonicalPermissionsOverrideSchema),
4923
5017
  "kimi-code": z.optional(KimiCodePermissionsOverrideSchema),
4924
- rovodev: z.optional(CanonicalPermissionsOverrideSchema),
4925
- zed: z.optional(CanonicalPermissionsOverrideSchema)
5018
+ rovodev: z.optional(CanonicalPermissionsOverrideSchema)
4926
5019
  });
4927
5020
  /**
4928
5021
  * Full permissions file schema including optional $schema field.
@@ -5635,6 +5728,33 @@ function resolveUserInvocable({ rootFrontmatter, section }) {
5635
5728
  return section?.["user-invocable"] ?? rootFrontmatter["user-invocable"];
5636
5729
  }
5637
5730
  //#endregion
5731
+ //#region src/constants/augmentcode-paths.ts
5732
+ const AUGMENTCODE_DIR = ".augment";
5733
+ const AUGMENTCODE_COMMANDS_DIR_PATH = join(AUGMENTCODE_DIR, "commands");
5734
+ /**
5735
+ * The cross-tool `.agents/commands/` root Auggie also discovers commands from.
5736
+ * Auggie resolves commands over `.augment`, `.claude` and `.agents` in both the
5737
+ * workspace and the home directory; rulesync writes to its own `.augment` root
5738
+ * and reads this one as well, mirroring how skills treat `.agents/skills`.
5739
+ * @see https://docs.augmentcode.com/cli/custom-commands
5740
+ */
5741
+ const AUGMENTCODE_AGENTS_COMMANDS_DIR_PATH = join(".agents", "commands");
5742
+ const AUGMENTCODE_SKILLS_DIR_PATH = join(AUGMENTCODE_DIR, "skills");
5743
+ const AUGMENTCODE_AGENTS_DIR_PATH = join(AUGMENTCODE_DIR, "agents");
5744
+ const AUGMENTCODE_ALT_AGENTS_DIR_PATH = ".agents";
5745
+ const AUGMENTCODE_AGENTS_SKILLS_DIR_PATH = join(".agents", "skills");
5746
+ const AUGMENTCODE_SETTINGS_FILE_NAME = "settings.json";
5747
+ const AUGMENTCODE_SETTINGS_LOCAL_FILE_NAME = "settings.local.json";
5748
+ /**
5749
+ * Augment Code Review's custom guidelines file. Read from the repository root's
5750
+ * `.augment/` folder, project scope only — Augment documents no user-level
5751
+ * equivalent, and the reviewer runs against the committed tree.
5752
+ * @see https://docs.augmentcode.com/codereview/review-guidelines
5753
+ */
5754
+ const AUGMENTCODE_CODE_REVIEW_GUIDELINES_FILE_NAME = "code_review_guidelines.yaml";
5755
+ const AUGMENTCODE_IGNORE_FILE_NAME = ".augmentignore";
5756
+ const AUGMENTCODE_LEGACY_RULE_FILE_NAME = ".augment-guidelines";
5757
+ //#endregion
5638
5758
  //#region src/constants/cursor-paths.ts
5639
5759
  const CURSOR_DIR = ".cursor";
5640
5760
  const CURSOR_COMMANDS_DIR_PATH = join(CURSOR_DIR, "commands");
@@ -5684,6 +5804,19 @@ const TAKT_RULE_OVERVIEW_FILE_NAME = "overview.md";
5684
5804
  */
5685
5805
  const TAKT_CONFIG_FILE_NAME = "config.yaml";
5686
5806
  /**
5807
+ * Takt's runtime provider config (Takt 0.56.0+). Lives at `.takt/runtime.yaml`
5808
+ * (project) and `~/.takt/runtime.yaml` (global), and owns provider/model/
5809
+ * provider-option assignment. Takt generates the global file on first launch,
5810
+ * so most installs have one; "runtime mode" only activates when its `provider:`
5811
+ * section carries an actual assignment.
5812
+ *
5813
+ * rulesync only ever READS this file: it resolves the active provider from it,
5814
+ * and it refuses to write the legacy `provider_options` key into `config.yaml`
5815
+ * while runtime mode is active (Takt hard-fails on that combination).
5816
+ * @see https://github.com/nrslib/takt/blob/main/docs/configuration.md
5817
+ */
5818
+ const TAKT_RUNTIME_CONFIG_FILE_NAME = "runtime.yaml";
5819
+ /**
5687
5820
  * Top-level key in Takt's `config.yaml` holding the workflow MCP transport
5688
5821
  * allowlist (`stdio` / `sse` / `http` booleans). Takt is default-deny: a
5689
5822
  * transport must be explicitly enabled here before any workflow-defined MCP
@@ -6159,6 +6292,363 @@ function slugifyCheckName(value) {
6159
6292
  return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48).replace(/-+$/, "");
6160
6293
  }
6161
6294
  //#endregion
6295
+ //#region src/features/checks/augmentcode-check.ts
6296
+ /** Augment's severity scale. Narrower than the canonical one. */
6297
+ const AUGMENTCODE_SEVERITIES = [
6298
+ "high",
6299
+ "medium",
6300
+ "low"
6301
+ ];
6302
+ /**
6303
+ * Canonical severity to Augment's scale. Augment has no band above `high`, so
6304
+ * canonical `critical` folds into it — the alternative, dropping the rule or
6305
+ * demoting it to `medium`, would either lose the check or understate the one
6306
+ * severity a reviewer most wants raised. The fold is one-way: a `critical`
6307
+ * check generates `high` and imports back as `high`, so the canonical value is
6308
+ * not recoverable from Augment's file alone.
6309
+ */
6310
+ const CANONICAL_TO_AUGMENTCODE_SEVERITY = {
6311
+ low: "low",
6312
+ medium: "medium",
6313
+ high: "high",
6314
+ critical: "high"
6315
+ };
6316
+ /**
6317
+ * Every field of an `areas.<key>` entry is required upstream, so a check with no
6318
+ * `severity` still has to emit one. `medium` is the neutral middle of Augment's
6319
+ * three bands: defaulting to `high` would inflate every unannotated check past
6320
+ * the ones deliberately marked `medium`, and `low` would bury them.
6321
+ */
6322
+ const DEFAULT_AUGMENTCODE_SEVERITY = "medium";
6323
+ /**
6324
+ * Augment's own example uses `["**"]`, and `globs` is required per area. Built
6325
+ * fresh per area rather than shared: js-yaml serializes a repeated reference as
6326
+ * an anchor/alias pair, which would litter a file Augment tells users to edit by
6327
+ * hand with `&ref_0` / `*ref_0`.
6328
+ */
6329
+ const defaultAreaGlobs = () => ["**"];
6330
+ /**
6331
+ * The `augmentcode` block of a check's frontmatter, carrying the two things the
6332
+ * canonical check model has no home for: which area a rule belongs to (so
6333
+ * several checks can share one), and the area's `globs`.
6334
+ */
6335
+ const AugmentcodeCheckOverrideSchema = z.looseObject({
6336
+ /** Area key this check's rule is grouped under; defaults to the check's slug. */
6337
+ area: z.optional(z.string().check(z.minLength(1))),
6338
+ /** Human-readable `areas.<key>.description`; defaults to the check's own. */
6339
+ areaDescription: z.optional(z.string()),
6340
+ globs: z.optional(z.array(z.string().check(z.minLength(1)))),
6341
+ /** Rule `id`; defaults to the check's slug. */
6342
+ id: z.optional(z.string().check(z.minLength(1)))
6343
+ });
6344
+ function parseOverride$1(raw, filePath, logger) {
6345
+ if (raw === void 0) return {};
6346
+ if (!isPlainObject$1(raw)) {
6347
+ logger?.warn(`Ignoring the \`augmentcode\` block in ${filePath}: expected a mapping.`);
6348
+ return {};
6349
+ }
6350
+ const result = AugmentcodeCheckOverrideSchema.safeParse(raw);
6351
+ if (!result.success) throw new Error(`Invalid \`augmentcode\` block in ${filePath}: ${result.error.message}`, { cause: result.error });
6352
+ return result.data;
6353
+ }
6354
+ function stemOf$1(rulesyncCheck) {
6355
+ return basename(rulesyncCheck.getRelativeFilePath(), ".md");
6356
+ }
6357
+ /**
6358
+ * Disambiguate a name against the ones already taken. Used for rule ids on
6359
+ * generate (two same-named checks in different subdirectories) and for check
6360
+ * file names on import (one rule id repeated across two areas), since a
6361
+ * collision in either direction silently loses one of the pair.
6362
+ */
6363
+ function uniqueName(preferred, used) {
6364
+ let name = preferred;
6365
+ let suffix = 2;
6366
+ while (used.has(name)) {
6367
+ name = `${preferred}-${suffix}`;
6368
+ suffix += 1;
6369
+ }
6370
+ used.add(name);
6371
+ return name;
6372
+ }
6373
+ /**
6374
+ * A rule's `description` is the whole instruction Augment acts on, so the check
6375
+ * body is preferred; the frontmatter `description` is a summary, used only when
6376
+ * there is no body. Both empty falls back to the check name, which at least
6377
+ * names the concern rather than emitting an empty required field.
6378
+ */
6379
+ function toRuleDescription(rulesyncCheck) {
6380
+ const body = rulesyncCheck.getBody().trim();
6381
+ if (body.length > 0) return body;
6382
+ return rulesyncCheck.getFrontmatter().description?.trim() || stemOf$1(rulesyncCheck);
6383
+ }
6384
+ function toRule(rulesyncCheck, override, usedRuleIds) {
6385
+ const severity = rulesyncCheck.getFrontmatter().severity;
6386
+ return {
6387
+ id: uniqueName(override.id ?? slugifyCheckName(stemOf$1(rulesyncCheck)), usedRuleIds),
6388
+ description: toRuleDescription(rulesyncCheck),
6389
+ severity: severity ? CANONICAL_TO_AUGMENTCODE_SEVERITY[severity] ?? DEFAULT_AUGMENTCODE_SEVERITY : DEFAULT_AUGMENTCODE_SEVERITY
6390
+ };
6391
+ }
6392
+ /**
6393
+ * An authored area key is used verbatim. Slugifying it would rewrite the
6394
+ * underscores in Augment's own documented example (`memory_safety`), and since
6395
+ * import writes the key back unchanged, the next generate would build a second
6396
+ * area under the slugified spelling while the original stayed put — the same
6397
+ * rules twice. Only the file-stem default is slugified, because that one has to
6398
+ * become a legal area key from an arbitrary file name.
6399
+ */
6400
+ function areaKeyFor(rulesyncCheck, override) {
6401
+ return override.area ?? slugifyCheckName(stemOf$1(rulesyncCheck));
6402
+ }
6403
+ /** Rule ids belonging to areas this generate preserves rather than rewrites. */
6404
+ function collectRuleIds(existingAreas, claimedKeys) {
6405
+ const ids = /* @__PURE__ */ new Set();
6406
+ for (const [areaKey, rawArea] of Object.entries(existingAreas)) {
6407
+ if (claimedKeys.has(areaKey) || !isPlainObject$1(rawArea)) continue;
6408
+ for (const rawRule of readArea(rawArea).rules) {
6409
+ const rule = readRule(rawRule);
6410
+ if (rule) ids.add(rule.id);
6411
+ }
6412
+ }
6413
+ return ids;
6414
+ }
6415
+ /**
6416
+ * Group the checks into `areas`. One area per check by default; checks naming
6417
+ * the same `augmentcode.area` share one, with the first check to name it
6418
+ * supplying the area's `description` and `globs`.
6419
+ */
6420
+ function buildAreas({ entries, reservedRuleIds }) {
6421
+ const areas = /* @__PURE__ */ new Map();
6422
+ const usedRuleIds = new Set(reservedRuleIds);
6423
+ for (const { rulesyncCheck, override } of entries) {
6424
+ const key = areaKeyFor(rulesyncCheck, override);
6425
+ const rule = toRule(rulesyncCheck, override, usedRuleIds);
6426
+ const existing = areas.get(key);
6427
+ if (existing) {
6428
+ existing.rules.push(rule);
6429
+ continue;
6430
+ }
6431
+ areas.set(key, {
6432
+ description: override.areaDescription || rulesyncCheck.getFrontmatter().description?.trim() || stemOf$1(rulesyncCheck),
6433
+ globs: override.globs ?? defaultAreaGlobs(),
6434
+ rules: [rule]
6435
+ });
6436
+ }
6437
+ return Object.fromEntries(areas);
6438
+ }
6439
+ function parseGuidelines(fileContent, filePath) {
6440
+ if (fileContent.trim().length === 0) return {};
6441
+ let parsed;
6442
+ try {
6443
+ parsed = loadYaml(fileContent);
6444
+ } catch (error) {
6445
+ throw new Error(`Failed to parse AugmentCode code review guidelines at ${filePath}: ${formatError(error)}`, { cause: error });
6446
+ }
6447
+ if (parsed === void 0 || parsed === null) return {};
6448
+ if (!isPlainObject$1(parsed)) throw new Error(`Failed to parse AugmentCode code review guidelines at ${filePath}: expected a mapping at the document root.`);
6449
+ return parsed;
6450
+ }
6451
+ /** An area's shared fields, read defensively out of hand-written YAML. */
6452
+ function readArea(rawArea) {
6453
+ const rawGlobs = Array.isArray(rawArea.globs) ? rawArea.globs : void 0;
6454
+ const globs = rawGlobs?.filter((glob) => typeof glob === "string");
6455
+ const authoredGlobs = globs && (globs.length > 0 || rawGlobs?.length === 0) ? globs : void 0;
6456
+ return {
6457
+ ...typeof rawArea.description === "string" && { description: rawArea.description },
6458
+ ...authoredGlobs && { globs: authoredGlobs },
6459
+ rules: Array.isArray(rawArea.rules) ? rawArea.rules : []
6460
+ };
6461
+ }
6462
+ /**
6463
+ * A rule missing `id` or `description` is not a shape Augment itself reads, so
6464
+ * it is skipped rather than imported as a check with an invented field.
6465
+ */
6466
+ function readRule(rawRule) {
6467
+ if (!isPlainObject$1(rawRule)) return void 0;
6468
+ const id = typeof rawRule.id === "string" ? rawRule.id : void 0;
6469
+ const description = typeof rawRule.description === "string" ? rawRule.description : void 0;
6470
+ if (!id || !description) return void 0;
6471
+ const severity = AUGMENTCODE_SEVERITIES.find((value) => value === rawRule.severity);
6472
+ return {
6473
+ id,
6474
+ description,
6475
+ ...severity && { severity }
6476
+ };
6477
+ }
6478
+ /**
6479
+ * Checks adapter for Augment Code Review's custom guidelines
6480
+ * (`.augment/code_review_guidelines.yaml`).
6481
+ *
6482
+ * Augment groups review rules into named **areas**: each area carries a
6483
+ * `description`, the `globs` it applies to, and a list of `rules`, every rule
6484
+ * being an `id` / `description` / `severity` triple. Every one of those fields
6485
+ * is required upstream. Rulesync maps one `.rulesync/checks/*.md` onto one rule,
6486
+ * in an area of its own unless an `augmentcode.area` groups several together —
6487
+ * so the whole set collapses into this one file via {@link fromRulesyncChecks}
6488
+ * rather than a file per check.
6489
+ *
6490
+ * **Severity is lossy in one direction.** Augment's scale is `high` / `medium` /
6491
+ * `low` with no band above `high`, so canonical `critical` maps to `high` and
6492
+ * imports back as `high`. A check with no `severity` at all emits `medium`,
6493
+ * since the field cannot be omitted.
6494
+ *
6495
+ * **`file_paths_to_ignore`** is recognized and preserved verbatim, but never
6496
+ * authored or imported: the canonical check model has no ignore surface, and
6497
+ * inventing one here is a separate design question.
6498
+ *
6499
+ * **Generation merges rather than replaces.** Augment's own documentation tells
6500
+ * users to hand-write this file, so only the areas the current check set claims
6501
+ * are rewritten; every other area, `file_paths_to_ignore`, and any unknown
6502
+ * top-level key survive. The cost of that is that rulesync cannot tell its own
6503
+ * leftovers from a hand-written area: renaming a check strands the area under
6504
+ * the old key, and dropping the last Augment-targeting check leaves the areas in
6505
+ * place with a warning rather than guessing which of them to delete. Deleting
6506
+ * the file outright is likewise refused whenever it exists.
6507
+ *
6508
+ * Project scope only — the reviewer reads the file from the committed
6509
+ * repository, and Augment documents no user-level equivalent.
6510
+ *
6511
+ * @see https://docs.augmentcode.com/codereview/review-guidelines
6512
+ */
6513
+ var AugmentcodeCheck = class AugmentcodeCheck extends ToolCheck {
6514
+ static getSettablePaths(_options = {}) {
6515
+ return {
6516
+ relativeDirPath: AUGMENTCODE_DIR,
6517
+ relativeFilePath: AUGMENTCODE_CODE_REVIEW_GUIDELINES_FILE_NAME
6518
+ };
6519
+ }
6520
+ static isTargetedByRulesyncCheck(rulesyncCheck) {
6521
+ return this.isTargetedByRulesyncCheckDefault({
6522
+ rulesyncCheck,
6523
+ toolTarget: "augmentcode"
6524
+ });
6525
+ }
6526
+ /**
6527
+ * Ownership guard the processor consults before it deletes anything for this
6528
+ * tool. Unlike the Markdown adapters, whose section markers say which text is
6529
+ * rulesync's, YAML carries no such marker — js-yaml drops comments on rewrite,
6530
+ * and an unknown top-level key risks Augment's own parser. So an existing file
6531
+ * is never rulesync's to delete: it may hold hand-written areas, and there is
6532
+ * no way to prove otherwise.
6533
+ */
6534
+ static async canDeleteAuxiliaryFiles({ outputRoot }) {
6535
+ const paths = AugmentcodeCheck.getSettablePaths();
6536
+ return !await fileExists(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath ?? "code_review_guidelines.yaml"));
6537
+ }
6538
+ static fromRulesyncCheck(_params) {
6539
+ throw new Error("AugmentCode checks are built from all checks at once; use fromRulesyncChecks.");
6540
+ }
6541
+ static async fromRulesyncChecks({ outputRoot = process.cwd(), rulesyncChecks, global = false, logger }) {
6542
+ const paths = AugmentcodeCheck.getSettablePaths({ global });
6543
+ const relativeFilePath = paths.relativeFilePath ?? "code_review_guidelines.yaml";
6544
+ const filePath = join(outputRoot, paths.relativeDirPath, relativeFilePath);
6545
+ const existing = parseGuidelines(await readFileContentOrNull(filePath) ?? "", filePath);
6546
+ const existingAreas = isPlainObject$1(existing.areas) ? existing.areas : {};
6547
+ if (rulesyncChecks.length === 0) {
6548
+ if (Object.keys(existingAreas).length > 0) logger?.warn(`AugmentCode checks: no check targets AugmentCode, but ${filePath} still holds review areas. They are left in place — rulesync cannot tell the ones it generated from ones you wrote, so removing them is a manual edit.`);
6549
+ return [];
6550
+ }
6551
+ const entries = rulesyncChecks.map((rulesyncCheck) => ({
6552
+ rulesyncCheck,
6553
+ override: parseOverride$1(rulesyncCheck.getFrontmatter().augmentcode, join(RULESYNC_CHECKS_RELATIVE_DIR_PATH, rulesyncCheck.getRelativeFilePath()), logger)
6554
+ }));
6555
+ const generatedAreas = buildAreas({
6556
+ entries,
6557
+ reservedRuleIds: collectRuleIds(existingAreas, new Set(entries.map(({ rulesyncCheck, override }) => areaKeyFor(rulesyncCheck, override))))
6558
+ });
6559
+ const fileContent = dump({
6560
+ ...existing,
6561
+ areas: {
6562
+ ...existingAreas,
6563
+ ...generatedAreas
6564
+ }
6565
+ }, {
6566
+ lineWidth: -1,
6567
+ noRefs: true
6568
+ });
6569
+ return [new AugmentcodeCheck({
6570
+ outputRoot,
6571
+ relativeDirPath: paths.relativeDirPath,
6572
+ relativeFilePath,
6573
+ fileContent,
6574
+ global
6575
+ })];
6576
+ }
6577
+ static async fromFile({ outputRoot = process.cwd(), global = false }) {
6578
+ const paths = AugmentcodeCheck.getSettablePaths({ global });
6579
+ const relativeFilePath = paths.relativeFilePath ?? "code_review_guidelines.yaml";
6580
+ const filePath = join(outputRoot, paths.relativeDirPath, relativeFilePath);
6581
+ return new AugmentcodeCheck({
6582
+ outputRoot,
6583
+ relativeDirPath: paths.relativeDirPath,
6584
+ relativeFilePath,
6585
+ fileContent: await readFileContentOrNull(filePath) ?? "",
6586
+ global
6587
+ });
6588
+ }
6589
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
6590
+ return new AugmentcodeCheck({
6591
+ outputRoot,
6592
+ relativeDirPath,
6593
+ relativeFilePath,
6594
+ fileContent: "",
6595
+ validate: false,
6596
+ global
6597
+ });
6598
+ }
6599
+ validate() {
6600
+ return {
6601
+ success: true,
6602
+ error: null
6603
+ };
6604
+ }
6605
+ toRulesyncCheck() {
6606
+ const first = this.toRulesyncChecks()[0];
6607
+ if (!first) throw new Error(`No review areas found in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}.`);
6608
+ return first;
6609
+ }
6610
+ /**
6611
+ * One check per rule, not per area: an area with three rules is three
6612
+ * independent review instructions, and collapsing them into one check would
6613
+ * make the next generate emit a single merged rule in their place. The area's
6614
+ * key, description and globs ride along in each check's `augmentcode` block,
6615
+ * so regenerating regroups the rules exactly where they were.
6616
+ */
6617
+ toRulesyncChecks() {
6618
+ const filePath = join(this.getRelativeDirPath(), this.getRelativeFilePath());
6619
+ const guidelines = parseGuidelines(this.getFileContent(), filePath);
6620
+ const areas = isPlainObject$1(guidelines.areas) ? guidelines.areas : {};
6621
+ const checks = [];
6622
+ const usedNames = /* @__PURE__ */ new Set();
6623
+ for (const [areaKey, rawArea] of Object.entries(areas)) {
6624
+ if (!isPlainObject$1(rawArea)) continue;
6625
+ const { description: areaDescription, globs, rules } = readArea(rawArea);
6626
+ for (const rawRule of rules) {
6627
+ const rule = readRule(rawRule);
6628
+ if (!rule) continue;
6629
+ const name = uniqueName(slugifyCheckName(rule.id) || slugifyCheckName(areaKey) || "check", usedNames);
6630
+ checks.push(new RulesyncCheck({
6631
+ outputRoot: ".",
6632
+ relativeDirPath: RULESYNC_CHECKS_RELATIVE_DIR_PATH,
6633
+ relativeFilePath: `${name}.md`,
6634
+ frontmatter: {
6635
+ targets: ["*"],
6636
+ ...rule.severity && { severity: rule.severity },
6637
+ augmentcode: {
6638
+ area: areaKey,
6639
+ ...areaDescription !== void 0 && { areaDescription },
6640
+ ...globs && { globs },
6641
+ id: rule.id
6642
+ }
6643
+ },
6644
+ body: rule.description
6645
+ }));
6646
+ }
6647
+ }
6648
+ return checks;
6649
+ }
6650
+ };
6651
+ //#endregion
6162
6652
  //#region src/features/checks/aggregated-check-file.ts
6163
6653
  /**
6164
6654
  * Shared machinery for the tools whose checks surface is **one aggregated
@@ -7081,6 +7571,22 @@ const SHARED_CONFIG_OWNERSHIP = {
7081
7571
  }
7082
7572
  }
7083
7573
  },
7574
+ ".github/copilot/settings.json": {
7575
+ format: "json",
7576
+ invalidRootPolicy: "error",
7577
+ features: { permissions: {
7578
+ kind: "replace-owned-keys",
7579
+ ownedKeys: ["deniedUrls"]
7580
+ } }
7581
+ },
7582
+ ".copilot/settings.json": {
7583
+ format: "json",
7584
+ invalidRootPolicy: "error",
7585
+ features: { permissions: {
7586
+ kind: "replace-owned-keys",
7587
+ ownedKeys: ["allowedUrls", "deniedUrls"]
7588
+ } }
7589
+ },
7084
7590
  ".vscode/settings.json": {
7085
7591
  format: "jsonc",
7086
7592
  invalidRootPolicy: "error",
@@ -7161,6 +7667,14 @@ const SHARED_CONFIG_OWNERSHIP = {
7161
7667
  }
7162
7668
  }
7163
7669
  },
7670
+ ".config/muse/settings.json": {
7671
+ format: "json",
7672
+ invalidRootPolicy: "error",
7673
+ features: { mcp: {
7674
+ kind: "replace-owned-keys",
7675
+ ownedKeys: ["mcp_servers", "schema_version"]
7676
+ } }
7677
+ },
7164
7678
  ".kiro/agents/default.json": {
7165
7679
  format: "json",
7166
7680
  features: {
@@ -7808,6 +8322,14 @@ const toolCheckFactories = /* @__PURE__ */ new Map([
7808
8322
  filePattern: "*.md"
7809
8323
  }
7810
8324
  }],
8325
+ ["augmentcode", {
8326
+ class: AugmentcodeCheck,
8327
+ meta: {
8328
+ supportsGlobal: false,
8329
+ filePattern: AUGMENTCODE_CODE_REVIEW_GUIDELINES_FILE_NAME,
8330
+ committedOutput: true
8331
+ }
8332
+ }],
7811
8333
  ["cursor", {
7812
8334
  class: CursorCheck,
7813
8335
  meta: {
@@ -8595,26 +9117,6 @@ var AntigravityIdeCommand = class extends AntigravitySharedCommand {
8595
9117
  }
8596
9118
  };
8597
9119
  //#endregion
8598
- //#region src/constants/augmentcode-paths.ts
8599
- const AUGMENTCODE_DIR = ".augment";
8600
- const AUGMENTCODE_COMMANDS_DIR_PATH = join(AUGMENTCODE_DIR, "commands");
8601
- /**
8602
- * The cross-tool `.agents/commands/` root Auggie also discovers commands from.
8603
- * Auggie resolves commands over `.augment`, `.claude` and `.agents` in both the
8604
- * workspace and the home directory; rulesync writes to its own `.augment` root
8605
- * and reads this one as well, mirroring how skills treat `.agents/skills`.
8606
- * @see https://docs.augmentcode.com/cli/custom-commands
8607
- */
8608
- const AUGMENTCODE_AGENTS_COMMANDS_DIR_PATH = join(".agents", "commands");
8609
- const AUGMENTCODE_SKILLS_DIR_PATH = join(AUGMENTCODE_DIR, "skills");
8610
- const AUGMENTCODE_AGENTS_DIR_PATH = join(AUGMENTCODE_DIR, "agents");
8611
- const AUGMENTCODE_ALT_AGENTS_DIR_PATH = ".agents";
8612
- const AUGMENTCODE_AGENTS_SKILLS_DIR_PATH = join(".agents", "skills");
8613
- const AUGMENTCODE_SETTINGS_FILE_NAME = "settings.json";
8614
- const AUGMENTCODE_SETTINGS_LOCAL_FILE_NAME = "settings.local.json";
8615
- const AUGMENTCODE_IGNORE_FILE_NAME = ".augmentignore";
8616
- const AUGMENTCODE_LEGACY_RULE_FILE_NAME = ".augment-guidelines";
8617
- //#endregion
8618
9120
  //#region src/features/commands/augmentcode-command.ts
8619
9121
  const AugmentcodeCommandFrontmatterSchema = z.looseObject({
8620
9122
  description: z.optional(z.string()),
@@ -9148,6 +9650,8 @@ const COPILOT_GLOBAL_HOOKS_FILE_NAME = "copilot-ide-hooks.json";
9148
9650
  const COPILOT_MCP_DIR = ".vscode";
9149
9651
  const COPILOT_MCP_FILE_NAME = "mcp.json";
9150
9652
  const COPILOT_VSCODE_SETTINGS_FILE_NAME = "settings.json";
9653
+ const COPILOTCLI_SETTINGS_FILE_NAME = "settings.json";
9654
+ const COPILOTCLI_PROJECT_SETTINGS_DIR_PATH = join(GITHUB_DIR, "copilot");
9151
9655
  const COPILOTCLI_MCP_FILE_NAME = "mcp-config.json";
9152
9656
  const COPILOTCLI_PROJECT_MCP_FILE_NAME = "mcp.json";
9153
9657
  const COPILOTCLI_AGENTS_DIR_PATH = join(COPILOT_DIR, "agents");
@@ -9407,6 +9911,7 @@ const DEVIN_HOOKS_V1_FILE_NAME = "hooks.v1.json";
9407
9911
  const DEVIN_GLOBAL_AGENTS_FILE_NAME = "AGENTS.md";
9408
9912
  const DEVIN_IGNORE_FILE_NAME = ".devinignore";
9409
9913
  const DEVIN_LEGACY_IGNORE_FILE_NAME = ".codeiumignore";
9914
+ const DEVIN_WINDSURF_IGNORE_FILE_NAME = ".windsurfignore";
9410
9915
  const DEVIN_GLOBAL_IGNORE_DIR_PATH = ".codeium";
9411
9916
  const DEVIN_GLOBAL_IGNORE_FILE_NAME = DEVIN_LEGACY_IGNORE_FILE_NAME;
9412
9917
  //#endregion
@@ -9761,7 +10266,7 @@ var GooseCommandConfigFile = class extends ToolFile {
9761
10266
  * The whole file is a YAML mapping (not frontmatter + markdown body), so the
9762
10267
  * class stores the parsed recipe object rather than a frontmatter/body split.
9763
10268
  *
9764
- * @see https://block.github.io/goose/docs/guides/recipes/recipe-reference/
10269
+ * @see https://goose-docs.ai/docs/guides/recipes/recipe-reference/
9765
10270
  */
9766
10271
  const GooseCommandRecipeSchema = z.looseObject({
9767
10272
  version: z.optional(z.string()),
@@ -13255,7 +13760,7 @@ function collectHandlers({ effectiveHooks, eventMap }) {
13255
13760
  }
13256
13761
  return handlerGroups;
13257
13762
  }
13258
- function buildCommandLines({ handler, usesToolName, blocksToolCall }) {
13763
+ function buildCommandLines$1({ handler, usesToolName, blocksToolCall }) {
13259
13764
  const lines = [];
13260
13765
  const indent = usesToolName && handler.matcher ? " " : " ";
13261
13766
  if (usesToolName && handler.matcher) lines.push(` if (new RegExp(${matcherToEmbeddedLiteral$1(handler.matcher)}).test(event.tool)) {`);
@@ -13280,7 +13785,7 @@ function buildSubscriptionLines$1(handlerGroups) {
13280
13785
  const usesToolName = AMP_TOOL_EVENTS.has(ampEvent) && handlers.some((handler) => handler.matcher);
13281
13786
  const blocksToolCall = ampEvent === "tool.call";
13282
13787
  lines.push(` amp.on(${JSON.stringify(ampEvent)}, async (${usesToolName ? "event, ctx" : "_event, ctx"}) => {`);
13283
- for (const handler of handlers) lines.push(...buildCommandLines({
13788
+ for (const handler of handlers) lines.push(...buildCommandLines$1({
13284
13789
  handler,
13285
13790
  usesToolName,
13286
13791
  blocksToolCall
@@ -15265,8 +15770,10 @@ const CopilotHookEntrySchema = z.looseObject({
15265
15770
  bash: z.optional(z.string()),
15266
15771
  powershell: z.optional(z.string()),
15267
15772
  command: z.optional(z.string()),
15773
+ cwd: z.optional(z.string()),
15268
15774
  env: z.optional(z.record(z.string(), z.string())),
15269
- timeoutSec: z.optional(z.number())
15775
+ timeoutSec: z.optional(z.number()),
15776
+ timeout: z.optional(z.number())
15270
15777
  });
15271
15778
  /**
15272
15779
  * Convert canonical hooks config to Copilot format.
@@ -15346,6 +15853,22 @@ function resolveImportCommand$1(entry, logger) {
15346
15853
  return typeof entry.command === "string" ? { command: entry.command } : {};
15347
15854
  }
15348
15855
  /**
15856
+ * Extract the non-command fields preserved across import.
15857
+ *
15858
+ * Generate re-emits any non-canonical key verbatim through `rest`, so a key
15859
+ * dropped here does not survive an import → generate round trip. `cwd` is a
15860
+ * documented Copilot hook field and was previously lost that way.
15861
+ *
15862
+ * @see https://docs.github.com/en/copilot/reference/hooks-reference
15863
+ * @see https://docs.github.com/en/copilot/concepts/agents/coding-agent/about-hooks
15864
+ */
15865
+ function importPassthrough$1(entry) {
15866
+ const passthrough = {};
15867
+ if (entry.cwd !== void 0) passthrough.cwd = entry.cwd;
15868
+ if (entry.env !== void 0) passthrough.env = entry.env;
15869
+ return passthrough;
15870
+ }
15871
+ /**
15349
15872
  * Extract hooks from Copilot hooks JSON into canonical format.
15350
15873
  * Copilot format: { version: 1, hooks: { eventName: [...hookEntries] } }
15351
15874
  */
@@ -15361,13 +15884,13 @@ function copilotHooksToCanonical(copilotHooks, logger) {
15361
15884
  if (!parseResult.success) continue;
15362
15885
  const entry = parseResult.data;
15363
15886
  const { command, shell } = resolveImportCommand$1(entry, logger);
15364
- const timeout = entry.timeoutSec;
15887
+ const timeout = entry.timeoutSec ?? entry.timeout;
15365
15888
  defs.push({
15366
15889
  type: "command",
15367
15890
  ...command !== void 0 && { command },
15368
15891
  ...shell !== void 0 && { shell },
15369
- ...entry.env !== void 0 && { env: entry.env },
15370
- ...timeout !== void 0 && { timeout }
15892
+ ...timeout !== void 0 && { timeout },
15893
+ ...importPassthrough$1(entry)
15371
15894
  });
15372
15895
  }
15373
15896
  if (defs.length > 0) canonical[eventName] = defs;
@@ -15650,19 +16173,19 @@ function importPassthrough(entry) {
15650
16173
  * A shell-specific field carries its `shell` through so re-export writes the
15651
16174
  * same field back. An entry using only the portable `command` field leaves
15652
16175
  * `shell` unset, which re-export renders as the portable field again.
16176
+ *
16177
+ * When both shell-specific fields are present, `bash` wins and a warning names
16178
+ * the ignored `powershell`. The choice is deliberately not platform-dependent:
16179
+ * importing on Windows must not produce a different canonical config than
16180
+ * importing the same file on Linux, which would make the rulesync hooks file
16181
+ * differ per machine for anyone who checks it in.
15653
16182
  */
15654
16183
  function resolveImportCommand(entry, logger) {
15655
16184
  const hasBash = typeof entry.bash === "string";
15656
16185
  const hasPowershell = typeof entry.powershell === "string";
15657
16186
  if (hasBash && hasPowershell) {
15658
- const isWindows = process.platform === "win32";
15659
- const chosen = isWindows ? "powershell" : "bash";
15660
- const ignored = isWindows ? "bash" : "powershell";
15661
- logger?.warn(`Copilot CLI hook has both bash and powershell commands; using ${chosen} and ignoring ${ignored} on this platform.`);
15662
- return isWindows ? {
15663
- command: entry.powershell,
15664
- shell: "powershell"
15665
- } : {
16187
+ logger?.warn("Copilot CLI hook has both bash and powershell commands; using bash and ignoring powershell, so the imported config does not depend on the machine the import ran on.");
16188
+ return {
15666
16189
  command: entry.bash,
15667
16190
  shell: "bash"
15668
16191
  };
@@ -16321,7 +16844,7 @@ const GOOSE_CONVERTER_CONFIG = {
16321
16844
  *
16322
16845
  * The JSON shape matches Claude Code's: each PascalCase event maps to an array of
16323
16846
  * `{ matcher, hooks: [{ type: "command", command }] }` entries.
16324
- * @see https://block.github.io/goose/docs/guides/context-engineering/hooks/
16847
+ * @see https://goose-docs.ai/docs/guides/context-engineering/hooks/
16325
16848
  */
16326
16849
  var GooseHooks = class GooseHooks extends ToolHooks {
16327
16850
  constructor(params) {
@@ -17230,6 +17753,38 @@ function stripTrustedDirectoryWrapper(command) {
17230
17753
  if (posix?.[1]) return posix[1];
17231
17754
  return command.match(/^set "RULESYNC_KIMI_HOOK_CWD=1" && cd \/d "(?:""|[^"])*" && ([\s\S]*)$/)?.[1] ?? command;
17232
17755
  }
17756
+ /**
17757
+ * Native Kimi Code events whose Event Reference row lists the matcher as
17758
+ * "Empty string". Kimi Code documents `matcher` as "a regular expression to
17759
+ * filter event targets; if omitted, matches all", so on these events the
17760
+ * regex is tested against `""`: any non-trivial matcher simply never matches
17761
+ * and the hook silently never runs. Dropping the matcher is what makes the
17762
+ * hook fire at all, which is the authored intent — these events have no target
17763
+ * to filter on in the first place.
17764
+ *
17765
+ * Keyed on native names because the check runs after the canonical → native
17766
+ * mapping: `SessionHeartbeat` and `Interrupt` have no canonical counterpart and
17767
+ * are only reachable through a per-tool `kimi-code` override naming them
17768
+ * directly.
17769
+ *
17770
+ * Deliberately narrower than Claude Code's equivalent set: Kimi Code's
17771
+ * `UserPromptSubmit` matches the submitted prompt text, and `PermissionResult`
17772
+ * matches the tool name, so a matcher on either is meaningful and is kept.
17773
+ *
17774
+ * @see https://moonshotai.github.io/kimi-code/en/customization/hooks.html
17775
+ */
17776
+ const KIMI_CODE_NO_MATCHER_EVENTS = /* @__PURE__ */ new Set([
17777
+ "Stop",
17778
+ "SessionHeartbeat",
17779
+ "Interrupt"
17780
+ ]);
17781
+ /** Resolve the `matcher` part of an emitted entry, dropping dead matchers. */
17782
+ function resolveMatcherPart({ matcher, nativeEvent, logger }) {
17783
+ if (!matcher) return {};
17784
+ if (!KIMI_CODE_NO_MATCHER_EVENTS.has(nativeEvent)) return { matcher };
17785
+ logger?.warn(`matcher "${matcher}" on "${nativeEvent}" hook will be ignored — this event does not support matchers`);
17786
+ return {};
17787
+ }
17233
17788
  function buildEffectiveHooks(config, toolOverrideHooks) {
17234
17789
  const supported = new Set(KIMI_CODE_HOOK_EVENTS);
17235
17790
  const shared = {};
@@ -17259,7 +17814,11 @@ function canonicalToKimiCodeHooks({ config, toolOverrideHooks, trustedDirectory,
17259
17814
  command: definition.command,
17260
17815
  trustedDirectory
17261
17816
  }),
17262
- ...definition.matcher && { matcher: definition.matcher },
17817
+ ...resolveMatcherPart({
17818
+ matcher: definition.matcher,
17819
+ nativeEvent,
17820
+ logger
17821
+ }),
17263
17822
  ...validTimeout && timeout !== void 0 && { timeout }
17264
17823
  });
17265
17824
  }
@@ -17457,17 +18016,33 @@ function buildKiroIdeEntriesForEvent(trigger, definitions) {
17457
18016
  }
17458
18017
  return entries;
17459
18018
  }
17460
- function canonicalToKiroIdeHooks(config, overrideKey) {
18019
+ /**
18020
+ * The single `HooksConfig` key every Kiro target reads its tool-specific
18021
+ * overrides from.
18022
+ *
18023
+ * `kiro-ide` and `kiro-cli` write the same `.kiro/hooks/rulesync.json` at both
18024
+ * scopes, so per-target override blocks would make that one file's content
18025
+ * depend on generation order (last writer wins). All Kiro variants therefore
18026
+ * share the `kiro` block — the same resolution the Kiro MCP and permissions
18027
+ * wiring already use for the file they share.
18028
+ */
18029
+ const KIRO_HOOKS_OVERRIDE_KEY = "kiro";
18030
+ /**
18031
+ * Override keys a user might reach for that nothing reads, mapped to the key
18032
+ * that is actually read.
18033
+ */
18034
+ const KIRO_HOOKS_IGNORED_OVERRIDE_KEYS = ["kiro-ide", "kiro-cli"];
18035
+ function canonicalToKiroIdeHooks(config) {
17461
18036
  const kiroIdeSupported = new Set(KIRO_IDE_HOOK_EVENTS);
17462
18037
  const sharedHooks = {};
17463
18038
  for (const [event, defs] of Object.entries(config.hooks)) if (kiroIdeSupported.has(event)) sharedHooks[event] = defs;
17464
18039
  const effectiveHooks = {
17465
18040
  ...sharedHooks,
17466
- ...config[overrideKey]?.hooks
18041
+ ...config[KIRO_HOOKS_OVERRIDE_KEY]?.hooks
17467
18042
  };
17468
18043
  const entries = [];
17469
18044
  for (const [eventName, definitions] of Object.entries(effectiveHooks)) {
17470
- const trigger = CANONICAL_TO_KIRO_IDE_EVENT_NAMES[eventName] ?? eventName;
18045
+ const trigger = CANONICAL_TO_KIRO_IDE_EVENT_NAMES[eventName] ?? KIRO_LEGACY_TO_KIRO_IDE_TRIGGER_NAMES[eventName] ?? eventName;
17471
18046
  entries.push(...buildKiroIdeEntriesForEvent(trigger, definitions));
17472
18047
  }
17473
18048
  return entries;
@@ -17512,6 +18087,11 @@ function kiroIdeHooksToCanonical(entries) {
17512
18087
  * still writes the embedded `.kiro/agents/default.json` agent-config shape,
17513
18088
  * which Kiro CLI 3.0 no longer reads.
17514
18089
  *
18090
+ * Because both targets write the very same file, they resolve their
18091
+ * tool-specific overrides from one shared block
18092
+ * ({@link KIRO_HOOKS_OVERRIDE_KEY}) rather than per-target blocks, so
18093
+ * generating either or both targets always yields the same file.
18094
+ *
17515
18095
  * @see https://kiro.dev/docs/hooks/
17516
18096
  */
17517
18097
  var KiroIdeHooks = class extends ToolHooks {
@@ -17524,14 +18104,6 @@ var KiroIdeHooks = class extends ToolHooks {
17524
18104
  }, null, 2)
17525
18105
  });
17526
18106
  }
17527
- /**
17528
- * The `HooksConfig` key whose `hooks` block provides tool-specific overrides
17529
- * for this target. {@link import("./kiro-cli-hooks.js").KiroCliHooks}
17530
- * overrides this to `kiro-cli`.
17531
- */
17532
- static getOverrideKey() {
17533
- return "kiro-ide";
17534
- }
17535
18107
  static getSettablePaths(_options = {}) {
17536
18108
  return {
17537
18109
  relativeDirPath: KIRO_IDE_HOOKS_DIR_PATH,
@@ -17552,9 +18124,14 @@ var KiroIdeHooks = class extends ToolHooks {
17552
18124
  validate
17553
18125
  });
17554
18126
  }
17555
- static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false }) {
18127
+ static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
17556
18128
  const paths = this.getSettablePaths({ global });
17557
- const hooks = canonicalToKiroIdeHooks(rulesyncHooks.getJson(), this.getOverrideKey());
18129
+ const config = rulesyncHooks.getJson();
18130
+ for (const ignoredKey of KIRO_HOOKS_IGNORED_OVERRIDE_KEYS) {
18131
+ if (config[ignoredKey]?.hooks === void 0) continue;
18132
+ logger?.warn(`The "${ignoredKey}.hooks" block in ${join(rulesyncHooks.getRelativeDirPath(), rulesyncHooks.getRelativeFilePath())} is ignored. Author it under the "${KIRO_HOOKS_OVERRIDE_KEY}.hooks" key instead: the Kiro IDE and Kiro CLI targets write the same hooks file, so they read one shared block.`);
18133
+ }
18134
+ const hooks = canonicalToKiroIdeHooks(config);
17558
18135
  const fileContent = JSON.stringify({
17559
18136
  version: "v1",
17560
18137
  hooks
@@ -17575,10 +18152,9 @@ var KiroIdeHooks = class extends ToolHooks {
17575
18152
  throw new Error(`Failed to parse Kiro IDE hooks content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
17576
18153
  }
17577
18154
  const hooks = kiroIdeHooksToCanonical(parsed.hooks ?? []);
17578
- const overrideKey = this.constructor.getOverrideKey();
17579
18155
  return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
17580
18156
  hooks,
17581
- overrideKey
18157
+ overrideKey: KIRO_HOOKS_OVERRIDE_KEY
17582
18158
  }), null, 2) });
17583
18159
  }
17584
18160
  validate() {
@@ -17606,9 +18182,13 @@ var KiroIdeHooks = class extends ToolHooks {
17606
18182
  * Hooks generator for the **Kiro CLI**.
17607
18183
  *
17608
18184
  * Kiro CLI 3.0 reads the same standalone `.kiro/hooks/*.json` v1 format the
17609
- * Kiro IDE reads, so this reuses {@link KiroIdeHooks} and only redirects the
17610
- * tool-specific override key to `kiro-cli` (so `kiro-cli.hooks` overrides in
17611
- * the rulesync hooks config are honored, rather than `kiro-ide.hooks`).
18185
+ * Kiro IDE reads the same directory, and in rulesync the same
18186
+ * `.kiro/hooks/rulesync.json` file so this is {@link KiroIdeHooks} under a
18187
+ * second target name, down to the shared `kiro` override block both resolve
18188
+ * their tool-specific hooks from. A per-target override block would make that
18189
+ * one file's content depend on which target was generated last, which is
18190
+ * exactly what the shared block avoids (the Kiro MCP and permissions wiring
18191
+ * resolve their shared files the same way).
17612
18192
  *
17613
18193
  * The embedded `.kiro/agents/default.json` agent-hook format this target used
17614
18194
  * to emit is documented as not working in 3.0, so it is left to the deprecated
@@ -17617,11 +18197,7 @@ var KiroIdeHooks = class extends ToolHooks {
17617
18197
  * @see https://kiro.dev/docs/cli/v3/hooks-migration/
17618
18198
  * @see https://kiro.dev/docs/hooks/
17619
18199
  */
17620
- var KiroCliHooks = class extends KiroIdeHooks {
17621
- static getOverrideKey() {
17622
- return "kiro-cli";
17623
- }
17624
- };
18200
+ var KiroCliHooks = class extends KiroIdeHooks {};
17625
18201
  //#endregion
17626
18202
  //#region src/features/hooks/kiro-hooks.ts
17627
18203
  /**
@@ -17646,14 +18222,30 @@ function buildKiroEntriesForEvent(definitions) {
17646
18222
  }
17647
18223
  return entries;
17648
18224
  }
17649
- function canonicalToKiroHooks(config) {
18225
+ /**
18226
+ * Event keys the embedded agent-config format defines: the canonical events it
18227
+ * supports plus its own native spellings (`agentSpawn`, `userPromptSubmit`, …).
18228
+ *
18229
+ * The `kiro` override block is shared with the standalone `.kiro/hooks/*.json`
18230
+ * targets, whose vocabulary is different (`PostFileSave`, `PreTaskExec`, …).
18231
+ * Passing those through here would write event keys Kiro does not define into
18232
+ * `.kiro/agents/default.json`, so they are dropped instead.
18233
+ * @see https://kiro.dev/docs/cli/v3/hooks-migration/
18234
+ */
18235
+ const KIRO_AGENT_CONFIG_EVENT_KEYS = /* @__PURE__ */ new Set([...KIRO_HOOK_EVENTS, ...KIRO_AGENT_CONFIG_NATIVE_EVENT_NAMES]);
18236
+ function canonicalToKiroHooks({ config, logger }) {
17650
18237
  const overrideKey = "kiro";
17651
18238
  const kiroSupported = new Set(KIRO_HOOK_EVENTS);
17652
18239
  const sharedHooks = {};
17653
18240
  for (const [event, defs] of Object.entries(config.hooks)) if (kiroSupported.has(event)) sharedHooks[event] = defs;
18241
+ const overrideHooks = {};
18242
+ const droppedEvents = [];
18243
+ for (const [event, defs] of Object.entries(config[overrideKey]?.hooks ?? {})) if (KIRO_AGENT_CONFIG_EVENT_KEYS.has(event)) overrideHooks[event] = defs;
18244
+ else droppedEvents.push(event);
18245
+ if (droppedEvents.length > 0) logger?.warn(`Skipped hook event(s) from the "kiro" override block for the deprecated kiro agent config (no event key of that format): ${droppedEvents.join(", ")}. They are emitted for the kiro-cli / kiro-ide targets, which read the same block.`);
17654
18246
  const effectiveHooks = {
17655
18247
  ...sharedHooks,
17656
- ...config[overrideKey]?.hooks
18248
+ ...overrideHooks
17657
18249
  };
17658
18250
  const kiro = {};
17659
18251
  for (const [eventName, definitions] of Object.entries(effectiveHooks)) {
@@ -17748,11 +18340,14 @@ var KiroHooks = class KiroHooks extends ToolHooks {
17748
18340
  validate
17749
18341
  });
17750
18342
  }
17751
- static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false }) {
18343
+ static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
17752
18344
  const paths = KiroHooks.getSettablePaths({ global });
17753
18345
  const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
17754
18346
  const existingContent = await readFileContentOrNull(filePath) ?? JSON.stringify({}, null, 2);
17755
- const kiroHooks = canonicalToKiroHooks(rulesyncHooks.getJson());
18347
+ const kiroHooks = canonicalToKiroHooks({
18348
+ config: rulesyncHooks.getJson(),
18349
+ logger
18350
+ });
17756
18351
  const fileContent = applySharedConfigPatch({
17757
18352
  fileKey: sharedConfigFileKey(paths),
17758
18353
  feature: "hooks",
@@ -17868,6 +18463,30 @@ const PI_TOOL_EVENTS = /* @__PURE__ */ new Set(["tool_call", "tool_result"]);
17868
18463
  */
17869
18464
  const PI_ASSISTANT_MESSAGE_EVENTS = /* @__PURE__ */ new Set(["message_end"]);
17870
18465
  /**
18466
+ * `tool_call` is the only Pi extension event that can block, and it is Pi's
18467
+ * only tool gate. Its return contract is
18468
+ * `{ block: true, reason?: string, terminate?: boolean }`.
18469
+ *
18470
+ * @see https://github.com/earendil-works/pi/blob/v0.84.1/packages/coding-agent/docs/extensions.md#tool_call
18471
+ */
18472
+ const PI_BLOCKING_EVENT = "tool_call";
18473
+ /**
18474
+ * Helper emitted alongside blocking handlers. `promisify(exec)` rejects on a
18475
+ * non-zero exit with an error carrying `stdout` / `stderr` / `code`, so the
18476
+ * reason is derived from the rejection rather than from a resolved exit code.
18477
+ */
18478
+ const BLOCK_REASON_HELPER_LINES = [
18479
+ "function toBlockReason(error: unknown): string {",
18480
+ " const result = error as { stdout?: unknown; stderr?: unknown; code?: unknown } | null;",
18481
+ " const stderr = String(result?.stderr ?? \"\").trim();",
18482
+ " if (stderr) return stderr;",
18483
+ " const stdout = String(result?.stdout ?? \"\").trim();",
18484
+ " if (stdout) return stdout;",
18485
+ " if (result?.code !== undefined) return `Hook command failed with exit code ${result.code}.`;",
18486
+ " return error instanceof Error ? error.message : String(error);",
18487
+ "}"
18488
+ ];
18489
+ /**
17871
18490
  * Validate a hook matcher as a regular expression and return it as a JS
17872
18491
  * string-literal (JSON.stringify quoting) safe to embed in generated code.
17873
18492
  */
@@ -17904,6 +18523,22 @@ function collectPiHandlers({ effectiveHooks, eventMap }) {
17904
18523
  }
17905
18524
  return handlerGroups;
17906
18525
  }
18526
+ function buildCommandLines({ handler, usesToolName, blocksToolCall }) {
18527
+ const lines = [];
18528
+ const gated = usesToolName && Boolean(handler.matcher);
18529
+ const indent = gated ? " " : " ";
18530
+ const embeddedCommand = JSON.stringify(handler.command);
18531
+ if (gated && handler.matcher) lines.push(` if (new RegExp(${matcherToEmbeddedLiteral(handler.matcher)}).test(event.toolName)) {`);
18532
+ if (blocksToolCall) {
18533
+ lines.push(`${indent}try {`);
18534
+ lines.push(`${indent} await run(${embeddedCommand});`);
18535
+ lines.push(`${indent}} catch (error) {`);
18536
+ lines.push(`${indent} return { block: true, reason: toBlockReason(error) };`);
18537
+ lines.push(`${indent}}`);
18538
+ } else lines.push(`${indent}await run(${embeddedCommand});`);
18539
+ if (gated) lines.push(" }");
18540
+ return lines;
18541
+ }
17907
18542
  function buildSubscriptionLines(handlerGroups) {
17908
18543
  const lines = [];
17909
18544
  for (const [piEvent, handlers] of Object.entries(handlerGroups)) {
@@ -17912,14 +18547,11 @@ function buildSubscriptionLines(handlerGroups) {
17912
18547
  const usesEvent = usesToolName || gatesOnAssistant;
17913
18548
  lines.push(` pi.on(${JSON.stringify(piEvent)}, async (${usesEvent ? "event" : ""}) => {`);
17914
18549
  if (gatesOnAssistant) lines.push(` if (event.message.role !== "assistant") return;`);
17915
- for (const handler of handlers) {
17916
- const embeddedCommand = JSON.stringify(handler.command);
17917
- if (usesToolName && handler.matcher) {
17918
- lines.push(` if (new RegExp(${matcherToEmbeddedLiteral(handler.matcher)}).test(event.toolName)) {`);
17919
- lines.push(` await run(${embeddedCommand});`);
17920
- lines.push(" }");
17921
- } else lines.push(` await run(${embeddedCommand});`);
17922
- }
18550
+ for (const handler of handlers) lines.push(...buildCommandLines({
18551
+ handler,
18552
+ usesToolName,
18553
+ blocksToolCall: piEvent === PI_BLOCKING_EVENT
18554
+ }));
17923
18555
  lines.push(" });");
17924
18556
  }
17925
18557
  return lines;
@@ -17928,8 +18560,9 @@ function buildSubscriptionLines(handlerGroups) {
17928
18560
  * Generate the rulesync-owned Pi extension (a TypeScript module with a
17929
18561
  * default-export factory receiving Pi's ExtensionAPI) that subscribes to the
17930
18562
  * mapped lifecycle events and executes the configured hook commands via the
17931
- * platform shell. The generated extension observes events only: it never
17932
- * blocks or mutates Pi events.
18563
+ * platform shell. Handlers observe events, except on `tool_call` Pi's only
18564
+ * blocking event and its only tool gate — where a hook command that exits
18565
+ * non-zero denies the call with `{ block: true, reason }`.
17933
18566
  *
17934
18567
  * @see https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/extensions.md
17935
18568
  */
@@ -17941,10 +18574,12 @@ function generatePiExtensionCode({ config, supportedEvents, eventMap }) {
17941
18574
  };
17942
18575
  const effectiveHooks = {};
17943
18576
  for (const [event, defs] of Object.entries(configHooks)) if (supported.has(event)) effectiveHooks[event] = defs;
17944
- const subscriptionLines = buildSubscriptionLines(collectPiHandlers({
18577
+ const handlerGroups = collectPiHandlers({
17945
18578
  effectiveHooks,
17946
18579
  eventMap
17947
- }));
18580
+ });
18581
+ const subscriptionLines = buildSubscriptionLines(handlerGroups);
18582
+ const needsBlockReasonHelper = Boolean(handlerGroups[PI_BLOCKING_EVENT]);
17948
18583
  const lines = ["// Generated by rulesync. Do not edit manually."];
17949
18584
  if (subscriptionLines.length === 0) {
17950
18585
  lines.push("export default function () {}");
@@ -17958,6 +18593,10 @@ function generatePiExtensionCode({ config, supportedEvents, eventMap }) {
17958
18593
  lines.push("");
17959
18594
  lines.push("const run = promisify(exec);");
17960
18595
  lines.push("");
18596
+ if (needsBlockReasonHelper) {
18597
+ lines.push(...BLOCK_REASON_HELPER_LINES);
18598
+ lines.push("");
18599
+ }
17961
18600
  lines.push("export default function (pi: ExtensionAPI) {");
17962
18601
  lines.push(...subscriptionLines);
17963
18602
  lines.push("}");
@@ -18687,6 +19326,20 @@ function unsupportedMatcherEventNames({ factory, effectiveHooks }) {
18687
19326
  }
18688
19327
  return [...eventsWithMatcher];
18689
19328
  }
19329
+ /**
19330
+ * Targets that read their tool-scoped `{key}.hooks` override from a differently
19331
+ * named key. The two Kiro standalone-format targets write the very same
19332
+ * `.kiro/hooks/rulesync.json`, so they share the `kiro` block: a per-target
19333
+ * block would make that one file's content depend on generation order. The same
19334
+ * resolution is used by the MCP (`MCP_BLOCK_KEY_ALIASES`) and permissions
19335
+ * (`PERMISSION_OVERRIDE_KEY_ALIASES`) features for the files they share.
19336
+ */
19337
+ const HOOKS_OVERRIDE_KEY_ALIASES = {
19338
+ "kiro-cli": KIRO_HOOKS_OVERRIDE_KEY,
19339
+ "kiro-ide": KIRO_HOOKS_OVERRIDE_KEY
19340
+ };
19341
+ /** The targets writing the standalone `.kiro/hooks/*.json` v1 format. */
19342
+ const KIRO_STANDALONE_HOOKS_TARGETS = /* @__PURE__ */ new Set(["kiro-cli", "kiro-ide"]);
18690
19343
  const toolHooksFactories = /* @__PURE__ */ new Map([
18691
19344
  ["amp", {
18692
19345
  class: AmpHooks,
@@ -18927,7 +19580,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
18927
19580
  },
18928
19581
  supportedEvents: KIRO_HOOK_EVENTS,
18929
19582
  supportedHookTypes: ["command"],
18930
- supportsMatcher: true
19583
+ supportsMatcher: true,
19584
+ passthroughOverrideEvents: true
18931
19585
  }],
18932
19586
  ["kiro-cli", {
18933
19587
  class: KiroCliHooks,
@@ -19107,7 +19761,7 @@ var HooksProcessor = class extends FeatureProcessor {
19107
19761
  if (!factory) throw new Error(`Unsupported tool target: ${this.toolTarget}`);
19108
19762
  const config = rulesyncHooks.getJson();
19109
19763
  const sharedHooks = config.hooks;
19110
- const overrideHooks = config[this.toolTarget]?.hooks ?? {};
19764
+ const overrideHooks = config[HOOKS_OVERRIDE_KEY_ALIASES[this.toolTarget] ?? this.toolTarget]?.hooks ?? {};
19111
19765
  const effectiveHooks = {
19112
19766
  ...sharedHooks,
19113
19767
  ...overrideHooks
@@ -19133,14 +19787,14 @@ var HooksProcessor = class extends FeatureProcessor {
19133
19787
  }
19134
19788
  for (const [hookType, events] of unsupportedTypeToEvents) this.logger.warn(`Skipped ${hookType}-type hook(s) for ${this.toolTarget} (not supported): ${Array.from(events).join(", ")}`);
19135
19789
  }
19136
- if (this.toolTarget !== "kiro-ide") {
19790
+ if (!KIRO_STANDALONE_HOOKS_TARGETS.has(this.toolTarget)) {
19137
19791
  const skippedEvents = new Set(unsupportedEventNames({
19138
19792
  factory,
19139
19793
  sharedHooks,
19140
19794
  effectiveHooks
19141
19795
  }));
19142
19796
  const eventsWithDisabledHooks = Object.entries(sharedHooks).filter(([event, defs]) => !skippedEvents.has(event) && defs.some((def) => def.enabled === false)).map(([event]) => event);
19143
- if (eventsWithDisabledHooks.length > 0) this.logger.warn(`Emitting "enabled: false" hook(s) as active for ${this.toolTarget} (only kiro-ide supports the flag): ${eventsWithDisabledHooks.join(", ")}`);
19797
+ if (eventsWithDisabledHooks.length > 0) this.logger.warn(`Emitting "enabled: false" hook(s) as active for ${this.toolTarget} (only the kiro-cli / kiro-ide standalone hooks format supports the flag): ${eventsWithDisabledHooks.join(", ")}`);
19144
19798
  }
19145
19799
  const eventsWithUnsupportedMatcher = unsupportedMatcherEventNames({
19146
19800
  factory,
@@ -19639,9 +20293,11 @@ var CursorIgnore = class CursorIgnore extends ToolIgnore {
19639
20293
  *
19640
20294
  * Generates the brand-aligned `.devinignore` file with gitignore-compatible
19641
20295
  * syntax. Devin automatically respects `.gitignore` patterns and has built-in
19642
- * defaults for node_modules/ and hidden files. On import, the legacy
19643
- * `.codeiumignore` filename is read as a fallback so existing projects still
19644
- * round-trip.
20296
+ * defaults for node_modules/ and hidden files. On import, the pre-rebrand
20297
+ * `.codeiumignore` and `.windsurfignore` filenames are read as fallbacks so
20298
+ * existing projects still round-trip. The docs list the three names side by
20299
+ * side without defining a precedence between them, so the order below is
20300
+ * rulesync's own choice: brand-aligned name first, then the legacy names.
19645
20301
  *
19646
20302
  * In global mode the enterprise-wide `~/.codeium/.codeiumignore` is written
19647
20303
  * instead; see `DEVIN_GLOBAL_IGNORE_DIR_PATH` for why that path keeps the
@@ -19680,9 +20336,16 @@ var DevinIgnore = class DevinIgnore extends ToolIgnore {
19680
20336
  validate,
19681
20337
  global
19682
20338
  });
19683
- const primaryPath = join(outputRoot, relativeDirPath, relativeFilePath);
19684
- const legacyPath = join(outputRoot, relativeDirPath, DEVIN_LEGACY_IGNORE_FILE_NAME);
19685
- const resolvedFilePath = !await fileExists(primaryPath) && await fileExists(legacyPath) ? DEVIN_LEGACY_IGNORE_FILE_NAME : relativeFilePath;
20339
+ const candidateFileNames = [
20340
+ relativeFilePath,
20341
+ DEVIN_LEGACY_IGNORE_FILE_NAME,
20342
+ DEVIN_WINDSURF_IGNORE_FILE_NAME
20343
+ ];
20344
+ let resolvedFilePath = relativeFilePath;
20345
+ for (const candidateFileName of candidateFileNames) if (await fileExists(join(outputRoot, relativeDirPath, candidateFileName))) {
20346
+ resolvedFilePath = candidateFileName;
20347
+ break;
20348
+ }
19686
20349
  const fileContent = await readFileContent(join(outputRoot, relativeDirPath, resolvedFilePath));
19687
20350
  return new DevinIgnore({
19688
20351
  outputRoot,
@@ -22911,8 +23574,8 @@ function convertToGoosePluginMcpServers(mcpServers, logger) {
22911
23574
  * shape and cannot express `url`/`headers`, so remote servers are skipped with
22912
23575
  * a warning in project mode (use `--global` to sync them instead).
22913
23576
  *
22914
- * @see https://block.github.io/goose/docs/getting-started/using-extensions/
22915
- * @see https://github.com/block/goose/pull/9471
23577
+ * @see https://goose-docs.ai/docs/getting-started/using-extensions/
23578
+ * @see https://github.com/aaif-goose/goose/pull/9471
22916
23579
  */
22917
23580
  var GooseMcp = class GooseMcp extends ToolMcp {
22918
23581
  config;
@@ -24416,6 +25079,213 @@ var KiroMcp = class KiroMcp extends ToolMcp {
24416
25079
  }
24417
25080
  };
24418
25081
  //#endregion
25082
+ //#region src/constants/musecode-paths.ts
25083
+ const MUSECODE_RULE_FILE_NAME = "AGENTS.md";
25084
+ const MUSECODE_SKILLS_DIR_PATH = join(".agents", "skills");
25085
+ const MUSECODE_GLOBAL_CONFIG_DIR_PATH = join(".config", "muse");
25086
+ const MUSECODE_GLOBAL_SKILLS_DIR_PATH = join(MUSECODE_GLOBAL_CONFIG_DIR_PATH, "skills");
25087
+ const MUSECODE_SETTINGS_FILE_NAME = "settings.json";
25088
+ //#endregion
25089
+ //#region src/features/mcp/musecode-mcp.ts
25090
+ const MUSECODE_GLOBAL_ONLY_MESSAGE = "Muse Code MCP is global-only; use --global to sync ~/.config/muse/settings.json";
25091
+ /**
25092
+ * Single spelling of the settings.json codec/policy, matching the
25093
+ * `SHARED_CONFIG_OWNERSHIP` declaration for `.config/muse/settings.json`:
25094
+ * fail closed on an unparseable root rather than replacing the user's primary
25095
+ * Muse Code config with generated output.
25096
+ */
25097
+ function parseMusecodeSettings(fileContent, filePath) {
25098
+ return parseSharedConfig({
25099
+ format: "json",
25100
+ fileContent,
25101
+ filePath,
25102
+ invalidRootPolicy: "error"
25103
+ });
25104
+ }
25105
+ /**
25106
+ * Convert canonical rulesync servers to Muse Code's native `mcp_servers` shape.
25107
+ * Each entry carries a `transport` discriminator: `stdio` servers spawn a
25108
+ * `command` (single string) with `args`/`env`, and `streamable_http` servers
25109
+ * are reached at a `url` with optional `headers`. Only documented fields are
25110
+ * emitted; a canonical `disabled: true` maps to Muse's `enabled: false`.
25111
+ */
25112
+ function convertToMusecodeFormat(mcpServers, logger) {
25113
+ const result = {};
25114
+ for (const [name, config] of Object.entries(mcpServers)) {
25115
+ if (PROTOTYPE_POLLUTION_KEYS.has(name)) continue;
25116
+ if (!isRecord$1(config)) continue;
25117
+ if (declaresNoTransport(config)) {
25118
+ warnAndSkipMcpServer({
25119
+ toolName: "Muse Code",
25120
+ serverName: name,
25121
+ reason: "no transport",
25122
+ logger
25123
+ });
25124
+ continue;
25125
+ }
25126
+ const converted = {};
25127
+ if (isRemoteMcpServer(config)) {
25128
+ const url = resolveRemoteMcpUrl(config);
25129
+ if (!url) {
25130
+ warnAndSkipMcpServer({
25131
+ toolName: "Muse Code",
25132
+ serverName: name,
25133
+ reason: "a remote transport without a url",
25134
+ logger
25135
+ });
25136
+ continue;
25137
+ }
25138
+ const stated = config.type ?? config.transport;
25139
+ if (stated === "sse" || stated === "ws" || stated === void 0 && /^wss?:\/\//i.test(url)) {
25140
+ warnAndSkipMcpServer({
25141
+ toolName: "Muse Code",
25142
+ serverName: name,
25143
+ reason: `the "${stated ?? "ws"}" transport, which Muse Code does not implement (only stdio and streamable_http are supported)`,
25144
+ logger
25145
+ });
25146
+ continue;
25147
+ }
25148
+ converted.transport = "streamable_http";
25149
+ converted.url = url;
25150
+ if (config.headers && Object.keys(config.headers).length > 0) converted.headers = config.headers;
25151
+ } else {
25152
+ const [command, ...args] = resolveLocalMcpCommand(config);
25153
+ if (!command) {
25154
+ warnAndSkipMcpServer({
25155
+ toolName: "Muse Code",
25156
+ serverName: name,
25157
+ reason: "a stdio transport without a command",
25158
+ logger
25159
+ });
25160
+ continue;
25161
+ }
25162
+ converted.transport = "stdio";
25163
+ converted.command = command;
25164
+ converted.args = args;
25165
+ if (config.env && Object.keys(config.env).length > 0) converted.env = config.env;
25166
+ }
25167
+ if (config.disabled === true) converted.enabled = false;
25168
+ result[name] = converted;
25169
+ }
25170
+ return result;
25171
+ }
25172
+ /**
25173
+ * Convert Muse Code's native `mcp_servers` shape back to canonical rulesync
25174
+ * servers. The `transport` discriminator is dropped (`streamable_http` is not a
25175
+ * canonical enum value; the transport is re-derived from `command`/`url` on the
25176
+ * next generate), `enabled: false` maps back to `disabled: true`, and unknown
25177
+ * keys (e.g. `mode`, `framing`) pass through untouched.
25178
+ */
25179
+ function convertFromMusecodeFormat(musecodeMcp) {
25180
+ const result = {};
25181
+ for (const [name, config] of Object.entries(musecodeMcp)) {
25182
+ if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
25183
+ const converted = {};
25184
+ for (const [key, value] of Object.entries(config)) {
25185
+ if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
25186
+ if (key === "transport") continue;
25187
+ if (key === "enabled") {
25188
+ if (value === false) converted.disabled = true;
25189
+ continue;
25190
+ }
25191
+ converted[key] = value;
25192
+ }
25193
+ result[name] = converted;
25194
+ }
25195
+ return result;
25196
+ }
25197
+ /**
25198
+ * Meta Muse Code MCP servers.
25199
+ *
25200
+ * Muse Code reads MCP servers only from the `mcp_servers` block of the GLOBAL
25201
+ * user settings file `~/.config/muse/settings.json`; no project-scoped MCP
25202
+ * location is documented. The settings file must carry
25203
+ * `"schema_version": 1` — a file that omits that key fails every command at
25204
+ * startup with `malformed settings file` — so the key is bootstrapped when the
25205
+ * file is created and preserved when it already exists. Other settings keys are
25206
+ * preserved via the shared-config gateway, and the file is never deleted.
25207
+ *
25208
+ * @see https://dev.meta.ai/docs/muse-code/configuration.md
25209
+ * @see https://dev.meta.ai/docs/muse-code/extending.md
25210
+ */
25211
+ var MusecodeMcp = class MusecodeMcp extends ToolMcp {
25212
+ json;
25213
+ constructor(params) {
25214
+ super(params);
25215
+ this.json = parseMusecodeSettings(this.fileContent ?? "", join(this.relativeDirPath, this.relativeFilePath));
25216
+ }
25217
+ getJson() {
25218
+ return this.json;
25219
+ }
25220
+ isDeletable() {
25221
+ return false;
25222
+ }
25223
+ static getSettablePaths(_options) {
25224
+ return {
25225
+ relativeDirPath: MUSECODE_GLOBAL_CONFIG_DIR_PATH,
25226
+ relativeFilePath: MUSECODE_SETTINGS_FILE_NAME
25227
+ };
25228
+ }
25229
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
25230
+ if (!global) throw new Error(MUSECODE_GLOBAL_ONLY_MESSAGE);
25231
+ const paths = this.getSettablePaths({ global });
25232
+ const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{}";
25233
+ return new MusecodeMcp({
25234
+ outputRoot,
25235
+ relativeDirPath: paths.relativeDirPath,
25236
+ relativeFilePath: paths.relativeFilePath,
25237
+ fileContent,
25238
+ validate,
25239
+ global
25240
+ });
25241
+ }
25242
+ static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
25243
+ if (!global) throw new Error(MUSECODE_GLOBAL_ONLY_MESSAGE);
25244
+ const paths = this.getSettablePaths({ global });
25245
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
25246
+ const existingContent = await readFileContentOrNull(filePath) ?? "";
25247
+ const existing = parseMusecodeSettings(existingContent, filePath);
25248
+ const converted = convertToMusecodeFormat(rulesyncMcp.getMcpServers(), logger);
25249
+ return new MusecodeMcp({
25250
+ outputRoot,
25251
+ relativeDirPath: paths.relativeDirPath,
25252
+ relativeFilePath: paths.relativeFilePath,
25253
+ fileContent: applySharedConfigPatch({
25254
+ fileKey: sharedConfigFileKey(paths),
25255
+ feature: "mcp",
25256
+ existingContent,
25257
+ patch: {
25258
+ mcp_servers: converted,
25259
+ ...existing.schema_version === void 0 && { schema_version: 1 }
25260
+ },
25261
+ filePath
25262
+ }),
25263
+ validate,
25264
+ global
25265
+ });
25266
+ }
25267
+ toRulesyncMcp() {
25268
+ const converted = convertFromMusecodeFormat(isRecord$1(this.json.mcp_servers) ? this.json.mcp_servers : {});
25269
+ return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers: converted }, null, 2) });
25270
+ }
25271
+ validate() {
25272
+ return {
25273
+ success: true,
25274
+ error: null
25275
+ };
25276
+ }
25277
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
25278
+ return new MusecodeMcp({
25279
+ outputRoot,
25280
+ relativeDirPath,
25281
+ relativeFilePath,
25282
+ fileContent: JSON.stringify({ mcp_servers: {} }, null, 2),
25283
+ validate: false,
25284
+ global
25285
+ });
25286
+ }
25287
+ };
25288
+ //#endregion
24419
25289
  //#region src/features/mcp/opencode-mcp.ts
24420
25290
  const OPENCODE_ENV_VAR_PATTERN = /(?<!\$)\{env:([^}:]+)\}/g;
24421
25291
  const OpencodeMcpLocalServerSchema = z.looseObject({
@@ -26372,6 +27242,15 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
26372
27242
  supportsDisabledTools: false
26373
27243
  }
26374
27244
  }],
27245
+ ["musecode", {
27246
+ class: MusecodeMcp,
27247
+ meta: {
27248
+ supportsProject: false,
27249
+ supportsGlobal: true,
27250
+ supportsEnabledTools: false,
27251
+ supportsDisabledTools: false
27252
+ }
27253
+ }],
26375
27254
  ["opencode", {
26376
27255
  class: OpencodeMcp,
26377
27256
  meta: {
@@ -29187,6 +30066,163 @@ var CopilotPermissions = class CopilotPermissions extends ToolPermissions {
29187
30066
  }
29188
30067
  };
29189
30068
  //#endregion
30069
+ //#region src/features/permissions/copilotcli-permissions.ts
30070
+ /**
30071
+ * The only canonical category the Copilot CLI settings can express: URL
30072
+ * approvals. Every other category (`bash`, `edit`, `read`, ...) has no
30073
+ * user-authorable surface — the CLI's `permissions.allow`/`ask`/`deny` rule
30074
+ * arrays are accepted only in MDM/enterprise managed settings, and session
30075
+ * tool approvals are machine-written to `permissions-config.json`.
30076
+ */
30077
+ const WEBFETCH_CATEGORY = "webfetch";
30078
+ /** `~/.copilot/settings.json` and `.github/copilot/settings.json` keys. */
30079
+ const ALLOWED_URLS_KEY = "allowedUrls";
30080
+ const DENIED_URLS_KEY = "deniedUrls";
30081
+ function toUrlList(value) {
30082
+ if (!Array.isArray(value)) return [];
30083
+ return value.filter((entry) => typeof entry === "string");
30084
+ }
30085
+ /**
30086
+ * Split one canonical category's rules into the two URL lists. `ask` entries
30087
+ * are omitted: the CLI prompts for any URL that is in neither list, which is
30088
+ * exactly what `ask` means.
30089
+ */
30090
+ function splitUrlRules(rules) {
30091
+ const allowedUrls = [];
30092
+ const deniedUrls = [];
30093
+ for (const [pattern, action] of Object.entries(rules)) if (action === "allow") allowedUrls.push(pattern);
30094
+ else if (action === "deny") deniedUrls.push(pattern);
30095
+ return {
30096
+ allowedUrls,
30097
+ deniedUrls
30098
+ };
30099
+ }
30100
+ /**
30101
+ * Permissions generator for the GitHub Copilot CLI.
30102
+ *
30103
+ * The CLI keeps two persistent settings files: the user-scope
30104
+ * `~/.copilot/settings.json` and the repository-scope
30105
+ * `.github/copilot/settings.json` (shipped in v1.0.60). Both are shared,
30106
+ * hand-edited files carrying unrelated keys (`model`, `effortLevel`, `hooks`,
30107
+ * `sandbox.*`, ...), so writes go through the shared-config gateway, only the
30108
+ * URL keys are owned, and the file is never deleted.
30109
+ *
30110
+ * The canonical `webfetch` category maps onto the CLI's two URL lists:
30111
+ * `allow` → `allowedUrls`, `deny` → `deniedUrls`, `ask` → the pattern is
30112
+ * omitted so the CLI falls through to its approval prompt.
30113
+ *
30114
+ * Scope asymmetry: the repository-scope key table documents `deniedUrls`
30115
+ * (union — a repository may add entries, never remove them) but NOT
30116
+ * `allowedUrls`, so an allow rule is only enforceable at user scope. At project
30117
+ * scope allow rules are therefore dropped with a warning rather than written to
30118
+ * a key the CLI ignores. (v1.0.79 additionally warns on startup about unknown
30119
+ * top-level keys in the user `settings.json`, so only documented keys are ever
30120
+ * emitted there either.)
30121
+ *
30122
+ * @see https://docs.github.com/en/copilot/reference/copilot-cli-reference/cli-config-dir-reference
30123
+ * @see https://github.com/github/copilot-cli/blob/main/changelog.md
30124
+ */
30125
+ var CopilotcliPermissions = class CopilotcliPermissions extends ToolPermissions {
30126
+ constructor(params) {
30127
+ super({
30128
+ ...params,
30129
+ fileContent: params.fileContent ?? "{}"
30130
+ });
30131
+ }
30132
+ /**
30133
+ * `settings.json` holds unrelated user settings (`model`, `effortLevel`,
30134
+ * `hooks`, ...), so it must not be deleted.
30135
+ */
30136
+ isDeletable() {
30137
+ return false;
30138
+ }
30139
+ static getSettablePaths({ global = false } = {}) {
30140
+ return global ? {
30141
+ relativeDirPath: COPILOT_DIR,
30142
+ relativeFilePath: COPILOTCLI_SETTINGS_FILE_NAME
30143
+ } : {
30144
+ relativeDirPath: COPILOTCLI_PROJECT_SETTINGS_DIR_PATH,
30145
+ relativeFilePath: COPILOTCLI_SETTINGS_FILE_NAME
30146
+ };
30147
+ }
30148
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
30149
+ const paths = CopilotcliPermissions.getSettablePaths({ global });
30150
+ const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{}";
30151
+ return new CopilotcliPermissions({
30152
+ outputRoot,
30153
+ relativeDirPath: paths.relativeDirPath,
30154
+ relativeFilePath: paths.relativeFilePath,
30155
+ fileContent,
30156
+ validate,
30157
+ global
30158
+ });
30159
+ }
30160
+ static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, global = false, logger }) {
30161
+ const paths = CopilotcliPermissions.getSettablePaths({ global });
30162
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
30163
+ const existingContent = await readFileContentOrNull(filePath) ?? "{}";
30164
+ const rules = rulesyncPermissions.getJson().permission[WEBFETCH_CATEGORY];
30165
+ const patch = {};
30166
+ if (rules !== void 0) {
30167
+ const { allowedUrls, deniedUrls } = splitUrlRules(rules);
30168
+ patch[DENIED_URLS_KEY] = deniedUrls.length > 0 ? deniedUrls : void 0;
30169
+ if (global) patch[ALLOWED_URLS_KEY] = allowedUrls.length > 0 ? allowedUrls : void 0;
30170
+ else if (allowedUrls.length > 0) logger?.warn(`Copilot CLI permissions: dropping ${allowedUrls.length} "webfetch" allow rule(s) at project scope — ${join(paths.relativeDirPath, paths.relativeFilePath)} does not accept "${ALLOWED_URLS_KEY}" (repository settings may only add denials). Author allow rules in global scope (\`--global\`) instead.`);
30171
+ }
30172
+ return new CopilotcliPermissions({
30173
+ outputRoot,
30174
+ relativeDirPath: paths.relativeDirPath,
30175
+ relativeFilePath: paths.relativeFilePath,
30176
+ fileContent: applySharedConfigPatch({
30177
+ fileKey: sharedConfigFileKey(paths),
30178
+ feature: "permissions",
30179
+ existingContent,
30180
+ patch,
30181
+ filePath
30182
+ }),
30183
+ validate: true,
30184
+ global
30185
+ });
30186
+ }
30187
+ toRulesyncPermissions() {
30188
+ const settings = this.parseSettings();
30189
+ const rules = {};
30190
+ for (const pattern of toUrlList(settings[DENIED_URLS_KEY])) rules[pattern] = "deny";
30191
+ if (this.global) for (const pattern of toUrlList(settings[ALLOWED_URLS_KEY])) rules[pattern] ??= "allow";
30192
+ const permission = Object.keys(rules).length > 0 ? { [WEBFETCH_CATEGORY]: rules } : {};
30193
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({ permission }, null, 2) });
30194
+ }
30195
+ parseSettings() {
30196
+ const relativePath = join(this.getRelativeDirPath(), this.getRelativeFilePath());
30197
+ try {
30198
+ return parseSharedConfig({
30199
+ format: "json",
30200
+ fileContent: this.getFileContent() || "{}",
30201
+ filePath: relativePath,
30202
+ invalidRootPolicy: "error"
30203
+ });
30204
+ } catch (error) {
30205
+ throw new Error(`Failed to parse Copilot CLI settings in ${relativePath}: ${formatError(error)}`, { cause: error });
30206
+ }
30207
+ }
30208
+ validate() {
30209
+ return {
30210
+ success: true,
30211
+ error: null
30212
+ };
30213
+ }
30214
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
30215
+ return new CopilotcliPermissions({
30216
+ outputRoot,
30217
+ relativeDirPath,
30218
+ relativeFilePath,
30219
+ fileContent: "{}",
30220
+ validate: false,
30221
+ global
30222
+ });
30223
+ }
30224
+ };
30225
+ //#endregion
29190
30226
  //#region src/features/permissions/cursor-permissions.ts
29191
30227
  /**
29192
30228
  * Mapping from rulesync canonical tool category names (lowercase) to Cursor CLI
@@ -30204,6 +31240,9 @@ const GROK_TOOL_TO_CATEGORY = {
30204
31240
  WebSearch: "websearch"
30205
31241
  };
30206
31242
  const GROK_MCP_TOOL = "MCPTool";
31243
+ const GROK_TOOL_TO_CATEGORY_LOWER = Object.fromEntries(Object.entries(GROK_TOOL_TO_CATEGORY).map(([tool, category]) => [tool.toLowerCase(), category]));
31244
+ const GROK_MCP_TOOL_ALIASES = /* @__PURE__ */ new Set(["mcp", GROK_MCP_TOOL.toLowerCase()]);
31245
+ const GROKCLI_PERMISSION_RULES_KEY = "rules";
30207
31246
  /**
30208
31247
  * Build a Grok Claude-style permission entry (e.g. `Bash(git *)`, `Read`,
30209
31248
  * `MCPTool(server__tool)`) from a canonical category + pattern. Returns `null`
@@ -30256,6 +31295,44 @@ function parseGrokEntry(entry) {
30256
31295
  };
30257
31296
  }
30258
31297
  /**
31298
+ * Parse one entry of the verbose `[[permission.rules]]` form
31299
+ * (`{ action = "allow", tool = "bash", pattern = "git *" }`) into a canonical
31300
+ * category + pattern + action. Unlike the compact array entries these are TOML
31301
+ * tables, so the tool name arrives as its own field instead of an entry string.
31302
+ * Returns `null` for malformed entries and for tools with no canonical
31303
+ * equivalent, so callers can skip them (the whole `rules` array is preserved
31304
+ * verbatim on generate regardless).
31305
+ */
31306
+ function parseGrokRule(rule) {
31307
+ if (!isRecord$1(rule)) return null;
31308
+ const { action: rawAction, tool, pattern } = rule;
31309
+ const parsedAction = PermissionActionSchema.safeParse(rawAction);
31310
+ if (!parsedAction.success) return null;
31311
+ const action = parsedAction.data;
31312
+ if (typeof tool !== "string") return null;
31313
+ if (pattern !== void 0 && typeof pattern !== "string") return null;
31314
+ const trimmedPattern = pattern?.trim() ?? "";
31315
+ const resolvedPattern = trimmedPattern.length > 0 ? trimmedPattern : CATCH_ALL_PATTERN$2;
31316
+ const lowerTool = tool.trim().toLowerCase();
31317
+ if (GROK_MCP_TOOL_ALIASES.has(lowerTool)) return resolvedPattern === CATCH_ALL_PATTERN$2 ? {
31318
+ category: "mcp",
31319
+ pattern: CATCH_ALL_PATTERN$2,
31320
+ action
31321
+ } : {
31322
+ category: `${MCP_CANONICAL_PREFIX$1}${resolvedPattern}`,
31323
+ pattern: CATCH_ALL_PATTERN$2,
31324
+ action
31325
+ };
31326
+ if (!Object.hasOwn(GROK_TOOL_TO_CATEGORY_LOWER, lowerTool)) return null;
31327
+ const category = GROK_TOOL_TO_CATEGORY_LOWER[lowerTool];
31328
+ if (category === void 0) return null;
31329
+ return {
31330
+ category,
31331
+ pattern: resolvedPattern,
31332
+ action
31333
+ };
31334
+ }
31335
+ /**
30259
31336
  * Permissions adapter for the xAI Grok Build CLI (`grokcli`).
30260
31337
  *
30261
31338
  * Grok Build CLI ships a Claude-style rule system under `[permission]` in
@@ -30276,9 +31353,17 @@ function parseGrokEntry(entry) {
30276
31353
  * entry with different actions (e.g. `edit` allow + `write` deny → `Edit`),
30277
31354
  * the strictest wins (`deny > ask > allow`) and a warning is logged, so the
30278
31355
  * entry never lands contradictorily in two arrays.
30279
- * - Import: the `[permission]` arrays are parsed back into canonical
30280
- * categories. When no `[permission]` section is present (older configs), the
30281
- * coarse `[ui] permission_mode` is used as a fallback.
31356
+ * - Import: both documented `[permission]` forms are parsed back into
31357
+ * canonical categories the compact `allow`/`deny`/`ask` arrays and the
31358
+ * verbose `[[permission.rules]]` tables
31359
+ * (`{ action = "allow", tool = "bash", pattern = "git *" }`), whose `tool`
31360
+ * field is matched case-insensitively against the same tool table, plus
31361
+ * the documented `mcp` alias for the compact form's `MCPTool`. Rules
31362
+ * from the two forms are merged with the same `deny > ask > allow`
31363
+ * precedence. When neither form carries a rule (older configs), the coarse
31364
+ * `[ui] permission_mode` is used as a fallback. Generate always emits the
31365
+ * compact arrays, so a user-authored `rules` array is preserved verbatim
31366
+ * but not reconciled against them.
30282
31367
  *
30283
31368
  * The coarse `[ui] permission_mode` toggle is still written for backward
30284
31369
  * compatibility with older Grok versions: `always-approve` when the config is
@@ -30445,29 +31530,44 @@ function buildGrokPermissionArrays(config, existingPermission, logger) {
30445
31530
  };
30446
31531
  }
30447
31532
  /**
30448
- * Parse Grok's `[permission]` allow/deny/ask arrays back into a canonical
30449
- * permission map. Returns `null` when the section defines none of the three
30450
- * arrays, so the caller can fall back to the coarse `permission_mode`.
30451
- * Precedence `deny > ask > allow` is applied so a tool listed in multiple
30452
- * arrays resolves to the strictest action.
31533
+ * Parse Grok's `[permission]` section back into a canonical permission map,
31534
+ * reading both supported forms: the compact `allow`/`deny`/`ask` arrays of
31535
+ * Claude-style entries and the verbose `[[permission.rules]]` tables
31536
+ * (`{ action, tool, pattern }`). Returns `null` only when neither form carries
31537
+ * any rule, so the caller can fall back to the coarse `permission_mode`.
31538
+ * Precedence `deny > ask > allow` is applied across both forms, so a tool
31539
+ * described more than once resolves to the strictest action.
30453
31540
  */
30454
31541
  function parseGrokPermissionArrays(permission) {
30455
31542
  const allow = isStringArray$1(permission.allow) ? permission.allow : void 0;
30456
31543
  const deny = isStringArray$1(permission.deny) ? permission.deny : void 0;
30457
31544
  const ask = isStringArray$1(permission.ask) ? permission.ask : void 0;
30458
- if ((allow?.length ?? 0) + (deny?.length ?? 0) + (ask?.length ?? 0) === 0) return null;
31545
+ const rules = Array.isArray(permission[GROKCLI_PERMISSION_RULES_KEY]) ? permission[GROKCLI_PERMISSION_RULES_KEY] : void 0;
31546
+ if ((allow?.length ?? 0) + (deny?.length ?? 0) + (ask?.length ?? 0) + (rules?.length ?? 0) === 0) return null;
30459
31547
  const result = {};
30460
- const apply = (entries, action) => {
31548
+ const record = ({ category, pattern, action }) => {
31549
+ const bucket = result[category] ??= {};
31550
+ const existing = bucket[pattern];
31551
+ if (existing === void 0 || ACTION_RANK[action] > ACTION_RANK[existing]) bucket[pattern] = action;
31552
+ };
31553
+ const applyEntries = (entries, action) => {
30461
31554
  for (const entry of entries ?? []) {
30462
31555
  const parsed = parseGrokEntry(entry);
30463
31556
  if (parsed === null) continue;
30464
- const bucket = result[parsed.category] ??= {};
30465
- bucket[parsed.pattern] = action;
31557
+ record({
31558
+ ...parsed,
31559
+ action
31560
+ });
30466
31561
  }
30467
31562
  };
30468
- apply(allow, "allow");
30469
- apply(ask, "ask");
30470
- apply(deny, "deny");
31563
+ applyEntries(allow, "allow");
31564
+ applyEntries(ask, "ask");
31565
+ applyEntries(deny, "deny");
31566
+ for (const rule of rules ?? []) {
31567
+ const parsed = parseGrokRule(rule);
31568
+ if (parsed === null) continue;
31569
+ record(parsed);
31570
+ }
30471
31571
  return result;
30472
31572
  }
30473
31573
  /**
@@ -33152,6 +34252,13 @@ const TAKT_PROVIDER_PROFILES_KEY = "provider_profiles";
33152
34252
  const TAKT_DEFAULT_PERMISSION_MODE_KEY = "default_permission_mode";
33153
34253
  const TAKT_STEP_PERMISSION_OVERRIDES_KEY = "step_permission_overrides";
33154
34254
  const TAKT_PROVIDER_OPTIONS_KEY = "provider_options";
34255
+ const TAKT_RUNTIME_PROVIDER_KEY = "provider";
34256
+ const TAKT_RUNTIME_DEFAULTS_KEY = "defaults";
34257
+ const TAKT_RUNTIME_PROFILES_KEY = "profiles";
34258
+ const TAKT_RUNTIME_TARGETS_KEY = "targets";
34259
+ const TAKT_RUNTIME_AUTO_ROUTING_KEY = "auto_routing";
34260
+ const TAKT_RUNTIME_PROFILE_KEY = "profile";
34261
+ const TAKT_RUNTIME_OPTIONS_KEY = "options";
33155
34262
  const TAKT_SECURITY_POLICIES = {
33156
34263
  workflow_arpeggio: [
33157
34264
  "custom_data_source_modules",
@@ -33204,13 +34311,36 @@ const CATCH_ALL_PATTERN = "*";
33204
34311
  * profile, layered on top of `default_permission_mode`) and `provider_options`
33205
34312
  * (a top-level per-provider sandbox/network table). Both are authored on
33206
34313
  * generate and re-extracted on import.
34314
+ *
34315
+ * Takt 0.56.0 moved provider configuration into a separate `runtime.yaml`
34316
+ * (`.takt/runtime.yaml`, `~/.takt/runtime.yaml`). Once its `provider:` section
34317
+ * carries an assignment ("runtime mode" — the state a freshly installed Takt is
34318
+ * in, since it generates an active global runtime.yaml on first launch), any
34319
+ * legacy provider setting left in `config.yaml` makes Takt stop before running
34320
+ * an agent with "Mixed provider configuration detected". rulesync only reads
34321
+ * that file, in two places:
34322
+ * - the active provider is resolved from it first (the `provider_profiles`
34323
+ * key that carries the permission mode is still keyed by provider name, and
34324
+ * `provider_profiles` is not itself a legacy signal);
34325
+ * - an authored `provider_options` is refused with a warning rather than
34326
+ * written while runtime mode is active, and its runtime-side counterpart
34327
+ * (`provider.profiles.*.options`) is lifted back out on import.
34328
+ * Installs with no runtime.yaml, or an inactive one, keep the legacy behavior
34329
+ * unchanged.
33207
34330
  */
33208
34331
  var TaktPermissions = class TaktPermissions extends ToolPermissions {
33209
- constructor(params) {
34332
+ /**
34333
+ * The sibling `runtime.yaml` as read from the same scope, or `""` when the
34334
+ * install has none. Import needs it — `toRulesyncPermissions()` is
34335
+ * synchronous, so the file is read alongside config.yaml in `fromFile()`.
34336
+ */
34337
+ runtimeFileContent;
34338
+ constructor({ runtimeFileContent, ...params }) {
33210
34339
  super({
33211
34340
  ...params,
33212
34341
  fileContent: params.fileContent ?? ""
33213
34342
  });
34343
+ this.runtimeFileContent = runtimeFileContent ?? "";
33214
34344
  }
33215
34345
  isDeletable() {
33216
34346
  return false;
@@ -33224,11 +34354,13 @@ var TaktPermissions = class TaktPermissions extends ToolPermissions {
33224
34354
  static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
33225
34355
  const paths = TaktPermissions.getSettablePaths({ global });
33226
34356
  const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
34357
+ const runtimeFileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, "runtime.yaml")) ?? "";
33227
34358
  return new TaktPermissions({
33228
34359
  outputRoot,
33229
34360
  relativeDirPath: paths.relativeDirPath,
33230
34361
  relativeFilePath: paths.relativeFilePath,
33231
34362
  fileContent,
34363
+ runtimeFileContent,
33232
34364
  validate,
33233
34365
  global
33234
34366
  });
@@ -33243,12 +34375,22 @@ var TaktPermissions = class TaktPermissions extends ToolPermissions {
33243
34375
  filePath,
33244
34376
  invalidRootPolicy: "error"
33245
34377
  });
34378
+ const runtime = await readTaktRuntimeConfig({
34379
+ outputRoot,
34380
+ logger
34381
+ });
33246
34382
  const rulesyncJson = rulesyncPermissions.getJson();
33247
- const provider = resolveActiveProvider(config);
34383
+ const provider = resolveActiveProvider({
34384
+ config,
34385
+ runtime: runtime.active ? runtime.config : void 0
34386
+ });
33248
34387
  const mode = deriveTaktPermissionMode(rulesyncJson);
33249
34388
  const override = isPlainObject$1(rulesyncJson.takt) ? rulesyncJson.takt : void 0;
33250
34389
  const stepOverrides = isPlainObject$1(override?.[TAKT_STEP_PERMISSION_OVERRIDES_KEY]) ? override[TAKT_STEP_PERMISSION_OVERRIDES_KEY] : void 0;
33251
- const overrideProviderOptions = isPlainObject$1(override?.[TAKT_PROVIDER_OPTIONS_KEY]) ? override[TAKT_PROVIDER_OPTIONS_KEY] : void 0;
34390
+ const authoredProviderOptions = isPlainObject$1(override?.[TAKT_PROVIDER_OPTIONS_KEY]) ? override[TAKT_PROVIDER_OPTIONS_KEY] : void 0;
34391
+ const runtimeModeRefusesProviderOptions = authoredProviderOptions !== void 0 && runtime.active;
34392
+ if (runtimeModeRefusesProviderOptions) logger?.warn(`Takt permissions: not writing "${TAKT_PROVIDER_OPTIONS_KEY}" to ${filePath} because ${runtime.filePaths.join(" + ")} puts Takt in runtime provider mode (Takt 0.56.0+), where any legacy provider setting in config.yaml makes Takt fail with "Mixed provider configuration detected". Move those options to \`provider.profiles.<profile>.options\` in runtime.yaml, and remove them from the \`takt\` block of the rulesync source. A "${TAKT_PROVIDER_OPTIONS_KEY}" already in ${filePath} is left untouched — rulesync does not own that key.`);
34393
+ const overrideProviderOptions = runtimeModeRefusesProviderOptions ? void 0 : authoredProviderOptions;
33252
34394
  const authoredPolicies = pickSecurityPolicies(override, {
33253
34395
  filePath,
33254
34396
  logger,
@@ -33294,13 +34436,26 @@ var TaktPermissions = class TaktPermissions extends ToolPermissions {
33294
34436
  filePath: join(this.getRelativeDirPath(), this.getRelativeFilePath()),
33295
34437
  invalidRootPolicy: "error"
33296
34438
  });
33297
- const provider = resolveActiveProvider(config);
34439
+ const runtimeFilePath = join(this.getRelativeDirPath(), TAKT_RUNTIME_CONFIG_FILE_NAME);
34440
+ const runtime = parseSharedConfig({
34441
+ format: "yaml",
34442
+ fileContent: this.runtimeFileContent,
34443
+ filePath: runtimeFilePath
34444
+ });
34445
+ const runtimeActive = isRuntimeModeActive(runtime);
34446
+ const provider = resolveActiveProvider({
34447
+ config,
34448
+ runtime: runtimeActive ? runtime : void 0
34449
+ });
33298
34450
  const profiles = isPlainObject$1(config[TAKT_PROVIDER_PROFILES_KEY]) ? config[TAKT_PROVIDER_PROFILES_KEY] : {};
33299
34451
  const profile = isPlainObject$1(profiles[provider]) ? profiles[provider] : {};
33300
34452
  const mode = profile[TAKT_DEFAULT_PERMISSION_MODE_KEY];
33301
34453
  const rulesyncConfig = taktModeToRulesyncConfig(mode);
33302
34454
  const stepOverrides = isPlainObject$1(profile[TAKT_STEP_PERMISSION_OVERRIDES_KEY]) ? profile[TAKT_STEP_PERMISSION_OVERRIDES_KEY] : void 0;
33303
- const providerOptions = isPlainObject$1(config[TAKT_PROVIDER_OPTIONS_KEY]) ? config[TAKT_PROVIDER_OPTIONS_KEY] : void 0;
34455
+ const providerOptions = mergeSharedConfigDeep({
34456
+ base: isPlainObject$1(config[TAKT_PROVIDER_OPTIONS_KEY]) ? config[TAKT_PROVIDER_OPTIONS_KEY] : {},
34457
+ patch: runtimeActive ? collectRuntimeProviderOptions(runtime) : {}
34458
+ });
33304
34459
  const taktOverride = {};
33305
34460
  if (stepOverrides && Object.keys(stepOverrides).length > 0) taktOverride[TAKT_STEP_PERMISSION_OVERRIDES_KEY] = stepOverrides;
33306
34461
  if (providerOptions && Object.keys(providerOptions).length > 0) taktOverride[TAKT_PROVIDER_OPTIONS_KEY] = providerOptions;
@@ -33327,10 +34482,18 @@ var TaktPermissions = class TaktPermissions extends ToolPermissions {
33327
34482
  }
33328
34483
  };
33329
34484
  /**
33330
- * Resolve the active Takt provider: the top-level `provider:` value, else the
33331
- * sole key in `provider_profiles`, else the `claude` default.
34485
+ * Resolve the active Takt provider.
34486
+ *
34487
+ * Under runtime mode (Takt 0.56.0+) the real provider assignment lives in
34488
+ * `runtime.yaml`, and `config.yaml:provider` cannot coexist with it, so the
34489
+ * runtime document is consulted first: the provider of the profile named by
34490
+ * `provider.defaults.profile`. Everything else falls through to the legacy
34491
+ * chain — the top-level `provider:` value, else the sole key in
34492
+ * `provider_profiles`, else the `claude` default.
33332
34493
  */
33333
- function resolveActiveProvider(config) {
34494
+ function resolveActiveProvider({ config, runtime }) {
34495
+ const fromRuntime = runtime === void 0 ? void 0 : resolveRuntimeProvider(runtime);
34496
+ if (fromRuntime !== void 0) return fromRuntime;
33334
34497
  if (typeof config[TAKT_PROVIDER_KEY] === "string" && config[TAKT_PROVIDER_KEY].trim() !== "") return config[TAKT_PROVIDER_KEY];
33335
34498
  const profiles = config[TAKT_PROVIDER_PROFILES_KEY];
33336
34499
  if (isPlainObject$1(profiles)) {
@@ -33339,6 +34502,170 @@ function resolveActiveProvider(config) {
33339
34502
  }
33340
34503
  return TAKT_DEFAULT_PROVIDER;
33341
34504
  }
34505
+ /** A plain object carrying at least one entry (Takt's "assignment" test). */
34506
+ function hasEntries(value) {
34507
+ return isPlainObject$1(value) && Object.keys(value).length > 0;
34508
+ }
34509
+ function runtimeProfiles(runtime) {
34510
+ const provider = runtime[TAKT_RUNTIME_PROVIDER_KEY];
34511
+ if (!isPlainObject$1(provider)) return {};
34512
+ const profiles = provider[TAKT_RUNTIME_PROFILES_KEY];
34513
+ return isPlainObject$1(profiles) ? profiles : {};
34514
+ }
34515
+ /**
34516
+ * Whether a parsed `runtime.yaml` puts Takt into runtime mode, mirroring
34517
+ * upstream's mode detection: the `provider:` section must carry an actual
34518
+ * assignment — a non-empty `defaults`, `profiles` or `auto_routing`, or a
34519
+ * `targets` map with at least one non-empty nested map. The file existing is not
34520
+ * enough, and empty nested maps (`defaults: {}`, `targets: { personas: {} }`)
34521
+ * must not flip the mode.
34522
+ * https://github.com/nrslib/takt/blob/main/src/infra/config/runtime-provider/mode.ts
34523
+ */
34524
+ function isRuntimeModeActive(runtime) {
34525
+ const provider = runtime[TAKT_RUNTIME_PROVIDER_KEY];
34526
+ if (!isPlainObject$1(provider)) return false;
34527
+ if (hasEntries(provider[TAKT_RUNTIME_DEFAULTS_KEY]) || hasEntries(provider[TAKT_RUNTIME_PROFILES_KEY]) || hasEntries(provider[TAKT_RUNTIME_AUTO_ROUTING_KEY])) return true;
34528
+ const targets = provider[TAKT_RUNTIME_TARGETS_KEY];
34529
+ return isPlainObject$1(targets) && Object.values(targets).some(hasEntries);
34530
+ }
34531
+ /** The provider named by the runtime profile Takt would use by default. */
34532
+ function resolveRuntimeProvider(runtime) {
34533
+ const provider = runtime[TAKT_RUNTIME_PROVIDER_KEY];
34534
+ const profiles = runtimeProfiles(runtime);
34535
+ const providerOf = (profileName) => {
34536
+ if (profileName === void 0) return;
34537
+ const profile = profiles[profileName];
34538
+ if (!isPlainObject$1(profile)) return;
34539
+ const value = profile[TAKT_RUNTIME_PROVIDER_KEY];
34540
+ return typeof value === "string" && value.trim() !== "" ? value : void 0;
34541
+ };
34542
+ if (!isPlainObject$1(provider)) return;
34543
+ const defaults = provider[TAKT_RUNTIME_DEFAULTS_KEY];
34544
+ if (!isPlainObject$1(defaults) || typeof defaults[TAKT_RUNTIME_PROFILE_KEY] !== "string") return;
34545
+ return providerOf(defaults[TAKT_RUNTIME_PROFILE_KEY]);
34546
+ }
34547
+ /**
34548
+ * Lift the runtime profiles' flat `options` bags back into the per-provider
34549
+ * shape the `takt` override uses. Each profile's options belong to that
34550
+ * profile's own provider; when several profiles name the same provider they are
34551
+ * merged in document order, so a later profile wins on a colliding option key.
34552
+ */
34553
+ function collectRuntimeProviderOptions(runtime) {
34554
+ const collected = {};
34555
+ for (const profile of Object.values(runtimeProfiles(runtime))) {
34556
+ if (!isPlainObject$1(profile)) continue;
34557
+ const providerName = profile[TAKT_RUNTIME_PROVIDER_KEY];
34558
+ const options = profile[TAKT_RUNTIME_OPTIONS_KEY];
34559
+ if (typeof providerName !== "string" || providerName.trim() === "" || !hasEntries(options)) continue;
34560
+ const existing = collected[providerName];
34561
+ collected[providerName] = {
34562
+ ...isPlainObject$1(existing) ? existing : {},
34563
+ ...options
34564
+ };
34565
+ }
34566
+ return collected;
34567
+ }
34568
+ /**
34569
+ * Read one `runtime.yaml`. A file that cannot be parsed reports `unparsable`
34570
+ * rather than a document: Takt refuses to start on a broken runtime.yaml, and
34571
+ * treating it as legacy would be the one outcome that writes the
34572
+ * mixed-configuration key into the user's config.yaml.
34573
+ */
34574
+ async function readTaktRuntimeFile({ filePath, logger }) {
34575
+ const fileContent = await readFileContentOrNull(filePath);
34576
+ if (fileContent === null) return;
34577
+ try {
34578
+ return {
34579
+ config: parseSharedConfig({
34580
+ format: "yaml",
34581
+ fileContent,
34582
+ filePath
34583
+ }),
34584
+ unparsable: false
34585
+ };
34586
+ } catch (error) {
34587
+ logger?.warn(`Takt permissions: could not parse ${filePath} (${formatError(error)}); assuming Takt's runtime provider mode is active so no legacy provider setting is written to config.yaml.`);
34588
+ return {
34589
+ config: {},
34590
+ unparsable: true
34591
+ };
34592
+ }
34593
+ }
34594
+ /**
34595
+ * Collapse the project and global `runtime.yaml` into the single document Takt
34596
+ * itself resolves against, matching upstream's loader: `profiles` is a union
34597
+ * with the project definition of a same-named profile replacing the global one,
34598
+ * while `defaults`, `targets` and `auto_routing` are taken from the project file
34599
+ * whole whenever it states them at all (`??`, so a project `targets: {}` masks
34600
+ * the global one rather than merging with it).
34601
+ * https://github.com/nrslib/takt/blob/main/src/infra/config/runtime-provider/loader.ts
34602
+ *
34603
+ * Merging before mode detection matters in both directions: a project file
34604
+ * active only through `targets:` still resolves its provider from the global
34605
+ * file's `defaults`/`profiles`, and a project section that masks the global one
34606
+ * leaves the merged document inactive even though the global file alone was not.
34607
+ */
34608
+ function mergeTaktRuntimeConfigs({ project, global: globalConfig }) {
34609
+ const sectionOf = (config) => {
34610
+ const provider = config?.[TAKT_RUNTIME_PROVIDER_KEY];
34611
+ return isPlainObject$1(provider) ? provider : {};
34612
+ };
34613
+ const projectSection = sectionOf(project);
34614
+ const globalSection = sectionOf(globalConfig);
34615
+ const replaced = (key) => projectSection[key] ?? globalSection[key];
34616
+ const profilesOf = (section) => isPlainObject$1(section[TAKT_RUNTIME_PROFILES_KEY]) ? section[TAKT_RUNTIME_PROFILES_KEY] : void 0;
34617
+ const globalProfiles = profilesOf(globalSection);
34618
+ const projectProfiles = profilesOf(projectSection);
34619
+ const provider = {};
34620
+ if (globalProfiles !== void 0 || projectProfiles !== void 0) provider[TAKT_RUNTIME_PROFILES_KEY] = {
34621
+ ...globalProfiles,
34622
+ ...projectProfiles
34623
+ };
34624
+ for (const key of [
34625
+ TAKT_RUNTIME_DEFAULTS_KEY,
34626
+ TAKT_RUNTIME_TARGETS_KEY,
34627
+ TAKT_RUNTIME_AUTO_ROUTING_KEY
34628
+ ]) {
34629
+ const value = replaced(key);
34630
+ if (value !== void 0) provider[key] = value;
34631
+ }
34632
+ return { [TAKT_RUNTIME_PROVIDER_KEY]: provider };
34633
+ }
34634
+ /**
34635
+ * The runtime provider configuration in force for a generate run: the project
34636
+ * and global `runtime.yaml` merged the way Takt merges them, plus whether the
34637
+ * result puts Takt in runtime mode.
34638
+ *
34639
+ * Both scopes are read whichever scope is being generated: Takt collects legacy
34640
+ * signals from the project and global `config.yaml` alike, so a global
34641
+ * `runtime.yaml` — the one Takt generates on first launch — makes a legacy key
34642
+ * in the project config.yaml a hard failure just the same.
34643
+ */
34644
+ async function readTaktRuntimeConfig({ outputRoot, logger }) {
34645
+ const projectPath = join(outputRoot, TAKT_DIR, TAKT_RUNTIME_CONFIG_FILE_NAME);
34646
+ let globalPath;
34647
+ try {
34648
+ const resolved = join(getHomeDirectory(), TAKT_DIR, TAKT_RUNTIME_CONFIG_FILE_NAME);
34649
+ globalPath = resolved === projectPath ? void 0 : resolved;
34650
+ } catch {}
34651
+ const [project, globalFile] = await Promise.all([readTaktRuntimeFile({
34652
+ filePath: projectPath,
34653
+ logger
34654
+ }), globalPath === void 0 ? Promise.resolve(void 0) : readTaktRuntimeFile({
34655
+ filePath: globalPath,
34656
+ logger
34657
+ })]);
34658
+ const filePaths = [...project === void 0 ? [] : [projectPath], ...globalFile === void 0 || globalPath === void 0 ? [] : [globalPath]];
34659
+ const config = mergeTaktRuntimeConfigs({
34660
+ project: project?.config,
34661
+ global: globalFile?.config
34662
+ });
34663
+ return {
34664
+ filePaths,
34665
+ config,
34666
+ active: project?.unparsable === true || globalFile?.unparsable === true || isRuntimeModeActive(config)
34667
+ };
34668
+ }
33342
34669
  /**
33343
34670
  * Collapse a rulesync permissions config into Takt's single coarse mode.
33344
34671
  *
@@ -34198,6 +35525,57 @@ function asRecord(value) {
34198
35525
  return Object.fromEntries(Object.entries(value));
34199
35526
  }
34200
35527
  /**
35528
+ * The `agent` keys the `zed` override may author. Everything else — above all
35529
+ * `tool_permissions`, which the canonical `permission` block owns end to end —
35530
+ * is refused, so an override can never reach past its own surface and weaken a
35531
+ * canonical deny. The override object is read key by key rather than spread, so
35532
+ * an unlisted key is inert whether or not it is named here — an allowlist, not a
35533
+ * denylist, because `agent` carries blunt instruments of its own (Zed's
35534
+ * `always_allow_tool_actions` would disarm every permission rule at once), and a
35535
+ * verbatim `agent` merge would hand them to the override.
35536
+ *
35537
+ * Every key the patch does not consume is reported, so an unsupported or
35538
+ * misspelled one surfaces as a warning rather than as config that quietly does
35539
+ * nothing. `permission` is exempt: it is the canonical tool-scoped block, and
35540
+ * `RulesyncPermissions.forTarget` has already consumed and stripped it.
35541
+ */
35542
+ const ZED_OVERRIDE_AGENT_KEYS = ["sandbox_permissions", "profiles"];
35543
+ const ZED_CANONICAL_AGENT_KEY = "tool_permissions";
35544
+ const ZED_OVERRIDE_CONSUMED_KEYS = /* @__PURE__ */ new Set([...ZED_OVERRIDE_AGENT_KEYS, "permission"]);
35545
+ /**
35546
+ * Build the `agent` patch fragment carrying the `zed` override's verbatim
35547
+ * blocks. A key the override supplies replaces the existing block wholesale
35548
+ * (Zed reads each as one unit — a deep merge would leave half of a rewritten
35549
+ * sandbox policy behind); a key it omits is left out of the fragment, so the
35550
+ * existing value survives via the caller's spread of `agent`.
35551
+ */
35552
+ function buildZedOverridePatch({ override, logger }) {
35553
+ if (!isPlainObject$1(override)) return {};
35554
+ if (ZED_CANONICAL_AGENT_KEY in override) logger?.warn(`Zed permissions: ignoring the 'zed.${ZED_CANONICAL_AGENT_KEY}' override; \`agent.${ZED_CANONICAL_AGENT_KEY}\` is driven by the canonical permission block.`);
35555
+ const unsupportedKeys = Object.keys(override).filter((key) => key !== ZED_CANONICAL_AGENT_KEY && !ZED_OVERRIDE_CONSUMED_KEYS.has(key));
35556
+ if (unsupportedKeys.length > 0) logger?.warn(`Zed permissions: ignoring the ${unsupportedKeys.map((key) => `'zed.${key}'`).join(", ")} override ${unsupportedKeys.length === 1 ? "key" : "keys"} — the \`zed\` block authors only ${ZED_OVERRIDE_AGENT_KEYS.map((key) => `\`${key}\``).join(" and ")}.`);
35557
+ const patch = {};
35558
+ for (const key of ZED_OVERRIDE_AGENT_KEYS) {
35559
+ const value = override[key];
35560
+ if (isPlainObject$1(value)) patch[key] = value;
35561
+ }
35562
+ return patch;
35563
+ }
35564
+ /**
35565
+ * The write-side inverse: lift `agent.sandbox_permissions` / `agent.profiles`
35566
+ * back into the `zed` override so a hand-written sandbox policy or profile set
35567
+ * round-trips instead of being lost on the next generate. Returns `undefined`
35568
+ * when the settings carry neither, so the override key is omitted.
35569
+ */
35570
+ function extractZedOverride(agent) {
35571
+ const override = {};
35572
+ for (const key of ZED_OVERRIDE_AGENT_KEYS) {
35573
+ const value = agent[key];
35574
+ if (isPlainObject$1(value)) override[key] = value;
35575
+ }
35576
+ return Object.keys(override).length > 0 ? override : void 0;
35577
+ }
35578
+ /**
34201
35579
  * Permissions generator for the Zed editor.
34202
35580
  *
34203
35581
  * Zed maps tool permissions onto `agent.tool_permissions` inside its settings
@@ -34205,6 +35583,11 @@ function asRecord(value) {
34205
35583
  * global). That file is shared with the MCP (`context_servers`) and ignore
34206
35584
  * (`private_files`) features, so reads and writes merge into the existing JSON
34207
35585
  * rather than overwriting it, and the file is never deleted.
35586
+ *
35587
+ * Zed's OS sandbox (`agent.sandbox_permissions`) and its tool-availability
35588
+ * profiles (`agent.profiles`) are separate enforcement layers with no canonical
35589
+ * counterpart; they are authored verbatim through the `zed` override and lifted
35590
+ * back out of the settings on import. See `ZedPermissionsOverrideSchema`.
34208
35591
  */
34209
35592
  var ZedPermissions = class ZedPermissions extends ToolPermissions {
34210
35593
  constructor(params) {
@@ -34283,6 +35666,10 @@ var ZedPermissions = class ZedPermissions extends ToolPermissions {
34283
35666
  existingContent,
34284
35667
  patch: { agent: {
34285
35668
  ...agent,
35669
+ ...buildZedOverridePatch({
35670
+ override: config.zed,
35671
+ logger
35672
+ }),
34286
35673
  tool_permissions: {
34287
35674
  ...toolPermissions,
34288
35675
  ...managedDefault !== void 0 && { default: managedDefault },
@@ -34304,7 +35691,8 @@ var ZedPermissions = class ZedPermissions extends ToolPermissions {
34304
35691
  } catch (error) {
34305
35692
  throw new Error(`Failed to parse Zed permissions content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
34306
35693
  }
34307
- const toolPermissionsRaw = asRecord(settings.agent).tool_permissions;
35694
+ const agent = asRecord(settings.agent);
35695
+ const toolPermissionsRaw = agent.tool_permissions;
34308
35696
  const parsed = ZedToolPermissionsSchema.safeParse(toolPermissionsRaw ?? {});
34309
35697
  const tools = parsed.success ? parsed.data.tools ?? {} : {};
34310
35698
  const globalDefault = parsed.success ? parsed.data.default : void 0;
@@ -34328,7 +35716,10 @@ var ZedPermissions = class ZedPermissions extends ToolPermissions {
34328
35716
  for (const entry of toolPermission.always_deny ?? []) ensure(category)[entry.pattern] = "deny";
34329
35717
  for (const entry of toolPermission.always_confirm ?? []) ensure(category)[entry.pattern] = "ask";
34330
35718
  }
34331
- return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({ permission }, null, 2) });
35719
+ const zedOverride = extractZedOverride(agent);
35720
+ const result = { permission };
35721
+ if (zedOverride !== void 0) result.zed = zedOverride;
35722
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
34332
35723
  }
34333
35724
  validate() {
34334
35725
  return {
@@ -34414,6 +35805,14 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
34414
35805
  supportsImport: true
34415
35806
  }
34416
35807
  }],
35808
+ ["copilotcli", {
35809
+ class: CopilotcliPermissions,
35810
+ meta: {
35811
+ supportsProject: true,
35812
+ supportsGlobal: true,
35813
+ supportsImport: true
35814
+ }
35815
+ }],
34417
35816
  ["cursor", {
34418
35817
  class: CursorPermissions,
34419
35818
  meta: {
@@ -36367,7 +37766,8 @@ function openaiYamlToCodexcliSection(parsed) {
36367
37766
  /**
36368
37767
  * Represents a Codex CLI skill directory.
36369
37768
  * Codex CLI supports skills in both project mode (under $CWD/.agents/skills)
36370
- * and global mode (under $CODEX_HOME/skills, typically ~/.agents/skills).
37769
+ * and global mode (under ~/.agents/skills Rulesync resolves the home
37770
+ * directory rather than $CODEX_HOME).
36371
37771
  */
36372
37772
  var CodexCliSkill = class CodexCliSkill extends ToolSkill {
36373
37773
  constructor({ outputRoot = process.cwd(), relativeDirPath = CODEXCLI_SKILLS_DIR_PATH, dirName, frontmatter, body, otherFiles = [], validate = true, global = false }) {
@@ -38403,6 +39803,145 @@ var KiroIdeSkill = class extends KiroSkill {
38403
39803
  }
38404
39804
  };
38405
39805
  //#endregion
39806
+ //#region src/features/skills/musecode-skill.ts
39807
+ const MusecodeSkillFrontmatterSchema = z.looseObject({
39808
+ name: z.string(),
39809
+ description: z.string()
39810
+ });
39811
+ /**
39812
+ * Represents a Meta Muse Code skill directory.
39813
+ *
39814
+ * Muse Code reads Agent Skills as `<skill-id>/SKILL.md` directories from the
39815
+ * project (`.agents/skills/`) and from the user config
39816
+ * (`$XDG_CONFIG_HOME/muse/skills`, plus `~/.agents/skills`). rulesync emits the
39817
+ * project directory and, at global scope, only the XDG-default
39818
+ * `~/.config/muse/skills` so a skill is written exactly once. Muse Code's
39819
+ * compat scans of repo-local `.codex/skills` and `.claude/skills` belong to
39820
+ * other tools and are not emitted.
39821
+ * @see https://dev.meta.ai/docs/muse-code/extending.md
39822
+ */
39823
+ var MusecodeSkill = class MusecodeSkill extends ToolSkill {
39824
+ constructor({ outputRoot = process.cwd(), relativeDirPath = MUSECODE_SKILLS_DIR_PATH, dirName, frontmatter, body, otherFiles = [], validate = true, global = false }) {
39825
+ super({
39826
+ outputRoot,
39827
+ relativeDirPath,
39828
+ dirName,
39829
+ mainFile: {
39830
+ name: SKILL_FILE_NAME,
39831
+ body,
39832
+ frontmatter: { ...frontmatter }
39833
+ },
39834
+ otherFiles,
39835
+ global
39836
+ });
39837
+ if (validate) {
39838
+ const result = this.validate();
39839
+ if (!result.success) throw result.error;
39840
+ }
39841
+ }
39842
+ static getSettablePaths({ global = false } = {}) {
39843
+ return { relativeDirPath: global ? MUSECODE_GLOBAL_SKILLS_DIR_PATH : MUSECODE_SKILLS_DIR_PATH };
39844
+ }
39845
+ getFrontmatter() {
39846
+ return MusecodeSkillFrontmatterSchema.parse(this.requireMainFileFrontmatter());
39847
+ }
39848
+ getBody() {
39849
+ return this.mainFile?.body ?? "";
39850
+ }
39851
+ validate() {
39852
+ if (!this.mainFile) return {
39853
+ success: false,
39854
+ error: /* @__PURE__ */ new Error(`${this.getDirPath()}: ${SKILL_FILE_NAME} file does not exist`)
39855
+ };
39856
+ const result = MusecodeSkillFrontmatterSchema.safeParse(this.mainFile.frontmatter);
39857
+ if (!result.success) return {
39858
+ success: false,
39859
+ error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${this.getDirPath()}: ${formatError(result.error)}`)
39860
+ };
39861
+ return {
39862
+ success: true,
39863
+ error: null
39864
+ };
39865
+ }
39866
+ toRulesyncSkill() {
39867
+ const frontmatter = this.getFrontmatter();
39868
+ const rulesyncFrontmatter = {
39869
+ name: frontmatter.name,
39870
+ description: frontmatter.description,
39871
+ targets: ["*"]
39872
+ };
39873
+ return new RulesyncSkill({
39874
+ outputRoot: this.outputRoot,
39875
+ relativeDirPath: RULESYNC_SKILLS_RELATIVE_DIR_PATH,
39876
+ dirName: this.getDirName(),
39877
+ frontmatter: rulesyncFrontmatter,
39878
+ body: this.getBody(),
39879
+ otherFiles: this.getOtherFiles(),
39880
+ validate: true,
39881
+ global: this.global
39882
+ });
39883
+ }
39884
+ static fromRulesyncSkill({ outputRoot = process.cwd(), rulesyncSkill, validate = true, global = false }) {
39885
+ const settablePaths = MusecodeSkill.getSettablePaths({ global });
39886
+ const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
39887
+ const musecodeFrontmatter = {
39888
+ name: rulesyncFrontmatter.name,
39889
+ description: rulesyncFrontmatter.description
39890
+ };
39891
+ return new MusecodeSkill({
39892
+ outputRoot,
39893
+ relativeDirPath: settablePaths.relativeDirPath,
39894
+ dirName: rulesyncSkill.getDirName(),
39895
+ frontmatter: musecodeFrontmatter,
39896
+ body: rulesyncSkill.getBody(),
39897
+ otherFiles: rulesyncSkill.getOtherFiles(),
39898
+ validate,
39899
+ global
39900
+ });
39901
+ }
39902
+ static isTargetedByRulesyncSkill(rulesyncSkill) {
39903
+ const targets = rulesyncSkill.getFrontmatter().targets;
39904
+ return targets.includes("*") || targets.includes("musecode");
39905
+ }
39906
+ static async fromDir(params) {
39907
+ const loaded = await this.loadSkillDirContent({
39908
+ ...params,
39909
+ getSettablePaths: MusecodeSkill.getSettablePaths
39910
+ });
39911
+ const result = MusecodeSkillFrontmatterSchema.safeParse(loaded.frontmatter);
39912
+ if (!result.success) {
39913
+ const skillDirPath = join(loaded.outputRoot, loaded.relativeDirPath, loaded.dirName);
39914
+ throw new Error(`Invalid frontmatter in ${join(skillDirPath, SKILL_FILE_NAME)}: ${formatError(result.error)}`);
39915
+ }
39916
+ return new MusecodeSkill({
39917
+ outputRoot: loaded.outputRoot,
39918
+ relativeDirPath: loaded.relativeDirPath,
39919
+ dirName: loaded.dirName,
39920
+ frontmatter: result.data,
39921
+ body: loaded.body,
39922
+ otherFiles: loaded.otherFiles,
39923
+ validate: true,
39924
+ global: loaded.global
39925
+ });
39926
+ }
39927
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, dirName, global = false }) {
39928
+ const settablePaths = MusecodeSkill.getSettablePaths({ global });
39929
+ return new MusecodeSkill({
39930
+ outputRoot,
39931
+ relativeDirPath: relativeDirPath ?? settablePaths.relativeDirPath,
39932
+ dirName,
39933
+ frontmatter: {
39934
+ name: "",
39935
+ description: ""
39936
+ },
39937
+ body: "",
39938
+ otherFiles: [],
39939
+ validate: false,
39940
+ global
39941
+ });
39942
+ }
39943
+ };
39944
+ //#endregion
38406
39945
  //#region src/features/skills/opencode-skill.ts
38407
39946
  const OpenCodeSkillFrontmatterSchema = z.looseObject({
38408
39947
  name: z.string(),
@@ -40231,6 +41770,14 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
40231
41770
  supportsGlobal: true
40232
41771
  }
40233
41772
  }],
41773
+ ["musecode", {
41774
+ class: MusecodeSkill,
41775
+ meta: {
41776
+ supportsProject: true,
41777
+ supportsSimulated: false,
41778
+ supportsGlobal: true
41779
+ }
41780
+ }],
40234
41781
  ["opencode", {
40235
41782
  class: OpenCodeSkill,
40236
41783
  meta: {
@@ -42751,7 +44298,7 @@ var FactorydroidSubagent = class FactorydroidSubagent extends ToolSubagent {
42751
44298
  * `sub_recipes` list, which rulesync never wrote) — so those files were inert.
42752
44299
  * The custom-agent surface is the one Goose actually reads.
42753
44300
  *
42754
- * @see https://block.github.io/goose/docs/guides/context-engineering/custom-agents/
44301
+ * @see https://goose-docs.ai/docs/guides/context-engineering/custom-agents/
42755
44302
  */
42756
44303
  const GooseSubagentFrontmatterSchema = z.looseObject({
42757
44304
  name: z.string(),
@@ -48085,7 +49632,7 @@ var FactorydroidRule = class FactorydroidRule extends ToolRule {
48085
49632
  * touches during a session. The separate `.goose/memories/` tree is the Memory
48086
49633
  * extension's storage and is NOT auto-loaded as session context.
48087
49634
  * (Verified against the official docs:
48088
- * https://block.github.io/goose/docs/guides/context-engineering/using-goosehints/)
49635
+ * https://goose-docs.ai/docs/guides/context-engineering/using-goosehints/)
48089
49636
  *
48090
49637
  * rulesync's topic-based non-root rules have no project subdirectory to map onto,
48091
49638
  * so writing them under `.goose/memories/` made them effectively invisible to
@@ -48096,7 +49643,7 @@ var FactorydroidRule = class FactorydroidRule extends ToolRule {
48096
49643
  * Goose uses plain markdown files (.goosehints) without frontmatter.
48097
49644
  *
48098
49645
  * Global scope emits only `~/.config/goose/.goosehints`. Goose v1.41.0 (PR
48099
- * block/goose#9736) additionally loads the vendor-neutral
49646
+ * aaif-goose/goose#9736) additionally loads the vendor-neutral
48100
49647
  * `~/.agents/AGENTS.md` alongside the config-dir hints, but rulesync
48101
49648
  * deliberately does not emit that shared path from the goose target: the
48102
49649
  * config-dir hints remain fully loaded (no capability loss), and the
@@ -48804,6 +50351,73 @@ var KiroIdeRule = class extends KiroRule {
48804
50351
  }
48805
50352
  };
48806
50353
  //#endregion
50354
+ //#region src/features/rules/musecode-rule.ts
50355
+ var MusecodeRule = class MusecodeRule extends ToolRule {
50356
+ constructor({ fileContent, root, ...rest }) {
50357
+ super({
50358
+ ...rest,
50359
+ fileContent,
50360
+ root: root ?? false
50361
+ });
50362
+ }
50363
+ static getSettablePaths(_options = {}) {
50364
+ return { root: {
50365
+ relativeDirPath: ".",
50366
+ relativeFilePath: MUSECODE_RULE_FILE_NAME
50367
+ } };
50368
+ }
50369
+ static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, validate = true, global = false }) {
50370
+ const { root } = this.getSettablePaths({ global });
50371
+ const relativePath = join(root.relativeDirPath, root.relativeFilePath);
50372
+ const fileContent = await readFileContent(join(outputRoot, relativePath));
50373
+ return new MusecodeRule({
50374
+ outputRoot,
50375
+ relativeDirPath: root.relativeDirPath,
50376
+ relativeFilePath: root.relativeFilePath,
50377
+ fileContent,
50378
+ validate,
50379
+ root: true
50380
+ });
50381
+ }
50382
+ static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
50383
+ const { root } = this.getSettablePaths({ global });
50384
+ const isRoot = rulesyncRule.getFrontmatter().root ?? false;
50385
+ return new MusecodeRule({
50386
+ outputRoot,
50387
+ relativeDirPath: root.relativeDirPath,
50388
+ relativeFilePath: root.relativeFilePath,
50389
+ fileContent: rulesyncRule.getBody(),
50390
+ validate,
50391
+ root: isRoot
50392
+ });
50393
+ }
50394
+ toRulesyncRule() {
50395
+ return this.toRulesyncRuleDefault();
50396
+ }
50397
+ validate() {
50398
+ return {
50399
+ success: true,
50400
+ error: null
50401
+ };
50402
+ }
50403
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
50404
+ return new MusecodeRule({
50405
+ outputRoot,
50406
+ relativeDirPath,
50407
+ relativeFilePath,
50408
+ fileContent: "",
50409
+ validate: false,
50410
+ root: relativeFilePath === "AGENTS.md" && relativeDirPath === "."
50411
+ });
50412
+ }
50413
+ static isTargetedByRulesyncRule(rulesyncRule) {
50414
+ return this.isTargetedByRulesyncRuleDefault({
50415
+ rulesyncRule,
50416
+ toolTarget: "musecode"
50417
+ });
50418
+ }
50419
+ };
50420
+ //#endregion
48807
50421
  //#region src/features/rules/opencode-rule.ts
48808
50422
  var OpenCodeRule = class OpenCodeRule extends ToolRule {
48809
50423
  static getSettablePaths({ global, excludeToolDir } = {}) {
@@ -50384,6 +51998,15 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
50384
51998
  ruleDiscoveryMode: "toon"
50385
51999
  }
50386
52000
  }],
52001
+ ["musecode", {
52002
+ class: MusecodeRule,
52003
+ meta: {
52004
+ extension: "md",
52005
+ supportsGlobal: false,
52006
+ ruleDiscoveryMode: "auto",
52007
+ collisionPolicy: "fold"
52008
+ }
52009
+ }],
50387
52010
  ["opencode", {
50388
52011
  class: OpenCodeRule,
50389
52012
  meta: {
@@ -53043,4 +54666,4 @@ async function importChecksCore(params) {
53043
54666
  //#endregion
53044
54667
  export { JsonLogger as $, RULESYNC_PERMISSIONS_LEGACY_FILE_NAME as $t, RulesyncRuleFrontmatterSchema as A, ALL_TOOL_TARGETS_WITH_WILDCARD as At, RulesyncCheck as B, RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH as Bt, CODEXCLI_DIR as C, removeTempDirectory as Ct, RulesyncSkill as D, writeFileBuffer as Dt, RulesyncSubagentFrontmatterSchema as E, toPosixPath as Et, getRulesyncSourceCandidates as F, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as Ft, SKILL_FILE_NAME as G, RULESYNC_IGNORE_RELATIVE_FILE_PATH as Gt, stringifyFrontmatter as H, RULESYNC_HOOKS_FILE_NAME as Ht, resolveRulesyncSourceWritePath as I, RULESYNC_CHECKS_RELATIVE_DIR_PATH as It, ConfigFileSchema as J, RULESYNC_MCP_LEGACY_FILE_NAME as Jt, ConfigResolver as K, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as Kt, parseJsonc as L, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as Lt, RulesyncMcp as M, ToolTargetSchema as Mt, RulesyncIgnore as N, MAX_FILE_SIZE as Nt, RulesyncSkillFrontmatterSchema as O, writeFileContent as Ot, RulesyncHooks as P, RULESYNC_AIIGNORE_FILE_NAME as Pt, ConsoleLogger as Q, RULESYNC_PERMISSIONS_FILE_NAME as Qt, RulesyncCommand as R, RULESYNC_CONFIG_RELATIVE_FILE_PATH as Rt, CODEXCLI_BASH_RULES_FILE_NAME as S, removeFileStrict as St, RulesyncSubagent as T, runWithDirectoryRollback as Tt, loadYaml as U, RULESYNC_HOOKS_LEGACY_FILE_NAME as Ut, RulesyncCheckFrontmatterSchema as V, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as Vt, SHARED_USER_MANAGED_CONFIG_PATHS as W, RULESYNC_HOOKS_RELATIVE_FILE_PATH as Wt, SourceEntrySchema as X, RULESYNC_MCP_SCHEMA_URL as Xt, GITIGNORE_DESTINATION_KEY as Y, RULESYNC_MCP_RELATIVE_FILE_PATH as Yt, findControlCharacter as Z, RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH as Zt, CLAUDECODE_LOCAL_RULE_FILE_NAME as _, readFileContent as _t, convertFromTool as a, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as an, assertTreeContainsNoSymlinks as at, CLAUDECODE_SKILLS_DIR_PATH as b, removeDirectoryStrict as bt, SubagentsProcessor as c, ALL_FEATURES_WITH_WILDCARD as cn, createTempDirectory as ct, IgnoreProcessor as d, fileExists as dt, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as en, fallbackLogger as et, HooksProcessor as f, findFilesByGlobs as ft, CLAUDECODE_DIR as g, listDirectoryFiles as gt, QWENCODE_LOCAL_RULE_FILE_NAME as h, isSymlink as ht, getProcessorRegistryEntry as i, RULESYNC_SKILLS_RELATIVE_DIR_PATH as in, assertDirectoryIfExists as it, RulesyncPermissions as j, PACKAGING_TOOL_TARGETS as jt, RulesyncRule as k, ALL_TOOL_TARGETS as kt, SkillsProcessor as l, DEPRECATED_FEATURE_REPLACEMENTS as ln, directoryExists as lt, QWENCODE_DIR as m, getHomeDirectory as mt, checkRulesyncDirExists as n, RULESYNC_RELATIVE_DIR_PATH as nn, CLIError as nt, isPackagingToolTarget as o, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as on, assertWritablePathInsideRoot as ot, CommandsProcessor as p, getFileSize as pt, CONFLICTING_TARGET_PAIRS as q, RULESYNC_MCP_FILE_NAME as qt, generate as r, RULESYNC_RULES_RELATIVE_DIR_PATH as rn, ErrorCodes as rt, RulesProcessor as s, ALL_FEATURES as sn, checkPathTraversal as st, importFromTool as t, RULESYNC_PERMISSIONS_SCHEMA_URL as tn, warnOnConflictingFlags as tt, McpProcessor as u, formatError as un, ensureDir as ut, CLAUDECODE_MEMORIES_DIR_NAME as v, readFileContentOrNull as vt, getLocalSkillDirNames as w, resolvePath as wt, ChecksProcessor as x, removeFile as xt, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as y, removeDirectory as yt, RulesyncCommandFrontmatterSchema as z, RULESYNC_CONFIG_SCHEMA_URL as zt };
53045
54668
 
53046
- //# sourceMappingURL=import-BpKoN2US.js.map
54669
+ //# sourceMappingURL=import-CXJwVed1.js.map