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.
@@ -134,27 +134,27 @@ const isFeatureValueEnabled = (value) => {
134
134
  };
135
135
  //#endregion
136
136
  //#region src/constants/rulesync-paths.ts
137
- const { join: join$282 } = node_path.posix;
137
+ const { join: join$288 } = node_path.posix;
138
138
  const RULESYNC_CONFIG_RELATIVE_FILE_PATH = "rulesync.jsonc";
139
139
  const RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH = "rulesync.local.jsonc";
140
140
  const RULESYNC_RELATIVE_DIR_PATH = ".rulesync";
141
- const RULESYNC_RULES_RELATIVE_DIR_PATH = join$282(RULESYNC_RELATIVE_DIR_PATH, "rules");
142
- const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$282(RULESYNC_RULES_RELATIVE_DIR_PATH, ".curated");
143
- const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$282(RULESYNC_RELATIVE_DIR_PATH, "commands");
144
- const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$282(RULESYNC_RELATIVE_DIR_PATH, "subagents");
145
- const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$282(RULESYNC_RELATIVE_DIR_PATH, "checks");
146
- const RULESYNC_MCP_RELATIVE_FILE_PATH = join$282(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
147
- const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$282(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
148
- const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$282(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
149
- join$282(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
150
- const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$282(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
151
- const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$282(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
141
+ const RULESYNC_RULES_RELATIVE_DIR_PATH = join$288(RULESYNC_RELATIVE_DIR_PATH, "rules");
142
+ const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$288(RULESYNC_RULES_RELATIVE_DIR_PATH, ".curated");
143
+ const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$288(RULESYNC_RELATIVE_DIR_PATH, "commands");
144
+ const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$288(RULESYNC_RELATIVE_DIR_PATH, "subagents");
145
+ const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$288(RULESYNC_RELATIVE_DIR_PATH, "checks");
146
+ const RULESYNC_MCP_RELATIVE_FILE_PATH = join$288(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
147
+ const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$288(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
148
+ const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$288(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
149
+ join$288(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
150
+ const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$288(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
151
+ const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$288(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
152
152
  const RULESYNC_AIIGNORE_FILE_NAME = ".aiignore";
153
- const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$282(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
153
+ const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$288(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
154
154
  const RULESYNC_IGNORE_RELATIVE_FILE_PATH = ".rulesyncignore";
155
155
  const RULESYNC_OVERVIEW_FILE_NAME = "overview.md";
156
- const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$282(RULESYNC_RELATIVE_DIR_PATH, "skills");
157
- const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$282(RULESYNC_SKILLS_RELATIVE_DIR_PATH, ".curated");
156
+ const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$288(RULESYNC_RELATIVE_DIR_PATH, "skills");
157
+ const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$288(RULESYNC_SKILLS_RELATIVE_DIR_PATH, ".curated");
158
158
  const RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync.lock";
159
159
  const RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync-npm.lock.json";
160
160
  const RULESYNC_MCP_FILE_NAME = "mcp.jsonc";
@@ -196,6 +196,7 @@ const rulesProcessorToolTargetTuple = [
196
196
  "kiro",
197
197
  "kiro-cli",
198
198
  "kiro-ide",
199
+ "musecode",
199
200
  "opencode",
200
201
  "pi",
201
202
  "qwencode",
@@ -259,6 +260,7 @@ const mcpProcessorToolTargetTuple = [
259
260
  "kiro-cli",
260
261
  "kiro-ide",
261
262
  "junie",
263
+ "musecode",
262
264
  "opencode",
263
265
  "qwencode",
264
266
  "reasonix",
@@ -366,6 +368,7 @@ const skillsProcessorToolTargetTuple = [
366
368
  "kiro",
367
369
  "kiro-cli",
368
370
  "kiro-ide",
371
+ "musecode",
369
372
  "opencode",
370
373
  "pi",
371
374
  "qwencode",
@@ -420,6 +423,7 @@ const permissionsProcessorToolTargetTuple = [
420
423
  "cline",
421
424
  "codexcli",
422
425
  "copilot",
426
+ "copilotcli",
423
427
  "cursor",
424
428
  "devin",
425
429
  "factorydroid",
@@ -443,6 +447,7 @@ const permissionsProcessorToolTargetTuple = [
443
447
  ];
444
448
  const checksProcessorToolTargetTuple = [
445
449
  "amp",
450
+ "augmentcode",
446
451
  "cursor",
447
452
  "hermesagent",
448
453
  "rovodev",
@@ -1755,6 +1760,8 @@ const SHARED_USER_MANAGED_CONFIG_PATHS = [
1755
1760
  ".claude/settings.json",
1756
1761
  ".claude/settings.local.json",
1757
1762
  ".codex/config.toml",
1763
+ ".copilot/settings.json",
1764
+ ".github/copilot/settings.json",
1758
1765
  ".devin/config.json",
1759
1766
  ".factory/settings.json",
1760
1767
  ".grok/config.toml",
@@ -2613,8 +2620,8 @@ const CODEXCLI_HOOK_EVENTS = [
2613
2620
  * has NO `SubagentStart`/`SubagentStop` arms — emitting them would write keys
2614
2621
  * Goose silently ignores, so `subagentStart`/`subagentStop` are intentionally
2615
2622
  * excluded here and from `CANONICAL_TO_GOOSE_EVENT_NAMES`.
2616
- * @see https://github.com/block/goose/blob/v1.41.0/crates/goose/src/hooks/mod.rs
2617
- * @see https://block.github.io/goose/docs/guides/context-engineering/hooks/
2623
+ * @see https://github.com/aaif-goose/goose/blob/v1.41.0/crates/goose/src/hooks/mod.rs
2624
+ * @see https://goose-docs.ai/docs/guides/context-engineering/hooks/
2618
2625
  */
2619
2626
  const GOOSE_HOOK_EVENTS = [
2620
2627
  "sessionStart",
@@ -2651,8 +2658,8 @@ const KIRO_HOOK_EVENTS = [
2651
2658
  * `UserPromptSubmit`, `PreToolUse`, and `PostToolUse`. Kiro also documents
2652
2659
  * file-event (`PostFileCreate`/`PostFileSave`/`PostFileDelete`) and spec-task
2653
2660
  * (`PreTaskExec`/`PostTaskExec`) triggers that have no canonical equivalent;
2654
- * those can still be emitted verbatim via a `kiro-ide` or `kiro-cli` override
2655
- * block (unknown event keys pass through unchanged). There is no `SessionEnd`
2661
+ * those can still be emitted verbatim via the shared `kiro` override block
2662
+ * (unknown event keys pass through unchanged). There is no `SessionEnd`
2656
2663
  * trigger, so the canonical `sessionEnd` has no home here.
2657
2664
  * @see https://kiro.dev/docs/hooks/types/
2658
2665
  */
@@ -3385,6 +3392,47 @@ const CANONICAL_TO_KIRO_EVENT_NAMES = {
3385
3392
  stop: "stop"
3386
3393
  };
3387
3394
  /**
3395
+ * Native event keys of the embedded agent-config hook format, as listed by the
3396
+ * Kiro CLI 3.0 migration guide's old-format column: `agentSpawn`,
3397
+ * `userPromptSubmit`, `preToolUse`, `postToolUse`, `fileEdited`, `fileCreated`
3398
+ * and `agentStop`/`stop`. Two of them (`fileEdited`, `fileCreated`) have no
3399
+ * canonical equivalent, so they exist only here.
3400
+ *
3401
+ * The `kiro` override block is shared with the standalone-format targets, whose
3402
+ * trigger vocabulary is different, so the alias writer uses this list to decide
3403
+ * what it can express.
3404
+ * @see https://kiro.dev/docs/cli/v3/hooks-migration/
3405
+ */
3406
+ const KIRO_AGENT_CONFIG_NATIVE_EVENT_NAMES = [
3407
+ "agentSpawn",
3408
+ "userPromptSubmit",
3409
+ "preToolUse",
3410
+ "postToolUse",
3411
+ "fileEdited",
3412
+ "fileCreated",
3413
+ "agentStop",
3414
+ "stop"
3415
+ ];
3416
+ /**
3417
+ * Old agent-config event keys mapped to their standalone v1 trigger, so a
3418
+ * `kiro.hooks` block authored in the deprecated spelling still emits a valid
3419
+ * trigger for the `kiro-cli` / `kiro-ide` targets that read the same block.
3420
+ * The migration guide states the old names "map directly to their newer
3421
+ * equivalents" and spells out `agentSpawn` → `SessionStart` and `fileEdited` →
3422
+ * `PostFileSave`; the rest are the same event under the v1 casing.
3423
+ * @see https://kiro.dev/docs/cli/v3/hooks-migration/
3424
+ */
3425
+ const KIRO_LEGACY_TO_KIRO_IDE_TRIGGER_NAMES = {
3426
+ agentSpawn: "SessionStart",
3427
+ userPromptSubmit: "UserPromptSubmit",
3428
+ preToolUse: "PreToolUse",
3429
+ postToolUse: "PostToolUse",
3430
+ fileEdited: "PostFileSave",
3431
+ fileCreated: "PostFileCreate",
3432
+ agentStop: "Stop",
3433
+ stop: "Stop"
3434
+ };
3435
+ /**
3388
3436
  * Map Kiro CLI camelCase event names to canonical camelCase.
3389
3437
  */
3390
3438
  const KIRO_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_KIRO_EVENT_NAMES).map(([k, v]) => [v, k]));
@@ -3392,8 +3440,8 @@ const KIRO_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICA
3392
3440
  * Map canonical camelCase event names to Kiro IDE PascalCase triggers.
3393
3441
  *
3394
3442
  * Only the canonical lifecycle events with a clean IDE equivalent are mapped.
3395
- * Unknown keys (e.g. IDE-only `PostFileSave`/`PreTaskExec` set via a `kiro-ide`
3396
- * override) pass through unchanged.
3443
+ * Unknown keys (e.g. IDE-only `PostFileSave`/`PreTaskExec` set via the shared
3444
+ * `kiro` override block) pass through unchanged.
3397
3445
  * @see https://kiro.dev/docs/hooks/types/
3398
3446
  */
3399
3447
  const CANONICAL_TO_KIRO_IDE_EVENT_NAMES = {
@@ -4894,13 +4942,57 @@ const CodexcliPermissionsOverrideSchema = zod_mini.z.looseObject({
4894
4942
  git_write_rules: zod_mini.z.optional(zod_mini.z.boolean())
4895
4943
  });
4896
4944
  /**
4945
+ * Tool-scoped override block for Zed. Two Zed surfaces sit outside the canonical
4946
+ * allow/ask/deny model that `agent.tool_permissions` implements, and both are
4947
+ * authored here verbatim:
4948
+ *
4949
+ * - `sandbox_permissions` — the OS-level agent sandbox, on by default since Zed
4950
+ * 1.14.2 for the `terminal` and `fetch` tools. Its defaults forbid network
4951
+ * access, writing outside the project directories, and writing to `.git`, so a
4952
+ * real setup usually needs to relax one of them (`network_hosts` with exact
4953
+ * hostnames or leading `*.` wildcards, `allow_all_hosts`, `write_paths`,
4954
+ * `allow_fs_write_all`, `allow_unsandboxed`). This is containment, not tool
4955
+ * gating: no canonical category expresses it, so the block is written straight
4956
+ * into `agent.sandbox_permissions`, mirroring the Kilo / Claude Code / Codex
4957
+ * CLI `sandbox` passthrough precedent. Kept a bare `looseObject` rather than
4958
+ * enumerating the keys, since upstream adds to them release over release.
4959
+ * - `profiles` — the tool-availability layer, a separate enforcement stage from
4960
+ * `tool_permissions`: a tool absent from the active profile cannot be used no
4961
+ * matter what the permission rules say. Per-profile keys are `name`, `tools`
4962
+ * (per-tool booleans), `enable_all_context_servers`, `context_servers` and
4963
+ * `default_model`. Like Kimi Code's `tools.enabled`/`disabled` block this is a
4964
+ * verbatim passthrough — no canonicalization is attempted.
4965
+ *
4966
+ * Neither surface can weaken a canonical deny, because `agent.tool_permissions`
4967
+ * is off-limits to this override: the translator consumes only the two keys
4968
+ * above by name and rejects a `tool_permissions` key with a warning.
4969
+ *
4970
+ * @see https://zed.dev/docs/ai/sandboxing
4971
+ * @see https://zed.dev/docs/ai/agent-profiles
4972
+ *
4973
+ * @example
4974
+ * { "sandbox_permissions": { "network_hosts": ["*.github.com"], "write_paths": ["/tmp"] },
4975
+ * "profiles": { "review": { "name": "Review", "tools": { "terminal": false } } } }
4976
+ */
4977
+ const ZedPermissionsOverrideSchema = zod_mini.z.looseObject({
4978
+ permission: zod_mini.z.optional(ToolScopedPermissionSchema),
4979
+ sandbox_permissions: zod_mini.z.optional(zod_mini.z.looseObject({})),
4980
+ profiles: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.looseObject({
4981
+ name: zod_mini.z.optional(zod_mini.z.string()),
4982
+ tools: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.boolean())),
4983
+ enable_all_context_servers: zod_mini.z.optional(zod_mini.z.boolean()),
4984
+ context_servers: zod_mini.z.optional(zod_mini.z.looseObject({})),
4985
+ default_model: zod_mini.z.optional(zod_mini.z.looseObject({}))
4986
+ })))
4987
+ });
4988
+ /**
4897
4989
  * Permissions configuration.
4898
4990
  * Keys are tool category names (e.g., "bash", "edit", "read", "webfetch").
4899
4991
  * Values are pattern-to-action mappings for that tool category.
4900
4992
  *
4901
4993
  * The optional `opencode`/`hermes`/`cline`/`kilo`/`claudecode`/`vibe`/`cursor`/
4902
4994
  * `qwencode`/`reasonix`/`factorydroid`/`warp`/`junie`/`takt`/`amp`/
4903
- * `antigravity-cli`/`augmentcode`/`kiro`/`codexcli` keys are tool-scoped
4995
+ * `antigravity-cli`/`augmentcode`/`kiro`/`codexcli`/`zed` keys are tool-scoped
4904
4996
  * overrides consumed only by their respective translator (see the matching
4905
4997
  * `*PermissionsOverrideSchema`); every other tool reads the shared `permission`
4906
4998
  * block and ignores them.
@@ -4940,14 +5032,15 @@ const PermissionsConfigSchema = zod_mini.z.looseObject({
4940
5032
  augmentcode: zod_mini.z.optional(AugmentcodePermissionsOverrideSchema),
4941
5033
  kiro: zod_mini.z.optional(KiroPermissionsOverrideSchema),
4942
5034
  codexcli: zod_mini.z.optional(CodexcliPermissionsOverrideSchema),
5035
+ zed: zod_mini.z.optional(ZedPermissionsOverrideSchema),
4943
5036
  "antigravity-ide": zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
4944
5037
  copilot: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
5038
+ copilotcli: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
4945
5039
  devin: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
4946
5040
  goose: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
4947
5041
  grokcli: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
4948
5042
  "kimi-code": zod_mini.z.optional(KimiCodePermissionsOverrideSchema),
4949
- rovodev: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
4950
- zed: zod_mini.z.optional(CanonicalPermissionsOverrideSchema)
5043
+ rovodev: zod_mini.z.optional(CanonicalPermissionsOverrideSchema)
4951
5044
  });
4952
5045
  /**
4953
5046
  * Full permissions file schema including optional $schema field.
@@ -5660,6 +5753,33 @@ function resolveUserInvocable({ rootFrontmatter, section }) {
5660
5753
  return section?.["user-invocable"] ?? rootFrontmatter["user-invocable"];
5661
5754
  }
5662
5755
  //#endregion
5756
+ //#region src/constants/augmentcode-paths.ts
5757
+ const AUGMENTCODE_DIR = ".augment";
5758
+ const AUGMENTCODE_COMMANDS_DIR_PATH = (0, node_path.join)(AUGMENTCODE_DIR, "commands");
5759
+ /**
5760
+ * The cross-tool `.agents/commands/` root Auggie also discovers commands from.
5761
+ * Auggie resolves commands over `.augment`, `.claude` and `.agents` in both the
5762
+ * workspace and the home directory; rulesync writes to its own `.augment` root
5763
+ * and reads this one as well, mirroring how skills treat `.agents/skills`.
5764
+ * @see https://docs.augmentcode.com/cli/custom-commands
5765
+ */
5766
+ const AUGMENTCODE_AGENTS_COMMANDS_DIR_PATH = (0, node_path.join)(".agents", "commands");
5767
+ const AUGMENTCODE_SKILLS_DIR_PATH = (0, node_path.join)(AUGMENTCODE_DIR, "skills");
5768
+ const AUGMENTCODE_AGENTS_DIR_PATH = (0, node_path.join)(AUGMENTCODE_DIR, "agents");
5769
+ const AUGMENTCODE_ALT_AGENTS_DIR_PATH = ".agents";
5770
+ const AUGMENTCODE_AGENTS_SKILLS_DIR_PATH = (0, node_path.join)(".agents", "skills");
5771
+ const AUGMENTCODE_SETTINGS_FILE_NAME = "settings.json";
5772
+ const AUGMENTCODE_SETTINGS_LOCAL_FILE_NAME = "settings.local.json";
5773
+ /**
5774
+ * Augment Code Review's custom guidelines file. Read from the repository root's
5775
+ * `.augment/` folder, project scope only — Augment documents no user-level
5776
+ * equivalent, and the reviewer runs against the committed tree.
5777
+ * @see https://docs.augmentcode.com/codereview/review-guidelines
5778
+ */
5779
+ const AUGMENTCODE_CODE_REVIEW_GUIDELINES_FILE_NAME = "code_review_guidelines.yaml";
5780
+ const AUGMENTCODE_IGNORE_FILE_NAME = ".augmentignore";
5781
+ const AUGMENTCODE_LEGACY_RULE_FILE_NAME = ".augment-guidelines";
5782
+ //#endregion
5663
5783
  //#region src/constants/cursor-paths.ts
5664
5784
  const CURSOR_DIR = ".cursor";
5665
5785
  const CURSOR_COMMANDS_DIR_PATH = (0, node_path.join)(CURSOR_DIR, "commands");
@@ -5709,6 +5829,19 @@ const TAKT_RULE_OVERVIEW_FILE_NAME = "overview.md";
5709
5829
  */
5710
5830
  const TAKT_CONFIG_FILE_NAME = "config.yaml";
5711
5831
  /**
5832
+ * Takt's runtime provider config (Takt 0.56.0+). Lives at `.takt/runtime.yaml`
5833
+ * (project) and `~/.takt/runtime.yaml` (global), and owns provider/model/
5834
+ * provider-option assignment. Takt generates the global file on first launch,
5835
+ * so most installs have one; "runtime mode" only activates when its `provider:`
5836
+ * section carries an actual assignment.
5837
+ *
5838
+ * rulesync only ever READS this file: it resolves the active provider from it,
5839
+ * and it refuses to write the legacy `provider_options` key into `config.yaml`
5840
+ * while runtime mode is active (Takt hard-fails on that combination).
5841
+ * @see https://github.com/nrslib/takt/blob/main/docs/configuration.md
5842
+ */
5843
+ const TAKT_RUNTIME_CONFIG_FILE_NAME = "runtime.yaml";
5844
+ /**
5712
5845
  * Top-level key in Takt's `config.yaml` holding the workflow MCP transport
5713
5846
  * allowlist (`stdio` / `sse` / `http` booleans). Takt is default-deny: a
5714
5847
  * transport must be explicitly enabled here before any workflow-defined MCP
@@ -6184,6 +6317,363 @@ function slugifyCheckName(value) {
6184
6317
  return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48).replace(/-+$/, "");
6185
6318
  }
6186
6319
  //#endregion
6320
+ //#region src/features/checks/augmentcode-check.ts
6321
+ /** Augment's severity scale. Narrower than the canonical one. */
6322
+ const AUGMENTCODE_SEVERITIES = [
6323
+ "high",
6324
+ "medium",
6325
+ "low"
6326
+ ];
6327
+ /**
6328
+ * Canonical severity to Augment's scale. Augment has no band above `high`, so
6329
+ * canonical `critical` folds into it — the alternative, dropping the rule or
6330
+ * demoting it to `medium`, would either lose the check or understate the one
6331
+ * severity a reviewer most wants raised. The fold is one-way: a `critical`
6332
+ * check generates `high` and imports back as `high`, so the canonical value is
6333
+ * not recoverable from Augment's file alone.
6334
+ */
6335
+ const CANONICAL_TO_AUGMENTCODE_SEVERITY = {
6336
+ low: "low",
6337
+ medium: "medium",
6338
+ high: "high",
6339
+ critical: "high"
6340
+ };
6341
+ /**
6342
+ * Every field of an `areas.<key>` entry is required upstream, so a check with no
6343
+ * `severity` still has to emit one. `medium` is the neutral middle of Augment's
6344
+ * three bands: defaulting to `high` would inflate every unannotated check past
6345
+ * the ones deliberately marked `medium`, and `low` would bury them.
6346
+ */
6347
+ const DEFAULT_AUGMENTCODE_SEVERITY = "medium";
6348
+ /**
6349
+ * Augment's own example uses `["**"]`, and `globs` is required per area. Built
6350
+ * fresh per area rather than shared: js-yaml serializes a repeated reference as
6351
+ * an anchor/alias pair, which would litter a file Augment tells users to edit by
6352
+ * hand with `&ref_0` / `*ref_0`.
6353
+ */
6354
+ const defaultAreaGlobs = () => ["**"];
6355
+ /**
6356
+ * The `augmentcode` block of a check's frontmatter, carrying the two things the
6357
+ * canonical check model has no home for: which area a rule belongs to (so
6358
+ * several checks can share one), and the area's `globs`.
6359
+ */
6360
+ const AugmentcodeCheckOverrideSchema = zod_mini.z.looseObject({
6361
+ /** Area key this check's rule is grouped under; defaults to the check's slug. */
6362
+ area: zod_mini.z.optional(zod_mini.z.string().check(zod_mini.z.minLength(1))),
6363
+ /** Human-readable `areas.<key>.description`; defaults to the check's own. */
6364
+ areaDescription: zod_mini.z.optional(zod_mini.z.string()),
6365
+ globs: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string().check(zod_mini.z.minLength(1)))),
6366
+ /** Rule `id`; defaults to the check's slug. */
6367
+ id: zod_mini.z.optional(zod_mini.z.string().check(zod_mini.z.minLength(1)))
6368
+ });
6369
+ function parseOverride$1(raw, filePath, logger) {
6370
+ if (raw === void 0) return {};
6371
+ if (!isPlainObject$1(raw)) {
6372
+ logger?.warn(`Ignoring the \`augmentcode\` block in ${filePath}: expected a mapping.`);
6373
+ return {};
6374
+ }
6375
+ const result = AugmentcodeCheckOverrideSchema.safeParse(raw);
6376
+ if (!result.success) throw new Error(`Invalid \`augmentcode\` block in ${filePath}: ${result.error.message}`, { cause: result.error });
6377
+ return result.data;
6378
+ }
6379
+ function stemOf$1(rulesyncCheck) {
6380
+ return (0, node_path.basename)(rulesyncCheck.getRelativeFilePath(), ".md");
6381
+ }
6382
+ /**
6383
+ * Disambiguate a name against the ones already taken. Used for rule ids on
6384
+ * generate (two same-named checks in different subdirectories) and for check
6385
+ * file names on import (one rule id repeated across two areas), since a
6386
+ * collision in either direction silently loses one of the pair.
6387
+ */
6388
+ function uniqueName(preferred, used) {
6389
+ let name = preferred;
6390
+ let suffix = 2;
6391
+ while (used.has(name)) {
6392
+ name = `${preferred}-${suffix}`;
6393
+ suffix += 1;
6394
+ }
6395
+ used.add(name);
6396
+ return name;
6397
+ }
6398
+ /**
6399
+ * A rule's `description` is the whole instruction Augment acts on, so the check
6400
+ * body is preferred; the frontmatter `description` is a summary, used only when
6401
+ * there is no body. Both empty falls back to the check name, which at least
6402
+ * names the concern rather than emitting an empty required field.
6403
+ */
6404
+ function toRuleDescription(rulesyncCheck) {
6405
+ const body = rulesyncCheck.getBody().trim();
6406
+ if (body.length > 0) return body;
6407
+ return rulesyncCheck.getFrontmatter().description?.trim() || stemOf$1(rulesyncCheck);
6408
+ }
6409
+ function toRule(rulesyncCheck, override, usedRuleIds) {
6410
+ const severity = rulesyncCheck.getFrontmatter().severity;
6411
+ return {
6412
+ id: uniqueName(override.id ?? slugifyCheckName(stemOf$1(rulesyncCheck)), usedRuleIds),
6413
+ description: toRuleDescription(rulesyncCheck),
6414
+ severity: severity ? CANONICAL_TO_AUGMENTCODE_SEVERITY[severity] ?? DEFAULT_AUGMENTCODE_SEVERITY : DEFAULT_AUGMENTCODE_SEVERITY
6415
+ };
6416
+ }
6417
+ /**
6418
+ * An authored area key is used verbatim. Slugifying it would rewrite the
6419
+ * underscores in Augment's own documented example (`memory_safety`), and since
6420
+ * import writes the key back unchanged, the next generate would build a second
6421
+ * area under the slugified spelling while the original stayed put — the same
6422
+ * rules twice. Only the file-stem default is slugified, because that one has to
6423
+ * become a legal area key from an arbitrary file name.
6424
+ */
6425
+ function areaKeyFor(rulesyncCheck, override) {
6426
+ return override.area ?? slugifyCheckName(stemOf$1(rulesyncCheck));
6427
+ }
6428
+ /** Rule ids belonging to areas this generate preserves rather than rewrites. */
6429
+ function collectRuleIds(existingAreas, claimedKeys) {
6430
+ const ids = /* @__PURE__ */ new Set();
6431
+ for (const [areaKey, rawArea] of Object.entries(existingAreas)) {
6432
+ if (claimedKeys.has(areaKey) || !isPlainObject$1(rawArea)) continue;
6433
+ for (const rawRule of readArea(rawArea).rules) {
6434
+ const rule = readRule(rawRule);
6435
+ if (rule) ids.add(rule.id);
6436
+ }
6437
+ }
6438
+ return ids;
6439
+ }
6440
+ /**
6441
+ * Group the checks into `areas`. One area per check by default; checks naming
6442
+ * the same `augmentcode.area` share one, with the first check to name it
6443
+ * supplying the area's `description` and `globs`.
6444
+ */
6445
+ function buildAreas({ entries, reservedRuleIds }) {
6446
+ const areas = /* @__PURE__ */ new Map();
6447
+ const usedRuleIds = new Set(reservedRuleIds);
6448
+ for (const { rulesyncCheck, override } of entries) {
6449
+ const key = areaKeyFor(rulesyncCheck, override);
6450
+ const rule = toRule(rulesyncCheck, override, usedRuleIds);
6451
+ const existing = areas.get(key);
6452
+ if (existing) {
6453
+ existing.rules.push(rule);
6454
+ continue;
6455
+ }
6456
+ areas.set(key, {
6457
+ description: override.areaDescription || rulesyncCheck.getFrontmatter().description?.trim() || stemOf$1(rulesyncCheck),
6458
+ globs: override.globs ?? defaultAreaGlobs(),
6459
+ rules: [rule]
6460
+ });
6461
+ }
6462
+ return Object.fromEntries(areas);
6463
+ }
6464
+ function parseGuidelines(fileContent, filePath) {
6465
+ if (fileContent.trim().length === 0) return {};
6466
+ let parsed;
6467
+ try {
6468
+ parsed = loadYaml(fileContent);
6469
+ } catch (error) {
6470
+ throw new Error(`Failed to parse AugmentCode code review guidelines at ${filePath}: ${formatError(error)}`, { cause: error });
6471
+ }
6472
+ if (parsed === void 0 || parsed === null) return {};
6473
+ if (!isPlainObject$1(parsed)) throw new Error(`Failed to parse AugmentCode code review guidelines at ${filePath}: expected a mapping at the document root.`);
6474
+ return parsed;
6475
+ }
6476
+ /** An area's shared fields, read defensively out of hand-written YAML. */
6477
+ function readArea(rawArea) {
6478
+ const rawGlobs = Array.isArray(rawArea.globs) ? rawArea.globs : void 0;
6479
+ const globs = rawGlobs?.filter((glob) => typeof glob === "string");
6480
+ const authoredGlobs = globs && (globs.length > 0 || rawGlobs?.length === 0) ? globs : void 0;
6481
+ return {
6482
+ ...typeof rawArea.description === "string" && { description: rawArea.description },
6483
+ ...authoredGlobs && { globs: authoredGlobs },
6484
+ rules: Array.isArray(rawArea.rules) ? rawArea.rules : []
6485
+ };
6486
+ }
6487
+ /**
6488
+ * A rule missing `id` or `description` is not a shape Augment itself reads, so
6489
+ * it is skipped rather than imported as a check with an invented field.
6490
+ */
6491
+ function readRule(rawRule) {
6492
+ if (!isPlainObject$1(rawRule)) return void 0;
6493
+ const id = typeof rawRule.id === "string" ? rawRule.id : void 0;
6494
+ const description = typeof rawRule.description === "string" ? rawRule.description : void 0;
6495
+ if (!id || !description) return void 0;
6496
+ const severity = AUGMENTCODE_SEVERITIES.find((value) => value === rawRule.severity);
6497
+ return {
6498
+ id,
6499
+ description,
6500
+ ...severity && { severity }
6501
+ };
6502
+ }
6503
+ /**
6504
+ * Checks adapter for Augment Code Review's custom guidelines
6505
+ * (`.augment/code_review_guidelines.yaml`).
6506
+ *
6507
+ * Augment groups review rules into named **areas**: each area carries a
6508
+ * `description`, the `globs` it applies to, and a list of `rules`, every rule
6509
+ * being an `id` / `description` / `severity` triple. Every one of those fields
6510
+ * is required upstream. Rulesync maps one `.rulesync/checks/*.md` onto one rule,
6511
+ * in an area of its own unless an `augmentcode.area` groups several together —
6512
+ * so the whole set collapses into this one file via {@link fromRulesyncChecks}
6513
+ * rather than a file per check.
6514
+ *
6515
+ * **Severity is lossy in one direction.** Augment's scale is `high` / `medium` /
6516
+ * `low` with no band above `high`, so canonical `critical` maps to `high` and
6517
+ * imports back as `high`. A check with no `severity` at all emits `medium`,
6518
+ * since the field cannot be omitted.
6519
+ *
6520
+ * **`file_paths_to_ignore`** is recognized and preserved verbatim, but never
6521
+ * authored or imported: the canonical check model has no ignore surface, and
6522
+ * inventing one here is a separate design question.
6523
+ *
6524
+ * **Generation merges rather than replaces.** Augment's own documentation tells
6525
+ * users to hand-write this file, so only the areas the current check set claims
6526
+ * are rewritten; every other area, `file_paths_to_ignore`, and any unknown
6527
+ * top-level key survive. The cost of that is that rulesync cannot tell its own
6528
+ * leftovers from a hand-written area: renaming a check strands the area under
6529
+ * the old key, and dropping the last Augment-targeting check leaves the areas in
6530
+ * place with a warning rather than guessing which of them to delete. Deleting
6531
+ * the file outright is likewise refused whenever it exists.
6532
+ *
6533
+ * Project scope only — the reviewer reads the file from the committed
6534
+ * repository, and Augment documents no user-level equivalent.
6535
+ *
6536
+ * @see https://docs.augmentcode.com/codereview/review-guidelines
6537
+ */
6538
+ var AugmentcodeCheck = class AugmentcodeCheck extends ToolCheck {
6539
+ static getSettablePaths(_options = {}) {
6540
+ return {
6541
+ relativeDirPath: AUGMENTCODE_DIR,
6542
+ relativeFilePath: AUGMENTCODE_CODE_REVIEW_GUIDELINES_FILE_NAME
6543
+ };
6544
+ }
6545
+ static isTargetedByRulesyncCheck(rulesyncCheck) {
6546
+ return this.isTargetedByRulesyncCheckDefault({
6547
+ rulesyncCheck,
6548
+ toolTarget: "augmentcode"
6549
+ });
6550
+ }
6551
+ /**
6552
+ * Ownership guard the processor consults before it deletes anything for this
6553
+ * tool. Unlike the Markdown adapters, whose section markers say which text is
6554
+ * rulesync's, YAML carries no such marker — js-yaml drops comments on rewrite,
6555
+ * and an unknown top-level key risks Augment's own parser. So an existing file
6556
+ * is never rulesync's to delete: it may hold hand-written areas, and there is
6557
+ * no way to prove otherwise.
6558
+ */
6559
+ static async canDeleteAuxiliaryFiles({ outputRoot }) {
6560
+ const paths = AugmentcodeCheck.getSettablePaths();
6561
+ return !await fileExists((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath ?? "code_review_guidelines.yaml"));
6562
+ }
6563
+ static fromRulesyncCheck(_params) {
6564
+ throw new Error("AugmentCode checks are built from all checks at once; use fromRulesyncChecks.");
6565
+ }
6566
+ static async fromRulesyncChecks({ outputRoot = process.cwd(), rulesyncChecks, global = false, logger }) {
6567
+ const paths = AugmentcodeCheck.getSettablePaths({ global });
6568
+ const relativeFilePath = paths.relativeFilePath ?? "code_review_guidelines.yaml";
6569
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, relativeFilePath);
6570
+ const existing = parseGuidelines(await readFileContentOrNull(filePath) ?? "", filePath);
6571
+ const existingAreas = isPlainObject$1(existing.areas) ? existing.areas : {};
6572
+ if (rulesyncChecks.length === 0) {
6573
+ 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.`);
6574
+ return [];
6575
+ }
6576
+ const entries = rulesyncChecks.map((rulesyncCheck) => ({
6577
+ rulesyncCheck,
6578
+ override: parseOverride$1(rulesyncCheck.getFrontmatter().augmentcode, (0, node_path.join)(RULESYNC_CHECKS_RELATIVE_DIR_PATH, rulesyncCheck.getRelativeFilePath()), logger)
6579
+ }));
6580
+ const generatedAreas = buildAreas({
6581
+ entries,
6582
+ reservedRuleIds: collectRuleIds(existingAreas, new Set(entries.map(({ rulesyncCheck, override }) => areaKeyFor(rulesyncCheck, override))))
6583
+ });
6584
+ const fileContent = (0, js_yaml.dump)({
6585
+ ...existing,
6586
+ areas: {
6587
+ ...existingAreas,
6588
+ ...generatedAreas
6589
+ }
6590
+ }, {
6591
+ lineWidth: -1,
6592
+ noRefs: true
6593
+ });
6594
+ return [new AugmentcodeCheck({
6595
+ outputRoot,
6596
+ relativeDirPath: paths.relativeDirPath,
6597
+ relativeFilePath,
6598
+ fileContent,
6599
+ global
6600
+ })];
6601
+ }
6602
+ static async fromFile({ outputRoot = process.cwd(), global = false }) {
6603
+ const paths = AugmentcodeCheck.getSettablePaths({ global });
6604
+ const relativeFilePath = paths.relativeFilePath ?? "code_review_guidelines.yaml";
6605
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, relativeFilePath);
6606
+ return new AugmentcodeCheck({
6607
+ outputRoot,
6608
+ relativeDirPath: paths.relativeDirPath,
6609
+ relativeFilePath,
6610
+ fileContent: await readFileContentOrNull(filePath) ?? "",
6611
+ global
6612
+ });
6613
+ }
6614
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
6615
+ return new AugmentcodeCheck({
6616
+ outputRoot,
6617
+ relativeDirPath,
6618
+ relativeFilePath,
6619
+ fileContent: "",
6620
+ validate: false,
6621
+ global
6622
+ });
6623
+ }
6624
+ validate() {
6625
+ return {
6626
+ success: true,
6627
+ error: null
6628
+ };
6629
+ }
6630
+ toRulesyncCheck() {
6631
+ const first = this.toRulesyncChecks()[0];
6632
+ if (!first) throw new Error(`No review areas found in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}.`);
6633
+ return first;
6634
+ }
6635
+ /**
6636
+ * One check per rule, not per area: an area with three rules is three
6637
+ * independent review instructions, and collapsing them into one check would
6638
+ * make the next generate emit a single merged rule in their place. The area's
6639
+ * key, description and globs ride along in each check's `augmentcode` block,
6640
+ * so regenerating regroups the rules exactly where they were.
6641
+ */
6642
+ toRulesyncChecks() {
6643
+ const filePath = (0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath());
6644
+ const guidelines = parseGuidelines(this.getFileContent(), filePath);
6645
+ const areas = isPlainObject$1(guidelines.areas) ? guidelines.areas : {};
6646
+ const checks = [];
6647
+ const usedNames = /* @__PURE__ */ new Set();
6648
+ for (const [areaKey, rawArea] of Object.entries(areas)) {
6649
+ if (!isPlainObject$1(rawArea)) continue;
6650
+ const { description: areaDescription, globs, rules } = readArea(rawArea);
6651
+ for (const rawRule of rules) {
6652
+ const rule = readRule(rawRule);
6653
+ if (!rule) continue;
6654
+ const name = uniqueName(slugifyCheckName(rule.id) || slugifyCheckName(areaKey) || "check", usedNames);
6655
+ checks.push(new RulesyncCheck({
6656
+ outputRoot: ".",
6657
+ relativeDirPath: RULESYNC_CHECKS_RELATIVE_DIR_PATH,
6658
+ relativeFilePath: `${name}.md`,
6659
+ frontmatter: {
6660
+ targets: ["*"],
6661
+ ...rule.severity && { severity: rule.severity },
6662
+ augmentcode: {
6663
+ area: areaKey,
6664
+ ...areaDescription !== void 0 && { areaDescription },
6665
+ ...globs && { globs },
6666
+ id: rule.id
6667
+ }
6668
+ },
6669
+ body: rule.description
6670
+ }));
6671
+ }
6672
+ }
6673
+ return checks;
6674
+ }
6675
+ };
6676
+ //#endregion
6187
6677
  //#region src/features/checks/aggregated-check-file.ts
6188
6678
  /**
6189
6679
  * Shared machinery for the tools whose checks surface is **one aggregated
@@ -7106,6 +7596,22 @@ const SHARED_CONFIG_OWNERSHIP = {
7106
7596
  }
7107
7597
  }
7108
7598
  },
7599
+ ".github/copilot/settings.json": {
7600
+ format: "json",
7601
+ invalidRootPolicy: "error",
7602
+ features: { permissions: {
7603
+ kind: "replace-owned-keys",
7604
+ ownedKeys: ["deniedUrls"]
7605
+ } }
7606
+ },
7607
+ ".copilot/settings.json": {
7608
+ format: "json",
7609
+ invalidRootPolicy: "error",
7610
+ features: { permissions: {
7611
+ kind: "replace-owned-keys",
7612
+ ownedKeys: ["allowedUrls", "deniedUrls"]
7613
+ } }
7614
+ },
7109
7615
  ".vscode/settings.json": {
7110
7616
  format: "jsonc",
7111
7617
  invalidRootPolicy: "error",
@@ -7186,6 +7692,14 @@ const SHARED_CONFIG_OWNERSHIP = {
7186
7692
  }
7187
7693
  }
7188
7694
  },
7695
+ ".config/muse/settings.json": {
7696
+ format: "json",
7697
+ invalidRootPolicy: "error",
7698
+ features: { mcp: {
7699
+ kind: "replace-owned-keys",
7700
+ ownedKeys: ["mcp_servers", "schema_version"]
7701
+ } }
7702
+ },
7189
7703
  ".kiro/agents/default.json": {
7190
7704
  format: "json",
7191
7705
  features: {
@@ -7833,6 +8347,14 @@ const toolCheckFactories = /* @__PURE__ */ new Map([
7833
8347
  filePattern: "*.md"
7834
8348
  }
7835
8349
  }],
8350
+ ["augmentcode", {
8351
+ class: AugmentcodeCheck,
8352
+ meta: {
8353
+ supportsGlobal: false,
8354
+ filePattern: AUGMENTCODE_CODE_REVIEW_GUIDELINES_FILE_NAME,
8355
+ committedOutput: true
8356
+ }
8357
+ }],
7836
8358
  ["cursor", {
7837
8359
  class: CursorCheck,
7838
8360
  meta: {
@@ -8620,26 +9142,6 @@ var AntigravityIdeCommand = class extends AntigravitySharedCommand {
8620
9142
  }
8621
9143
  };
8622
9144
  //#endregion
8623
- //#region src/constants/augmentcode-paths.ts
8624
- const AUGMENTCODE_DIR = ".augment";
8625
- const AUGMENTCODE_COMMANDS_DIR_PATH = (0, node_path.join)(AUGMENTCODE_DIR, "commands");
8626
- /**
8627
- * The cross-tool `.agents/commands/` root Auggie also discovers commands from.
8628
- * Auggie resolves commands over `.augment`, `.claude` and `.agents` in both the
8629
- * workspace and the home directory; rulesync writes to its own `.augment` root
8630
- * and reads this one as well, mirroring how skills treat `.agents/skills`.
8631
- * @see https://docs.augmentcode.com/cli/custom-commands
8632
- */
8633
- const AUGMENTCODE_AGENTS_COMMANDS_DIR_PATH = (0, node_path.join)(".agents", "commands");
8634
- const AUGMENTCODE_SKILLS_DIR_PATH = (0, node_path.join)(AUGMENTCODE_DIR, "skills");
8635
- const AUGMENTCODE_AGENTS_DIR_PATH = (0, node_path.join)(AUGMENTCODE_DIR, "agents");
8636
- const AUGMENTCODE_ALT_AGENTS_DIR_PATH = ".agents";
8637
- const AUGMENTCODE_AGENTS_SKILLS_DIR_PATH = (0, node_path.join)(".agents", "skills");
8638
- const AUGMENTCODE_SETTINGS_FILE_NAME = "settings.json";
8639
- const AUGMENTCODE_SETTINGS_LOCAL_FILE_NAME = "settings.local.json";
8640
- const AUGMENTCODE_IGNORE_FILE_NAME = ".augmentignore";
8641
- const AUGMENTCODE_LEGACY_RULE_FILE_NAME = ".augment-guidelines";
8642
- //#endregion
8643
9145
  //#region src/features/commands/augmentcode-command.ts
8644
9146
  const AugmentcodeCommandFrontmatterSchema = zod_mini.z.looseObject({
8645
9147
  description: zod_mini.z.optional(zod_mini.z.string()),
@@ -9173,6 +9675,8 @@ const COPILOT_GLOBAL_HOOKS_FILE_NAME = "copilot-ide-hooks.json";
9173
9675
  const COPILOT_MCP_DIR = ".vscode";
9174
9676
  const COPILOT_MCP_FILE_NAME = "mcp.json";
9175
9677
  const COPILOT_VSCODE_SETTINGS_FILE_NAME = "settings.json";
9678
+ const COPILOTCLI_SETTINGS_FILE_NAME = "settings.json";
9679
+ const COPILOTCLI_PROJECT_SETTINGS_DIR_PATH = (0, node_path.join)(GITHUB_DIR, "copilot");
9176
9680
  const COPILOTCLI_MCP_FILE_NAME = "mcp-config.json";
9177
9681
  const COPILOTCLI_PROJECT_MCP_FILE_NAME = "mcp.json";
9178
9682
  const COPILOTCLI_AGENTS_DIR_PATH = (0, node_path.join)(COPILOT_DIR, "agents");
@@ -9432,6 +9936,7 @@ const DEVIN_HOOKS_V1_FILE_NAME = "hooks.v1.json";
9432
9936
  const DEVIN_GLOBAL_AGENTS_FILE_NAME = "AGENTS.md";
9433
9937
  const DEVIN_IGNORE_FILE_NAME = ".devinignore";
9434
9938
  const DEVIN_LEGACY_IGNORE_FILE_NAME = ".codeiumignore";
9939
+ const DEVIN_WINDSURF_IGNORE_FILE_NAME = ".windsurfignore";
9435
9940
  const DEVIN_GLOBAL_IGNORE_DIR_PATH = ".codeium";
9436
9941
  const DEVIN_GLOBAL_IGNORE_FILE_NAME = DEVIN_LEGACY_IGNORE_FILE_NAME;
9437
9942
  //#endregion
@@ -9786,7 +10291,7 @@ var GooseCommandConfigFile = class extends ToolFile {
9786
10291
  * The whole file is a YAML mapping (not frontmatter + markdown body), so the
9787
10292
  * class stores the parsed recipe object rather than a frontmatter/body split.
9788
10293
  *
9789
- * @see https://block.github.io/goose/docs/guides/recipes/recipe-reference/
10294
+ * @see https://goose-docs.ai/docs/guides/recipes/recipe-reference/
9790
10295
  */
9791
10296
  const GooseCommandRecipeSchema = zod_mini.z.looseObject({
9792
10297
  version: zod_mini.z.optional(zod_mini.z.string()),
@@ -13280,7 +13785,7 @@ function collectHandlers({ effectiveHooks, eventMap }) {
13280
13785
  }
13281
13786
  return handlerGroups;
13282
13787
  }
13283
- function buildCommandLines({ handler, usesToolName, blocksToolCall }) {
13788
+ function buildCommandLines$1({ handler, usesToolName, blocksToolCall }) {
13284
13789
  const lines = [];
13285
13790
  const indent = usesToolName && handler.matcher ? " " : " ";
13286
13791
  if (usesToolName && handler.matcher) lines.push(` if (new RegExp(${matcherToEmbeddedLiteral$1(handler.matcher)}).test(event.tool)) {`);
@@ -13305,7 +13810,7 @@ function buildSubscriptionLines$1(handlerGroups) {
13305
13810
  const usesToolName = AMP_TOOL_EVENTS.has(ampEvent) && handlers.some((handler) => handler.matcher);
13306
13811
  const blocksToolCall = ampEvent === "tool.call";
13307
13812
  lines.push(` amp.on(${JSON.stringify(ampEvent)}, async (${usesToolName ? "event, ctx" : "_event, ctx"}) => {`);
13308
- for (const handler of handlers) lines.push(...buildCommandLines({
13813
+ for (const handler of handlers) lines.push(...buildCommandLines$1({
13309
13814
  handler,
13310
13815
  usesToolName,
13311
13816
  blocksToolCall
@@ -15290,8 +15795,10 @@ const CopilotHookEntrySchema = zod_mini.z.looseObject({
15290
15795
  bash: zod_mini.z.optional(zod_mini.z.string()),
15291
15796
  powershell: zod_mini.z.optional(zod_mini.z.string()),
15292
15797
  command: zod_mini.z.optional(zod_mini.z.string()),
15798
+ cwd: zod_mini.z.optional(zod_mini.z.string()),
15293
15799
  env: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.string())),
15294
- timeoutSec: zod_mini.z.optional(zod_mini.z.number())
15800
+ timeoutSec: zod_mini.z.optional(zod_mini.z.number()),
15801
+ timeout: zod_mini.z.optional(zod_mini.z.number())
15295
15802
  });
15296
15803
  /**
15297
15804
  * Convert canonical hooks config to Copilot format.
@@ -15371,6 +15878,22 @@ function resolveImportCommand$1(entry, logger) {
15371
15878
  return typeof entry.command === "string" ? { command: entry.command } : {};
15372
15879
  }
15373
15880
  /**
15881
+ * Extract the non-command fields preserved across import.
15882
+ *
15883
+ * Generate re-emits any non-canonical key verbatim through `rest`, so a key
15884
+ * dropped here does not survive an import → generate round trip. `cwd` is a
15885
+ * documented Copilot hook field and was previously lost that way.
15886
+ *
15887
+ * @see https://docs.github.com/en/copilot/reference/hooks-reference
15888
+ * @see https://docs.github.com/en/copilot/concepts/agents/coding-agent/about-hooks
15889
+ */
15890
+ function importPassthrough$1(entry) {
15891
+ const passthrough = {};
15892
+ if (entry.cwd !== void 0) passthrough.cwd = entry.cwd;
15893
+ if (entry.env !== void 0) passthrough.env = entry.env;
15894
+ return passthrough;
15895
+ }
15896
+ /**
15374
15897
  * Extract hooks from Copilot hooks JSON into canonical format.
15375
15898
  * Copilot format: { version: 1, hooks: { eventName: [...hookEntries] } }
15376
15899
  */
@@ -15386,13 +15909,13 @@ function copilotHooksToCanonical(copilotHooks, logger) {
15386
15909
  if (!parseResult.success) continue;
15387
15910
  const entry = parseResult.data;
15388
15911
  const { command, shell } = resolveImportCommand$1(entry, logger);
15389
- const timeout = entry.timeoutSec;
15912
+ const timeout = entry.timeoutSec ?? entry.timeout;
15390
15913
  defs.push({
15391
15914
  type: "command",
15392
15915
  ...command !== void 0 && { command },
15393
15916
  ...shell !== void 0 && { shell },
15394
- ...entry.env !== void 0 && { env: entry.env },
15395
- ...timeout !== void 0 && { timeout }
15917
+ ...timeout !== void 0 && { timeout },
15918
+ ...importPassthrough$1(entry)
15396
15919
  });
15397
15920
  }
15398
15921
  if (defs.length > 0) canonical[eventName] = defs;
@@ -15675,19 +16198,19 @@ function importPassthrough(entry) {
15675
16198
  * A shell-specific field carries its `shell` through so re-export writes the
15676
16199
  * same field back. An entry using only the portable `command` field leaves
15677
16200
  * `shell` unset, which re-export renders as the portable field again.
16201
+ *
16202
+ * When both shell-specific fields are present, `bash` wins and a warning names
16203
+ * the ignored `powershell`. The choice is deliberately not platform-dependent:
16204
+ * importing on Windows must not produce a different canonical config than
16205
+ * importing the same file on Linux, which would make the rulesync hooks file
16206
+ * differ per machine for anyone who checks it in.
15678
16207
  */
15679
16208
  function resolveImportCommand(entry, logger) {
15680
16209
  const hasBash = typeof entry.bash === "string";
15681
16210
  const hasPowershell = typeof entry.powershell === "string";
15682
16211
  if (hasBash && hasPowershell) {
15683
- const isWindows = process.platform === "win32";
15684
- const chosen = isWindows ? "powershell" : "bash";
15685
- const ignored = isWindows ? "bash" : "powershell";
15686
- logger?.warn(`Copilot CLI hook has both bash and powershell commands; using ${chosen} and ignoring ${ignored} on this platform.`);
15687
- return isWindows ? {
15688
- command: entry.powershell,
15689
- shell: "powershell"
15690
- } : {
16212
+ 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.");
16213
+ return {
15691
16214
  command: entry.bash,
15692
16215
  shell: "bash"
15693
16216
  };
@@ -16346,7 +16869,7 @@ const GOOSE_CONVERTER_CONFIG = {
16346
16869
  *
16347
16870
  * The JSON shape matches Claude Code's: each PascalCase event maps to an array of
16348
16871
  * `{ matcher, hooks: [{ type: "command", command }] }` entries.
16349
- * @see https://block.github.io/goose/docs/guides/context-engineering/hooks/
16872
+ * @see https://goose-docs.ai/docs/guides/context-engineering/hooks/
16350
16873
  */
16351
16874
  var GooseHooks = class GooseHooks extends ToolHooks {
16352
16875
  constructor(params) {
@@ -17255,6 +17778,38 @@ function stripTrustedDirectoryWrapper(command) {
17255
17778
  if (posix?.[1]) return posix[1];
17256
17779
  return command.match(/^set "RULESYNC_KIMI_HOOK_CWD=1" && cd \/d "(?:""|[^"])*" && ([\s\S]*)$/)?.[1] ?? command;
17257
17780
  }
17781
+ /**
17782
+ * Native Kimi Code events whose Event Reference row lists the matcher as
17783
+ * "Empty string". Kimi Code documents `matcher` as "a regular expression to
17784
+ * filter event targets; if omitted, matches all", so on these events the
17785
+ * regex is tested against `""`: any non-trivial matcher simply never matches
17786
+ * and the hook silently never runs. Dropping the matcher is what makes the
17787
+ * hook fire at all, which is the authored intent — these events have no target
17788
+ * to filter on in the first place.
17789
+ *
17790
+ * Keyed on native names because the check runs after the canonical → native
17791
+ * mapping: `SessionHeartbeat` and `Interrupt` have no canonical counterpart and
17792
+ * are only reachable through a per-tool `kimi-code` override naming them
17793
+ * directly.
17794
+ *
17795
+ * Deliberately narrower than Claude Code's equivalent set: Kimi Code's
17796
+ * `UserPromptSubmit` matches the submitted prompt text, and `PermissionResult`
17797
+ * matches the tool name, so a matcher on either is meaningful and is kept.
17798
+ *
17799
+ * @see https://moonshotai.github.io/kimi-code/en/customization/hooks.html
17800
+ */
17801
+ const KIMI_CODE_NO_MATCHER_EVENTS = /* @__PURE__ */ new Set([
17802
+ "Stop",
17803
+ "SessionHeartbeat",
17804
+ "Interrupt"
17805
+ ]);
17806
+ /** Resolve the `matcher` part of an emitted entry, dropping dead matchers. */
17807
+ function resolveMatcherPart({ matcher, nativeEvent, logger }) {
17808
+ if (!matcher) return {};
17809
+ if (!KIMI_CODE_NO_MATCHER_EVENTS.has(nativeEvent)) return { matcher };
17810
+ logger?.warn(`matcher "${matcher}" on "${nativeEvent}" hook will be ignored — this event does not support matchers`);
17811
+ return {};
17812
+ }
17258
17813
  function buildEffectiveHooks(config, toolOverrideHooks) {
17259
17814
  const supported = new Set(KIMI_CODE_HOOK_EVENTS);
17260
17815
  const shared = {};
@@ -17284,7 +17839,11 @@ function canonicalToKimiCodeHooks({ config, toolOverrideHooks, trustedDirectory,
17284
17839
  command: definition.command,
17285
17840
  trustedDirectory
17286
17841
  }),
17287
- ...definition.matcher && { matcher: definition.matcher },
17842
+ ...resolveMatcherPart({
17843
+ matcher: definition.matcher,
17844
+ nativeEvent,
17845
+ logger
17846
+ }),
17288
17847
  ...validTimeout && timeout !== void 0 && { timeout }
17289
17848
  });
17290
17849
  }
@@ -17482,17 +18041,33 @@ function buildKiroIdeEntriesForEvent(trigger, definitions) {
17482
18041
  }
17483
18042
  return entries;
17484
18043
  }
17485
- function canonicalToKiroIdeHooks(config, overrideKey) {
18044
+ /**
18045
+ * The single `HooksConfig` key every Kiro target reads its tool-specific
18046
+ * overrides from.
18047
+ *
18048
+ * `kiro-ide` and `kiro-cli` write the same `.kiro/hooks/rulesync.json` at both
18049
+ * scopes, so per-target override blocks would make that one file's content
18050
+ * depend on generation order (last writer wins). All Kiro variants therefore
18051
+ * share the `kiro` block — the same resolution the Kiro MCP and permissions
18052
+ * wiring already use for the file they share.
18053
+ */
18054
+ const KIRO_HOOKS_OVERRIDE_KEY = "kiro";
18055
+ /**
18056
+ * Override keys a user might reach for that nothing reads, mapped to the key
18057
+ * that is actually read.
18058
+ */
18059
+ const KIRO_HOOKS_IGNORED_OVERRIDE_KEYS = ["kiro-ide", "kiro-cli"];
18060
+ function canonicalToKiroIdeHooks(config) {
17486
18061
  const kiroIdeSupported = new Set(KIRO_IDE_HOOK_EVENTS);
17487
18062
  const sharedHooks = {};
17488
18063
  for (const [event, defs] of Object.entries(config.hooks)) if (kiroIdeSupported.has(event)) sharedHooks[event] = defs;
17489
18064
  const effectiveHooks = {
17490
18065
  ...sharedHooks,
17491
- ...config[overrideKey]?.hooks
18066
+ ...config[KIRO_HOOKS_OVERRIDE_KEY]?.hooks
17492
18067
  };
17493
18068
  const entries = [];
17494
18069
  for (const [eventName, definitions] of Object.entries(effectiveHooks)) {
17495
- const trigger = CANONICAL_TO_KIRO_IDE_EVENT_NAMES[eventName] ?? eventName;
18070
+ const trigger = CANONICAL_TO_KIRO_IDE_EVENT_NAMES[eventName] ?? KIRO_LEGACY_TO_KIRO_IDE_TRIGGER_NAMES[eventName] ?? eventName;
17496
18071
  entries.push(...buildKiroIdeEntriesForEvent(trigger, definitions));
17497
18072
  }
17498
18073
  return entries;
@@ -17537,6 +18112,11 @@ function kiroIdeHooksToCanonical(entries) {
17537
18112
  * still writes the embedded `.kiro/agents/default.json` agent-config shape,
17538
18113
  * which Kiro CLI 3.0 no longer reads.
17539
18114
  *
18115
+ * Because both targets write the very same file, they resolve their
18116
+ * tool-specific overrides from one shared block
18117
+ * ({@link KIRO_HOOKS_OVERRIDE_KEY}) rather than per-target blocks, so
18118
+ * generating either or both targets always yields the same file.
18119
+ *
17540
18120
  * @see https://kiro.dev/docs/hooks/
17541
18121
  */
17542
18122
  var KiroIdeHooks = class extends ToolHooks {
@@ -17549,14 +18129,6 @@ var KiroIdeHooks = class extends ToolHooks {
17549
18129
  }, null, 2)
17550
18130
  });
17551
18131
  }
17552
- /**
17553
- * The `HooksConfig` key whose `hooks` block provides tool-specific overrides
17554
- * for this target. {@link import("./kiro-cli-hooks.js").KiroCliHooks}
17555
- * overrides this to `kiro-cli`.
17556
- */
17557
- static getOverrideKey() {
17558
- return "kiro-ide";
17559
- }
17560
18132
  static getSettablePaths(_options = {}) {
17561
18133
  return {
17562
18134
  relativeDirPath: KIRO_IDE_HOOKS_DIR_PATH,
@@ -17577,9 +18149,14 @@ var KiroIdeHooks = class extends ToolHooks {
17577
18149
  validate
17578
18150
  });
17579
18151
  }
17580
- static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false }) {
18152
+ static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
17581
18153
  const paths = this.getSettablePaths({ global });
17582
- const hooks = canonicalToKiroIdeHooks(rulesyncHooks.getJson(), this.getOverrideKey());
18154
+ const config = rulesyncHooks.getJson();
18155
+ for (const ignoredKey of KIRO_HOOKS_IGNORED_OVERRIDE_KEYS) {
18156
+ if (config[ignoredKey]?.hooks === void 0) continue;
18157
+ logger?.warn(`The "${ignoredKey}.hooks" block in ${(0, node_path.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.`);
18158
+ }
18159
+ const hooks = canonicalToKiroIdeHooks(config);
17583
18160
  const fileContent = JSON.stringify({
17584
18161
  version: "v1",
17585
18162
  hooks
@@ -17600,10 +18177,9 @@ var KiroIdeHooks = class extends ToolHooks {
17600
18177
  throw new Error(`Failed to parse Kiro IDE hooks content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
17601
18178
  }
17602
18179
  const hooks = kiroIdeHooksToCanonical(parsed.hooks ?? []);
17603
- const overrideKey = this.constructor.getOverrideKey();
17604
18180
  return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
17605
18181
  hooks,
17606
- overrideKey
18182
+ overrideKey: KIRO_HOOKS_OVERRIDE_KEY
17607
18183
  }), null, 2) });
17608
18184
  }
17609
18185
  validate() {
@@ -17631,9 +18207,13 @@ var KiroIdeHooks = class extends ToolHooks {
17631
18207
  * Hooks generator for the **Kiro CLI**.
17632
18208
  *
17633
18209
  * Kiro CLI 3.0 reads the same standalone `.kiro/hooks/*.json` v1 format the
17634
- * Kiro IDE reads, so this reuses {@link KiroIdeHooks} and only redirects the
17635
- * tool-specific override key to `kiro-cli` (so `kiro-cli.hooks` overrides in
17636
- * the rulesync hooks config are honored, rather than `kiro-ide.hooks`).
18210
+ * Kiro IDE reads the same directory, and in rulesync the same
18211
+ * `.kiro/hooks/rulesync.json` file so this is {@link KiroIdeHooks} under a
18212
+ * second target name, down to the shared `kiro` override block both resolve
18213
+ * their tool-specific hooks from. A per-target override block would make that
18214
+ * one file's content depend on which target was generated last, which is
18215
+ * exactly what the shared block avoids (the Kiro MCP and permissions wiring
18216
+ * resolve their shared files the same way).
17637
18217
  *
17638
18218
  * The embedded `.kiro/agents/default.json` agent-hook format this target used
17639
18219
  * to emit is documented as not working in 3.0, so it is left to the deprecated
@@ -17642,11 +18222,7 @@ var KiroIdeHooks = class extends ToolHooks {
17642
18222
  * @see https://kiro.dev/docs/cli/v3/hooks-migration/
17643
18223
  * @see https://kiro.dev/docs/hooks/
17644
18224
  */
17645
- var KiroCliHooks = class extends KiroIdeHooks {
17646
- static getOverrideKey() {
17647
- return "kiro-cli";
17648
- }
17649
- };
18225
+ var KiroCliHooks = class extends KiroIdeHooks {};
17650
18226
  //#endregion
17651
18227
  //#region src/features/hooks/kiro-hooks.ts
17652
18228
  /**
@@ -17671,14 +18247,30 @@ function buildKiroEntriesForEvent(definitions) {
17671
18247
  }
17672
18248
  return entries;
17673
18249
  }
17674
- function canonicalToKiroHooks(config) {
18250
+ /**
18251
+ * Event keys the embedded agent-config format defines: the canonical events it
18252
+ * supports plus its own native spellings (`agentSpawn`, `userPromptSubmit`, …).
18253
+ *
18254
+ * The `kiro` override block is shared with the standalone `.kiro/hooks/*.json`
18255
+ * targets, whose vocabulary is different (`PostFileSave`, `PreTaskExec`, …).
18256
+ * Passing those through here would write event keys Kiro does not define into
18257
+ * `.kiro/agents/default.json`, so they are dropped instead.
18258
+ * @see https://kiro.dev/docs/cli/v3/hooks-migration/
18259
+ */
18260
+ const KIRO_AGENT_CONFIG_EVENT_KEYS = /* @__PURE__ */ new Set([...KIRO_HOOK_EVENTS, ...KIRO_AGENT_CONFIG_NATIVE_EVENT_NAMES]);
18261
+ function canonicalToKiroHooks({ config, logger }) {
17675
18262
  const overrideKey = "kiro";
17676
18263
  const kiroSupported = new Set(KIRO_HOOK_EVENTS);
17677
18264
  const sharedHooks = {};
17678
18265
  for (const [event, defs] of Object.entries(config.hooks)) if (kiroSupported.has(event)) sharedHooks[event] = defs;
18266
+ const overrideHooks = {};
18267
+ const droppedEvents = [];
18268
+ for (const [event, defs] of Object.entries(config[overrideKey]?.hooks ?? {})) if (KIRO_AGENT_CONFIG_EVENT_KEYS.has(event)) overrideHooks[event] = defs;
18269
+ else droppedEvents.push(event);
18270
+ 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.`);
17679
18271
  const effectiveHooks = {
17680
18272
  ...sharedHooks,
17681
- ...config[overrideKey]?.hooks
18273
+ ...overrideHooks
17682
18274
  };
17683
18275
  const kiro = {};
17684
18276
  for (const [eventName, definitions] of Object.entries(effectiveHooks)) {
@@ -17773,11 +18365,14 @@ var KiroHooks = class KiroHooks extends ToolHooks {
17773
18365
  validate
17774
18366
  });
17775
18367
  }
17776
- static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false }) {
18368
+ static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
17777
18369
  const paths = KiroHooks.getSettablePaths({ global });
17778
18370
  const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
17779
18371
  const existingContent = await readFileContentOrNull(filePath) ?? JSON.stringify({}, null, 2);
17780
- const kiroHooks = canonicalToKiroHooks(rulesyncHooks.getJson());
18372
+ const kiroHooks = canonicalToKiroHooks({
18373
+ config: rulesyncHooks.getJson(),
18374
+ logger
18375
+ });
17781
18376
  const fileContent = applySharedConfigPatch({
17782
18377
  fileKey: sharedConfigFileKey(paths),
17783
18378
  feature: "hooks",
@@ -17893,6 +18488,30 @@ const PI_TOOL_EVENTS = /* @__PURE__ */ new Set(["tool_call", "tool_result"]);
17893
18488
  */
17894
18489
  const PI_ASSISTANT_MESSAGE_EVENTS = /* @__PURE__ */ new Set(["message_end"]);
17895
18490
  /**
18491
+ * `tool_call` is the only Pi extension event that can block, and it is Pi's
18492
+ * only tool gate. Its return contract is
18493
+ * `{ block: true, reason?: string, terminate?: boolean }`.
18494
+ *
18495
+ * @see https://github.com/earendil-works/pi/blob/v0.84.1/packages/coding-agent/docs/extensions.md#tool_call
18496
+ */
18497
+ const PI_BLOCKING_EVENT = "tool_call";
18498
+ /**
18499
+ * Helper emitted alongside blocking handlers. `promisify(exec)` rejects on a
18500
+ * non-zero exit with an error carrying `stdout` / `stderr` / `code`, so the
18501
+ * reason is derived from the rejection rather than from a resolved exit code.
18502
+ */
18503
+ const BLOCK_REASON_HELPER_LINES = [
18504
+ "function toBlockReason(error: unknown): string {",
18505
+ " const result = error as { stdout?: unknown; stderr?: unknown; code?: unknown } | null;",
18506
+ " const stderr = String(result?.stderr ?? \"\").trim();",
18507
+ " if (stderr) return stderr;",
18508
+ " const stdout = String(result?.stdout ?? \"\").trim();",
18509
+ " if (stdout) return stdout;",
18510
+ " if (result?.code !== undefined) return `Hook command failed with exit code ${result.code}.`;",
18511
+ " return error instanceof Error ? error.message : String(error);",
18512
+ "}"
18513
+ ];
18514
+ /**
17896
18515
  * Validate a hook matcher as a regular expression and return it as a JS
17897
18516
  * string-literal (JSON.stringify quoting) safe to embed in generated code.
17898
18517
  */
@@ -17929,6 +18548,22 @@ function collectPiHandlers({ effectiveHooks, eventMap }) {
17929
18548
  }
17930
18549
  return handlerGroups;
17931
18550
  }
18551
+ function buildCommandLines({ handler, usesToolName, blocksToolCall }) {
18552
+ const lines = [];
18553
+ const gated = usesToolName && Boolean(handler.matcher);
18554
+ const indent = gated ? " " : " ";
18555
+ const embeddedCommand = JSON.stringify(handler.command);
18556
+ if (gated && handler.matcher) lines.push(` if (new RegExp(${matcherToEmbeddedLiteral(handler.matcher)}).test(event.toolName)) {`);
18557
+ if (blocksToolCall) {
18558
+ lines.push(`${indent}try {`);
18559
+ lines.push(`${indent} await run(${embeddedCommand});`);
18560
+ lines.push(`${indent}} catch (error) {`);
18561
+ lines.push(`${indent} return { block: true, reason: toBlockReason(error) };`);
18562
+ lines.push(`${indent}}`);
18563
+ } else lines.push(`${indent}await run(${embeddedCommand});`);
18564
+ if (gated) lines.push(" }");
18565
+ return lines;
18566
+ }
17932
18567
  function buildSubscriptionLines(handlerGroups) {
17933
18568
  const lines = [];
17934
18569
  for (const [piEvent, handlers] of Object.entries(handlerGroups)) {
@@ -17937,14 +18572,11 @@ function buildSubscriptionLines(handlerGroups) {
17937
18572
  const usesEvent = usesToolName || gatesOnAssistant;
17938
18573
  lines.push(` pi.on(${JSON.stringify(piEvent)}, async (${usesEvent ? "event" : ""}) => {`);
17939
18574
  if (gatesOnAssistant) lines.push(` if (event.message.role !== "assistant") return;`);
17940
- for (const handler of handlers) {
17941
- const embeddedCommand = JSON.stringify(handler.command);
17942
- if (usesToolName && handler.matcher) {
17943
- lines.push(` if (new RegExp(${matcherToEmbeddedLiteral(handler.matcher)}).test(event.toolName)) {`);
17944
- lines.push(` await run(${embeddedCommand});`);
17945
- lines.push(" }");
17946
- } else lines.push(` await run(${embeddedCommand});`);
17947
- }
18575
+ for (const handler of handlers) lines.push(...buildCommandLines({
18576
+ handler,
18577
+ usesToolName,
18578
+ blocksToolCall: piEvent === PI_BLOCKING_EVENT
18579
+ }));
17948
18580
  lines.push(" });");
17949
18581
  }
17950
18582
  return lines;
@@ -17953,8 +18585,9 @@ function buildSubscriptionLines(handlerGroups) {
17953
18585
  * Generate the rulesync-owned Pi extension (a TypeScript module with a
17954
18586
  * default-export factory receiving Pi's ExtensionAPI) that subscribes to the
17955
18587
  * mapped lifecycle events and executes the configured hook commands via the
17956
- * platform shell. The generated extension observes events only: it never
17957
- * blocks or mutates Pi events.
18588
+ * platform shell. Handlers observe events, except on `tool_call` Pi's only
18589
+ * blocking event and its only tool gate — where a hook command that exits
18590
+ * non-zero denies the call with `{ block: true, reason }`.
17958
18591
  *
17959
18592
  * @see https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/extensions.md
17960
18593
  */
@@ -17966,10 +18599,12 @@ function generatePiExtensionCode({ config, supportedEvents, eventMap }) {
17966
18599
  };
17967
18600
  const effectiveHooks = {};
17968
18601
  for (const [event, defs] of Object.entries(configHooks)) if (supported.has(event)) effectiveHooks[event] = defs;
17969
- const subscriptionLines = buildSubscriptionLines(collectPiHandlers({
18602
+ const handlerGroups = collectPiHandlers({
17970
18603
  effectiveHooks,
17971
18604
  eventMap
17972
- }));
18605
+ });
18606
+ const subscriptionLines = buildSubscriptionLines(handlerGroups);
18607
+ const needsBlockReasonHelper = Boolean(handlerGroups[PI_BLOCKING_EVENT]);
17973
18608
  const lines = ["// Generated by rulesync. Do not edit manually."];
17974
18609
  if (subscriptionLines.length === 0) {
17975
18610
  lines.push("export default function () {}");
@@ -17983,6 +18618,10 @@ function generatePiExtensionCode({ config, supportedEvents, eventMap }) {
17983
18618
  lines.push("");
17984
18619
  lines.push("const run = promisify(exec);");
17985
18620
  lines.push("");
18621
+ if (needsBlockReasonHelper) {
18622
+ lines.push(...BLOCK_REASON_HELPER_LINES);
18623
+ lines.push("");
18624
+ }
17986
18625
  lines.push("export default function (pi: ExtensionAPI) {");
17987
18626
  lines.push(...subscriptionLines);
17988
18627
  lines.push("}");
@@ -18712,6 +19351,20 @@ function unsupportedMatcherEventNames({ factory, effectiveHooks }) {
18712
19351
  }
18713
19352
  return [...eventsWithMatcher];
18714
19353
  }
19354
+ /**
19355
+ * Targets that read their tool-scoped `{key}.hooks` override from a differently
19356
+ * named key. The two Kiro standalone-format targets write the very same
19357
+ * `.kiro/hooks/rulesync.json`, so they share the `kiro` block: a per-target
19358
+ * block would make that one file's content depend on generation order. The same
19359
+ * resolution is used by the MCP (`MCP_BLOCK_KEY_ALIASES`) and permissions
19360
+ * (`PERMISSION_OVERRIDE_KEY_ALIASES`) features for the files they share.
19361
+ */
19362
+ const HOOKS_OVERRIDE_KEY_ALIASES = {
19363
+ "kiro-cli": KIRO_HOOKS_OVERRIDE_KEY,
19364
+ "kiro-ide": KIRO_HOOKS_OVERRIDE_KEY
19365
+ };
19366
+ /** The targets writing the standalone `.kiro/hooks/*.json` v1 format. */
19367
+ const KIRO_STANDALONE_HOOKS_TARGETS = /* @__PURE__ */ new Set(["kiro-cli", "kiro-ide"]);
18715
19368
  const toolHooksFactories = /* @__PURE__ */ new Map([
18716
19369
  ["amp", {
18717
19370
  class: AmpHooks,
@@ -18952,7 +19605,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
18952
19605
  },
18953
19606
  supportedEvents: KIRO_HOOK_EVENTS,
18954
19607
  supportedHookTypes: ["command"],
18955
- supportsMatcher: true
19608
+ supportsMatcher: true,
19609
+ passthroughOverrideEvents: true
18956
19610
  }],
18957
19611
  ["kiro-cli", {
18958
19612
  class: KiroCliHooks,
@@ -19132,7 +19786,7 @@ var HooksProcessor = class extends FeatureProcessor {
19132
19786
  if (!factory) throw new Error(`Unsupported tool target: ${this.toolTarget}`);
19133
19787
  const config = rulesyncHooks.getJson();
19134
19788
  const sharedHooks = config.hooks;
19135
- const overrideHooks = config[this.toolTarget]?.hooks ?? {};
19789
+ const overrideHooks = config[HOOKS_OVERRIDE_KEY_ALIASES[this.toolTarget] ?? this.toolTarget]?.hooks ?? {};
19136
19790
  const effectiveHooks = {
19137
19791
  ...sharedHooks,
19138
19792
  ...overrideHooks
@@ -19158,14 +19812,14 @@ var HooksProcessor = class extends FeatureProcessor {
19158
19812
  }
19159
19813
  for (const [hookType, events] of unsupportedTypeToEvents) this.logger.warn(`Skipped ${hookType}-type hook(s) for ${this.toolTarget} (not supported): ${Array.from(events).join(", ")}`);
19160
19814
  }
19161
- if (this.toolTarget !== "kiro-ide") {
19815
+ if (!KIRO_STANDALONE_HOOKS_TARGETS.has(this.toolTarget)) {
19162
19816
  const skippedEvents = new Set(unsupportedEventNames({
19163
19817
  factory,
19164
19818
  sharedHooks,
19165
19819
  effectiveHooks
19166
19820
  }));
19167
19821
  const eventsWithDisabledHooks = Object.entries(sharedHooks).filter(([event, defs]) => !skippedEvents.has(event) && defs.some((def) => def.enabled === false)).map(([event]) => event);
19168
- 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(", ")}`);
19822
+ 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(", ")}`);
19169
19823
  }
19170
19824
  const eventsWithUnsupportedMatcher = unsupportedMatcherEventNames({
19171
19825
  factory,
@@ -19664,9 +20318,11 @@ var CursorIgnore = class CursorIgnore extends ToolIgnore {
19664
20318
  *
19665
20319
  * Generates the brand-aligned `.devinignore` file with gitignore-compatible
19666
20320
  * syntax. Devin automatically respects `.gitignore` patterns and has built-in
19667
- * defaults for node_modules/ and hidden files. On import, the legacy
19668
- * `.codeiumignore` filename is read as a fallback so existing projects still
19669
- * round-trip.
20321
+ * defaults for node_modules/ and hidden files. On import, the pre-rebrand
20322
+ * `.codeiumignore` and `.windsurfignore` filenames are read as fallbacks so
20323
+ * existing projects still round-trip. The docs list the three names side by
20324
+ * side without defining a precedence between them, so the order below is
20325
+ * rulesync's own choice: brand-aligned name first, then the legacy names.
19670
20326
  *
19671
20327
  * In global mode the enterprise-wide `~/.codeium/.codeiumignore` is written
19672
20328
  * instead; see `DEVIN_GLOBAL_IGNORE_DIR_PATH` for why that path keeps the
@@ -19705,9 +20361,16 @@ var DevinIgnore = class DevinIgnore extends ToolIgnore {
19705
20361
  validate,
19706
20362
  global
19707
20363
  });
19708
- const primaryPath = (0, node_path.join)(outputRoot, relativeDirPath, relativeFilePath);
19709
- const legacyPath = (0, node_path.join)(outputRoot, relativeDirPath, DEVIN_LEGACY_IGNORE_FILE_NAME);
19710
- const resolvedFilePath = !await fileExists(primaryPath) && await fileExists(legacyPath) ? DEVIN_LEGACY_IGNORE_FILE_NAME : relativeFilePath;
20364
+ const candidateFileNames = [
20365
+ relativeFilePath,
20366
+ DEVIN_LEGACY_IGNORE_FILE_NAME,
20367
+ DEVIN_WINDSURF_IGNORE_FILE_NAME
20368
+ ];
20369
+ let resolvedFilePath = relativeFilePath;
20370
+ for (const candidateFileName of candidateFileNames) if (await fileExists((0, node_path.join)(outputRoot, relativeDirPath, candidateFileName))) {
20371
+ resolvedFilePath = candidateFileName;
20372
+ break;
20373
+ }
19711
20374
  const fileContent = await readFileContent((0, node_path.join)(outputRoot, relativeDirPath, resolvedFilePath));
19712
20375
  return new DevinIgnore({
19713
20376
  outputRoot,
@@ -22936,8 +23599,8 @@ function convertToGoosePluginMcpServers(mcpServers, logger) {
22936
23599
  * shape and cannot express `url`/`headers`, so remote servers are skipped with
22937
23600
  * a warning in project mode (use `--global` to sync them instead).
22938
23601
  *
22939
- * @see https://block.github.io/goose/docs/getting-started/using-extensions/
22940
- * @see https://github.com/block/goose/pull/9471
23602
+ * @see https://goose-docs.ai/docs/getting-started/using-extensions/
23603
+ * @see https://github.com/aaif-goose/goose/pull/9471
22941
23604
  */
22942
23605
  var GooseMcp = class GooseMcp extends ToolMcp {
22943
23606
  config;
@@ -24441,6 +25104,213 @@ var KiroMcp = class KiroMcp extends ToolMcp {
24441
25104
  }
24442
25105
  };
24443
25106
  //#endregion
25107
+ //#region src/constants/musecode-paths.ts
25108
+ const MUSECODE_RULE_FILE_NAME = "AGENTS.md";
25109
+ const MUSECODE_SKILLS_DIR_PATH = (0, node_path.join)(".agents", "skills");
25110
+ const MUSECODE_GLOBAL_CONFIG_DIR_PATH = (0, node_path.join)(".config", "muse");
25111
+ const MUSECODE_GLOBAL_SKILLS_DIR_PATH = (0, node_path.join)(MUSECODE_GLOBAL_CONFIG_DIR_PATH, "skills");
25112
+ const MUSECODE_SETTINGS_FILE_NAME = "settings.json";
25113
+ //#endregion
25114
+ //#region src/features/mcp/musecode-mcp.ts
25115
+ const MUSECODE_GLOBAL_ONLY_MESSAGE = "Muse Code MCP is global-only; use --global to sync ~/.config/muse/settings.json";
25116
+ /**
25117
+ * Single spelling of the settings.json codec/policy, matching the
25118
+ * `SHARED_CONFIG_OWNERSHIP` declaration for `.config/muse/settings.json`:
25119
+ * fail closed on an unparseable root rather than replacing the user's primary
25120
+ * Muse Code config with generated output.
25121
+ */
25122
+ function parseMusecodeSettings(fileContent, filePath) {
25123
+ return parseSharedConfig({
25124
+ format: "json",
25125
+ fileContent,
25126
+ filePath,
25127
+ invalidRootPolicy: "error"
25128
+ });
25129
+ }
25130
+ /**
25131
+ * Convert canonical rulesync servers to Muse Code's native `mcp_servers` shape.
25132
+ * Each entry carries a `transport` discriminator: `stdio` servers spawn a
25133
+ * `command` (single string) with `args`/`env`, and `streamable_http` servers
25134
+ * are reached at a `url` with optional `headers`. Only documented fields are
25135
+ * emitted; a canonical `disabled: true` maps to Muse's `enabled: false`.
25136
+ */
25137
+ function convertToMusecodeFormat(mcpServers, logger) {
25138
+ const result = {};
25139
+ for (const [name, config] of Object.entries(mcpServers)) {
25140
+ if (PROTOTYPE_POLLUTION_KEYS.has(name)) continue;
25141
+ if (!isRecord$1(config)) continue;
25142
+ if (declaresNoTransport(config)) {
25143
+ warnAndSkipMcpServer({
25144
+ toolName: "Muse Code",
25145
+ serverName: name,
25146
+ reason: "no transport",
25147
+ logger
25148
+ });
25149
+ continue;
25150
+ }
25151
+ const converted = {};
25152
+ if (isRemoteMcpServer(config)) {
25153
+ const url = resolveRemoteMcpUrl(config);
25154
+ if (!url) {
25155
+ warnAndSkipMcpServer({
25156
+ toolName: "Muse Code",
25157
+ serverName: name,
25158
+ reason: "a remote transport without a url",
25159
+ logger
25160
+ });
25161
+ continue;
25162
+ }
25163
+ const stated = config.type ?? config.transport;
25164
+ if (stated === "sse" || stated === "ws" || stated === void 0 && /^wss?:\/\//i.test(url)) {
25165
+ warnAndSkipMcpServer({
25166
+ toolName: "Muse Code",
25167
+ serverName: name,
25168
+ reason: `the "${stated ?? "ws"}" transport, which Muse Code does not implement (only stdio and streamable_http are supported)`,
25169
+ logger
25170
+ });
25171
+ continue;
25172
+ }
25173
+ converted.transport = "streamable_http";
25174
+ converted.url = url;
25175
+ if (config.headers && Object.keys(config.headers).length > 0) converted.headers = config.headers;
25176
+ } else {
25177
+ const [command, ...args] = resolveLocalMcpCommand(config);
25178
+ if (!command) {
25179
+ warnAndSkipMcpServer({
25180
+ toolName: "Muse Code",
25181
+ serverName: name,
25182
+ reason: "a stdio transport without a command",
25183
+ logger
25184
+ });
25185
+ continue;
25186
+ }
25187
+ converted.transport = "stdio";
25188
+ converted.command = command;
25189
+ converted.args = args;
25190
+ if (config.env && Object.keys(config.env).length > 0) converted.env = config.env;
25191
+ }
25192
+ if (config.disabled === true) converted.enabled = false;
25193
+ result[name] = converted;
25194
+ }
25195
+ return result;
25196
+ }
25197
+ /**
25198
+ * Convert Muse Code's native `mcp_servers` shape back to canonical rulesync
25199
+ * servers. The `transport` discriminator is dropped (`streamable_http` is not a
25200
+ * canonical enum value; the transport is re-derived from `command`/`url` on the
25201
+ * next generate), `enabled: false` maps back to `disabled: true`, and unknown
25202
+ * keys (e.g. `mode`, `framing`) pass through untouched.
25203
+ */
25204
+ function convertFromMusecodeFormat(musecodeMcp) {
25205
+ const result = {};
25206
+ for (const [name, config] of Object.entries(musecodeMcp)) {
25207
+ if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
25208
+ const converted = {};
25209
+ for (const [key, value] of Object.entries(config)) {
25210
+ if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
25211
+ if (key === "transport") continue;
25212
+ if (key === "enabled") {
25213
+ if (value === false) converted.disabled = true;
25214
+ continue;
25215
+ }
25216
+ converted[key] = value;
25217
+ }
25218
+ result[name] = converted;
25219
+ }
25220
+ return result;
25221
+ }
25222
+ /**
25223
+ * Meta Muse Code MCP servers.
25224
+ *
25225
+ * Muse Code reads MCP servers only from the `mcp_servers` block of the GLOBAL
25226
+ * user settings file `~/.config/muse/settings.json`; no project-scoped MCP
25227
+ * location is documented. The settings file must carry
25228
+ * `"schema_version": 1` — a file that omits that key fails every command at
25229
+ * startup with `malformed settings file` — so the key is bootstrapped when the
25230
+ * file is created and preserved when it already exists. Other settings keys are
25231
+ * preserved via the shared-config gateway, and the file is never deleted.
25232
+ *
25233
+ * @see https://dev.meta.ai/docs/muse-code/configuration.md
25234
+ * @see https://dev.meta.ai/docs/muse-code/extending.md
25235
+ */
25236
+ var MusecodeMcp = class MusecodeMcp extends ToolMcp {
25237
+ json;
25238
+ constructor(params) {
25239
+ super(params);
25240
+ this.json = parseMusecodeSettings(this.fileContent ?? "", (0, node_path.join)(this.relativeDirPath, this.relativeFilePath));
25241
+ }
25242
+ getJson() {
25243
+ return this.json;
25244
+ }
25245
+ isDeletable() {
25246
+ return false;
25247
+ }
25248
+ static getSettablePaths(_options) {
25249
+ return {
25250
+ relativeDirPath: MUSECODE_GLOBAL_CONFIG_DIR_PATH,
25251
+ relativeFilePath: MUSECODE_SETTINGS_FILE_NAME
25252
+ };
25253
+ }
25254
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
25255
+ if (!global) throw new Error(MUSECODE_GLOBAL_ONLY_MESSAGE);
25256
+ const paths = this.getSettablePaths({ global });
25257
+ const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{}";
25258
+ return new MusecodeMcp({
25259
+ outputRoot,
25260
+ relativeDirPath: paths.relativeDirPath,
25261
+ relativeFilePath: paths.relativeFilePath,
25262
+ fileContent,
25263
+ validate,
25264
+ global
25265
+ });
25266
+ }
25267
+ static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
25268
+ if (!global) throw new Error(MUSECODE_GLOBAL_ONLY_MESSAGE);
25269
+ const paths = this.getSettablePaths({ global });
25270
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
25271
+ const existingContent = await readFileContentOrNull(filePath) ?? "";
25272
+ const existing = parseMusecodeSettings(existingContent, filePath);
25273
+ const converted = convertToMusecodeFormat(rulesyncMcp.getMcpServers(), logger);
25274
+ return new MusecodeMcp({
25275
+ outputRoot,
25276
+ relativeDirPath: paths.relativeDirPath,
25277
+ relativeFilePath: paths.relativeFilePath,
25278
+ fileContent: applySharedConfigPatch({
25279
+ fileKey: sharedConfigFileKey(paths),
25280
+ feature: "mcp",
25281
+ existingContent,
25282
+ patch: {
25283
+ mcp_servers: converted,
25284
+ ...existing.schema_version === void 0 && { schema_version: 1 }
25285
+ },
25286
+ filePath
25287
+ }),
25288
+ validate,
25289
+ global
25290
+ });
25291
+ }
25292
+ toRulesyncMcp() {
25293
+ const converted = convertFromMusecodeFormat(isRecord$1(this.json.mcp_servers) ? this.json.mcp_servers : {});
25294
+ return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers: converted }, null, 2) });
25295
+ }
25296
+ validate() {
25297
+ return {
25298
+ success: true,
25299
+ error: null
25300
+ };
25301
+ }
25302
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
25303
+ return new MusecodeMcp({
25304
+ outputRoot,
25305
+ relativeDirPath,
25306
+ relativeFilePath,
25307
+ fileContent: JSON.stringify({ mcp_servers: {} }, null, 2),
25308
+ validate: false,
25309
+ global
25310
+ });
25311
+ }
25312
+ };
25313
+ //#endregion
24444
25314
  //#region src/features/mcp/opencode-mcp.ts
24445
25315
  const OPENCODE_ENV_VAR_PATTERN = /(?<!\$)\{env:([^}:]+)\}/g;
24446
25316
  const OpencodeMcpLocalServerSchema = zod_mini.z.looseObject({
@@ -26397,6 +27267,15 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
26397
27267
  supportsDisabledTools: false
26398
27268
  }
26399
27269
  }],
27270
+ ["musecode", {
27271
+ class: MusecodeMcp,
27272
+ meta: {
27273
+ supportsProject: false,
27274
+ supportsGlobal: true,
27275
+ supportsEnabledTools: false,
27276
+ supportsDisabledTools: false
27277
+ }
27278
+ }],
26400
27279
  ["opencode", {
26401
27280
  class: OpencodeMcp,
26402
27281
  meta: {
@@ -29212,6 +30091,163 @@ var CopilotPermissions = class CopilotPermissions extends ToolPermissions {
29212
30091
  }
29213
30092
  };
29214
30093
  //#endregion
30094
+ //#region src/features/permissions/copilotcli-permissions.ts
30095
+ /**
30096
+ * The only canonical category the Copilot CLI settings can express: URL
30097
+ * approvals. Every other category (`bash`, `edit`, `read`, ...) has no
30098
+ * user-authorable surface — the CLI's `permissions.allow`/`ask`/`deny` rule
30099
+ * arrays are accepted only in MDM/enterprise managed settings, and session
30100
+ * tool approvals are machine-written to `permissions-config.json`.
30101
+ */
30102
+ const WEBFETCH_CATEGORY = "webfetch";
30103
+ /** `~/.copilot/settings.json` and `.github/copilot/settings.json` keys. */
30104
+ const ALLOWED_URLS_KEY = "allowedUrls";
30105
+ const DENIED_URLS_KEY = "deniedUrls";
30106
+ function toUrlList(value) {
30107
+ if (!Array.isArray(value)) return [];
30108
+ return value.filter((entry) => typeof entry === "string");
30109
+ }
30110
+ /**
30111
+ * Split one canonical category's rules into the two URL lists. `ask` entries
30112
+ * are omitted: the CLI prompts for any URL that is in neither list, which is
30113
+ * exactly what `ask` means.
30114
+ */
30115
+ function splitUrlRules(rules) {
30116
+ const allowedUrls = [];
30117
+ const deniedUrls = [];
30118
+ for (const [pattern, action] of Object.entries(rules)) if (action === "allow") allowedUrls.push(pattern);
30119
+ else if (action === "deny") deniedUrls.push(pattern);
30120
+ return {
30121
+ allowedUrls,
30122
+ deniedUrls
30123
+ };
30124
+ }
30125
+ /**
30126
+ * Permissions generator for the GitHub Copilot CLI.
30127
+ *
30128
+ * The CLI keeps two persistent settings files: the user-scope
30129
+ * `~/.copilot/settings.json` and the repository-scope
30130
+ * `.github/copilot/settings.json` (shipped in v1.0.60). Both are shared,
30131
+ * hand-edited files carrying unrelated keys (`model`, `effortLevel`, `hooks`,
30132
+ * `sandbox.*`, ...), so writes go through the shared-config gateway, only the
30133
+ * URL keys are owned, and the file is never deleted.
30134
+ *
30135
+ * The canonical `webfetch` category maps onto the CLI's two URL lists:
30136
+ * `allow` → `allowedUrls`, `deny` → `deniedUrls`, `ask` → the pattern is
30137
+ * omitted so the CLI falls through to its approval prompt.
30138
+ *
30139
+ * Scope asymmetry: the repository-scope key table documents `deniedUrls`
30140
+ * (union — a repository may add entries, never remove them) but NOT
30141
+ * `allowedUrls`, so an allow rule is only enforceable at user scope. At project
30142
+ * scope allow rules are therefore dropped with a warning rather than written to
30143
+ * a key the CLI ignores. (v1.0.79 additionally warns on startup about unknown
30144
+ * top-level keys in the user `settings.json`, so only documented keys are ever
30145
+ * emitted there either.)
30146
+ *
30147
+ * @see https://docs.github.com/en/copilot/reference/copilot-cli-reference/cli-config-dir-reference
30148
+ * @see https://github.com/github/copilot-cli/blob/main/changelog.md
30149
+ */
30150
+ var CopilotcliPermissions = class CopilotcliPermissions extends ToolPermissions {
30151
+ constructor(params) {
30152
+ super({
30153
+ ...params,
30154
+ fileContent: params.fileContent ?? "{}"
30155
+ });
30156
+ }
30157
+ /**
30158
+ * `settings.json` holds unrelated user settings (`model`, `effortLevel`,
30159
+ * `hooks`, ...), so it must not be deleted.
30160
+ */
30161
+ isDeletable() {
30162
+ return false;
30163
+ }
30164
+ static getSettablePaths({ global = false } = {}) {
30165
+ return global ? {
30166
+ relativeDirPath: COPILOT_DIR,
30167
+ relativeFilePath: COPILOTCLI_SETTINGS_FILE_NAME
30168
+ } : {
30169
+ relativeDirPath: COPILOTCLI_PROJECT_SETTINGS_DIR_PATH,
30170
+ relativeFilePath: COPILOTCLI_SETTINGS_FILE_NAME
30171
+ };
30172
+ }
30173
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
30174
+ const paths = CopilotcliPermissions.getSettablePaths({ global });
30175
+ const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{}";
30176
+ return new CopilotcliPermissions({
30177
+ outputRoot,
30178
+ relativeDirPath: paths.relativeDirPath,
30179
+ relativeFilePath: paths.relativeFilePath,
30180
+ fileContent,
30181
+ validate,
30182
+ global
30183
+ });
30184
+ }
30185
+ static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, global = false, logger }) {
30186
+ const paths = CopilotcliPermissions.getSettablePaths({ global });
30187
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
30188
+ const existingContent = await readFileContentOrNull(filePath) ?? "{}";
30189
+ const rules = rulesyncPermissions.getJson().permission[WEBFETCH_CATEGORY];
30190
+ const patch = {};
30191
+ if (rules !== void 0) {
30192
+ const { allowedUrls, deniedUrls } = splitUrlRules(rules);
30193
+ patch[DENIED_URLS_KEY] = deniedUrls.length > 0 ? deniedUrls : void 0;
30194
+ if (global) patch[ALLOWED_URLS_KEY] = allowedUrls.length > 0 ? allowedUrls : void 0;
30195
+ else if (allowedUrls.length > 0) logger?.warn(`Copilot CLI permissions: dropping ${allowedUrls.length} "webfetch" allow rule(s) at project scope — ${(0, node_path.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.`);
30196
+ }
30197
+ return new CopilotcliPermissions({
30198
+ outputRoot,
30199
+ relativeDirPath: paths.relativeDirPath,
30200
+ relativeFilePath: paths.relativeFilePath,
30201
+ fileContent: applySharedConfigPatch({
30202
+ fileKey: sharedConfigFileKey(paths),
30203
+ feature: "permissions",
30204
+ existingContent,
30205
+ patch,
30206
+ filePath
30207
+ }),
30208
+ validate: true,
30209
+ global
30210
+ });
30211
+ }
30212
+ toRulesyncPermissions() {
30213
+ const settings = this.parseSettings();
30214
+ const rules = {};
30215
+ for (const pattern of toUrlList(settings[DENIED_URLS_KEY])) rules[pattern] = "deny";
30216
+ if (this.global) for (const pattern of toUrlList(settings[ALLOWED_URLS_KEY])) rules[pattern] ??= "allow";
30217
+ const permission = Object.keys(rules).length > 0 ? { [WEBFETCH_CATEGORY]: rules } : {};
30218
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({ permission }, null, 2) });
30219
+ }
30220
+ parseSettings() {
30221
+ const relativePath = (0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath());
30222
+ try {
30223
+ return parseSharedConfig({
30224
+ format: "json",
30225
+ fileContent: this.getFileContent() || "{}",
30226
+ filePath: relativePath,
30227
+ invalidRootPolicy: "error"
30228
+ });
30229
+ } catch (error) {
30230
+ throw new Error(`Failed to parse Copilot CLI settings in ${relativePath}: ${formatError(error)}`, { cause: error });
30231
+ }
30232
+ }
30233
+ validate() {
30234
+ return {
30235
+ success: true,
30236
+ error: null
30237
+ };
30238
+ }
30239
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
30240
+ return new CopilotcliPermissions({
30241
+ outputRoot,
30242
+ relativeDirPath,
30243
+ relativeFilePath,
30244
+ fileContent: "{}",
30245
+ validate: false,
30246
+ global
30247
+ });
30248
+ }
30249
+ };
30250
+ //#endregion
29215
30251
  //#region src/features/permissions/cursor-permissions.ts
29216
30252
  /**
29217
30253
  * Mapping from rulesync canonical tool category names (lowercase) to Cursor CLI
@@ -30229,6 +31265,9 @@ const GROK_TOOL_TO_CATEGORY = {
30229
31265
  WebSearch: "websearch"
30230
31266
  };
30231
31267
  const GROK_MCP_TOOL = "MCPTool";
31268
+ const GROK_TOOL_TO_CATEGORY_LOWER = Object.fromEntries(Object.entries(GROK_TOOL_TO_CATEGORY).map(([tool, category]) => [tool.toLowerCase(), category]));
31269
+ const GROK_MCP_TOOL_ALIASES = /* @__PURE__ */ new Set(["mcp", GROK_MCP_TOOL.toLowerCase()]);
31270
+ const GROKCLI_PERMISSION_RULES_KEY = "rules";
30232
31271
  /**
30233
31272
  * Build a Grok Claude-style permission entry (e.g. `Bash(git *)`, `Read`,
30234
31273
  * `MCPTool(server__tool)`) from a canonical category + pattern. Returns `null`
@@ -30281,6 +31320,44 @@ function parseGrokEntry(entry) {
30281
31320
  };
30282
31321
  }
30283
31322
  /**
31323
+ * Parse one entry of the verbose `[[permission.rules]]` form
31324
+ * (`{ action = "allow", tool = "bash", pattern = "git *" }`) into a canonical
31325
+ * category + pattern + action. Unlike the compact array entries these are TOML
31326
+ * tables, so the tool name arrives as its own field instead of an entry string.
31327
+ * Returns `null` for malformed entries and for tools with no canonical
31328
+ * equivalent, so callers can skip them (the whole `rules` array is preserved
31329
+ * verbatim on generate regardless).
31330
+ */
31331
+ function parseGrokRule(rule) {
31332
+ if (!isRecord$1(rule)) return null;
31333
+ const { action: rawAction, tool, pattern } = rule;
31334
+ const parsedAction = PermissionActionSchema.safeParse(rawAction);
31335
+ if (!parsedAction.success) return null;
31336
+ const action = parsedAction.data;
31337
+ if (typeof tool !== "string") return null;
31338
+ if (pattern !== void 0 && typeof pattern !== "string") return null;
31339
+ const trimmedPattern = pattern?.trim() ?? "";
31340
+ const resolvedPattern = trimmedPattern.length > 0 ? trimmedPattern : CATCH_ALL_PATTERN$2;
31341
+ const lowerTool = tool.trim().toLowerCase();
31342
+ if (GROK_MCP_TOOL_ALIASES.has(lowerTool)) return resolvedPattern === CATCH_ALL_PATTERN$2 ? {
31343
+ category: "mcp",
31344
+ pattern: CATCH_ALL_PATTERN$2,
31345
+ action
31346
+ } : {
31347
+ category: `${MCP_CANONICAL_PREFIX$1}${resolvedPattern}`,
31348
+ pattern: CATCH_ALL_PATTERN$2,
31349
+ action
31350
+ };
31351
+ if (!Object.hasOwn(GROK_TOOL_TO_CATEGORY_LOWER, lowerTool)) return null;
31352
+ const category = GROK_TOOL_TO_CATEGORY_LOWER[lowerTool];
31353
+ if (category === void 0) return null;
31354
+ return {
31355
+ category,
31356
+ pattern: resolvedPattern,
31357
+ action
31358
+ };
31359
+ }
31360
+ /**
30284
31361
  * Permissions adapter for the xAI Grok Build CLI (`grokcli`).
30285
31362
  *
30286
31363
  * Grok Build CLI ships a Claude-style rule system under `[permission]` in
@@ -30301,9 +31378,17 @@ function parseGrokEntry(entry) {
30301
31378
  * entry with different actions (e.g. `edit` allow + `write` deny → `Edit`),
30302
31379
  * the strictest wins (`deny > ask > allow`) and a warning is logged, so the
30303
31380
  * entry never lands contradictorily in two arrays.
30304
- * - Import: the `[permission]` arrays are parsed back into canonical
30305
- * categories. When no `[permission]` section is present (older configs), the
30306
- * coarse `[ui] permission_mode` is used as a fallback.
31381
+ * - Import: both documented `[permission]` forms are parsed back into
31382
+ * canonical categories the compact `allow`/`deny`/`ask` arrays and the
31383
+ * verbose `[[permission.rules]]` tables
31384
+ * (`{ action = "allow", tool = "bash", pattern = "git *" }`), whose `tool`
31385
+ * field is matched case-insensitively against the same tool table, plus
31386
+ * the documented `mcp` alias for the compact form's `MCPTool`. Rules
31387
+ * from the two forms are merged with the same `deny > ask > allow`
31388
+ * precedence. When neither form carries a rule (older configs), the coarse
31389
+ * `[ui] permission_mode` is used as a fallback. Generate always emits the
31390
+ * compact arrays, so a user-authored `rules` array is preserved verbatim
31391
+ * but not reconciled against them.
30307
31392
  *
30308
31393
  * The coarse `[ui] permission_mode` toggle is still written for backward
30309
31394
  * compatibility with older Grok versions: `always-approve` when the config is
@@ -30470,29 +31555,44 @@ function buildGrokPermissionArrays(config, existingPermission, logger) {
30470
31555
  };
30471
31556
  }
30472
31557
  /**
30473
- * Parse Grok's `[permission]` allow/deny/ask arrays back into a canonical
30474
- * permission map. Returns `null` when the section defines none of the three
30475
- * arrays, so the caller can fall back to the coarse `permission_mode`.
30476
- * Precedence `deny > ask > allow` is applied so a tool listed in multiple
30477
- * arrays resolves to the strictest action.
31558
+ * Parse Grok's `[permission]` section back into a canonical permission map,
31559
+ * reading both supported forms: the compact `allow`/`deny`/`ask` arrays of
31560
+ * Claude-style entries and the verbose `[[permission.rules]]` tables
31561
+ * (`{ action, tool, pattern }`). Returns `null` only when neither form carries
31562
+ * any rule, so the caller can fall back to the coarse `permission_mode`.
31563
+ * Precedence `deny > ask > allow` is applied across both forms, so a tool
31564
+ * described more than once resolves to the strictest action.
30478
31565
  */
30479
31566
  function parseGrokPermissionArrays(permission) {
30480
31567
  const allow = isStringArray$1(permission.allow) ? permission.allow : void 0;
30481
31568
  const deny = isStringArray$1(permission.deny) ? permission.deny : void 0;
30482
31569
  const ask = isStringArray$1(permission.ask) ? permission.ask : void 0;
30483
- if ((allow?.length ?? 0) + (deny?.length ?? 0) + (ask?.length ?? 0) === 0) return null;
31570
+ const rules = Array.isArray(permission[GROKCLI_PERMISSION_RULES_KEY]) ? permission[GROKCLI_PERMISSION_RULES_KEY] : void 0;
31571
+ if ((allow?.length ?? 0) + (deny?.length ?? 0) + (ask?.length ?? 0) + (rules?.length ?? 0) === 0) return null;
30484
31572
  const result = {};
30485
- const apply = (entries, action) => {
31573
+ const record = ({ category, pattern, action }) => {
31574
+ const bucket = result[category] ??= {};
31575
+ const existing = bucket[pattern];
31576
+ if (existing === void 0 || ACTION_RANK[action] > ACTION_RANK[existing]) bucket[pattern] = action;
31577
+ };
31578
+ const applyEntries = (entries, action) => {
30486
31579
  for (const entry of entries ?? []) {
30487
31580
  const parsed = parseGrokEntry(entry);
30488
31581
  if (parsed === null) continue;
30489
- const bucket = result[parsed.category] ??= {};
30490
- bucket[parsed.pattern] = action;
31582
+ record({
31583
+ ...parsed,
31584
+ action
31585
+ });
30491
31586
  }
30492
31587
  };
30493
- apply(allow, "allow");
30494
- apply(ask, "ask");
30495
- apply(deny, "deny");
31588
+ applyEntries(allow, "allow");
31589
+ applyEntries(ask, "ask");
31590
+ applyEntries(deny, "deny");
31591
+ for (const rule of rules ?? []) {
31592
+ const parsed = parseGrokRule(rule);
31593
+ if (parsed === null) continue;
31594
+ record(parsed);
31595
+ }
30496
31596
  return result;
30497
31597
  }
30498
31598
  /**
@@ -33177,6 +34277,13 @@ const TAKT_PROVIDER_PROFILES_KEY = "provider_profiles";
33177
34277
  const TAKT_DEFAULT_PERMISSION_MODE_KEY = "default_permission_mode";
33178
34278
  const TAKT_STEP_PERMISSION_OVERRIDES_KEY = "step_permission_overrides";
33179
34279
  const TAKT_PROVIDER_OPTIONS_KEY = "provider_options";
34280
+ const TAKT_RUNTIME_PROVIDER_KEY = "provider";
34281
+ const TAKT_RUNTIME_DEFAULTS_KEY = "defaults";
34282
+ const TAKT_RUNTIME_PROFILES_KEY = "profiles";
34283
+ const TAKT_RUNTIME_TARGETS_KEY = "targets";
34284
+ const TAKT_RUNTIME_AUTO_ROUTING_KEY = "auto_routing";
34285
+ const TAKT_RUNTIME_PROFILE_KEY = "profile";
34286
+ const TAKT_RUNTIME_OPTIONS_KEY = "options";
33180
34287
  const TAKT_SECURITY_POLICIES = {
33181
34288
  workflow_arpeggio: [
33182
34289
  "custom_data_source_modules",
@@ -33229,13 +34336,36 @@ const CATCH_ALL_PATTERN = "*";
33229
34336
  * profile, layered on top of `default_permission_mode`) and `provider_options`
33230
34337
  * (a top-level per-provider sandbox/network table). Both are authored on
33231
34338
  * generate and re-extracted on import.
34339
+ *
34340
+ * Takt 0.56.0 moved provider configuration into a separate `runtime.yaml`
34341
+ * (`.takt/runtime.yaml`, `~/.takt/runtime.yaml`). Once its `provider:` section
34342
+ * carries an assignment ("runtime mode" — the state a freshly installed Takt is
34343
+ * in, since it generates an active global runtime.yaml on first launch), any
34344
+ * legacy provider setting left in `config.yaml` makes Takt stop before running
34345
+ * an agent with "Mixed provider configuration detected". rulesync only reads
34346
+ * that file, in two places:
34347
+ * - the active provider is resolved from it first (the `provider_profiles`
34348
+ * key that carries the permission mode is still keyed by provider name, and
34349
+ * `provider_profiles` is not itself a legacy signal);
34350
+ * - an authored `provider_options` is refused with a warning rather than
34351
+ * written while runtime mode is active, and its runtime-side counterpart
34352
+ * (`provider.profiles.*.options`) is lifted back out on import.
34353
+ * Installs with no runtime.yaml, or an inactive one, keep the legacy behavior
34354
+ * unchanged.
33232
34355
  */
33233
34356
  var TaktPermissions = class TaktPermissions extends ToolPermissions {
33234
- constructor(params) {
34357
+ /**
34358
+ * The sibling `runtime.yaml` as read from the same scope, or `""` when the
34359
+ * install has none. Import needs it — `toRulesyncPermissions()` is
34360
+ * synchronous, so the file is read alongside config.yaml in `fromFile()`.
34361
+ */
34362
+ runtimeFileContent;
34363
+ constructor({ runtimeFileContent, ...params }) {
33235
34364
  super({
33236
34365
  ...params,
33237
34366
  fileContent: params.fileContent ?? ""
33238
34367
  });
34368
+ this.runtimeFileContent = runtimeFileContent ?? "";
33239
34369
  }
33240
34370
  isDeletable() {
33241
34371
  return false;
@@ -33249,11 +34379,13 @@ var TaktPermissions = class TaktPermissions extends ToolPermissions {
33249
34379
  static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
33250
34380
  const paths = TaktPermissions.getSettablePaths({ global });
33251
34381
  const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
34382
+ const runtimeFileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, "runtime.yaml")) ?? "";
33252
34383
  return new TaktPermissions({
33253
34384
  outputRoot,
33254
34385
  relativeDirPath: paths.relativeDirPath,
33255
34386
  relativeFilePath: paths.relativeFilePath,
33256
34387
  fileContent,
34388
+ runtimeFileContent,
33257
34389
  validate,
33258
34390
  global
33259
34391
  });
@@ -33268,12 +34400,22 @@ var TaktPermissions = class TaktPermissions extends ToolPermissions {
33268
34400
  filePath,
33269
34401
  invalidRootPolicy: "error"
33270
34402
  });
34403
+ const runtime = await readTaktRuntimeConfig({
34404
+ outputRoot,
34405
+ logger
34406
+ });
33271
34407
  const rulesyncJson = rulesyncPermissions.getJson();
33272
- const provider = resolveActiveProvider(config);
34408
+ const provider = resolveActiveProvider({
34409
+ config,
34410
+ runtime: runtime.active ? runtime.config : void 0
34411
+ });
33273
34412
  const mode = deriveTaktPermissionMode(rulesyncJson);
33274
34413
  const override = isPlainObject$1(rulesyncJson.takt) ? rulesyncJson.takt : void 0;
33275
34414
  const stepOverrides = isPlainObject$1(override?.[TAKT_STEP_PERMISSION_OVERRIDES_KEY]) ? override[TAKT_STEP_PERMISSION_OVERRIDES_KEY] : void 0;
33276
- const overrideProviderOptions = isPlainObject$1(override?.[TAKT_PROVIDER_OPTIONS_KEY]) ? override[TAKT_PROVIDER_OPTIONS_KEY] : void 0;
34415
+ const authoredProviderOptions = isPlainObject$1(override?.[TAKT_PROVIDER_OPTIONS_KEY]) ? override[TAKT_PROVIDER_OPTIONS_KEY] : void 0;
34416
+ const runtimeModeRefusesProviderOptions = authoredProviderOptions !== void 0 && runtime.active;
34417
+ 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.`);
34418
+ const overrideProviderOptions = runtimeModeRefusesProviderOptions ? void 0 : authoredProviderOptions;
33277
34419
  const authoredPolicies = pickSecurityPolicies(override, {
33278
34420
  filePath,
33279
34421
  logger,
@@ -33319,13 +34461,26 @@ var TaktPermissions = class TaktPermissions extends ToolPermissions {
33319
34461
  filePath: (0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath()),
33320
34462
  invalidRootPolicy: "error"
33321
34463
  });
33322
- const provider = resolveActiveProvider(config);
34464
+ const runtimeFilePath = (0, node_path.join)(this.getRelativeDirPath(), TAKT_RUNTIME_CONFIG_FILE_NAME);
34465
+ const runtime = parseSharedConfig({
34466
+ format: "yaml",
34467
+ fileContent: this.runtimeFileContent,
34468
+ filePath: runtimeFilePath
34469
+ });
34470
+ const runtimeActive = isRuntimeModeActive(runtime);
34471
+ const provider = resolveActiveProvider({
34472
+ config,
34473
+ runtime: runtimeActive ? runtime : void 0
34474
+ });
33323
34475
  const profiles = isPlainObject$1(config[TAKT_PROVIDER_PROFILES_KEY]) ? config[TAKT_PROVIDER_PROFILES_KEY] : {};
33324
34476
  const profile = isPlainObject$1(profiles[provider]) ? profiles[provider] : {};
33325
34477
  const mode = profile[TAKT_DEFAULT_PERMISSION_MODE_KEY];
33326
34478
  const rulesyncConfig = taktModeToRulesyncConfig(mode);
33327
34479
  const stepOverrides = isPlainObject$1(profile[TAKT_STEP_PERMISSION_OVERRIDES_KEY]) ? profile[TAKT_STEP_PERMISSION_OVERRIDES_KEY] : void 0;
33328
- const providerOptions = isPlainObject$1(config[TAKT_PROVIDER_OPTIONS_KEY]) ? config[TAKT_PROVIDER_OPTIONS_KEY] : void 0;
34480
+ const providerOptions = mergeSharedConfigDeep({
34481
+ base: isPlainObject$1(config[TAKT_PROVIDER_OPTIONS_KEY]) ? config[TAKT_PROVIDER_OPTIONS_KEY] : {},
34482
+ patch: runtimeActive ? collectRuntimeProviderOptions(runtime) : {}
34483
+ });
33329
34484
  const taktOverride = {};
33330
34485
  if (stepOverrides && Object.keys(stepOverrides).length > 0) taktOverride[TAKT_STEP_PERMISSION_OVERRIDES_KEY] = stepOverrides;
33331
34486
  if (providerOptions && Object.keys(providerOptions).length > 0) taktOverride[TAKT_PROVIDER_OPTIONS_KEY] = providerOptions;
@@ -33352,10 +34507,18 @@ var TaktPermissions = class TaktPermissions extends ToolPermissions {
33352
34507
  }
33353
34508
  };
33354
34509
  /**
33355
- * Resolve the active Takt provider: the top-level `provider:` value, else the
33356
- * sole key in `provider_profiles`, else the `claude` default.
34510
+ * Resolve the active Takt provider.
34511
+ *
34512
+ * Under runtime mode (Takt 0.56.0+) the real provider assignment lives in
34513
+ * `runtime.yaml`, and `config.yaml:provider` cannot coexist with it, so the
34514
+ * runtime document is consulted first: the provider of the profile named by
34515
+ * `provider.defaults.profile`. Everything else falls through to the legacy
34516
+ * chain — the top-level `provider:` value, else the sole key in
34517
+ * `provider_profiles`, else the `claude` default.
33357
34518
  */
33358
- function resolveActiveProvider(config) {
34519
+ function resolveActiveProvider({ config, runtime }) {
34520
+ const fromRuntime = runtime === void 0 ? void 0 : resolveRuntimeProvider(runtime);
34521
+ if (fromRuntime !== void 0) return fromRuntime;
33359
34522
  if (typeof config[TAKT_PROVIDER_KEY] === "string" && config[TAKT_PROVIDER_KEY].trim() !== "") return config[TAKT_PROVIDER_KEY];
33360
34523
  const profiles = config[TAKT_PROVIDER_PROFILES_KEY];
33361
34524
  if (isPlainObject$1(profiles)) {
@@ -33364,6 +34527,170 @@ function resolveActiveProvider(config) {
33364
34527
  }
33365
34528
  return TAKT_DEFAULT_PROVIDER;
33366
34529
  }
34530
+ /** A plain object carrying at least one entry (Takt's "assignment" test). */
34531
+ function hasEntries(value) {
34532
+ return isPlainObject$1(value) && Object.keys(value).length > 0;
34533
+ }
34534
+ function runtimeProfiles(runtime) {
34535
+ const provider = runtime[TAKT_RUNTIME_PROVIDER_KEY];
34536
+ if (!isPlainObject$1(provider)) return {};
34537
+ const profiles = provider[TAKT_RUNTIME_PROFILES_KEY];
34538
+ return isPlainObject$1(profiles) ? profiles : {};
34539
+ }
34540
+ /**
34541
+ * Whether a parsed `runtime.yaml` puts Takt into runtime mode, mirroring
34542
+ * upstream's mode detection: the `provider:` section must carry an actual
34543
+ * assignment — a non-empty `defaults`, `profiles` or `auto_routing`, or a
34544
+ * `targets` map with at least one non-empty nested map. The file existing is not
34545
+ * enough, and empty nested maps (`defaults: {}`, `targets: { personas: {} }`)
34546
+ * must not flip the mode.
34547
+ * https://github.com/nrslib/takt/blob/main/src/infra/config/runtime-provider/mode.ts
34548
+ */
34549
+ function isRuntimeModeActive(runtime) {
34550
+ const provider = runtime[TAKT_RUNTIME_PROVIDER_KEY];
34551
+ if (!isPlainObject$1(provider)) return false;
34552
+ if (hasEntries(provider[TAKT_RUNTIME_DEFAULTS_KEY]) || hasEntries(provider[TAKT_RUNTIME_PROFILES_KEY]) || hasEntries(provider[TAKT_RUNTIME_AUTO_ROUTING_KEY])) return true;
34553
+ const targets = provider[TAKT_RUNTIME_TARGETS_KEY];
34554
+ return isPlainObject$1(targets) && Object.values(targets).some(hasEntries);
34555
+ }
34556
+ /** The provider named by the runtime profile Takt would use by default. */
34557
+ function resolveRuntimeProvider(runtime) {
34558
+ const provider = runtime[TAKT_RUNTIME_PROVIDER_KEY];
34559
+ const profiles = runtimeProfiles(runtime);
34560
+ const providerOf = (profileName) => {
34561
+ if (profileName === void 0) return;
34562
+ const profile = profiles[profileName];
34563
+ if (!isPlainObject$1(profile)) return;
34564
+ const value = profile[TAKT_RUNTIME_PROVIDER_KEY];
34565
+ return typeof value === "string" && value.trim() !== "" ? value : void 0;
34566
+ };
34567
+ if (!isPlainObject$1(provider)) return;
34568
+ const defaults = provider[TAKT_RUNTIME_DEFAULTS_KEY];
34569
+ if (!isPlainObject$1(defaults) || typeof defaults[TAKT_RUNTIME_PROFILE_KEY] !== "string") return;
34570
+ return providerOf(defaults[TAKT_RUNTIME_PROFILE_KEY]);
34571
+ }
34572
+ /**
34573
+ * Lift the runtime profiles' flat `options` bags back into the per-provider
34574
+ * shape the `takt` override uses. Each profile's options belong to that
34575
+ * profile's own provider; when several profiles name the same provider they are
34576
+ * merged in document order, so a later profile wins on a colliding option key.
34577
+ */
34578
+ function collectRuntimeProviderOptions(runtime) {
34579
+ const collected = {};
34580
+ for (const profile of Object.values(runtimeProfiles(runtime))) {
34581
+ if (!isPlainObject$1(profile)) continue;
34582
+ const providerName = profile[TAKT_RUNTIME_PROVIDER_KEY];
34583
+ const options = profile[TAKT_RUNTIME_OPTIONS_KEY];
34584
+ if (typeof providerName !== "string" || providerName.trim() === "" || !hasEntries(options)) continue;
34585
+ const existing = collected[providerName];
34586
+ collected[providerName] = {
34587
+ ...isPlainObject$1(existing) ? existing : {},
34588
+ ...options
34589
+ };
34590
+ }
34591
+ return collected;
34592
+ }
34593
+ /**
34594
+ * Read one `runtime.yaml`. A file that cannot be parsed reports `unparsable`
34595
+ * rather than a document: Takt refuses to start on a broken runtime.yaml, and
34596
+ * treating it as legacy would be the one outcome that writes the
34597
+ * mixed-configuration key into the user's config.yaml.
34598
+ */
34599
+ async function readTaktRuntimeFile({ filePath, logger }) {
34600
+ const fileContent = await readFileContentOrNull(filePath);
34601
+ if (fileContent === null) return;
34602
+ try {
34603
+ return {
34604
+ config: parseSharedConfig({
34605
+ format: "yaml",
34606
+ fileContent,
34607
+ filePath
34608
+ }),
34609
+ unparsable: false
34610
+ };
34611
+ } catch (error) {
34612
+ 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.`);
34613
+ return {
34614
+ config: {},
34615
+ unparsable: true
34616
+ };
34617
+ }
34618
+ }
34619
+ /**
34620
+ * Collapse the project and global `runtime.yaml` into the single document Takt
34621
+ * itself resolves against, matching upstream's loader: `profiles` is a union
34622
+ * with the project definition of a same-named profile replacing the global one,
34623
+ * while `defaults`, `targets` and `auto_routing` are taken from the project file
34624
+ * whole whenever it states them at all (`??`, so a project `targets: {}` masks
34625
+ * the global one rather than merging with it).
34626
+ * https://github.com/nrslib/takt/blob/main/src/infra/config/runtime-provider/loader.ts
34627
+ *
34628
+ * Merging before mode detection matters in both directions: a project file
34629
+ * active only through `targets:` still resolves its provider from the global
34630
+ * file's `defaults`/`profiles`, and a project section that masks the global one
34631
+ * leaves the merged document inactive even though the global file alone was not.
34632
+ */
34633
+ function mergeTaktRuntimeConfigs({ project, global: globalConfig }) {
34634
+ const sectionOf = (config) => {
34635
+ const provider = config?.[TAKT_RUNTIME_PROVIDER_KEY];
34636
+ return isPlainObject$1(provider) ? provider : {};
34637
+ };
34638
+ const projectSection = sectionOf(project);
34639
+ const globalSection = sectionOf(globalConfig);
34640
+ const replaced = (key) => projectSection[key] ?? globalSection[key];
34641
+ const profilesOf = (section) => isPlainObject$1(section[TAKT_RUNTIME_PROFILES_KEY]) ? section[TAKT_RUNTIME_PROFILES_KEY] : void 0;
34642
+ const globalProfiles = profilesOf(globalSection);
34643
+ const projectProfiles = profilesOf(projectSection);
34644
+ const provider = {};
34645
+ if (globalProfiles !== void 0 || projectProfiles !== void 0) provider[TAKT_RUNTIME_PROFILES_KEY] = {
34646
+ ...globalProfiles,
34647
+ ...projectProfiles
34648
+ };
34649
+ for (const key of [
34650
+ TAKT_RUNTIME_DEFAULTS_KEY,
34651
+ TAKT_RUNTIME_TARGETS_KEY,
34652
+ TAKT_RUNTIME_AUTO_ROUTING_KEY
34653
+ ]) {
34654
+ const value = replaced(key);
34655
+ if (value !== void 0) provider[key] = value;
34656
+ }
34657
+ return { [TAKT_RUNTIME_PROVIDER_KEY]: provider };
34658
+ }
34659
+ /**
34660
+ * The runtime provider configuration in force for a generate run: the project
34661
+ * and global `runtime.yaml` merged the way Takt merges them, plus whether the
34662
+ * result puts Takt in runtime mode.
34663
+ *
34664
+ * Both scopes are read whichever scope is being generated: Takt collects legacy
34665
+ * signals from the project and global `config.yaml` alike, so a global
34666
+ * `runtime.yaml` — the one Takt generates on first launch — makes a legacy key
34667
+ * in the project config.yaml a hard failure just the same.
34668
+ */
34669
+ async function readTaktRuntimeConfig({ outputRoot, logger }) {
34670
+ const projectPath = (0, node_path.join)(outputRoot, TAKT_DIR, TAKT_RUNTIME_CONFIG_FILE_NAME);
34671
+ let globalPath;
34672
+ try {
34673
+ const resolved = (0, node_path.join)(getHomeDirectory(), TAKT_DIR, TAKT_RUNTIME_CONFIG_FILE_NAME);
34674
+ globalPath = resolved === projectPath ? void 0 : resolved;
34675
+ } catch {}
34676
+ const [project, globalFile] = await Promise.all([readTaktRuntimeFile({
34677
+ filePath: projectPath,
34678
+ logger
34679
+ }), globalPath === void 0 ? Promise.resolve(void 0) : readTaktRuntimeFile({
34680
+ filePath: globalPath,
34681
+ logger
34682
+ })]);
34683
+ const filePaths = [...project === void 0 ? [] : [projectPath], ...globalFile === void 0 || globalPath === void 0 ? [] : [globalPath]];
34684
+ const config = mergeTaktRuntimeConfigs({
34685
+ project: project?.config,
34686
+ global: globalFile?.config
34687
+ });
34688
+ return {
34689
+ filePaths,
34690
+ config,
34691
+ active: project?.unparsable === true || globalFile?.unparsable === true || isRuntimeModeActive(config)
34692
+ };
34693
+ }
33367
34694
  /**
33368
34695
  * Collapse a rulesync permissions config into Takt's single coarse mode.
33369
34696
  *
@@ -34223,6 +35550,57 @@ function asRecord(value) {
34223
35550
  return Object.fromEntries(Object.entries(value));
34224
35551
  }
34225
35552
  /**
35553
+ * The `agent` keys the `zed` override may author. Everything else — above all
35554
+ * `tool_permissions`, which the canonical `permission` block owns end to end —
35555
+ * is refused, so an override can never reach past its own surface and weaken a
35556
+ * canonical deny. The override object is read key by key rather than spread, so
35557
+ * an unlisted key is inert whether or not it is named here — an allowlist, not a
35558
+ * denylist, because `agent` carries blunt instruments of its own (Zed's
35559
+ * `always_allow_tool_actions` would disarm every permission rule at once), and a
35560
+ * verbatim `agent` merge would hand them to the override.
35561
+ *
35562
+ * Every key the patch does not consume is reported, so an unsupported or
35563
+ * misspelled one surfaces as a warning rather than as config that quietly does
35564
+ * nothing. `permission` is exempt: it is the canonical tool-scoped block, and
35565
+ * `RulesyncPermissions.forTarget` has already consumed and stripped it.
35566
+ */
35567
+ const ZED_OVERRIDE_AGENT_KEYS = ["sandbox_permissions", "profiles"];
35568
+ const ZED_CANONICAL_AGENT_KEY = "tool_permissions";
35569
+ const ZED_OVERRIDE_CONSUMED_KEYS = /* @__PURE__ */ new Set([...ZED_OVERRIDE_AGENT_KEYS, "permission"]);
35570
+ /**
35571
+ * Build the `agent` patch fragment carrying the `zed` override's verbatim
35572
+ * blocks. A key the override supplies replaces the existing block wholesale
35573
+ * (Zed reads each as one unit — a deep merge would leave half of a rewritten
35574
+ * sandbox policy behind); a key it omits is left out of the fragment, so the
35575
+ * existing value survives via the caller's spread of `agent`.
35576
+ */
35577
+ function buildZedOverridePatch({ override, logger }) {
35578
+ if (!isPlainObject$1(override)) return {};
35579
+ 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.`);
35580
+ const unsupportedKeys = Object.keys(override).filter((key) => key !== ZED_CANONICAL_AGENT_KEY && !ZED_OVERRIDE_CONSUMED_KEYS.has(key));
35581
+ 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 ")}.`);
35582
+ const patch = {};
35583
+ for (const key of ZED_OVERRIDE_AGENT_KEYS) {
35584
+ const value = override[key];
35585
+ if (isPlainObject$1(value)) patch[key] = value;
35586
+ }
35587
+ return patch;
35588
+ }
35589
+ /**
35590
+ * The write-side inverse: lift `agent.sandbox_permissions` / `agent.profiles`
35591
+ * back into the `zed` override so a hand-written sandbox policy or profile set
35592
+ * round-trips instead of being lost on the next generate. Returns `undefined`
35593
+ * when the settings carry neither, so the override key is omitted.
35594
+ */
35595
+ function extractZedOverride(agent) {
35596
+ const override = {};
35597
+ for (const key of ZED_OVERRIDE_AGENT_KEYS) {
35598
+ const value = agent[key];
35599
+ if (isPlainObject$1(value)) override[key] = value;
35600
+ }
35601
+ return Object.keys(override).length > 0 ? override : void 0;
35602
+ }
35603
+ /**
34226
35604
  * Permissions generator for the Zed editor.
34227
35605
  *
34228
35606
  * Zed maps tool permissions onto `agent.tool_permissions` inside its settings
@@ -34230,6 +35608,11 @@ function asRecord(value) {
34230
35608
  * global). That file is shared with the MCP (`context_servers`) and ignore
34231
35609
  * (`private_files`) features, so reads and writes merge into the existing JSON
34232
35610
  * rather than overwriting it, and the file is never deleted.
35611
+ *
35612
+ * Zed's OS sandbox (`agent.sandbox_permissions`) and its tool-availability
35613
+ * profiles (`agent.profiles`) are separate enforcement layers with no canonical
35614
+ * counterpart; they are authored verbatim through the `zed` override and lifted
35615
+ * back out of the settings on import. See `ZedPermissionsOverrideSchema`.
34233
35616
  */
34234
35617
  var ZedPermissions = class ZedPermissions extends ToolPermissions {
34235
35618
  constructor(params) {
@@ -34308,6 +35691,10 @@ var ZedPermissions = class ZedPermissions extends ToolPermissions {
34308
35691
  existingContent,
34309
35692
  patch: { agent: {
34310
35693
  ...agent,
35694
+ ...buildZedOverridePatch({
35695
+ override: config.zed,
35696
+ logger
35697
+ }),
34311
35698
  tool_permissions: {
34312
35699
  ...toolPermissions,
34313
35700
  ...managedDefault !== void 0 && { default: managedDefault },
@@ -34329,7 +35716,8 @@ var ZedPermissions = class ZedPermissions extends ToolPermissions {
34329
35716
  } catch (error) {
34330
35717
  throw new Error(`Failed to parse Zed permissions content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
34331
35718
  }
34332
- const toolPermissionsRaw = asRecord(settings.agent).tool_permissions;
35719
+ const agent = asRecord(settings.agent);
35720
+ const toolPermissionsRaw = agent.tool_permissions;
34333
35721
  const parsed = ZedToolPermissionsSchema.safeParse(toolPermissionsRaw ?? {});
34334
35722
  const tools = parsed.success ? parsed.data.tools ?? {} : {};
34335
35723
  const globalDefault = parsed.success ? parsed.data.default : void 0;
@@ -34353,7 +35741,10 @@ var ZedPermissions = class ZedPermissions extends ToolPermissions {
34353
35741
  for (const entry of toolPermission.always_deny ?? []) ensure(category)[entry.pattern] = "deny";
34354
35742
  for (const entry of toolPermission.always_confirm ?? []) ensure(category)[entry.pattern] = "ask";
34355
35743
  }
34356
- return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({ permission }, null, 2) });
35744
+ const zedOverride = extractZedOverride(agent);
35745
+ const result = { permission };
35746
+ if (zedOverride !== void 0) result.zed = zedOverride;
35747
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
34357
35748
  }
34358
35749
  validate() {
34359
35750
  return {
@@ -34439,6 +35830,14 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
34439
35830
  supportsImport: true
34440
35831
  }
34441
35832
  }],
35833
+ ["copilotcli", {
35834
+ class: CopilotcliPermissions,
35835
+ meta: {
35836
+ supportsProject: true,
35837
+ supportsGlobal: true,
35838
+ supportsImport: true
35839
+ }
35840
+ }],
34442
35841
  ["cursor", {
34443
35842
  class: CursorPermissions,
34444
35843
  meta: {
@@ -36392,7 +37791,8 @@ function openaiYamlToCodexcliSection(parsed) {
36392
37791
  /**
36393
37792
  * Represents a Codex CLI skill directory.
36394
37793
  * Codex CLI supports skills in both project mode (under $CWD/.agents/skills)
36395
- * and global mode (under $CODEX_HOME/skills, typically ~/.agents/skills).
37794
+ * and global mode (under ~/.agents/skills Rulesync resolves the home
37795
+ * directory rather than $CODEX_HOME).
36396
37796
  */
36397
37797
  var CodexCliSkill = class CodexCliSkill extends ToolSkill {
36398
37798
  constructor({ outputRoot = process.cwd(), relativeDirPath = CODEXCLI_SKILLS_DIR_PATH, dirName, frontmatter, body, otherFiles = [], validate = true, global = false }) {
@@ -38428,6 +39828,145 @@ var KiroIdeSkill = class extends KiroSkill {
38428
39828
  }
38429
39829
  };
38430
39830
  //#endregion
39831
+ //#region src/features/skills/musecode-skill.ts
39832
+ const MusecodeSkillFrontmatterSchema = zod_mini.z.looseObject({
39833
+ name: zod_mini.z.string(),
39834
+ description: zod_mini.z.string()
39835
+ });
39836
+ /**
39837
+ * Represents a Meta Muse Code skill directory.
39838
+ *
39839
+ * Muse Code reads Agent Skills as `<skill-id>/SKILL.md` directories from the
39840
+ * project (`.agents/skills/`) and from the user config
39841
+ * (`$XDG_CONFIG_HOME/muse/skills`, plus `~/.agents/skills`). rulesync emits the
39842
+ * project directory and, at global scope, only the XDG-default
39843
+ * `~/.config/muse/skills` so a skill is written exactly once. Muse Code's
39844
+ * compat scans of repo-local `.codex/skills` and `.claude/skills` belong to
39845
+ * other tools and are not emitted.
39846
+ * @see https://dev.meta.ai/docs/muse-code/extending.md
39847
+ */
39848
+ var MusecodeSkill = class MusecodeSkill extends ToolSkill {
39849
+ constructor({ outputRoot = process.cwd(), relativeDirPath = MUSECODE_SKILLS_DIR_PATH, dirName, frontmatter, body, otherFiles = [], validate = true, global = false }) {
39850
+ super({
39851
+ outputRoot,
39852
+ relativeDirPath,
39853
+ dirName,
39854
+ mainFile: {
39855
+ name: SKILL_FILE_NAME,
39856
+ body,
39857
+ frontmatter: { ...frontmatter }
39858
+ },
39859
+ otherFiles,
39860
+ global
39861
+ });
39862
+ if (validate) {
39863
+ const result = this.validate();
39864
+ if (!result.success) throw result.error;
39865
+ }
39866
+ }
39867
+ static getSettablePaths({ global = false } = {}) {
39868
+ return { relativeDirPath: global ? MUSECODE_GLOBAL_SKILLS_DIR_PATH : MUSECODE_SKILLS_DIR_PATH };
39869
+ }
39870
+ getFrontmatter() {
39871
+ return MusecodeSkillFrontmatterSchema.parse(this.requireMainFileFrontmatter());
39872
+ }
39873
+ getBody() {
39874
+ return this.mainFile?.body ?? "";
39875
+ }
39876
+ validate() {
39877
+ if (!this.mainFile) return {
39878
+ success: false,
39879
+ error: /* @__PURE__ */ new Error(`${this.getDirPath()}: ${SKILL_FILE_NAME} file does not exist`)
39880
+ };
39881
+ const result = MusecodeSkillFrontmatterSchema.safeParse(this.mainFile.frontmatter);
39882
+ if (!result.success) return {
39883
+ success: false,
39884
+ error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${this.getDirPath()}: ${formatError(result.error)}`)
39885
+ };
39886
+ return {
39887
+ success: true,
39888
+ error: null
39889
+ };
39890
+ }
39891
+ toRulesyncSkill() {
39892
+ const frontmatter = this.getFrontmatter();
39893
+ const rulesyncFrontmatter = {
39894
+ name: frontmatter.name,
39895
+ description: frontmatter.description,
39896
+ targets: ["*"]
39897
+ };
39898
+ return new RulesyncSkill({
39899
+ outputRoot: this.outputRoot,
39900
+ relativeDirPath: RULESYNC_SKILLS_RELATIVE_DIR_PATH,
39901
+ dirName: this.getDirName(),
39902
+ frontmatter: rulesyncFrontmatter,
39903
+ body: this.getBody(),
39904
+ otherFiles: this.getOtherFiles(),
39905
+ validate: true,
39906
+ global: this.global
39907
+ });
39908
+ }
39909
+ static fromRulesyncSkill({ outputRoot = process.cwd(), rulesyncSkill, validate = true, global = false }) {
39910
+ const settablePaths = MusecodeSkill.getSettablePaths({ global });
39911
+ const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
39912
+ const musecodeFrontmatter = {
39913
+ name: rulesyncFrontmatter.name,
39914
+ description: rulesyncFrontmatter.description
39915
+ };
39916
+ return new MusecodeSkill({
39917
+ outputRoot,
39918
+ relativeDirPath: settablePaths.relativeDirPath,
39919
+ dirName: rulesyncSkill.getDirName(),
39920
+ frontmatter: musecodeFrontmatter,
39921
+ body: rulesyncSkill.getBody(),
39922
+ otherFiles: rulesyncSkill.getOtherFiles(),
39923
+ validate,
39924
+ global
39925
+ });
39926
+ }
39927
+ static isTargetedByRulesyncSkill(rulesyncSkill) {
39928
+ const targets = rulesyncSkill.getFrontmatter().targets;
39929
+ return targets.includes("*") || targets.includes("musecode");
39930
+ }
39931
+ static async fromDir(params) {
39932
+ const loaded = await this.loadSkillDirContent({
39933
+ ...params,
39934
+ getSettablePaths: MusecodeSkill.getSettablePaths
39935
+ });
39936
+ const result = MusecodeSkillFrontmatterSchema.safeParse(loaded.frontmatter);
39937
+ if (!result.success) {
39938
+ const skillDirPath = (0, node_path.join)(loaded.outputRoot, loaded.relativeDirPath, loaded.dirName);
39939
+ throw new Error(`Invalid frontmatter in ${(0, node_path.join)(skillDirPath, SKILL_FILE_NAME)}: ${formatError(result.error)}`);
39940
+ }
39941
+ return new MusecodeSkill({
39942
+ outputRoot: loaded.outputRoot,
39943
+ relativeDirPath: loaded.relativeDirPath,
39944
+ dirName: loaded.dirName,
39945
+ frontmatter: result.data,
39946
+ body: loaded.body,
39947
+ otherFiles: loaded.otherFiles,
39948
+ validate: true,
39949
+ global: loaded.global
39950
+ });
39951
+ }
39952
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, dirName, global = false }) {
39953
+ const settablePaths = MusecodeSkill.getSettablePaths({ global });
39954
+ return new MusecodeSkill({
39955
+ outputRoot,
39956
+ relativeDirPath: relativeDirPath ?? settablePaths.relativeDirPath,
39957
+ dirName,
39958
+ frontmatter: {
39959
+ name: "",
39960
+ description: ""
39961
+ },
39962
+ body: "",
39963
+ otherFiles: [],
39964
+ validate: false,
39965
+ global
39966
+ });
39967
+ }
39968
+ };
39969
+ //#endregion
38431
39970
  //#region src/features/skills/opencode-skill.ts
38432
39971
  const OpenCodeSkillFrontmatterSchema = zod_mini.z.looseObject({
38433
39972
  name: zod_mini.z.string(),
@@ -40256,6 +41795,14 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
40256
41795
  supportsGlobal: true
40257
41796
  }
40258
41797
  }],
41798
+ ["musecode", {
41799
+ class: MusecodeSkill,
41800
+ meta: {
41801
+ supportsProject: true,
41802
+ supportsSimulated: false,
41803
+ supportsGlobal: true
41804
+ }
41805
+ }],
40259
41806
  ["opencode", {
40260
41807
  class: OpenCodeSkill,
40261
41808
  meta: {
@@ -42776,7 +44323,7 @@ var FactorydroidSubagent = class FactorydroidSubagent extends ToolSubagent {
42776
44323
  * `sub_recipes` list, which rulesync never wrote) — so those files were inert.
42777
44324
  * The custom-agent surface is the one Goose actually reads.
42778
44325
  *
42779
- * @see https://block.github.io/goose/docs/guides/context-engineering/custom-agents/
44326
+ * @see https://goose-docs.ai/docs/guides/context-engineering/custom-agents/
42780
44327
  */
42781
44328
  const GooseSubagentFrontmatterSchema = zod_mini.z.looseObject({
42782
44329
  name: zod_mini.z.string(),
@@ -48110,7 +49657,7 @@ var FactorydroidRule = class FactorydroidRule extends ToolRule {
48110
49657
  * touches during a session. The separate `.goose/memories/` tree is the Memory
48111
49658
  * extension's storage and is NOT auto-loaded as session context.
48112
49659
  * (Verified against the official docs:
48113
- * https://block.github.io/goose/docs/guides/context-engineering/using-goosehints/)
49660
+ * https://goose-docs.ai/docs/guides/context-engineering/using-goosehints/)
48114
49661
  *
48115
49662
  * rulesync's topic-based non-root rules have no project subdirectory to map onto,
48116
49663
  * so writing them under `.goose/memories/` made them effectively invisible to
@@ -48121,7 +49668,7 @@ var FactorydroidRule = class FactorydroidRule extends ToolRule {
48121
49668
  * Goose uses plain markdown files (.goosehints) without frontmatter.
48122
49669
  *
48123
49670
  * Global scope emits only `~/.config/goose/.goosehints`. Goose v1.41.0 (PR
48124
- * block/goose#9736) additionally loads the vendor-neutral
49671
+ * aaif-goose/goose#9736) additionally loads the vendor-neutral
48125
49672
  * `~/.agents/AGENTS.md` alongside the config-dir hints, but rulesync
48126
49673
  * deliberately does not emit that shared path from the goose target: the
48127
49674
  * config-dir hints remain fully loaded (no capability loss), and the
@@ -48829,6 +50376,73 @@ var KiroIdeRule = class extends KiroRule {
48829
50376
  }
48830
50377
  };
48831
50378
  //#endregion
50379
+ //#region src/features/rules/musecode-rule.ts
50380
+ var MusecodeRule = class MusecodeRule extends ToolRule {
50381
+ constructor({ fileContent, root, ...rest }) {
50382
+ super({
50383
+ ...rest,
50384
+ fileContent,
50385
+ root: root ?? false
50386
+ });
50387
+ }
50388
+ static getSettablePaths(_options = {}) {
50389
+ return { root: {
50390
+ relativeDirPath: ".",
50391
+ relativeFilePath: MUSECODE_RULE_FILE_NAME
50392
+ } };
50393
+ }
50394
+ static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, validate = true, global = false }) {
50395
+ const { root } = this.getSettablePaths({ global });
50396
+ const relativePath = (0, node_path.join)(root.relativeDirPath, root.relativeFilePath);
50397
+ const fileContent = await readFileContent((0, node_path.join)(outputRoot, relativePath));
50398
+ return new MusecodeRule({
50399
+ outputRoot,
50400
+ relativeDirPath: root.relativeDirPath,
50401
+ relativeFilePath: root.relativeFilePath,
50402
+ fileContent,
50403
+ validate,
50404
+ root: true
50405
+ });
50406
+ }
50407
+ static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
50408
+ const { root } = this.getSettablePaths({ global });
50409
+ const isRoot = rulesyncRule.getFrontmatter().root ?? false;
50410
+ return new MusecodeRule({
50411
+ outputRoot,
50412
+ relativeDirPath: root.relativeDirPath,
50413
+ relativeFilePath: root.relativeFilePath,
50414
+ fileContent: rulesyncRule.getBody(),
50415
+ validate,
50416
+ root: isRoot
50417
+ });
50418
+ }
50419
+ toRulesyncRule() {
50420
+ return this.toRulesyncRuleDefault();
50421
+ }
50422
+ validate() {
50423
+ return {
50424
+ success: true,
50425
+ error: null
50426
+ };
50427
+ }
50428
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
50429
+ return new MusecodeRule({
50430
+ outputRoot,
50431
+ relativeDirPath,
50432
+ relativeFilePath,
50433
+ fileContent: "",
50434
+ validate: false,
50435
+ root: relativeFilePath === "AGENTS.md" && relativeDirPath === "."
50436
+ });
50437
+ }
50438
+ static isTargetedByRulesyncRule(rulesyncRule) {
50439
+ return this.isTargetedByRulesyncRuleDefault({
50440
+ rulesyncRule,
50441
+ toolTarget: "musecode"
50442
+ });
50443
+ }
50444
+ };
50445
+ //#endregion
48832
50446
  //#region src/features/rules/opencode-rule.ts
48833
50447
  var OpenCodeRule = class OpenCodeRule extends ToolRule {
48834
50448
  static getSettablePaths({ global, excludeToolDir } = {}) {
@@ -50409,6 +52023,15 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
50409
52023
  ruleDiscoveryMode: "toon"
50410
52024
  }
50411
52025
  }],
52026
+ ["musecode", {
52027
+ class: MusecodeRule,
52028
+ meta: {
52029
+ extension: "md",
52030
+ supportsGlobal: false,
52031
+ ruleDiscoveryMode: "auto",
52032
+ collisionPolicy: "fold"
52033
+ }
52034
+ }],
50412
52035
  ["opencode", {
50413
52036
  class: OpenCodeRule,
50414
52037
  meta: {