rulesync 16.9.1 → 16.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -171,6 +171,7 @@ const rulesProcessorToolTargetTuple = [
171
171
  "kiro",
172
172
  "kiro-cli",
173
173
  "kiro-ide",
174
+ "musecode",
174
175
  "opencode",
175
176
  "pi",
176
177
  "qwencode",
@@ -234,6 +235,7 @@ const mcpProcessorToolTargetTuple = [
234
235
  "kiro-cli",
235
236
  "kiro-ide",
236
237
  "junie",
238
+ "musecode",
237
239
  "opencode",
238
240
  "qwencode",
239
241
  "reasonix",
@@ -341,6 +343,7 @@ const skillsProcessorToolTargetTuple = [
341
343
  "kiro",
342
344
  "kiro-cli",
343
345
  "kiro-ide",
346
+ "musecode",
344
347
  "opencode",
345
348
  "pi",
346
349
  "qwencode",
@@ -395,6 +398,7 @@ const permissionsProcessorToolTargetTuple = [
395
398
  "cline",
396
399
  "codexcli",
397
400
  "copilot",
401
+ "copilotcli",
398
402
  "cursor",
399
403
  "devin",
400
404
  "factorydroid",
@@ -418,6 +422,7 @@ const permissionsProcessorToolTargetTuple = [
418
422
  ];
419
423
  const checksProcessorToolTargetTuple = [
420
424
  "amp",
425
+ "augmentcode",
421
426
  "cursor",
422
427
  "hermesagent",
423
428
  "rovodev",
@@ -1730,6 +1735,8 @@ const SHARED_USER_MANAGED_CONFIG_PATHS = [
1730
1735
  ".claude/settings.json",
1731
1736
  ".claude/settings.local.json",
1732
1737
  ".codex/config.toml",
1738
+ ".copilot/settings.json",
1739
+ ".github/copilot/settings.json",
1733
1740
  ".devin/config.json",
1734
1741
  ".factory/settings.json",
1735
1742
  ".grok/config.toml",
@@ -2469,14 +2476,23 @@ const CLINE_HOOK_EVENTS = [
2469
2476
  /**
2470
2477
  * Hook events supported by GitHub Copilot (cloud coding agent).
2471
2478
  *
2472
- * GitHub now documents an eight-event surface for `.github/hooks/*.json`:
2479
+ * The events rulesync writes to `.github/hooks/*.json`:
2473
2480
  * `sessionStart`, `sessionEnd`, `userPromptSubmitted` ← `beforeSubmitPrompt`,
2474
- * `preToolUse`, `postToolUse`, `agentStop` ← `stop`, `subagentStop`, and
2475
- * `errorOccurred` ← `afterError`. `subagentStart` is intentionally absent: it is
2476
- * not part of the documented cloud-agent surface.
2481
+ * `preToolUse`, `postToolUse`, `agentStop` ← `stop`, `subagentStart`,
2482
+ * `subagentStop`, `errorOccurred` ← `afterError`, and `preCompact`.
2477
2483
  *
2478
- * @see https://docs.github.com/en/copilot/concepts/agents/coding-agent/about-hooks
2479
- * @see https://docs.github.com/en/copilot/concepts/agents/hooks
2484
+ * `preCompact` and `subagentStart` are authorable because the unified hooks
2485
+ * reference's per-event "Cloud agent" column says both fire there. That column
2486
+ * is the authority for this set: the older cloud-agent concept page still
2487
+ * lists only the eight events this set began as, and re-narrowing to it would
2488
+ * undo that. `notification` and `permissionRequest` stay out because the same
2489
+ * column is explicit that they do not fire on the cloud agent.
2490
+ *
2491
+ * Two further CLI events — `postToolUseFailure` and `userPromptTransformed`
2492
+ * ({@link COPILOTCLI_HOOK_EVENTS}) — are documented as firing on the cloud
2493
+ * agent as well but are not modelled here yet.
2494
+ *
2495
+ * @see https://docs.github.com/en/copilot/reference/hooks-reference
2480
2496
  */
2481
2497
  const COPILOT_HOOK_EVENTS = [
2482
2498
  "sessionStart",
@@ -2485,8 +2501,10 @@ const COPILOT_HOOK_EVENTS = [
2485
2501
  "preToolUse",
2486
2502
  "postToolUse",
2487
2503
  "stop",
2504
+ "subagentStart",
2488
2505
  "subagentStop",
2489
- "afterError"
2506
+ "afterError",
2507
+ "preCompact"
2490
2508
  ];
2491
2509
  /**
2492
2510
  * Hook events supported by the GitHub Copilot CLI (`copilotcli-hooks.ts`).
@@ -2588,8 +2606,8 @@ const CODEXCLI_HOOK_EVENTS = [
2588
2606
  * has NO `SubagentStart`/`SubagentStop` arms — emitting them would write keys
2589
2607
  * Goose silently ignores, so `subagentStart`/`subagentStop` are intentionally
2590
2608
  * excluded here and from `CANONICAL_TO_GOOSE_EVENT_NAMES`.
2591
- * @see https://github.com/block/goose/blob/v1.41.0/crates/goose/src/hooks/mod.rs
2592
- * @see https://block.github.io/goose/docs/guides/context-engineering/hooks/
2609
+ * @see https://github.com/aaif-goose/goose/blob/v1.41.0/crates/goose/src/hooks/mod.rs
2610
+ * @see https://goose-docs.ai/docs/guides/context-engineering/hooks/
2593
2611
  */
2594
2612
  const GOOSE_HOOK_EVENTS = [
2595
2613
  "sessionStart",
@@ -2626,8 +2644,8 @@ const KIRO_HOOK_EVENTS = [
2626
2644
  * `UserPromptSubmit`, `PreToolUse`, and `PostToolUse`. Kiro also documents
2627
2645
  * file-event (`PostFileCreate`/`PostFileSave`/`PostFileDelete`) and spec-task
2628
2646
  * (`PreTaskExec`/`PostTaskExec`) triggers that have no canonical equivalent;
2629
- * those can still be emitted verbatim via a `kiro-ide` or `kiro-cli` override
2630
- * block (unknown event keys pass through unchanged). There is no `SessionEnd`
2647
+ * those can still be emitted verbatim via the shared `kiro` override block
2648
+ * (unknown event keys pass through unchanged). There is no `SessionEnd`
2631
2649
  * trigger, so the canonical `sessionEnd` has no home here.
2632
2650
  * @see https://kiro.dev/docs/hooks/types/
2633
2651
  */
@@ -3222,8 +3240,10 @@ const CANONICAL_TO_COPILOT_EVENT_NAMES = {
3222
3240
  preToolUse: "preToolUse",
3223
3241
  postToolUse: "postToolUse",
3224
3242
  stop: "agentStop",
3243
+ subagentStart: "subagentStart",
3225
3244
  subagentStop: "subagentStop",
3226
- afterError: "errorOccurred"
3245
+ afterError: "errorOccurred",
3246
+ preCompact: "preCompact"
3227
3247
  };
3228
3248
  /**
3229
3249
  * Map Copilot camelCase event names to canonical camelCase.
@@ -3360,6 +3380,47 @@ const CANONICAL_TO_KIRO_EVENT_NAMES = {
3360
3380
  stop: "stop"
3361
3381
  };
3362
3382
  /**
3383
+ * Native event keys of the embedded agent-config hook format, as listed by the
3384
+ * Kiro CLI 3.0 migration guide's old-format column: `agentSpawn`,
3385
+ * `userPromptSubmit`, `preToolUse`, `postToolUse`, `fileEdited`, `fileCreated`
3386
+ * and `agentStop`/`stop`. Two of them (`fileEdited`, `fileCreated`) have no
3387
+ * canonical equivalent, so they exist only here.
3388
+ *
3389
+ * The `kiro` override block is shared with the standalone-format targets, whose
3390
+ * trigger vocabulary is different, so the alias writer uses this list to decide
3391
+ * what it can express.
3392
+ * @see https://kiro.dev/docs/cli/v3/hooks-migration/
3393
+ */
3394
+ const KIRO_AGENT_CONFIG_NATIVE_EVENT_NAMES = [
3395
+ "agentSpawn",
3396
+ "userPromptSubmit",
3397
+ "preToolUse",
3398
+ "postToolUse",
3399
+ "fileEdited",
3400
+ "fileCreated",
3401
+ "agentStop",
3402
+ "stop"
3403
+ ];
3404
+ /**
3405
+ * Old agent-config event keys mapped to their standalone v1 trigger, so a
3406
+ * `kiro.hooks` block authored in the deprecated spelling still emits a valid
3407
+ * trigger for the `kiro-cli` / `kiro-ide` targets that read the same block.
3408
+ * The migration guide states the old names "map directly to their newer
3409
+ * equivalents" and spells out `agentSpawn` → `SessionStart` and `fileEdited` →
3410
+ * `PostFileSave`; the rest are the same event under the v1 casing.
3411
+ * @see https://kiro.dev/docs/cli/v3/hooks-migration/
3412
+ */
3413
+ const KIRO_LEGACY_TO_KIRO_IDE_TRIGGER_NAMES = {
3414
+ agentSpawn: "SessionStart",
3415
+ userPromptSubmit: "UserPromptSubmit",
3416
+ preToolUse: "PreToolUse",
3417
+ postToolUse: "PostToolUse",
3418
+ fileEdited: "PostFileSave",
3419
+ fileCreated: "PostFileCreate",
3420
+ agentStop: "Stop",
3421
+ stop: "Stop"
3422
+ };
3423
+ /**
3363
3424
  * Map Kiro CLI camelCase event names to canonical camelCase.
3364
3425
  */
3365
3426
  const KIRO_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_KIRO_EVENT_NAMES).map(([k, v]) => [v, k]));
@@ -3367,8 +3428,8 @@ const KIRO_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICA
3367
3428
  * Map canonical camelCase event names to Kiro IDE PascalCase triggers.
3368
3429
  *
3369
3430
  * Only the canonical lifecycle events with a clean IDE equivalent are mapped.
3370
- * Unknown keys (e.g. IDE-only `PostFileSave`/`PreTaskExec` set via a `kiro-ide`
3371
- * override) pass through unchanged.
3431
+ * Unknown keys (e.g. IDE-only `PostFileSave`/`PreTaskExec` set via the shared
3432
+ * `kiro` override block) pass through unchanged.
3372
3433
  * @see https://kiro.dev/docs/hooks/types/
3373
3434
  */
3374
3435
  const CANONICAL_TO_KIRO_IDE_EVENT_NAMES = {
@@ -4869,13 +4930,57 @@ const CodexcliPermissionsOverrideSchema = z.looseObject({
4869
4930
  git_write_rules: z.optional(z.boolean())
4870
4931
  });
4871
4932
  /**
4933
+ * Tool-scoped override block for Zed. Two Zed surfaces sit outside the canonical
4934
+ * allow/ask/deny model that `agent.tool_permissions` implements, and both are
4935
+ * authored here verbatim:
4936
+ *
4937
+ * - `sandbox_permissions` — the OS-level agent sandbox, on by default since Zed
4938
+ * 1.14.2 for the `terminal` and `fetch` tools. Its defaults forbid network
4939
+ * access, writing outside the project directories, and writing to `.git`, so a
4940
+ * real setup usually needs to relax one of them (`network_hosts` with exact
4941
+ * hostnames or leading `*.` wildcards, `allow_all_hosts`, `write_paths`,
4942
+ * `allow_fs_write_all`, `allow_unsandboxed`). This is containment, not tool
4943
+ * gating: no canonical category expresses it, so the block is written straight
4944
+ * into `agent.sandbox_permissions`, mirroring the Kilo / Claude Code / Codex
4945
+ * CLI `sandbox` passthrough precedent. Kept a bare `looseObject` rather than
4946
+ * enumerating the keys, since upstream adds to them release over release.
4947
+ * - `profiles` — the tool-availability layer, a separate enforcement stage from
4948
+ * `tool_permissions`: a tool absent from the active profile cannot be used no
4949
+ * matter what the permission rules say. Per-profile keys are `name`, `tools`
4950
+ * (per-tool booleans), `enable_all_context_servers`, `context_servers` and
4951
+ * `default_model`. Like Kimi Code's `tools.enabled`/`disabled` block this is a
4952
+ * verbatim passthrough — no canonicalization is attempted.
4953
+ *
4954
+ * Neither surface can weaken a canonical deny, because `agent.tool_permissions`
4955
+ * is off-limits to this override: the translator consumes only the two keys
4956
+ * above by name and rejects a `tool_permissions` key with a warning.
4957
+ *
4958
+ * @see https://zed.dev/docs/ai/sandboxing
4959
+ * @see https://zed.dev/docs/ai/agent-profiles
4960
+ *
4961
+ * @example
4962
+ * { "sandbox_permissions": { "network_hosts": ["*.github.com"], "write_paths": ["/tmp"] },
4963
+ * "profiles": { "review": { "name": "Review", "tools": { "terminal": false } } } }
4964
+ */
4965
+ const ZedPermissionsOverrideSchema = z.looseObject({
4966
+ permission: z.optional(ToolScopedPermissionSchema),
4967
+ sandbox_permissions: z.optional(z.looseObject({})),
4968
+ profiles: z.optional(z.record(z.string(), z.looseObject({
4969
+ name: z.optional(z.string()),
4970
+ tools: z.optional(z.record(z.string(), z.boolean())),
4971
+ enable_all_context_servers: z.optional(z.boolean()),
4972
+ context_servers: z.optional(z.looseObject({})),
4973
+ default_model: z.optional(z.looseObject({}))
4974
+ })))
4975
+ });
4976
+ /**
4872
4977
  * Permissions configuration.
4873
4978
  * Keys are tool category names (e.g., "bash", "edit", "read", "webfetch").
4874
4979
  * Values are pattern-to-action mappings for that tool category.
4875
4980
  *
4876
4981
  * The optional `opencode`/`hermes`/`cline`/`kilo`/`claudecode`/`vibe`/`cursor`/
4877
4982
  * `qwencode`/`reasonix`/`factorydroid`/`warp`/`junie`/`takt`/`amp`/
4878
- * `antigravity-cli`/`augmentcode`/`kiro`/`codexcli` keys are tool-scoped
4983
+ * `antigravity-cli`/`augmentcode`/`kiro`/`codexcli`/`zed` keys are tool-scoped
4879
4984
  * overrides consumed only by their respective translator (see the matching
4880
4985
  * `*PermissionsOverrideSchema`); every other tool reads the shared `permission`
4881
4986
  * block and ignores them.
@@ -4915,14 +5020,15 @@ const PermissionsConfigSchema = z.looseObject({
4915
5020
  augmentcode: z.optional(AugmentcodePermissionsOverrideSchema),
4916
5021
  kiro: z.optional(KiroPermissionsOverrideSchema),
4917
5022
  codexcli: z.optional(CodexcliPermissionsOverrideSchema),
5023
+ zed: z.optional(ZedPermissionsOverrideSchema),
4918
5024
  "antigravity-ide": z.optional(CanonicalPermissionsOverrideSchema),
4919
5025
  copilot: z.optional(CanonicalPermissionsOverrideSchema),
5026
+ copilotcli: z.optional(CanonicalPermissionsOverrideSchema),
4920
5027
  devin: z.optional(CanonicalPermissionsOverrideSchema),
4921
5028
  goose: z.optional(CanonicalPermissionsOverrideSchema),
4922
5029
  grokcli: z.optional(CanonicalPermissionsOverrideSchema),
4923
5030
  "kimi-code": z.optional(KimiCodePermissionsOverrideSchema),
4924
- rovodev: z.optional(CanonicalPermissionsOverrideSchema),
4925
- zed: z.optional(CanonicalPermissionsOverrideSchema)
5031
+ rovodev: z.optional(CanonicalPermissionsOverrideSchema)
4926
5032
  });
4927
5033
  /**
4928
5034
  * Full permissions file schema including optional $schema field.
@@ -5635,6 +5741,33 @@ function resolveUserInvocable({ rootFrontmatter, section }) {
5635
5741
  return section?.["user-invocable"] ?? rootFrontmatter["user-invocable"];
5636
5742
  }
5637
5743
  //#endregion
5744
+ //#region src/constants/augmentcode-paths.ts
5745
+ const AUGMENTCODE_DIR = ".augment";
5746
+ const AUGMENTCODE_COMMANDS_DIR_PATH = join(AUGMENTCODE_DIR, "commands");
5747
+ /**
5748
+ * The cross-tool `.agents/commands/` root Auggie also discovers commands from.
5749
+ * Auggie resolves commands over `.augment`, `.claude` and `.agents` in both the
5750
+ * workspace and the home directory; rulesync writes to its own `.augment` root
5751
+ * and reads this one as well, mirroring how skills treat `.agents/skills`.
5752
+ * @see https://docs.augmentcode.com/cli/custom-commands
5753
+ */
5754
+ const AUGMENTCODE_AGENTS_COMMANDS_DIR_PATH = join(".agents", "commands");
5755
+ const AUGMENTCODE_SKILLS_DIR_PATH = join(AUGMENTCODE_DIR, "skills");
5756
+ const AUGMENTCODE_AGENTS_DIR_PATH = join(AUGMENTCODE_DIR, "agents");
5757
+ const AUGMENTCODE_ALT_AGENTS_DIR_PATH = ".agents";
5758
+ const AUGMENTCODE_AGENTS_SKILLS_DIR_PATH = join(".agents", "skills");
5759
+ const AUGMENTCODE_SETTINGS_FILE_NAME = "settings.json";
5760
+ const AUGMENTCODE_SETTINGS_LOCAL_FILE_NAME = "settings.local.json";
5761
+ /**
5762
+ * Augment Code Review's custom guidelines file. Read from the repository root's
5763
+ * `.augment/` folder, project scope only — Augment documents no user-level
5764
+ * equivalent, and the reviewer runs against the committed tree.
5765
+ * @see https://docs.augmentcode.com/codereview/review-guidelines
5766
+ */
5767
+ const AUGMENTCODE_CODE_REVIEW_GUIDELINES_FILE_NAME = "code_review_guidelines.yaml";
5768
+ const AUGMENTCODE_IGNORE_FILE_NAME = ".augmentignore";
5769
+ const AUGMENTCODE_LEGACY_RULE_FILE_NAME = ".augment-guidelines";
5770
+ //#endregion
5638
5771
  //#region src/constants/cursor-paths.ts
5639
5772
  const CURSOR_DIR = ".cursor";
5640
5773
  const CURSOR_COMMANDS_DIR_PATH = join(CURSOR_DIR, "commands");
@@ -5684,6 +5817,19 @@ const TAKT_RULE_OVERVIEW_FILE_NAME = "overview.md";
5684
5817
  */
5685
5818
  const TAKT_CONFIG_FILE_NAME = "config.yaml";
5686
5819
  /**
5820
+ * Takt's runtime provider config (Takt 0.56.0+). Lives at `.takt/runtime.yaml`
5821
+ * (project) and `~/.takt/runtime.yaml` (global), and owns provider/model/
5822
+ * provider-option assignment. Takt generates the global file on first launch,
5823
+ * so most installs have one; "runtime mode" only activates when its `provider:`
5824
+ * section carries an actual assignment.
5825
+ *
5826
+ * rulesync only ever READS this file: it resolves the active provider from it,
5827
+ * and it refuses to write the legacy `provider_options` key into `config.yaml`
5828
+ * while runtime mode is active (Takt hard-fails on that combination).
5829
+ * @see https://github.com/nrslib/takt/blob/main/docs/configuration.md
5830
+ */
5831
+ const TAKT_RUNTIME_CONFIG_FILE_NAME = "runtime.yaml";
5832
+ /**
5687
5833
  * Top-level key in Takt's `config.yaml` holding the workflow MCP transport
5688
5834
  * allowlist (`stdio` / `sse` / `http` booleans). Takt is default-deny: a
5689
5835
  * transport must be explicitly enabled here before any workflow-defined MCP
@@ -6159,6 +6305,363 @@ function slugifyCheckName(value) {
6159
6305
  return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48).replace(/-+$/, "");
6160
6306
  }
6161
6307
  //#endregion
6308
+ //#region src/features/checks/augmentcode-check.ts
6309
+ /** Augment's severity scale. Narrower than the canonical one. */
6310
+ const AUGMENTCODE_SEVERITIES = [
6311
+ "high",
6312
+ "medium",
6313
+ "low"
6314
+ ];
6315
+ /**
6316
+ * Canonical severity to Augment's scale. Augment has no band above `high`, so
6317
+ * canonical `critical` folds into it — the alternative, dropping the rule or
6318
+ * demoting it to `medium`, would either lose the check or understate the one
6319
+ * severity a reviewer most wants raised. The fold is one-way: a `critical`
6320
+ * check generates `high` and imports back as `high`, so the canonical value is
6321
+ * not recoverable from Augment's file alone.
6322
+ */
6323
+ const CANONICAL_TO_AUGMENTCODE_SEVERITY = {
6324
+ low: "low",
6325
+ medium: "medium",
6326
+ high: "high",
6327
+ critical: "high"
6328
+ };
6329
+ /**
6330
+ * Every field of an `areas.<key>` entry is required upstream, so a check with no
6331
+ * `severity` still has to emit one. `medium` is the neutral middle of Augment's
6332
+ * three bands: defaulting to `high` would inflate every unannotated check past
6333
+ * the ones deliberately marked `medium`, and `low` would bury them.
6334
+ */
6335
+ const DEFAULT_AUGMENTCODE_SEVERITY = "medium";
6336
+ /**
6337
+ * Augment's own example uses `["**"]`, and `globs` is required per area. Built
6338
+ * fresh per area rather than shared: js-yaml serializes a repeated reference as
6339
+ * an anchor/alias pair, which would litter a file Augment tells users to edit by
6340
+ * hand with `&ref_0` / `*ref_0`.
6341
+ */
6342
+ const defaultAreaGlobs = () => ["**"];
6343
+ /**
6344
+ * The `augmentcode` block of a check's frontmatter, carrying the two things the
6345
+ * canonical check model has no home for: which area a rule belongs to (so
6346
+ * several checks can share one), and the area's `globs`.
6347
+ */
6348
+ const AugmentcodeCheckOverrideSchema = z.looseObject({
6349
+ /** Area key this check's rule is grouped under; defaults to the check's slug. */
6350
+ area: z.optional(z.string().check(z.minLength(1))),
6351
+ /** Human-readable `areas.<key>.description`; defaults to the check's own. */
6352
+ areaDescription: z.optional(z.string()),
6353
+ globs: z.optional(z.array(z.string().check(z.minLength(1)))),
6354
+ /** Rule `id`; defaults to the check's slug. */
6355
+ id: z.optional(z.string().check(z.minLength(1)))
6356
+ });
6357
+ function parseOverride$1(raw, filePath, logger) {
6358
+ if (raw === void 0) return {};
6359
+ if (!isPlainObject$1(raw)) {
6360
+ logger?.warn(`Ignoring the \`augmentcode\` block in ${filePath}: expected a mapping.`);
6361
+ return {};
6362
+ }
6363
+ const result = AugmentcodeCheckOverrideSchema.safeParse(raw);
6364
+ if (!result.success) throw new Error(`Invalid \`augmentcode\` block in ${filePath}: ${result.error.message}`, { cause: result.error });
6365
+ return result.data;
6366
+ }
6367
+ function stemOf$1(rulesyncCheck) {
6368
+ return basename(rulesyncCheck.getRelativeFilePath(), ".md");
6369
+ }
6370
+ /**
6371
+ * Disambiguate a name against the ones already taken. Used for rule ids on
6372
+ * generate (two same-named checks in different subdirectories) and for check
6373
+ * file names on import (one rule id repeated across two areas), since a
6374
+ * collision in either direction silently loses one of the pair.
6375
+ */
6376
+ function uniqueName(preferred, used) {
6377
+ let name = preferred;
6378
+ let suffix = 2;
6379
+ while (used.has(name)) {
6380
+ name = `${preferred}-${suffix}`;
6381
+ suffix += 1;
6382
+ }
6383
+ used.add(name);
6384
+ return name;
6385
+ }
6386
+ /**
6387
+ * A rule's `description` is the whole instruction Augment acts on, so the check
6388
+ * body is preferred; the frontmatter `description` is a summary, used only when
6389
+ * there is no body. Both empty falls back to the check name, which at least
6390
+ * names the concern rather than emitting an empty required field.
6391
+ */
6392
+ function toRuleDescription(rulesyncCheck) {
6393
+ const body = rulesyncCheck.getBody().trim();
6394
+ if (body.length > 0) return body;
6395
+ return rulesyncCheck.getFrontmatter().description?.trim() || stemOf$1(rulesyncCheck);
6396
+ }
6397
+ function toRule(rulesyncCheck, override, usedRuleIds) {
6398
+ const severity = rulesyncCheck.getFrontmatter().severity;
6399
+ return {
6400
+ id: uniqueName(override.id ?? slugifyCheckName(stemOf$1(rulesyncCheck)), usedRuleIds),
6401
+ description: toRuleDescription(rulesyncCheck),
6402
+ severity: severity ? CANONICAL_TO_AUGMENTCODE_SEVERITY[severity] ?? DEFAULT_AUGMENTCODE_SEVERITY : DEFAULT_AUGMENTCODE_SEVERITY
6403
+ };
6404
+ }
6405
+ /**
6406
+ * An authored area key is used verbatim. Slugifying it would rewrite the
6407
+ * underscores in Augment's own documented example (`memory_safety`), and since
6408
+ * import writes the key back unchanged, the next generate would build a second
6409
+ * area under the slugified spelling while the original stayed put — the same
6410
+ * rules twice. Only the file-stem default is slugified, because that one has to
6411
+ * become a legal area key from an arbitrary file name.
6412
+ */
6413
+ function areaKeyFor(rulesyncCheck, override) {
6414
+ return override.area ?? slugifyCheckName(stemOf$1(rulesyncCheck));
6415
+ }
6416
+ /** Rule ids belonging to areas this generate preserves rather than rewrites. */
6417
+ function collectRuleIds(existingAreas, claimedKeys) {
6418
+ const ids = /* @__PURE__ */ new Set();
6419
+ for (const [areaKey, rawArea] of Object.entries(existingAreas)) {
6420
+ if (claimedKeys.has(areaKey) || !isPlainObject$1(rawArea)) continue;
6421
+ for (const rawRule of readArea(rawArea).rules) {
6422
+ const rule = readRule(rawRule);
6423
+ if (rule) ids.add(rule.id);
6424
+ }
6425
+ }
6426
+ return ids;
6427
+ }
6428
+ /**
6429
+ * Group the checks into `areas`. One area per check by default; checks naming
6430
+ * the same `augmentcode.area` share one, with the first check to name it
6431
+ * supplying the area's `description` and `globs`.
6432
+ */
6433
+ function buildAreas({ entries, reservedRuleIds }) {
6434
+ const areas = /* @__PURE__ */ new Map();
6435
+ const usedRuleIds = new Set(reservedRuleIds);
6436
+ for (const { rulesyncCheck, override } of entries) {
6437
+ const key = areaKeyFor(rulesyncCheck, override);
6438
+ const rule = toRule(rulesyncCheck, override, usedRuleIds);
6439
+ const existing = areas.get(key);
6440
+ if (existing) {
6441
+ existing.rules.push(rule);
6442
+ continue;
6443
+ }
6444
+ areas.set(key, {
6445
+ description: override.areaDescription || rulesyncCheck.getFrontmatter().description?.trim() || stemOf$1(rulesyncCheck),
6446
+ globs: override.globs ?? defaultAreaGlobs(),
6447
+ rules: [rule]
6448
+ });
6449
+ }
6450
+ return Object.fromEntries(areas);
6451
+ }
6452
+ function parseGuidelines(fileContent, filePath) {
6453
+ if (fileContent.trim().length === 0) return {};
6454
+ let parsed;
6455
+ try {
6456
+ parsed = loadYaml(fileContent);
6457
+ } catch (error) {
6458
+ throw new Error(`Failed to parse AugmentCode code review guidelines at ${filePath}: ${formatError(error)}`, { cause: error });
6459
+ }
6460
+ if (parsed === void 0 || parsed === null) return {};
6461
+ if (!isPlainObject$1(parsed)) throw new Error(`Failed to parse AugmentCode code review guidelines at ${filePath}: expected a mapping at the document root.`);
6462
+ return parsed;
6463
+ }
6464
+ /** An area's shared fields, read defensively out of hand-written YAML. */
6465
+ function readArea(rawArea) {
6466
+ const rawGlobs = Array.isArray(rawArea.globs) ? rawArea.globs : void 0;
6467
+ const globs = rawGlobs?.filter((glob) => typeof glob === "string");
6468
+ const authoredGlobs = globs && (globs.length > 0 || rawGlobs?.length === 0) ? globs : void 0;
6469
+ return {
6470
+ ...typeof rawArea.description === "string" && { description: rawArea.description },
6471
+ ...authoredGlobs && { globs: authoredGlobs },
6472
+ rules: Array.isArray(rawArea.rules) ? rawArea.rules : []
6473
+ };
6474
+ }
6475
+ /**
6476
+ * A rule missing `id` or `description` is not a shape Augment itself reads, so
6477
+ * it is skipped rather than imported as a check with an invented field.
6478
+ */
6479
+ function readRule(rawRule) {
6480
+ if (!isPlainObject$1(rawRule)) return void 0;
6481
+ const id = typeof rawRule.id === "string" ? rawRule.id : void 0;
6482
+ const description = typeof rawRule.description === "string" ? rawRule.description : void 0;
6483
+ if (!id || !description) return void 0;
6484
+ const severity = AUGMENTCODE_SEVERITIES.find((value) => value === rawRule.severity);
6485
+ return {
6486
+ id,
6487
+ description,
6488
+ ...severity && { severity }
6489
+ };
6490
+ }
6491
+ /**
6492
+ * Checks adapter for Augment Code Review's custom guidelines
6493
+ * (`.augment/code_review_guidelines.yaml`).
6494
+ *
6495
+ * Augment groups review rules into named **areas**: each area carries a
6496
+ * `description`, the `globs` it applies to, and a list of `rules`, every rule
6497
+ * being an `id` / `description` / `severity` triple. Every one of those fields
6498
+ * is required upstream. Rulesync maps one `.rulesync/checks/*.md` onto one rule,
6499
+ * in an area of its own unless an `augmentcode.area` groups several together —
6500
+ * so the whole set collapses into this one file via {@link fromRulesyncChecks}
6501
+ * rather than a file per check.
6502
+ *
6503
+ * **Severity is lossy in one direction.** Augment's scale is `high` / `medium` /
6504
+ * `low` with no band above `high`, so canonical `critical` maps to `high` and
6505
+ * imports back as `high`. A check with no `severity` at all emits `medium`,
6506
+ * since the field cannot be omitted.
6507
+ *
6508
+ * **`file_paths_to_ignore`** is recognized and preserved verbatim, but never
6509
+ * authored or imported: the canonical check model has no ignore surface, and
6510
+ * inventing one here is a separate design question.
6511
+ *
6512
+ * **Generation merges rather than replaces.** Augment's own documentation tells
6513
+ * users to hand-write this file, so only the areas the current check set claims
6514
+ * are rewritten; every other area, `file_paths_to_ignore`, and any unknown
6515
+ * top-level key survive. The cost of that is that rulesync cannot tell its own
6516
+ * leftovers from a hand-written area: renaming a check strands the area under
6517
+ * the old key, and dropping the last Augment-targeting check leaves the areas in
6518
+ * place with a warning rather than guessing which of them to delete. Deleting
6519
+ * the file outright is likewise refused whenever it exists.
6520
+ *
6521
+ * Project scope only — the reviewer reads the file from the committed
6522
+ * repository, and Augment documents no user-level equivalent.
6523
+ *
6524
+ * @see https://docs.augmentcode.com/codereview/review-guidelines
6525
+ */
6526
+ var AugmentcodeCheck = class AugmentcodeCheck extends ToolCheck {
6527
+ static getSettablePaths(_options = {}) {
6528
+ return {
6529
+ relativeDirPath: AUGMENTCODE_DIR,
6530
+ relativeFilePath: AUGMENTCODE_CODE_REVIEW_GUIDELINES_FILE_NAME
6531
+ };
6532
+ }
6533
+ static isTargetedByRulesyncCheck(rulesyncCheck) {
6534
+ return this.isTargetedByRulesyncCheckDefault({
6535
+ rulesyncCheck,
6536
+ toolTarget: "augmentcode"
6537
+ });
6538
+ }
6539
+ /**
6540
+ * Ownership guard the processor consults before it deletes anything for this
6541
+ * tool. Unlike the Markdown adapters, whose section markers say which text is
6542
+ * rulesync's, YAML carries no such marker — js-yaml drops comments on rewrite,
6543
+ * and an unknown top-level key risks Augment's own parser. So an existing file
6544
+ * is never rulesync's to delete: it may hold hand-written areas, and there is
6545
+ * no way to prove otherwise.
6546
+ */
6547
+ static async canDeleteAuxiliaryFiles({ outputRoot }) {
6548
+ const paths = AugmentcodeCheck.getSettablePaths();
6549
+ return !await fileExists(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath ?? "code_review_guidelines.yaml"));
6550
+ }
6551
+ static fromRulesyncCheck(_params) {
6552
+ throw new Error("AugmentCode checks are built from all checks at once; use fromRulesyncChecks.");
6553
+ }
6554
+ static async fromRulesyncChecks({ outputRoot = process.cwd(), rulesyncChecks, global = false, logger }) {
6555
+ const paths = AugmentcodeCheck.getSettablePaths({ global });
6556
+ const relativeFilePath = paths.relativeFilePath ?? "code_review_guidelines.yaml";
6557
+ const filePath = join(outputRoot, paths.relativeDirPath, relativeFilePath);
6558
+ const existing = parseGuidelines(await readFileContentOrNull(filePath) ?? "", filePath);
6559
+ const existingAreas = isPlainObject$1(existing.areas) ? existing.areas : {};
6560
+ if (rulesyncChecks.length === 0) {
6561
+ 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.`);
6562
+ return [];
6563
+ }
6564
+ const entries = rulesyncChecks.map((rulesyncCheck) => ({
6565
+ rulesyncCheck,
6566
+ override: parseOverride$1(rulesyncCheck.getFrontmatter().augmentcode, join(RULESYNC_CHECKS_RELATIVE_DIR_PATH, rulesyncCheck.getRelativeFilePath()), logger)
6567
+ }));
6568
+ const generatedAreas = buildAreas({
6569
+ entries,
6570
+ reservedRuleIds: collectRuleIds(existingAreas, new Set(entries.map(({ rulesyncCheck, override }) => areaKeyFor(rulesyncCheck, override))))
6571
+ });
6572
+ const fileContent = dump({
6573
+ ...existing,
6574
+ areas: {
6575
+ ...existingAreas,
6576
+ ...generatedAreas
6577
+ }
6578
+ }, {
6579
+ lineWidth: -1,
6580
+ noRefs: true
6581
+ });
6582
+ return [new AugmentcodeCheck({
6583
+ outputRoot,
6584
+ relativeDirPath: paths.relativeDirPath,
6585
+ relativeFilePath,
6586
+ fileContent,
6587
+ global
6588
+ })];
6589
+ }
6590
+ static async fromFile({ outputRoot = process.cwd(), global = false }) {
6591
+ const paths = AugmentcodeCheck.getSettablePaths({ global });
6592
+ const relativeFilePath = paths.relativeFilePath ?? "code_review_guidelines.yaml";
6593
+ const filePath = join(outputRoot, paths.relativeDirPath, relativeFilePath);
6594
+ return new AugmentcodeCheck({
6595
+ outputRoot,
6596
+ relativeDirPath: paths.relativeDirPath,
6597
+ relativeFilePath,
6598
+ fileContent: await readFileContentOrNull(filePath) ?? "",
6599
+ global
6600
+ });
6601
+ }
6602
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
6603
+ return new AugmentcodeCheck({
6604
+ outputRoot,
6605
+ relativeDirPath,
6606
+ relativeFilePath,
6607
+ fileContent: "",
6608
+ validate: false,
6609
+ global
6610
+ });
6611
+ }
6612
+ validate() {
6613
+ return {
6614
+ success: true,
6615
+ error: null
6616
+ };
6617
+ }
6618
+ toRulesyncCheck() {
6619
+ const first = this.toRulesyncChecks()[0];
6620
+ if (!first) throw new Error(`No review areas found in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}.`);
6621
+ return first;
6622
+ }
6623
+ /**
6624
+ * One check per rule, not per area: an area with three rules is three
6625
+ * independent review instructions, and collapsing them into one check would
6626
+ * make the next generate emit a single merged rule in their place. The area's
6627
+ * key, description and globs ride along in each check's `augmentcode` block,
6628
+ * so regenerating regroups the rules exactly where they were.
6629
+ */
6630
+ toRulesyncChecks() {
6631
+ const filePath = join(this.getRelativeDirPath(), this.getRelativeFilePath());
6632
+ const guidelines = parseGuidelines(this.getFileContent(), filePath);
6633
+ const areas = isPlainObject$1(guidelines.areas) ? guidelines.areas : {};
6634
+ const checks = [];
6635
+ const usedNames = /* @__PURE__ */ new Set();
6636
+ for (const [areaKey, rawArea] of Object.entries(areas)) {
6637
+ if (!isPlainObject$1(rawArea)) continue;
6638
+ const { description: areaDescription, globs, rules } = readArea(rawArea);
6639
+ for (const rawRule of rules) {
6640
+ const rule = readRule(rawRule);
6641
+ if (!rule) continue;
6642
+ const name = uniqueName(slugifyCheckName(rule.id) || slugifyCheckName(areaKey) || "check", usedNames);
6643
+ checks.push(new RulesyncCheck({
6644
+ outputRoot: ".",
6645
+ relativeDirPath: RULESYNC_CHECKS_RELATIVE_DIR_PATH,
6646
+ relativeFilePath: `${name}.md`,
6647
+ frontmatter: {
6648
+ targets: ["*"],
6649
+ ...rule.severity && { severity: rule.severity },
6650
+ augmentcode: {
6651
+ area: areaKey,
6652
+ ...areaDescription !== void 0 && { areaDescription },
6653
+ ...globs && { globs },
6654
+ id: rule.id
6655
+ }
6656
+ },
6657
+ body: rule.description
6658
+ }));
6659
+ }
6660
+ }
6661
+ return checks;
6662
+ }
6663
+ };
6664
+ //#endregion
6162
6665
  //#region src/features/checks/aggregated-check-file.ts
6163
6666
  /**
6164
6667
  * Shared machinery for the tools whose checks surface is **one aggregated
@@ -7081,6 +7584,22 @@ const SHARED_CONFIG_OWNERSHIP = {
7081
7584
  }
7082
7585
  }
7083
7586
  },
7587
+ ".github/copilot/settings.json": {
7588
+ format: "json",
7589
+ invalidRootPolicy: "error",
7590
+ features: { permissions: {
7591
+ kind: "replace-owned-keys",
7592
+ ownedKeys: ["deniedUrls"]
7593
+ } }
7594
+ },
7595
+ ".copilot/settings.json": {
7596
+ format: "json",
7597
+ invalidRootPolicy: "error",
7598
+ features: { permissions: {
7599
+ kind: "replace-owned-keys",
7600
+ ownedKeys: ["allowedUrls", "deniedUrls"]
7601
+ } }
7602
+ },
7084
7603
  ".vscode/settings.json": {
7085
7604
  format: "jsonc",
7086
7605
  invalidRootPolicy: "error",
@@ -7161,6 +7680,14 @@ const SHARED_CONFIG_OWNERSHIP = {
7161
7680
  }
7162
7681
  }
7163
7682
  },
7683
+ ".config/muse/settings.json": {
7684
+ format: "json",
7685
+ invalidRootPolicy: "error",
7686
+ features: { mcp: {
7687
+ kind: "replace-owned-keys",
7688
+ ownedKeys: ["mcp_servers", "schema_version"]
7689
+ } }
7690
+ },
7164
7691
  ".kiro/agents/default.json": {
7165
7692
  format: "json",
7166
7693
  features: {
@@ -7808,6 +8335,14 @@ const toolCheckFactories = /* @__PURE__ */ new Map([
7808
8335
  filePattern: "*.md"
7809
8336
  }
7810
8337
  }],
8338
+ ["augmentcode", {
8339
+ class: AugmentcodeCheck,
8340
+ meta: {
8341
+ supportsGlobal: false,
8342
+ filePattern: AUGMENTCODE_CODE_REVIEW_GUIDELINES_FILE_NAME,
8343
+ committedOutput: true
8344
+ }
8345
+ }],
7811
8346
  ["cursor", {
7812
8347
  class: CursorCheck,
7813
8348
  meta: {
@@ -8595,26 +9130,6 @@ var AntigravityIdeCommand = class extends AntigravitySharedCommand {
8595
9130
  }
8596
9131
  };
8597
9132
  //#endregion
8598
- //#region src/constants/augmentcode-paths.ts
8599
- const AUGMENTCODE_DIR = ".augment";
8600
- const AUGMENTCODE_COMMANDS_DIR_PATH = join(AUGMENTCODE_DIR, "commands");
8601
- /**
8602
- * The cross-tool `.agents/commands/` root Auggie also discovers commands from.
8603
- * Auggie resolves commands over `.augment`, `.claude` and `.agents` in both the
8604
- * workspace and the home directory; rulesync writes to its own `.augment` root
8605
- * and reads this one as well, mirroring how skills treat `.agents/skills`.
8606
- * @see https://docs.augmentcode.com/cli/custom-commands
8607
- */
8608
- const AUGMENTCODE_AGENTS_COMMANDS_DIR_PATH = join(".agents", "commands");
8609
- const AUGMENTCODE_SKILLS_DIR_PATH = join(AUGMENTCODE_DIR, "skills");
8610
- const AUGMENTCODE_AGENTS_DIR_PATH = join(AUGMENTCODE_DIR, "agents");
8611
- const AUGMENTCODE_ALT_AGENTS_DIR_PATH = ".agents";
8612
- const AUGMENTCODE_AGENTS_SKILLS_DIR_PATH = join(".agents", "skills");
8613
- const AUGMENTCODE_SETTINGS_FILE_NAME = "settings.json";
8614
- const AUGMENTCODE_SETTINGS_LOCAL_FILE_NAME = "settings.local.json";
8615
- const AUGMENTCODE_IGNORE_FILE_NAME = ".augmentignore";
8616
- const AUGMENTCODE_LEGACY_RULE_FILE_NAME = ".augment-guidelines";
8617
- //#endregion
8618
9133
  //#region src/features/commands/augmentcode-command.ts
8619
9134
  const AugmentcodeCommandFrontmatterSchema = z.looseObject({
8620
9135
  description: z.optional(z.string()),
@@ -9148,6 +9663,8 @@ const COPILOT_GLOBAL_HOOKS_FILE_NAME = "copilot-ide-hooks.json";
9148
9663
  const COPILOT_MCP_DIR = ".vscode";
9149
9664
  const COPILOT_MCP_FILE_NAME = "mcp.json";
9150
9665
  const COPILOT_VSCODE_SETTINGS_FILE_NAME = "settings.json";
9666
+ const COPILOTCLI_SETTINGS_FILE_NAME = "settings.json";
9667
+ const COPILOTCLI_PROJECT_SETTINGS_DIR_PATH = join(GITHUB_DIR, "copilot");
9151
9668
  const COPILOTCLI_MCP_FILE_NAME = "mcp-config.json";
9152
9669
  const COPILOTCLI_PROJECT_MCP_FILE_NAME = "mcp.json";
9153
9670
  const COPILOTCLI_AGENTS_DIR_PATH = join(COPILOT_DIR, "agents");
@@ -9407,6 +9924,7 @@ const DEVIN_HOOKS_V1_FILE_NAME = "hooks.v1.json";
9407
9924
  const DEVIN_GLOBAL_AGENTS_FILE_NAME = "AGENTS.md";
9408
9925
  const DEVIN_IGNORE_FILE_NAME = ".devinignore";
9409
9926
  const DEVIN_LEGACY_IGNORE_FILE_NAME = ".codeiumignore";
9927
+ const DEVIN_WINDSURF_IGNORE_FILE_NAME = ".windsurfignore";
9410
9928
  const DEVIN_GLOBAL_IGNORE_DIR_PATH = ".codeium";
9411
9929
  const DEVIN_GLOBAL_IGNORE_FILE_NAME = DEVIN_LEGACY_IGNORE_FILE_NAME;
9412
9930
  //#endregion
@@ -9761,7 +10279,7 @@ var GooseCommandConfigFile = class extends ToolFile {
9761
10279
  * The whole file is a YAML mapping (not frontmatter + markdown body), so the
9762
10280
  * class stores the parsed recipe object rather than a frontmatter/body split.
9763
10281
  *
9764
- * @see https://block.github.io/goose/docs/guides/recipes/recipe-reference/
10282
+ * @see https://goose-docs.ai/docs/guides/recipes/recipe-reference/
9765
10283
  */
9766
10284
  const GooseCommandRecipeSchema = z.looseObject({
9767
10285
  version: z.optional(z.string()),
@@ -13255,7 +13773,7 @@ function collectHandlers({ effectiveHooks, eventMap }) {
13255
13773
  }
13256
13774
  return handlerGroups;
13257
13775
  }
13258
- function buildCommandLines({ handler, usesToolName, blocksToolCall }) {
13776
+ function buildCommandLines$1({ handler, usesToolName, blocksToolCall }) {
13259
13777
  const lines = [];
13260
13778
  const indent = usesToolName && handler.matcher ? " " : " ";
13261
13779
  if (usesToolName && handler.matcher) lines.push(` if (new RegExp(${matcherToEmbeddedLiteral$1(handler.matcher)}).test(event.tool)) {`);
@@ -13280,7 +13798,7 @@ function buildSubscriptionLines$1(handlerGroups) {
13280
13798
  const usesToolName = AMP_TOOL_EVENTS.has(ampEvent) && handlers.some((handler) => handler.matcher);
13281
13799
  const blocksToolCall = ampEvent === "tool.call";
13282
13800
  lines.push(` amp.on(${JSON.stringify(ampEvent)}, async (${usesToolName ? "event, ctx" : "_event, ctx"}) => {`);
13283
- for (const handler of handlers) lines.push(...buildCommandLines({
13801
+ for (const handler of handlers) lines.push(...buildCommandLines$1({
13284
13802
  handler,
13285
13803
  usesToolName,
13286
13804
  blocksToolCall
@@ -15265,8 +15783,10 @@ const CopilotHookEntrySchema = z.looseObject({
15265
15783
  bash: z.optional(z.string()),
15266
15784
  powershell: z.optional(z.string()),
15267
15785
  command: z.optional(z.string()),
15786
+ cwd: z.optional(z.string()),
15268
15787
  env: z.optional(z.record(z.string(), z.string())),
15269
- timeoutSec: z.optional(z.number())
15788
+ timeoutSec: z.optional(z.number()),
15789
+ timeout: z.optional(z.number())
15270
15790
  });
15271
15791
  /**
15272
15792
  * Convert canonical hooks config to Copilot format.
@@ -15346,6 +15866,22 @@ function resolveImportCommand$1(entry, logger) {
15346
15866
  return typeof entry.command === "string" ? { command: entry.command } : {};
15347
15867
  }
15348
15868
  /**
15869
+ * Extract the non-command fields preserved across import.
15870
+ *
15871
+ * Generate re-emits any non-canonical key verbatim through `rest`, so a key
15872
+ * dropped here does not survive an import → generate round trip. `cwd` is a
15873
+ * documented Copilot hook field and was previously lost that way.
15874
+ *
15875
+ * @see https://docs.github.com/en/copilot/reference/hooks-reference
15876
+ * @see https://docs.github.com/en/copilot/concepts/agents/coding-agent/about-hooks
15877
+ */
15878
+ function importPassthrough$1(entry) {
15879
+ const passthrough = {};
15880
+ if (entry.cwd !== void 0) passthrough.cwd = entry.cwd;
15881
+ if (entry.env !== void 0) passthrough.env = entry.env;
15882
+ return passthrough;
15883
+ }
15884
+ /**
15349
15885
  * Extract hooks from Copilot hooks JSON into canonical format.
15350
15886
  * Copilot format: { version: 1, hooks: { eventName: [...hookEntries] } }
15351
15887
  */
@@ -15361,13 +15897,13 @@ function copilotHooksToCanonical(copilotHooks, logger) {
15361
15897
  if (!parseResult.success) continue;
15362
15898
  const entry = parseResult.data;
15363
15899
  const { command, shell } = resolveImportCommand$1(entry, logger);
15364
- const timeout = entry.timeoutSec;
15900
+ const timeout = entry.timeoutSec ?? entry.timeout;
15365
15901
  defs.push({
15366
15902
  type: "command",
15367
15903
  ...command !== void 0 && { command },
15368
15904
  ...shell !== void 0 && { shell },
15369
- ...entry.env !== void 0 && { env: entry.env },
15370
- ...timeout !== void 0 && { timeout }
15905
+ ...timeout !== void 0 && { timeout },
15906
+ ...importPassthrough$1(entry)
15371
15907
  });
15372
15908
  }
15373
15909
  if (defs.length > 0) canonical[eventName] = defs;
@@ -15650,19 +16186,19 @@ function importPassthrough(entry) {
15650
16186
  * A shell-specific field carries its `shell` through so re-export writes the
15651
16187
  * same field back. An entry using only the portable `command` field leaves
15652
16188
  * `shell` unset, which re-export renders as the portable field again.
16189
+ *
16190
+ * When both shell-specific fields are present, `bash` wins and a warning names
16191
+ * the ignored `powershell`. The choice is deliberately not platform-dependent:
16192
+ * importing on Windows must not produce a different canonical config than
16193
+ * importing the same file on Linux, which would make the rulesync hooks file
16194
+ * differ per machine for anyone who checks it in.
15653
16195
  */
15654
16196
  function resolveImportCommand(entry, logger) {
15655
16197
  const hasBash = typeof entry.bash === "string";
15656
16198
  const hasPowershell = typeof entry.powershell === "string";
15657
16199
  if (hasBash && hasPowershell) {
15658
- const isWindows = process.platform === "win32";
15659
- const chosen = isWindows ? "powershell" : "bash";
15660
- const ignored = isWindows ? "bash" : "powershell";
15661
- logger?.warn(`Copilot CLI hook has both bash and powershell commands; using ${chosen} and ignoring ${ignored} on this platform.`);
15662
- return isWindows ? {
15663
- command: entry.powershell,
15664
- shell: "powershell"
15665
- } : {
16200
+ 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.");
16201
+ return {
15666
16202
  command: entry.bash,
15667
16203
  shell: "bash"
15668
16204
  };
@@ -16321,7 +16857,7 @@ const GOOSE_CONVERTER_CONFIG = {
16321
16857
  *
16322
16858
  * The JSON shape matches Claude Code's: each PascalCase event maps to an array of
16323
16859
  * `{ matcher, hooks: [{ type: "command", command }] }` entries.
16324
- * @see https://block.github.io/goose/docs/guides/context-engineering/hooks/
16860
+ * @see https://goose-docs.ai/docs/guides/context-engineering/hooks/
16325
16861
  */
16326
16862
  var GooseHooks = class GooseHooks extends ToolHooks {
16327
16863
  constructor(params) {
@@ -16522,6 +17058,13 @@ var GrokcliHooks = class GrokcliHooks extends ToolHooks {
16522
17058
  * @see https://github.com/NousResearch/hermes-agent/blob/main/website/docs/user-guide/features/hooks.md
16523
17059
  */
16524
17060
  const HERMESAGENT_MATCHER_EVENTS = /* @__PURE__ */ new Set(["pre_tool_call", "post_tool_call"]);
17061
+ /**
17062
+ * The only Hermes event that can block, and therefore the only one whose
17063
+ * entries accept `fail_closed`. Upstream warns and ignores the key on every
17064
+ * other event ("only blocking-capable events can fail closed").
17065
+ * @see https://github.com/NousResearch/hermes-agent/blob/main/website/docs/user-guide/features/hooks.md
17066
+ */
17067
+ const HERMESAGENT_FAIL_CLOSED_EVENT = "pre_tool_call";
16525
17068
  const HERMESAGENT_CANONICAL_EVENTS = new Set(HERMESAGENT_HOOK_EVENTS);
16526
17069
  const HERMESAGENT_NATIVE_EVENTS = new Set(HERMESAGENT_NATIVE_HOOK_EVENTS);
16527
17070
  /**
@@ -16555,7 +17098,9 @@ function isHermesHookEventEntry(key, value) {
16555
17098
  * unsupported hook types centrally). `matcher` is only carried through for
16556
17099
  * `pre_tool_call`/`post_tool_call`; on any other event it is dropped with a
16557
17100
  * warning, mirroring how other adapters (e.g. AugmentCode) handle
16558
- * matcher-less lifecycle events.
17101
+ * matcher-less lifecycle events. The canonical `failClosed` field (shared with
17102
+ * Cursor, whose semantics Hermes copied) becomes `fail_closed`, which upstream
17103
+ * only honours on `pre_tool_call`.
16559
17104
  */
16560
17105
  function definitionsToHermesEntries({ event, sourceEvent = event, definitions, logger }) {
16561
17106
  const supportsMatcher = HERMESAGENT_MATCHER_EVENTS.has(event);
@@ -16566,6 +17111,10 @@ function definitionsToHermesEntries({ event, sourceEvent = event, definitions, l
16566
17111
  if (typeof definition.matcher === "string" && definition.matcher !== "") if (supportsMatcher) entry.matcher = definition.matcher;
16567
17112
  else logger?.warn(`matcher "${definition.matcher}" on "${sourceEvent}" hook will be ignored — Hermes Agent only supports matchers on pre_tool_call/post_tool_call`);
16568
17113
  if (typeof definition.timeout === "number") entry.timeout = definition.timeout;
17114
+ if (typeof definition.failClosed === "boolean") {
17115
+ if (event === HERMESAGENT_FAIL_CLOSED_EVENT) entry.fail_closed = definition.failClosed;
17116
+ else if (definition.failClosed) logger?.warn(`failClosed on "${sourceEvent}" hook will be ignored — Hermes Agent only supports fail_closed on pre_tool_call`);
17117
+ }
16569
17118
  entries.push(entry);
16570
17119
  }
16571
17120
  return entries;
@@ -16617,6 +17166,37 @@ function canonicalToHermesHooks({ config, toolOverrideHooks, logger }) {
16617
17166
  return result;
16618
17167
  }
16619
17168
  /**
17169
+ * Reads a hook entry's fail-closed flag. Upstream accepts its own `fail_closed`
17170
+ * and the Cursor/Claude Code `failClosed` spelling alike, so both have to be
17171
+ * read back into the single canonical field.
17172
+ */
17173
+ function readHermesFailClosed(entry) {
17174
+ if (typeof entry.fail_closed === "boolean") return entry.fail_closed;
17175
+ if (typeof entry.failClosed === "boolean") return entry.failClosed;
17176
+ }
17177
+ /**
17178
+ * Converts one serialized Hermes hook entry back into a canonical definition,
17179
+ * or `undefined` when it is not a hook rulesync models (no string `command`).
17180
+ * `matcher` and `fail_closed` are read only on the events upstream honours them
17181
+ * on, so an imported value is never one the next generate warns about and drops.
17182
+ */
17183
+ function hermesEntryToDefinition({ nativeEvent, raw }) {
17184
+ if (!isRecord$1(raw)) return;
17185
+ const entry = raw;
17186
+ if (typeof entry.command !== "string") return;
17187
+ const def = {
17188
+ type: "command",
17189
+ command: entry.command
17190
+ };
17191
+ if (HERMESAGENT_MATCHER_EVENTS.has(nativeEvent) && typeof entry.matcher === "string" && entry.matcher !== "") def.matcher = entry.matcher;
17192
+ if (typeof entry.timeout === "number") def.timeout = entry.timeout;
17193
+ if (nativeEvent === HERMESAGENT_FAIL_CLOSED_EVENT) {
17194
+ const failClosed = readHermesFailClosed(entry);
17195
+ if (failClosed !== void 0) def.failClosed = failClosed;
17196
+ }
17197
+ return def;
17198
+ }
17199
+ /**
16620
17200
  * Reverse {@link canonicalToHermesHooks}: parse Hermes's native
16621
17201
  * `hooks: { <event>: [...] }` map back into a canonical event → definition[]
16622
17202
  * record. Native events with no canonical equivalent (`pre_verify`,
@@ -16630,19 +17210,10 @@ function hermesHooksToCanonical(hooks) {
16630
17210
  if (PROTOTYPE_POLLUTION_KEYS.has(nativeEvent) || !Array.isArray(entries)) continue;
16631
17211
  if (!isHermesHookEventEntry(nativeEvent, entries)) continue;
16632
17212
  const rulesyncEvent = HERMESAGENT_TO_CANONICAL_EVENT_NAMES[nativeEvent] ?? nativeEvent;
16633
- const defs = [];
16634
- for (const raw of entries) {
16635
- if (raw === null || typeof raw !== "object" || Array.isArray(raw)) continue;
16636
- const entry = raw;
16637
- if (typeof entry.command !== "string") continue;
16638
- const def = {
16639
- type: "command",
16640
- command: entry.command
16641
- };
16642
- if (HERMESAGENT_MATCHER_EVENTS.has(nativeEvent) && typeof entry.matcher === "string" && entry.matcher !== "") def.matcher = entry.matcher;
16643
- if (typeof entry.timeout === "number") def.timeout = entry.timeout;
16644
- defs.push(def);
16645
- }
17213
+ const defs = entries.map((raw) => hermesEntryToDefinition({
17214
+ nativeEvent,
17215
+ raw
17216
+ })).filter((def) => def !== void 0);
16646
17217
  if (defs.length > 0) canonical[rulesyncEvent] = defs;
16647
17218
  }
16648
17219
  return canonical;
@@ -17230,6 +17801,38 @@ function stripTrustedDirectoryWrapper(command) {
17230
17801
  if (posix?.[1]) return posix[1];
17231
17802
  return command.match(/^set "RULESYNC_KIMI_HOOK_CWD=1" && cd \/d "(?:""|[^"])*" && ([\s\S]*)$/)?.[1] ?? command;
17232
17803
  }
17804
+ /**
17805
+ * Native Kimi Code events whose Event Reference row lists the matcher as
17806
+ * "Empty string". Kimi Code documents `matcher` as "a regular expression to
17807
+ * filter event targets; if omitted, matches all", so on these events the
17808
+ * regex is tested against `""`: any non-trivial matcher simply never matches
17809
+ * and the hook silently never runs. Dropping the matcher is what makes the
17810
+ * hook fire at all, which is the authored intent — these events have no target
17811
+ * to filter on in the first place.
17812
+ *
17813
+ * Keyed on native names because the check runs after the canonical → native
17814
+ * mapping: `SessionHeartbeat` and `Interrupt` have no canonical counterpart and
17815
+ * are only reachable through a per-tool `kimi-code` override naming them
17816
+ * directly.
17817
+ *
17818
+ * Deliberately narrower than Claude Code's equivalent set: Kimi Code's
17819
+ * `UserPromptSubmit` matches the submitted prompt text, and `PermissionResult`
17820
+ * matches the tool name, so a matcher on either is meaningful and is kept.
17821
+ *
17822
+ * @see https://moonshotai.github.io/kimi-code/en/customization/hooks.html
17823
+ */
17824
+ const KIMI_CODE_NO_MATCHER_EVENTS = /* @__PURE__ */ new Set([
17825
+ "Stop",
17826
+ "SessionHeartbeat",
17827
+ "Interrupt"
17828
+ ]);
17829
+ /** Resolve the `matcher` part of an emitted entry, dropping dead matchers. */
17830
+ function resolveMatcherPart({ matcher, nativeEvent, logger }) {
17831
+ if (!matcher) return {};
17832
+ if (!KIMI_CODE_NO_MATCHER_EVENTS.has(nativeEvent)) return { matcher };
17833
+ logger?.warn(`matcher "${matcher}" on "${nativeEvent}" hook will be ignored — this event does not support matchers`);
17834
+ return {};
17835
+ }
17233
17836
  function buildEffectiveHooks(config, toolOverrideHooks) {
17234
17837
  const supported = new Set(KIMI_CODE_HOOK_EVENTS);
17235
17838
  const shared = {};
@@ -17259,7 +17862,11 @@ function canonicalToKimiCodeHooks({ config, toolOverrideHooks, trustedDirectory,
17259
17862
  command: definition.command,
17260
17863
  trustedDirectory
17261
17864
  }),
17262
- ...definition.matcher && { matcher: definition.matcher },
17865
+ ...resolveMatcherPart({
17866
+ matcher: definition.matcher,
17867
+ nativeEvent,
17868
+ logger
17869
+ }),
17263
17870
  ...validTimeout && timeout !== void 0 && { timeout }
17264
17871
  });
17265
17872
  }
@@ -17457,17 +18064,33 @@ function buildKiroIdeEntriesForEvent(trigger, definitions) {
17457
18064
  }
17458
18065
  return entries;
17459
18066
  }
17460
- function canonicalToKiroIdeHooks(config, overrideKey) {
18067
+ /**
18068
+ * The single `HooksConfig` key every Kiro target reads its tool-specific
18069
+ * overrides from.
18070
+ *
18071
+ * `kiro-ide` and `kiro-cli` write the same `.kiro/hooks/rulesync.json` at both
18072
+ * scopes, so per-target override blocks would make that one file's content
18073
+ * depend on generation order (last writer wins). All Kiro variants therefore
18074
+ * share the `kiro` block — the same resolution the Kiro MCP and permissions
18075
+ * wiring already use for the file they share.
18076
+ */
18077
+ const KIRO_HOOKS_OVERRIDE_KEY = "kiro";
18078
+ /**
18079
+ * Override keys a user might reach for that nothing reads, mapped to the key
18080
+ * that is actually read.
18081
+ */
18082
+ const KIRO_HOOKS_IGNORED_OVERRIDE_KEYS = ["kiro-ide", "kiro-cli"];
18083
+ function canonicalToKiroIdeHooks(config) {
17461
18084
  const kiroIdeSupported = new Set(KIRO_IDE_HOOK_EVENTS);
17462
18085
  const sharedHooks = {};
17463
18086
  for (const [event, defs] of Object.entries(config.hooks)) if (kiroIdeSupported.has(event)) sharedHooks[event] = defs;
17464
18087
  const effectiveHooks = {
17465
18088
  ...sharedHooks,
17466
- ...config[overrideKey]?.hooks
18089
+ ...config[KIRO_HOOKS_OVERRIDE_KEY]?.hooks
17467
18090
  };
17468
18091
  const entries = [];
17469
18092
  for (const [eventName, definitions] of Object.entries(effectiveHooks)) {
17470
- const trigger = CANONICAL_TO_KIRO_IDE_EVENT_NAMES[eventName] ?? eventName;
18093
+ const trigger = CANONICAL_TO_KIRO_IDE_EVENT_NAMES[eventName] ?? KIRO_LEGACY_TO_KIRO_IDE_TRIGGER_NAMES[eventName] ?? eventName;
17471
18094
  entries.push(...buildKiroIdeEntriesForEvent(trigger, definitions));
17472
18095
  }
17473
18096
  return entries;
@@ -17512,6 +18135,11 @@ function kiroIdeHooksToCanonical(entries) {
17512
18135
  * still writes the embedded `.kiro/agents/default.json` agent-config shape,
17513
18136
  * which Kiro CLI 3.0 no longer reads.
17514
18137
  *
18138
+ * Because both targets write the very same file, they resolve their
18139
+ * tool-specific overrides from one shared block
18140
+ * ({@link KIRO_HOOKS_OVERRIDE_KEY}) rather than per-target blocks, so
18141
+ * generating either or both targets always yields the same file.
18142
+ *
17515
18143
  * @see https://kiro.dev/docs/hooks/
17516
18144
  */
17517
18145
  var KiroIdeHooks = class extends ToolHooks {
@@ -17524,14 +18152,6 @@ var KiroIdeHooks = class extends ToolHooks {
17524
18152
  }, null, 2)
17525
18153
  });
17526
18154
  }
17527
- /**
17528
- * The `HooksConfig` key whose `hooks` block provides tool-specific overrides
17529
- * for this target. {@link import("./kiro-cli-hooks.js").KiroCliHooks}
17530
- * overrides this to `kiro-cli`.
17531
- */
17532
- static getOverrideKey() {
17533
- return "kiro-ide";
17534
- }
17535
18155
  static getSettablePaths(_options = {}) {
17536
18156
  return {
17537
18157
  relativeDirPath: KIRO_IDE_HOOKS_DIR_PATH,
@@ -17552,9 +18172,14 @@ var KiroIdeHooks = class extends ToolHooks {
17552
18172
  validate
17553
18173
  });
17554
18174
  }
17555
- static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false }) {
18175
+ static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
17556
18176
  const paths = this.getSettablePaths({ global });
17557
- const hooks = canonicalToKiroIdeHooks(rulesyncHooks.getJson(), this.getOverrideKey());
18177
+ const config = rulesyncHooks.getJson();
18178
+ for (const ignoredKey of KIRO_HOOKS_IGNORED_OVERRIDE_KEYS) {
18179
+ if (config[ignoredKey]?.hooks === void 0) continue;
18180
+ logger?.warn(`The "${ignoredKey}.hooks" block in ${join(rulesyncHooks.getRelativeDirPath(), rulesyncHooks.getRelativeFilePath())} is ignored. Author it under the "${KIRO_HOOKS_OVERRIDE_KEY}.hooks" key instead: the Kiro IDE and Kiro CLI targets write the same hooks file, so they read one shared block.`);
18181
+ }
18182
+ const hooks = canonicalToKiroIdeHooks(config);
17558
18183
  const fileContent = JSON.stringify({
17559
18184
  version: "v1",
17560
18185
  hooks
@@ -17575,10 +18200,9 @@ var KiroIdeHooks = class extends ToolHooks {
17575
18200
  throw new Error(`Failed to parse Kiro IDE hooks content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
17576
18201
  }
17577
18202
  const hooks = kiroIdeHooksToCanonical(parsed.hooks ?? []);
17578
- const overrideKey = this.constructor.getOverrideKey();
17579
18203
  return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
17580
18204
  hooks,
17581
- overrideKey
18205
+ overrideKey: KIRO_HOOKS_OVERRIDE_KEY
17582
18206
  }), null, 2) });
17583
18207
  }
17584
18208
  validate() {
@@ -17606,9 +18230,13 @@ var KiroIdeHooks = class extends ToolHooks {
17606
18230
  * Hooks generator for the **Kiro CLI**.
17607
18231
  *
17608
18232
  * Kiro CLI 3.0 reads the same standalone `.kiro/hooks/*.json` v1 format the
17609
- * Kiro IDE reads, so this reuses {@link KiroIdeHooks} and only redirects the
17610
- * tool-specific override key to `kiro-cli` (so `kiro-cli.hooks` overrides in
17611
- * the rulesync hooks config are honored, rather than `kiro-ide.hooks`).
18233
+ * Kiro IDE reads the same directory, and in rulesync the same
18234
+ * `.kiro/hooks/rulesync.json` file so this is {@link KiroIdeHooks} under a
18235
+ * second target name, down to the shared `kiro` override block both resolve
18236
+ * their tool-specific hooks from. A per-target override block would make that
18237
+ * one file's content depend on which target was generated last, which is
18238
+ * exactly what the shared block avoids (the Kiro MCP and permissions wiring
18239
+ * resolve their shared files the same way).
17612
18240
  *
17613
18241
  * The embedded `.kiro/agents/default.json` agent-hook format this target used
17614
18242
  * to emit is documented as not working in 3.0, so it is left to the deprecated
@@ -17617,11 +18245,7 @@ var KiroIdeHooks = class extends ToolHooks {
17617
18245
  * @see https://kiro.dev/docs/cli/v3/hooks-migration/
17618
18246
  * @see https://kiro.dev/docs/hooks/
17619
18247
  */
17620
- var KiroCliHooks = class extends KiroIdeHooks {
17621
- static getOverrideKey() {
17622
- return "kiro-cli";
17623
- }
17624
- };
18248
+ var KiroCliHooks = class extends KiroIdeHooks {};
17625
18249
  //#endregion
17626
18250
  //#region src/features/hooks/kiro-hooks.ts
17627
18251
  /**
@@ -17646,14 +18270,30 @@ function buildKiroEntriesForEvent(definitions) {
17646
18270
  }
17647
18271
  return entries;
17648
18272
  }
17649
- function canonicalToKiroHooks(config) {
18273
+ /**
18274
+ * Event keys the embedded agent-config format defines: the canonical events it
18275
+ * supports plus its own native spellings (`agentSpawn`, `userPromptSubmit`, …).
18276
+ *
18277
+ * The `kiro` override block is shared with the standalone `.kiro/hooks/*.json`
18278
+ * targets, whose vocabulary is different (`PostFileSave`, `PreTaskExec`, …).
18279
+ * Passing those through here would write event keys Kiro does not define into
18280
+ * `.kiro/agents/default.json`, so they are dropped instead.
18281
+ * @see https://kiro.dev/docs/cli/v3/hooks-migration/
18282
+ */
18283
+ const KIRO_AGENT_CONFIG_EVENT_KEYS = /* @__PURE__ */ new Set([...KIRO_HOOK_EVENTS, ...KIRO_AGENT_CONFIG_NATIVE_EVENT_NAMES]);
18284
+ function canonicalToKiroHooks({ config, logger }) {
17650
18285
  const overrideKey = "kiro";
17651
18286
  const kiroSupported = new Set(KIRO_HOOK_EVENTS);
17652
18287
  const sharedHooks = {};
17653
18288
  for (const [event, defs] of Object.entries(config.hooks)) if (kiroSupported.has(event)) sharedHooks[event] = defs;
18289
+ const overrideHooks = {};
18290
+ const droppedEvents = [];
18291
+ for (const [event, defs] of Object.entries(config[overrideKey]?.hooks ?? {})) if (KIRO_AGENT_CONFIG_EVENT_KEYS.has(event)) overrideHooks[event] = defs;
18292
+ else droppedEvents.push(event);
18293
+ if (droppedEvents.length > 0) logger?.warn(`Skipped hook event(s) from the "kiro" override block for the deprecated kiro agent config (no event key of that format): ${droppedEvents.join(", ")}. They are emitted for the kiro-cli / kiro-ide targets, which read the same block.`);
17654
18294
  const effectiveHooks = {
17655
18295
  ...sharedHooks,
17656
- ...config[overrideKey]?.hooks
18296
+ ...overrideHooks
17657
18297
  };
17658
18298
  const kiro = {};
17659
18299
  for (const [eventName, definitions] of Object.entries(effectiveHooks)) {
@@ -17748,11 +18388,14 @@ var KiroHooks = class KiroHooks extends ToolHooks {
17748
18388
  validate
17749
18389
  });
17750
18390
  }
17751
- static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false }) {
18391
+ static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
17752
18392
  const paths = KiroHooks.getSettablePaths({ global });
17753
18393
  const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
17754
18394
  const existingContent = await readFileContentOrNull(filePath) ?? JSON.stringify({}, null, 2);
17755
- const kiroHooks = canonicalToKiroHooks(rulesyncHooks.getJson());
18395
+ const kiroHooks = canonicalToKiroHooks({
18396
+ config: rulesyncHooks.getJson(),
18397
+ logger
18398
+ });
17756
18399
  const fileContent = applySharedConfigPatch({
17757
18400
  fileKey: sharedConfigFileKey(paths),
17758
18401
  feature: "hooks",
@@ -17868,6 +18511,30 @@ const PI_TOOL_EVENTS = /* @__PURE__ */ new Set(["tool_call", "tool_result"]);
17868
18511
  */
17869
18512
  const PI_ASSISTANT_MESSAGE_EVENTS = /* @__PURE__ */ new Set(["message_end"]);
17870
18513
  /**
18514
+ * `tool_call` is the only Pi extension event that can block, and it is Pi's
18515
+ * only tool gate. Its return contract is
18516
+ * `{ block: true, reason?: string, terminate?: boolean }`.
18517
+ *
18518
+ * @see https://github.com/earendil-works/pi/blob/v0.84.1/packages/coding-agent/docs/extensions.md#tool_call
18519
+ */
18520
+ const PI_BLOCKING_EVENT = "tool_call";
18521
+ /**
18522
+ * Helper emitted alongside blocking handlers. `promisify(exec)` rejects on a
18523
+ * non-zero exit with an error carrying `stdout` / `stderr` / `code`, so the
18524
+ * reason is derived from the rejection rather than from a resolved exit code.
18525
+ */
18526
+ const BLOCK_REASON_HELPER_LINES = [
18527
+ "function toBlockReason(error: unknown): string {",
18528
+ " const result = error as { stdout?: unknown; stderr?: unknown; code?: unknown } | null;",
18529
+ " const stderr = String(result?.stderr ?? \"\").trim();",
18530
+ " if (stderr) return stderr;",
18531
+ " const stdout = String(result?.stdout ?? \"\").trim();",
18532
+ " if (stdout) return stdout;",
18533
+ " if (result?.code !== undefined) return `Hook command failed with exit code ${result.code}.`;",
18534
+ " return error instanceof Error ? error.message : String(error);",
18535
+ "}"
18536
+ ];
18537
+ /**
17871
18538
  * Validate a hook matcher as a regular expression and return it as a JS
17872
18539
  * string-literal (JSON.stringify quoting) safe to embed in generated code.
17873
18540
  */
@@ -17904,6 +18571,22 @@ function collectPiHandlers({ effectiveHooks, eventMap }) {
17904
18571
  }
17905
18572
  return handlerGroups;
17906
18573
  }
18574
+ function buildCommandLines({ handler, usesToolName, blocksToolCall }) {
18575
+ const lines = [];
18576
+ const gated = usesToolName && Boolean(handler.matcher);
18577
+ const indent = gated ? " " : " ";
18578
+ const embeddedCommand = JSON.stringify(handler.command);
18579
+ if (gated && handler.matcher) lines.push(` if (new RegExp(${matcherToEmbeddedLiteral(handler.matcher)}).test(event.toolName)) {`);
18580
+ if (blocksToolCall) {
18581
+ lines.push(`${indent}try {`);
18582
+ lines.push(`${indent} await run(${embeddedCommand});`);
18583
+ lines.push(`${indent}} catch (error) {`);
18584
+ lines.push(`${indent} return { block: true, reason: toBlockReason(error) };`);
18585
+ lines.push(`${indent}}`);
18586
+ } else lines.push(`${indent}await run(${embeddedCommand});`);
18587
+ if (gated) lines.push(" }");
18588
+ return lines;
18589
+ }
17907
18590
  function buildSubscriptionLines(handlerGroups) {
17908
18591
  const lines = [];
17909
18592
  for (const [piEvent, handlers] of Object.entries(handlerGroups)) {
@@ -17912,14 +18595,11 @@ function buildSubscriptionLines(handlerGroups) {
17912
18595
  const usesEvent = usesToolName || gatesOnAssistant;
17913
18596
  lines.push(` pi.on(${JSON.stringify(piEvent)}, async (${usesEvent ? "event" : ""}) => {`);
17914
18597
  if (gatesOnAssistant) lines.push(` if (event.message.role !== "assistant") return;`);
17915
- for (const handler of handlers) {
17916
- const embeddedCommand = JSON.stringify(handler.command);
17917
- if (usesToolName && handler.matcher) {
17918
- lines.push(` if (new RegExp(${matcherToEmbeddedLiteral(handler.matcher)}).test(event.toolName)) {`);
17919
- lines.push(` await run(${embeddedCommand});`);
17920
- lines.push(" }");
17921
- } else lines.push(` await run(${embeddedCommand});`);
17922
- }
18598
+ for (const handler of handlers) lines.push(...buildCommandLines({
18599
+ handler,
18600
+ usesToolName,
18601
+ blocksToolCall: piEvent === PI_BLOCKING_EVENT
18602
+ }));
17923
18603
  lines.push(" });");
17924
18604
  }
17925
18605
  return lines;
@@ -17928,8 +18608,9 @@ function buildSubscriptionLines(handlerGroups) {
17928
18608
  * Generate the rulesync-owned Pi extension (a TypeScript module with a
17929
18609
  * default-export factory receiving Pi's ExtensionAPI) that subscribes to the
17930
18610
  * mapped lifecycle events and executes the configured hook commands via the
17931
- * platform shell. The generated extension observes events only: it never
17932
- * blocks or mutates Pi events.
18611
+ * platform shell. Handlers observe events, except on `tool_call` Pi's only
18612
+ * blocking event and its only tool gate — where a hook command that exits
18613
+ * non-zero denies the call with `{ block: true, reason }`.
17933
18614
  *
17934
18615
  * @see https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/extensions.md
17935
18616
  */
@@ -17941,10 +18622,12 @@ function generatePiExtensionCode({ config, supportedEvents, eventMap }) {
17941
18622
  };
17942
18623
  const effectiveHooks = {};
17943
18624
  for (const [event, defs] of Object.entries(configHooks)) if (supported.has(event)) effectiveHooks[event] = defs;
17944
- const subscriptionLines = buildSubscriptionLines(collectPiHandlers({
18625
+ const handlerGroups = collectPiHandlers({
17945
18626
  effectiveHooks,
17946
18627
  eventMap
17947
- }));
18628
+ });
18629
+ const subscriptionLines = buildSubscriptionLines(handlerGroups);
18630
+ const needsBlockReasonHelper = Boolean(handlerGroups[PI_BLOCKING_EVENT]);
17948
18631
  const lines = ["// Generated by rulesync. Do not edit manually."];
17949
18632
  if (subscriptionLines.length === 0) {
17950
18633
  lines.push("export default function () {}");
@@ -17958,6 +18641,10 @@ function generatePiExtensionCode({ config, supportedEvents, eventMap }) {
17958
18641
  lines.push("");
17959
18642
  lines.push("const run = promisify(exec);");
17960
18643
  lines.push("");
18644
+ if (needsBlockReasonHelper) {
18645
+ lines.push(...BLOCK_REASON_HELPER_LINES);
18646
+ lines.push("");
18647
+ }
17961
18648
  lines.push("export default function (pi: ExtensionAPI) {");
17962
18649
  lines.push(...subscriptionLines);
17963
18650
  lines.push("}");
@@ -18687,6 +19374,20 @@ function unsupportedMatcherEventNames({ factory, effectiveHooks }) {
18687
19374
  }
18688
19375
  return [...eventsWithMatcher];
18689
19376
  }
19377
+ /**
19378
+ * Targets that read their tool-scoped `{key}.hooks` override from a differently
19379
+ * named key. The two Kiro standalone-format targets write the very same
19380
+ * `.kiro/hooks/rulesync.json`, so they share the `kiro` block: a per-target
19381
+ * block would make that one file's content depend on generation order. The same
19382
+ * resolution is used by the MCP (`MCP_BLOCK_KEY_ALIASES`) and permissions
19383
+ * (`PERMISSION_OVERRIDE_KEY_ALIASES`) features for the files they share.
19384
+ */
19385
+ const HOOKS_OVERRIDE_KEY_ALIASES = {
19386
+ "kiro-cli": KIRO_HOOKS_OVERRIDE_KEY,
19387
+ "kiro-ide": KIRO_HOOKS_OVERRIDE_KEY
19388
+ };
19389
+ /** The targets writing the standalone `.kiro/hooks/*.json` v1 format. */
19390
+ const KIRO_STANDALONE_HOOKS_TARGETS = /* @__PURE__ */ new Set(["kiro-cli", "kiro-ide"]);
18690
19391
  const toolHooksFactories = /* @__PURE__ */ new Map([
18691
19392
  ["amp", {
18692
19393
  class: AmpHooks,
@@ -18927,7 +19628,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
18927
19628
  },
18928
19629
  supportedEvents: KIRO_HOOK_EVENTS,
18929
19630
  supportedHookTypes: ["command"],
18930
- supportsMatcher: true
19631
+ supportsMatcher: true,
19632
+ passthroughOverrideEvents: true
18931
19633
  }],
18932
19634
  ["kiro-cli", {
18933
19635
  class: KiroCliHooks,
@@ -19107,7 +19809,7 @@ var HooksProcessor = class extends FeatureProcessor {
19107
19809
  if (!factory) throw new Error(`Unsupported tool target: ${this.toolTarget}`);
19108
19810
  const config = rulesyncHooks.getJson();
19109
19811
  const sharedHooks = config.hooks;
19110
- const overrideHooks = config[this.toolTarget]?.hooks ?? {};
19812
+ const overrideHooks = config[HOOKS_OVERRIDE_KEY_ALIASES[this.toolTarget] ?? this.toolTarget]?.hooks ?? {};
19111
19813
  const effectiveHooks = {
19112
19814
  ...sharedHooks,
19113
19815
  ...overrideHooks
@@ -19133,14 +19835,14 @@ var HooksProcessor = class extends FeatureProcessor {
19133
19835
  }
19134
19836
  for (const [hookType, events] of unsupportedTypeToEvents) this.logger.warn(`Skipped ${hookType}-type hook(s) for ${this.toolTarget} (not supported): ${Array.from(events).join(", ")}`);
19135
19837
  }
19136
- if (this.toolTarget !== "kiro-ide") {
19838
+ if (!KIRO_STANDALONE_HOOKS_TARGETS.has(this.toolTarget)) {
19137
19839
  const skippedEvents = new Set(unsupportedEventNames({
19138
19840
  factory,
19139
19841
  sharedHooks,
19140
19842
  effectiveHooks
19141
19843
  }));
19142
19844
  const eventsWithDisabledHooks = Object.entries(sharedHooks).filter(([event, defs]) => !skippedEvents.has(event) && defs.some((def) => def.enabled === false)).map(([event]) => event);
19143
- if (eventsWithDisabledHooks.length > 0) this.logger.warn(`Emitting "enabled: false" hook(s) as active for ${this.toolTarget} (only kiro-ide supports the flag): ${eventsWithDisabledHooks.join(", ")}`);
19845
+ if (eventsWithDisabledHooks.length > 0) this.logger.warn(`Emitting "enabled: false" hook(s) as active for ${this.toolTarget} (only the kiro-cli / kiro-ide standalone hooks format supports the flag): ${eventsWithDisabledHooks.join(", ")}`);
19144
19846
  }
19145
19847
  const eventsWithUnsupportedMatcher = unsupportedMatcherEventNames({
19146
19848
  factory,
@@ -19639,9 +20341,11 @@ var CursorIgnore = class CursorIgnore extends ToolIgnore {
19639
20341
  *
19640
20342
  * Generates the brand-aligned `.devinignore` file with gitignore-compatible
19641
20343
  * syntax. Devin automatically respects `.gitignore` patterns and has built-in
19642
- * defaults for node_modules/ and hidden files. On import, the legacy
19643
- * `.codeiumignore` filename is read as a fallback so existing projects still
19644
- * round-trip.
20344
+ * defaults for node_modules/ and hidden files. On import, the pre-rebrand
20345
+ * `.codeiumignore` and `.windsurfignore` filenames are read as fallbacks so
20346
+ * existing projects still round-trip. The docs list the three names side by
20347
+ * side without defining a precedence between them, so the order below is
20348
+ * rulesync's own choice: brand-aligned name first, then the legacy names.
19645
20349
  *
19646
20350
  * In global mode the enterprise-wide `~/.codeium/.codeiumignore` is written
19647
20351
  * instead; see `DEVIN_GLOBAL_IGNORE_DIR_PATH` for why that path keeps the
@@ -19680,9 +20384,16 @@ var DevinIgnore = class DevinIgnore extends ToolIgnore {
19680
20384
  validate,
19681
20385
  global
19682
20386
  });
19683
- const primaryPath = join(outputRoot, relativeDirPath, relativeFilePath);
19684
- const legacyPath = join(outputRoot, relativeDirPath, DEVIN_LEGACY_IGNORE_FILE_NAME);
19685
- const resolvedFilePath = !await fileExists(primaryPath) && await fileExists(legacyPath) ? DEVIN_LEGACY_IGNORE_FILE_NAME : relativeFilePath;
20387
+ const candidateFileNames = [
20388
+ relativeFilePath,
20389
+ DEVIN_LEGACY_IGNORE_FILE_NAME,
20390
+ DEVIN_WINDSURF_IGNORE_FILE_NAME
20391
+ ];
20392
+ let resolvedFilePath = relativeFilePath;
20393
+ for (const candidateFileName of candidateFileNames) if (await fileExists(join(outputRoot, relativeDirPath, candidateFileName))) {
20394
+ resolvedFilePath = candidateFileName;
20395
+ break;
20396
+ }
19686
20397
  const fileContent = await readFileContent(join(outputRoot, relativeDirPath, resolvedFilePath));
19687
20398
  return new DevinIgnore({
19688
20399
  outputRoot,
@@ -22911,8 +23622,8 @@ function convertToGoosePluginMcpServers(mcpServers, logger) {
22911
23622
  * shape and cannot express `url`/`headers`, so remote servers are skipped with
22912
23623
  * a warning in project mode (use `--global` to sync them instead).
22913
23624
  *
22914
- * @see https://block.github.io/goose/docs/getting-started/using-extensions/
22915
- * @see https://github.com/block/goose/pull/9471
23625
+ * @see https://goose-docs.ai/docs/getting-started/using-extensions/
23626
+ * @see https://github.com/aaif-goose/goose/pull/9471
22916
23627
  */
22917
23628
  var GooseMcp = class GooseMcp extends ToolMcp {
22918
23629
  config;
@@ -23183,7 +23894,7 @@ function resolveHermesTimeout(config) {
23183
23894
  * alias — `auth` (`oauth` for OAuth 2.1/PKCE), mTLS `client_cert` (string PEM
23184
23895
  * path, or `[cert, key]`/`[cert, key, password]` list) and `client_key`,
23185
23896
  * `connect_timeout` (seconds), `supports_parallel_tool_calls`,
23186
- * `keepalive_interval`, and `elicitation` — verbatim
23897
+ * `keepalive_interval`, `elicitation`, `trust`, and `identity_header` — verbatim
23187
23898
  * from `source` to `target`. Field names are identical on both sides (the
23188
23899
  * canonical `McpServerSchema` is a `looseObject`), so this serves export and
23189
23900
  * import alike. See the Hermes mcp-config-reference.
@@ -23252,6 +23963,14 @@ function copyHermesAdvancedFields(source, target) {
23252
23963
  target.elicitation = omitPrototypePollutionKeys(structuredClone(source.elicitation));
23253
23964
  copied = true;
23254
23965
  }
23966
+ if (typeof source.trust === "string") {
23967
+ target.trust = source.trust;
23968
+ copied = true;
23969
+ }
23970
+ if (isPlainObject$1(source.identity_header)) {
23971
+ target.identity_header = omitPrototypePollutionKeys(structuredClone(source.identity_header));
23972
+ copied = true;
23973
+ }
23255
23974
  return copied;
23256
23975
  }
23257
23976
  /**
@@ -24416,6 +25135,213 @@ var KiroMcp = class KiroMcp extends ToolMcp {
24416
25135
  }
24417
25136
  };
24418
25137
  //#endregion
25138
+ //#region src/constants/musecode-paths.ts
25139
+ const MUSECODE_RULE_FILE_NAME = "AGENTS.md";
25140
+ const MUSECODE_SKILLS_DIR_PATH = join(".agents", "skills");
25141
+ const MUSECODE_GLOBAL_CONFIG_DIR_PATH = join(".config", "muse");
25142
+ const MUSECODE_GLOBAL_SKILLS_DIR_PATH = join(MUSECODE_GLOBAL_CONFIG_DIR_PATH, "skills");
25143
+ const MUSECODE_SETTINGS_FILE_NAME = "settings.json";
25144
+ //#endregion
25145
+ //#region src/features/mcp/musecode-mcp.ts
25146
+ const MUSECODE_GLOBAL_ONLY_MESSAGE = "Muse Code MCP is global-only; use --global to sync ~/.config/muse/settings.json";
25147
+ /**
25148
+ * Single spelling of the settings.json codec/policy, matching the
25149
+ * `SHARED_CONFIG_OWNERSHIP` declaration for `.config/muse/settings.json`:
25150
+ * fail closed on an unparseable root rather than replacing the user's primary
25151
+ * Muse Code config with generated output.
25152
+ */
25153
+ function parseMusecodeSettings(fileContent, filePath) {
25154
+ return parseSharedConfig({
25155
+ format: "json",
25156
+ fileContent,
25157
+ filePath,
25158
+ invalidRootPolicy: "error"
25159
+ });
25160
+ }
25161
+ /**
25162
+ * Convert canonical rulesync servers to Muse Code's native `mcp_servers` shape.
25163
+ * Each entry carries a `transport` discriminator: `stdio` servers spawn a
25164
+ * `command` (single string) with `args`/`env`, and `streamable_http` servers
25165
+ * are reached at a `url` with optional `headers`. Only documented fields are
25166
+ * emitted; a canonical `disabled: true` maps to Muse's `enabled: false`.
25167
+ */
25168
+ function convertToMusecodeFormat(mcpServers, logger) {
25169
+ const result = {};
25170
+ for (const [name, config] of Object.entries(mcpServers)) {
25171
+ if (PROTOTYPE_POLLUTION_KEYS.has(name)) continue;
25172
+ if (!isRecord$1(config)) continue;
25173
+ if (declaresNoTransport(config)) {
25174
+ warnAndSkipMcpServer({
25175
+ toolName: "Muse Code",
25176
+ serverName: name,
25177
+ reason: "no transport",
25178
+ logger
25179
+ });
25180
+ continue;
25181
+ }
25182
+ const converted = {};
25183
+ if (isRemoteMcpServer(config)) {
25184
+ const url = resolveRemoteMcpUrl(config);
25185
+ if (!url) {
25186
+ warnAndSkipMcpServer({
25187
+ toolName: "Muse Code",
25188
+ serverName: name,
25189
+ reason: "a remote transport without a url",
25190
+ logger
25191
+ });
25192
+ continue;
25193
+ }
25194
+ const stated = config.type ?? config.transport;
25195
+ if (stated === "sse" || stated === "ws" || stated === void 0 && /^wss?:\/\//i.test(url)) {
25196
+ warnAndSkipMcpServer({
25197
+ toolName: "Muse Code",
25198
+ serverName: name,
25199
+ reason: `the "${stated ?? "ws"}" transport, which Muse Code does not implement (only stdio and streamable_http are supported)`,
25200
+ logger
25201
+ });
25202
+ continue;
25203
+ }
25204
+ converted.transport = "streamable_http";
25205
+ converted.url = url;
25206
+ if (config.headers && Object.keys(config.headers).length > 0) converted.headers = config.headers;
25207
+ } else {
25208
+ const [command, ...args] = resolveLocalMcpCommand(config);
25209
+ if (!command) {
25210
+ warnAndSkipMcpServer({
25211
+ toolName: "Muse Code",
25212
+ serverName: name,
25213
+ reason: "a stdio transport without a command",
25214
+ logger
25215
+ });
25216
+ continue;
25217
+ }
25218
+ converted.transport = "stdio";
25219
+ converted.command = command;
25220
+ converted.args = args;
25221
+ if (config.env && Object.keys(config.env).length > 0) converted.env = config.env;
25222
+ }
25223
+ if (config.disabled === true) converted.enabled = false;
25224
+ result[name] = converted;
25225
+ }
25226
+ return result;
25227
+ }
25228
+ /**
25229
+ * Convert Muse Code's native `mcp_servers` shape back to canonical rulesync
25230
+ * servers. The `transport` discriminator is dropped (`streamable_http` is not a
25231
+ * canonical enum value; the transport is re-derived from `command`/`url` on the
25232
+ * next generate), `enabled: false` maps back to `disabled: true`, and unknown
25233
+ * keys (e.g. `mode`, `framing`) pass through untouched.
25234
+ */
25235
+ function convertFromMusecodeFormat(musecodeMcp) {
25236
+ const result = {};
25237
+ for (const [name, config] of Object.entries(musecodeMcp)) {
25238
+ if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
25239
+ const converted = {};
25240
+ for (const [key, value] of Object.entries(config)) {
25241
+ if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
25242
+ if (key === "transport") continue;
25243
+ if (key === "enabled") {
25244
+ if (value === false) converted.disabled = true;
25245
+ continue;
25246
+ }
25247
+ converted[key] = value;
25248
+ }
25249
+ result[name] = converted;
25250
+ }
25251
+ return result;
25252
+ }
25253
+ /**
25254
+ * Meta Muse Code MCP servers.
25255
+ *
25256
+ * Muse Code reads MCP servers only from the `mcp_servers` block of the GLOBAL
25257
+ * user settings file `~/.config/muse/settings.json`; no project-scoped MCP
25258
+ * location is documented. The settings file must carry
25259
+ * `"schema_version": 1` — a file that omits that key fails every command at
25260
+ * startup with `malformed settings file` — so the key is bootstrapped when the
25261
+ * file is created and preserved when it already exists. Other settings keys are
25262
+ * preserved via the shared-config gateway, and the file is never deleted.
25263
+ *
25264
+ * @see https://dev.meta.ai/docs/muse-code/configuration.md
25265
+ * @see https://dev.meta.ai/docs/muse-code/extending.md
25266
+ */
25267
+ var MusecodeMcp = class MusecodeMcp extends ToolMcp {
25268
+ json;
25269
+ constructor(params) {
25270
+ super(params);
25271
+ this.json = parseMusecodeSettings(this.fileContent ?? "", join(this.relativeDirPath, this.relativeFilePath));
25272
+ }
25273
+ getJson() {
25274
+ return this.json;
25275
+ }
25276
+ isDeletable() {
25277
+ return false;
25278
+ }
25279
+ static getSettablePaths(_options) {
25280
+ return {
25281
+ relativeDirPath: MUSECODE_GLOBAL_CONFIG_DIR_PATH,
25282
+ relativeFilePath: MUSECODE_SETTINGS_FILE_NAME
25283
+ };
25284
+ }
25285
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
25286
+ if (!global) throw new Error(MUSECODE_GLOBAL_ONLY_MESSAGE);
25287
+ const paths = this.getSettablePaths({ global });
25288
+ const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{}";
25289
+ return new MusecodeMcp({
25290
+ outputRoot,
25291
+ relativeDirPath: paths.relativeDirPath,
25292
+ relativeFilePath: paths.relativeFilePath,
25293
+ fileContent,
25294
+ validate,
25295
+ global
25296
+ });
25297
+ }
25298
+ static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
25299
+ if (!global) throw new Error(MUSECODE_GLOBAL_ONLY_MESSAGE);
25300
+ const paths = this.getSettablePaths({ global });
25301
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
25302
+ const existingContent = await readFileContentOrNull(filePath) ?? "";
25303
+ const existing = parseMusecodeSettings(existingContent, filePath);
25304
+ const converted = convertToMusecodeFormat(rulesyncMcp.getMcpServers(), logger);
25305
+ return new MusecodeMcp({
25306
+ outputRoot,
25307
+ relativeDirPath: paths.relativeDirPath,
25308
+ relativeFilePath: paths.relativeFilePath,
25309
+ fileContent: applySharedConfigPatch({
25310
+ fileKey: sharedConfigFileKey(paths),
25311
+ feature: "mcp",
25312
+ existingContent,
25313
+ patch: {
25314
+ mcp_servers: converted,
25315
+ ...existing.schema_version === void 0 && { schema_version: 1 }
25316
+ },
25317
+ filePath
25318
+ }),
25319
+ validate,
25320
+ global
25321
+ });
25322
+ }
25323
+ toRulesyncMcp() {
25324
+ const converted = convertFromMusecodeFormat(isRecord$1(this.json.mcp_servers) ? this.json.mcp_servers : {});
25325
+ return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers: converted }, null, 2) });
25326
+ }
25327
+ validate() {
25328
+ return {
25329
+ success: true,
25330
+ error: null
25331
+ };
25332
+ }
25333
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
25334
+ return new MusecodeMcp({
25335
+ outputRoot,
25336
+ relativeDirPath,
25337
+ relativeFilePath,
25338
+ fileContent: JSON.stringify({ mcp_servers: {} }, null, 2),
25339
+ validate: false,
25340
+ global
25341
+ });
25342
+ }
25343
+ };
25344
+ //#endregion
24419
25345
  //#region src/features/mcp/opencode-mcp.ts
24420
25346
  const OPENCODE_ENV_VAR_PATTERN = /(?<!\$)\{env:([^}:]+)\}/g;
24421
25347
  const OpencodeMcpLocalServerSchema = z.looseObject({
@@ -26372,6 +27298,15 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
26372
27298
  supportsDisabledTools: false
26373
27299
  }
26374
27300
  }],
27301
+ ["musecode", {
27302
+ class: MusecodeMcp,
27303
+ meta: {
27304
+ supportsProject: false,
27305
+ supportsGlobal: true,
27306
+ supportsEnabledTools: false,
27307
+ supportsDisabledTools: false
27308
+ }
27309
+ }],
26375
27310
  ["opencode", {
26376
27311
  class: OpencodeMcp,
26377
27312
  meta: {
@@ -29187,6 +30122,163 @@ var CopilotPermissions = class CopilotPermissions extends ToolPermissions {
29187
30122
  }
29188
30123
  };
29189
30124
  //#endregion
30125
+ //#region src/features/permissions/copilotcli-permissions.ts
30126
+ /**
30127
+ * The only canonical category the Copilot CLI settings can express: URL
30128
+ * approvals. Every other category (`bash`, `edit`, `read`, ...) has no
30129
+ * user-authorable surface — the CLI's `permissions.allow`/`ask`/`deny` rule
30130
+ * arrays are accepted only in MDM/enterprise managed settings, and session
30131
+ * tool approvals are machine-written to `permissions-config.json`.
30132
+ */
30133
+ const WEBFETCH_CATEGORY = "webfetch";
30134
+ /** `~/.copilot/settings.json` and `.github/copilot/settings.json` keys. */
30135
+ const ALLOWED_URLS_KEY = "allowedUrls";
30136
+ const DENIED_URLS_KEY = "deniedUrls";
30137
+ function toUrlList(value) {
30138
+ if (!Array.isArray(value)) return [];
30139
+ return value.filter((entry) => typeof entry === "string");
30140
+ }
30141
+ /**
30142
+ * Split one canonical category's rules into the two URL lists. `ask` entries
30143
+ * are omitted: the CLI prompts for any URL that is in neither list, which is
30144
+ * exactly what `ask` means.
30145
+ */
30146
+ function splitUrlRules(rules) {
30147
+ const allowedUrls = [];
30148
+ const deniedUrls = [];
30149
+ for (const [pattern, action] of Object.entries(rules)) if (action === "allow") allowedUrls.push(pattern);
30150
+ else if (action === "deny") deniedUrls.push(pattern);
30151
+ return {
30152
+ allowedUrls,
30153
+ deniedUrls
30154
+ };
30155
+ }
30156
+ /**
30157
+ * Permissions generator for the GitHub Copilot CLI.
30158
+ *
30159
+ * The CLI keeps two persistent settings files: the user-scope
30160
+ * `~/.copilot/settings.json` and the repository-scope
30161
+ * `.github/copilot/settings.json` (shipped in v1.0.60). Both are shared,
30162
+ * hand-edited files carrying unrelated keys (`model`, `effortLevel`, `hooks`,
30163
+ * `sandbox.*`, ...), so writes go through the shared-config gateway, only the
30164
+ * URL keys are owned, and the file is never deleted.
30165
+ *
30166
+ * The canonical `webfetch` category maps onto the CLI's two URL lists:
30167
+ * `allow` → `allowedUrls`, `deny` → `deniedUrls`, `ask` → the pattern is
30168
+ * omitted so the CLI falls through to its approval prompt.
30169
+ *
30170
+ * Scope asymmetry: the repository-scope key table documents `deniedUrls`
30171
+ * (union — a repository may add entries, never remove them) but NOT
30172
+ * `allowedUrls`, so an allow rule is only enforceable at user scope. At project
30173
+ * scope allow rules are therefore dropped with a warning rather than written to
30174
+ * a key the CLI ignores. (v1.0.79 additionally warns on startup about unknown
30175
+ * top-level keys in the user `settings.json`, so only documented keys are ever
30176
+ * emitted there either.)
30177
+ *
30178
+ * @see https://docs.github.com/en/copilot/reference/copilot-cli-reference/cli-config-dir-reference
30179
+ * @see https://github.com/github/copilot-cli/blob/main/changelog.md
30180
+ */
30181
+ var CopilotcliPermissions = class CopilotcliPermissions extends ToolPermissions {
30182
+ constructor(params) {
30183
+ super({
30184
+ ...params,
30185
+ fileContent: params.fileContent ?? "{}"
30186
+ });
30187
+ }
30188
+ /**
30189
+ * `settings.json` holds unrelated user settings (`model`, `effortLevel`,
30190
+ * `hooks`, ...), so it must not be deleted.
30191
+ */
30192
+ isDeletable() {
30193
+ return false;
30194
+ }
30195
+ static getSettablePaths({ global = false } = {}) {
30196
+ return global ? {
30197
+ relativeDirPath: COPILOT_DIR,
30198
+ relativeFilePath: COPILOTCLI_SETTINGS_FILE_NAME
30199
+ } : {
30200
+ relativeDirPath: COPILOTCLI_PROJECT_SETTINGS_DIR_PATH,
30201
+ relativeFilePath: COPILOTCLI_SETTINGS_FILE_NAME
30202
+ };
30203
+ }
30204
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
30205
+ const paths = CopilotcliPermissions.getSettablePaths({ global });
30206
+ const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{}";
30207
+ return new CopilotcliPermissions({
30208
+ outputRoot,
30209
+ relativeDirPath: paths.relativeDirPath,
30210
+ relativeFilePath: paths.relativeFilePath,
30211
+ fileContent,
30212
+ validate,
30213
+ global
30214
+ });
30215
+ }
30216
+ static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, global = false, logger }) {
30217
+ const paths = CopilotcliPermissions.getSettablePaths({ global });
30218
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
30219
+ const existingContent = await readFileContentOrNull(filePath) ?? "{}";
30220
+ const rules = rulesyncPermissions.getJson().permission[WEBFETCH_CATEGORY];
30221
+ const patch = {};
30222
+ if (rules !== void 0) {
30223
+ const { allowedUrls, deniedUrls } = splitUrlRules(rules);
30224
+ patch[DENIED_URLS_KEY] = deniedUrls.length > 0 ? deniedUrls : void 0;
30225
+ if (global) patch[ALLOWED_URLS_KEY] = allowedUrls.length > 0 ? allowedUrls : void 0;
30226
+ else if (allowedUrls.length > 0) logger?.warn(`Copilot CLI permissions: dropping ${allowedUrls.length} "webfetch" allow rule(s) at project scope — ${join(paths.relativeDirPath, paths.relativeFilePath)} does not accept "${ALLOWED_URLS_KEY}" (repository settings may only add denials). Author allow rules in global scope (\`--global\`) instead.`);
30227
+ }
30228
+ return new CopilotcliPermissions({
30229
+ outputRoot,
30230
+ relativeDirPath: paths.relativeDirPath,
30231
+ relativeFilePath: paths.relativeFilePath,
30232
+ fileContent: applySharedConfigPatch({
30233
+ fileKey: sharedConfigFileKey(paths),
30234
+ feature: "permissions",
30235
+ existingContent,
30236
+ patch,
30237
+ filePath
30238
+ }),
30239
+ validate: true,
30240
+ global
30241
+ });
30242
+ }
30243
+ toRulesyncPermissions() {
30244
+ const settings = this.parseSettings();
30245
+ const rules = {};
30246
+ for (const pattern of toUrlList(settings[DENIED_URLS_KEY])) rules[pattern] = "deny";
30247
+ if (this.global) for (const pattern of toUrlList(settings[ALLOWED_URLS_KEY])) rules[pattern] ??= "allow";
30248
+ const permission = Object.keys(rules).length > 0 ? { [WEBFETCH_CATEGORY]: rules } : {};
30249
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({ permission }, null, 2) });
30250
+ }
30251
+ parseSettings() {
30252
+ const relativePath = join(this.getRelativeDirPath(), this.getRelativeFilePath());
30253
+ try {
30254
+ return parseSharedConfig({
30255
+ format: "json",
30256
+ fileContent: this.getFileContent() || "{}",
30257
+ filePath: relativePath,
30258
+ invalidRootPolicy: "error"
30259
+ });
30260
+ } catch (error) {
30261
+ throw new Error(`Failed to parse Copilot CLI settings in ${relativePath}: ${formatError(error)}`, { cause: error });
30262
+ }
30263
+ }
30264
+ validate() {
30265
+ return {
30266
+ success: true,
30267
+ error: null
30268
+ };
30269
+ }
30270
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
30271
+ return new CopilotcliPermissions({
30272
+ outputRoot,
30273
+ relativeDirPath,
30274
+ relativeFilePath,
30275
+ fileContent: "{}",
30276
+ validate: false,
30277
+ global
30278
+ });
30279
+ }
30280
+ };
30281
+ //#endregion
29190
30282
  //#region src/features/permissions/cursor-permissions.ts
29191
30283
  /**
29192
30284
  * Mapping from rulesync canonical tool category names (lowercase) to Cursor CLI
@@ -30204,6 +31296,9 @@ const GROK_TOOL_TO_CATEGORY = {
30204
31296
  WebSearch: "websearch"
30205
31297
  };
30206
31298
  const GROK_MCP_TOOL = "MCPTool";
31299
+ const GROK_TOOL_TO_CATEGORY_LOWER = Object.fromEntries(Object.entries(GROK_TOOL_TO_CATEGORY).map(([tool, category]) => [tool.toLowerCase(), category]));
31300
+ const GROK_MCP_TOOL_ALIASES = /* @__PURE__ */ new Set(["mcp", GROK_MCP_TOOL.toLowerCase()]);
31301
+ const GROKCLI_PERMISSION_RULES_KEY = "rules";
30207
31302
  /**
30208
31303
  * Build a Grok Claude-style permission entry (e.g. `Bash(git *)`, `Read`,
30209
31304
  * `MCPTool(server__tool)`) from a canonical category + pattern. Returns `null`
@@ -30256,6 +31351,44 @@ function parseGrokEntry(entry) {
30256
31351
  };
30257
31352
  }
30258
31353
  /**
31354
+ * Parse one entry of the verbose `[[permission.rules]]` form
31355
+ * (`{ action = "allow", tool = "bash", pattern = "git *" }`) into a canonical
31356
+ * category + pattern + action. Unlike the compact array entries these are TOML
31357
+ * tables, so the tool name arrives as its own field instead of an entry string.
31358
+ * Returns `null` for malformed entries and for tools with no canonical
31359
+ * equivalent, so callers can skip them (the whole `rules` array is preserved
31360
+ * verbatim on generate regardless).
31361
+ */
31362
+ function parseGrokRule(rule) {
31363
+ if (!isRecord$1(rule)) return null;
31364
+ const { action: rawAction, tool, pattern } = rule;
31365
+ const parsedAction = PermissionActionSchema.safeParse(rawAction);
31366
+ if (!parsedAction.success) return null;
31367
+ const action = parsedAction.data;
31368
+ if (typeof tool !== "string") return null;
31369
+ if (pattern !== void 0 && typeof pattern !== "string") return null;
31370
+ const trimmedPattern = pattern?.trim() ?? "";
31371
+ const resolvedPattern = trimmedPattern.length > 0 ? trimmedPattern : CATCH_ALL_PATTERN$2;
31372
+ const lowerTool = tool.trim().toLowerCase();
31373
+ if (GROK_MCP_TOOL_ALIASES.has(lowerTool)) return resolvedPattern === CATCH_ALL_PATTERN$2 ? {
31374
+ category: "mcp",
31375
+ pattern: CATCH_ALL_PATTERN$2,
31376
+ action
31377
+ } : {
31378
+ category: `${MCP_CANONICAL_PREFIX$1}${resolvedPattern}`,
31379
+ pattern: CATCH_ALL_PATTERN$2,
31380
+ action
31381
+ };
31382
+ if (!Object.hasOwn(GROK_TOOL_TO_CATEGORY_LOWER, lowerTool)) return null;
31383
+ const category = GROK_TOOL_TO_CATEGORY_LOWER[lowerTool];
31384
+ if (category === void 0) return null;
31385
+ return {
31386
+ category,
31387
+ pattern: resolvedPattern,
31388
+ action
31389
+ };
31390
+ }
31391
+ /**
30259
31392
  * Permissions adapter for the xAI Grok Build CLI (`grokcli`).
30260
31393
  *
30261
31394
  * Grok Build CLI ships a Claude-style rule system under `[permission]` in
@@ -30276,9 +31409,17 @@ function parseGrokEntry(entry) {
30276
31409
  * entry with different actions (e.g. `edit` allow + `write` deny → `Edit`),
30277
31410
  * the strictest wins (`deny > ask > allow`) and a warning is logged, so the
30278
31411
  * entry never lands contradictorily in two arrays.
30279
- * - Import: the `[permission]` arrays are parsed back into canonical
30280
- * categories. When no `[permission]` section is present (older configs), the
30281
- * coarse `[ui] permission_mode` is used as a fallback.
31412
+ * - Import: both documented `[permission]` forms are parsed back into
31413
+ * canonical categories the compact `allow`/`deny`/`ask` arrays and the
31414
+ * verbose `[[permission.rules]]` tables
31415
+ * (`{ action = "allow", tool = "bash", pattern = "git *" }`), whose `tool`
31416
+ * field is matched case-insensitively against the same tool table, plus
31417
+ * the documented `mcp` alias for the compact form's `MCPTool`. Rules
31418
+ * from the two forms are merged with the same `deny > ask > allow`
31419
+ * precedence. When neither form carries a rule (older configs), the coarse
31420
+ * `[ui] permission_mode` is used as a fallback. Generate always emits the
31421
+ * compact arrays, so a user-authored `rules` array is preserved verbatim
31422
+ * but not reconciled against them.
30282
31423
  *
30283
31424
  * The coarse `[ui] permission_mode` toggle is still written for backward
30284
31425
  * compatibility with older Grok versions: `always-approve` when the config is
@@ -30445,29 +31586,44 @@ function buildGrokPermissionArrays(config, existingPermission, logger) {
30445
31586
  };
30446
31587
  }
30447
31588
  /**
30448
- * Parse Grok's `[permission]` allow/deny/ask arrays back into a canonical
30449
- * permission map. Returns `null` when the section defines none of the three
30450
- * arrays, so the caller can fall back to the coarse `permission_mode`.
30451
- * Precedence `deny > ask > allow` is applied so a tool listed in multiple
30452
- * arrays resolves to the strictest action.
31589
+ * Parse Grok's `[permission]` section back into a canonical permission map,
31590
+ * reading both supported forms: the compact `allow`/`deny`/`ask` arrays of
31591
+ * Claude-style entries and the verbose `[[permission.rules]]` tables
31592
+ * (`{ action, tool, pattern }`). Returns `null` only when neither form carries
31593
+ * any rule, so the caller can fall back to the coarse `permission_mode`.
31594
+ * Precedence `deny > ask > allow` is applied across both forms, so a tool
31595
+ * described more than once resolves to the strictest action.
30453
31596
  */
30454
31597
  function parseGrokPermissionArrays(permission) {
30455
31598
  const allow = isStringArray$1(permission.allow) ? permission.allow : void 0;
30456
31599
  const deny = isStringArray$1(permission.deny) ? permission.deny : void 0;
30457
31600
  const ask = isStringArray$1(permission.ask) ? permission.ask : void 0;
30458
- if ((allow?.length ?? 0) + (deny?.length ?? 0) + (ask?.length ?? 0) === 0) return null;
31601
+ const rules = Array.isArray(permission[GROKCLI_PERMISSION_RULES_KEY]) ? permission[GROKCLI_PERMISSION_RULES_KEY] : void 0;
31602
+ if ((allow?.length ?? 0) + (deny?.length ?? 0) + (ask?.length ?? 0) + (rules?.length ?? 0) === 0) return null;
30459
31603
  const result = {};
30460
- const apply = (entries, action) => {
31604
+ const record = ({ category, pattern, action }) => {
31605
+ const bucket = result[category] ??= {};
31606
+ const existing = bucket[pattern];
31607
+ if (existing === void 0 || ACTION_RANK[action] > ACTION_RANK[existing]) bucket[pattern] = action;
31608
+ };
31609
+ const applyEntries = (entries, action) => {
30461
31610
  for (const entry of entries ?? []) {
30462
31611
  const parsed = parseGrokEntry(entry);
30463
31612
  if (parsed === null) continue;
30464
- const bucket = result[parsed.category] ??= {};
30465
- bucket[parsed.pattern] = action;
31613
+ record({
31614
+ ...parsed,
31615
+ action
31616
+ });
30466
31617
  }
30467
31618
  };
30468
- apply(allow, "allow");
30469
- apply(ask, "ask");
30470
- apply(deny, "deny");
31619
+ applyEntries(allow, "allow");
31620
+ applyEntries(ask, "ask");
31621
+ applyEntries(deny, "deny");
31622
+ for (const rule of rules ?? []) {
31623
+ const parsed = parseGrokRule(rule);
31624
+ if (parsed === null) continue;
31625
+ record(parsed);
31626
+ }
30471
31627
  return result;
30472
31628
  }
30473
31629
  /**
@@ -33152,6 +34308,13 @@ const TAKT_PROVIDER_PROFILES_KEY = "provider_profiles";
33152
34308
  const TAKT_DEFAULT_PERMISSION_MODE_KEY = "default_permission_mode";
33153
34309
  const TAKT_STEP_PERMISSION_OVERRIDES_KEY = "step_permission_overrides";
33154
34310
  const TAKT_PROVIDER_OPTIONS_KEY = "provider_options";
34311
+ const TAKT_RUNTIME_PROVIDER_KEY = "provider";
34312
+ const TAKT_RUNTIME_DEFAULTS_KEY = "defaults";
34313
+ const TAKT_RUNTIME_PROFILES_KEY = "profiles";
34314
+ const TAKT_RUNTIME_TARGETS_KEY = "targets";
34315
+ const TAKT_RUNTIME_AUTO_ROUTING_KEY = "auto_routing";
34316
+ const TAKT_RUNTIME_PROFILE_KEY = "profile";
34317
+ const TAKT_RUNTIME_OPTIONS_KEY = "options";
33155
34318
  const TAKT_SECURITY_POLICIES = {
33156
34319
  workflow_arpeggio: [
33157
34320
  "custom_data_source_modules",
@@ -33204,13 +34367,36 @@ const CATCH_ALL_PATTERN = "*";
33204
34367
  * profile, layered on top of `default_permission_mode`) and `provider_options`
33205
34368
  * (a top-level per-provider sandbox/network table). Both are authored on
33206
34369
  * generate and re-extracted on import.
34370
+ *
34371
+ * Takt 0.56.0 moved provider configuration into a separate `runtime.yaml`
34372
+ * (`.takt/runtime.yaml`, `~/.takt/runtime.yaml`). Once its `provider:` section
34373
+ * carries an assignment ("runtime mode" — the state a freshly installed Takt is
34374
+ * in, since it generates an active global runtime.yaml on first launch), any
34375
+ * legacy provider setting left in `config.yaml` makes Takt stop before running
34376
+ * an agent with "Mixed provider configuration detected". rulesync only reads
34377
+ * that file, in two places:
34378
+ * - the active provider is resolved from it first (the `provider_profiles`
34379
+ * key that carries the permission mode is still keyed by provider name, and
34380
+ * `provider_profiles` is not itself a legacy signal);
34381
+ * - an authored `provider_options` is refused with a warning rather than
34382
+ * written while runtime mode is active, and its runtime-side counterpart
34383
+ * (`provider.profiles.*.options`) is lifted back out on import.
34384
+ * Installs with no runtime.yaml, or an inactive one, keep the legacy behavior
34385
+ * unchanged.
33207
34386
  */
33208
34387
  var TaktPermissions = class TaktPermissions extends ToolPermissions {
33209
- constructor(params) {
34388
+ /**
34389
+ * The sibling `runtime.yaml` as read from the same scope, or `""` when the
34390
+ * install has none. Import needs it — `toRulesyncPermissions()` is
34391
+ * synchronous, so the file is read alongside config.yaml in `fromFile()`.
34392
+ */
34393
+ runtimeFileContent;
34394
+ constructor({ runtimeFileContent, ...params }) {
33210
34395
  super({
33211
34396
  ...params,
33212
34397
  fileContent: params.fileContent ?? ""
33213
34398
  });
34399
+ this.runtimeFileContent = runtimeFileContent ?? "";
33214
34400
  }
33215
34401
  isDeletable() {
33216
34402
  return false;
@@ -33224,11 +34410,13 @@ var TaktPermissions = class TaktPermissions extends ToolPermissions {
33224
34410
  static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
33225
34411
  const paths = TaktPermissions.getSettablePaths({ global });
33226
34412
  const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
34413
+ const runtimeFileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, "runtime.yaml")) ?? "";
33227
34414
  return new TaktPermissions({
33228
34415
  outputRoot,
33229
34416
  relativeDirPath: paths.relativeDirPath,
33230
34417
  relativeFilePath: paths.relativeFilePath,
33231
34418
  fileContent,
34419
+ runtimeFileContent,
33232
34420
  validate,
33233
34421
  global
33234
34422
  });
@@ -33243,12 +34431,22 @@ var TaktPermissions = class TaktPermissions extends ToolPermissions {
33243
34431
  filePath,
33244
34432
  invalidRootPolicy: "error"
33245
34433
  });
34434
+ const runtime = await readTaktRuntimeConfig({
34435
+ outputRoot,
34436
+ logger
34437
+ });
33246
34438
  const rulesyncJson = rulesyncPermissions.getJson();
33247
- const provider = resolveActiveProvider(config);
34439
+ const provider = resolveActiveProvider({
34440
+ config,
34441
+ runtime: runtime.active ? runtime.config : void 0
34442
+ });
33248
34443
  const mode = deriveTaktPermissionMode(rulesyncJson);
33249
34444
  const override = isPlainObject$1(rulesyncJson.takt) ? rulesyncJson.takt : void 0;
33250
34445
  const stepOverrides = isPlainObject$1(override?.[TAKT_STEP_PERMISSION_OVERRIDES_KEY]) ? override[TAKT_STEP_PERMISSION_OVERRIDES_KEY] : void 0;
33251
- const overrideProviderOptions = isPlainObject$1(override?.[TAKT_PROVIDER_OPTIONS_KEY]) ? override[TAKT_PROVIDER_OPTIONS_KEY] : void 0;
34446
+ const authoredProviderOptions = isPlainObject$1(override?.[TAKT_PROVIDER_OPTIONS_KEY]) ? override[TAKT_PROVIDER_OPTIONS_KEY] : void 0;
34447
+ const runtimeModeRefusesProviderOptions = authoredProviderOptions !== void 0 && runtime.active;
34448
+ 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.`);
34449
+ const overrideProviderOptions = runtimeModeRefusesProviderOptions ? void 0 : authoredProviderOptions;
33252
34450
  const authoredPolicies = pickSecurityPolicies(override, {
33253
34451
  filePath,
33254
34452
  logger,
@@ -33294,13 +34492,26 @@ var TaktPermissions = class TaktPermissions extends ToolPermissions {
33294
34492
  filePath: join(this.getRelativeDirPath(), this.getRelativeFilePath()),
33295
34493
  invalidRootPolicy: "error"
33296
34494
  });
33297
- const provider = resolveActiveProvider(config);
34495
+ const runtimeFilePath = join(this.getRelativeDirPath(), TAKT_RUNTIME_CONFIG_FILE_NAME);
34496
+ const runtime = parseSharedConfig({
34497
+ format: "yaml",
34498
+ fileContent: this.runtimeFileContent,
34499
+ filePath: runtimeFilePath
34500
+ });
34501
+ const runtimeActive = isRuntimeModeActive(runtime);
34502
+ const provider = resolveActiveProvider({
34503
+ config,
34504
+ runtime: runtimeActive ? runtime : void 0
34505
+ });
33298
34506
  const profiles = isPlainObject$1(config[TAKT_PROVIDER_PROFILES_KEY]) ? config[TAKT_PROVIDER_PROFILES_KEY] : {};
33299
34507
  const profile = isPlainObject$1(profiles[provider]) ? profiles[provider] : {};
33300
34508
  const mode = profile[TAKT_DEFAULT_PERMISSION_MODE_KEY];
33301
34509
  const rulesyncConfig = taktModeToRulesyncConfig(mode);
33302
34510
  const stepOverrides = isPlainObject$1(profile[TAKT_STEP_PERMISSION_OVERRIDES_KEY]) ? profile[TAKT_STEP_PERMISSION_OVERRIDES_KEY] : void 0;
33303
- const providerOptions = isPlainObject$1(config[TAKT_PROVIDER_OPTIONS_KEY]) ? config[TAKT_PROVIDER_OPTIONS_KEY] : void 0;
34511
+ const providerOptions = mergeSharedConfigDeep({
34512
+ base: isPlainObject$1(config[TAKT_PROVIDER_OPTIONS_KEY]) ? config[TAKT_PROVIDER_OPTIONS_KEY] : {},
34513
+ patch: runtimeActive ? collectRuntimeProviderOptions(runtime) : {}
34514
+ });
33304
34515
  const taktOverride = {};
33305
34516
  if (stepOverrides && Object.keys(stepOverrides).length > 0) taktOverride[TAKT_STEP_PERMISSION_OVERRIDES_KEY] = stepOverrides;
33306
34517
  if (providerOptions && Object.keys(providerOptions).length > 0) taktOverride[TAKT_PROVIDER_OPTIONS_KEY] = providerOptions;
@@ -33327,10 +34538,18 @@ var TaktPermissions = class TaktPermissions extends ToolPermissions {
33327
34538
  }
33328
34539
  };
33329
34540
  /**
33330
- * Resolve the active Takt provider: the top-level `provider:` value, else the
33331
- * sole key in `provider_profiles`, else the `claude` default.
34541
+ * Resolve the active Takt provider.
34542
+ *
34543
+ * Under runtime mode (Takt 0.56.0+) the real provider assignment lives in
34544
+ * `runtime.yaml`, and `config.yaml:provider` cannot coexist with it, so the
34545
+ * runtime document is consulted first: the provider of the profile named by
34546
+ * `provider.defaults.profile`. Everything else falls through to the legacy
34547
+ * chain — the top-level `provider:` value, else the sole key in
34548
+ * `provider_profiles`, else the `claude` default.
33332
34549
  */
33333
- function resolveActiveProvider(config) {
34550
+ function resolveActiveProvider({ config, runtime }) {
34551
+ const fromRuntime = runtime === void 0 ? void 0 : resolveRuntimeProvider(runtime);
34552
+ if (fromRuntime !== void 0) return fromRuntime;
33334
34553
  if (typeof config[TAKT_PROVIDER_KEY] === "string" && config[TAKT_PROVIDER_KEY].trim() !== "") return config[TAKT_PROVIDER_KEY];
33335
34554
  const profiles = config[TAKT_PROVIDER_PROFILES_KEY];
33336
34555
  if (isPlainObject$1(profiles)) {
@@ -33339,6 +34558,170 @@ function resolveActiveProvider(config) {
33339
34558
  }
33340
34559
  return TAKT_DEFAULT_PROVIDER;
33341
34560
  }
34561
+ /** A plain object carrying at least one entry (Takt's "assignment" test). */
34562
+ function hasEntries(value) {
34563
+ return isPlainObject$1(value) && Object.keys(value).length > 0;
34564
+ }
34565
+ function runtimeProfiles(runtime) {
34566
+ const provider = runtime[TAKT_RUNTIME_PROVIDER_KEY];
34567
+ if (!isPlainObject$1(provider)) return {};
34568
+ const profiles = provider[TAKT_RUNTIME_PROFILES_KEY];
34569
+ return isPlainObject$1(profiles) ? profiles : {};
34570
+ }
34571
+ /**
34572
+ * Whether a parsed `runtime.yaml` puts Takt into runtime mode, mirroring
34573
+ * upstream's mode detection: the `provider:` section must carry an actual
34574
+ * assignment — a non-empty `defaults`, `profiles` or `auto_routing`, or a
34575
+ * `targets` map with at least one non-empty nested map. The file existing is not
34576
+ * enough, and empty nested maps (`defaults: {}`, `targets: { personas: {} }`)
34577
+ * must not flip the mode.
34578
+ * https://github.com/nrslib/takt/blob/main/src/infra/config/runtime-provider/mode.ts
34579
+ */
34580
+ function isRuntimeModeActive(runtime) {
34581
+ const provider = runtime[TAKT_RUNTIME_PROVIDER_KEY];
34582
+ if (!isPlainObject$1(provider)) return false;
34583
+ if (hasEntries(provider[TAKT_RUNTIME_DEFAULTS_KEY]) || hasEntries(provider[TAKT_RUNTIME_PROFILES_KEY]) || hasEntries(provider[TAKT_RUNTIME_AUTO_ROUTING_KEY])) return true;
34584
+ const targets = provider[TAKT_RUNTIME_TARGETS_KEY];
34585
+ return isPlainObject$1(targets) && Object.values(targets).some(hasEntries);
34586
+ }
34587
+ /** The provider named by the runtime profile Takt would use by default. */
34588
+ function resolveRuntimeProvider(runtime) {
34589
+ const provider = runtime[TAKT_RUNTIME_PROVIDER_KEY];
34590
+ const profiles = runtimeProfiles(runtime);
34591
+ const providerOf = (profileName) => {
34592
+ if (profileName === void 0) return;
34593
+ const profile = profiles[profileName];
34594
+ if (!isPlainObject$1(profile)) return;
34595
+ const value = profile[TAKT_RUNTIME_PROVIDER_KEY];
34596
+ return typeof value === "string" && value.trim() !== "" ? value : void 0;
34597
+ };
34598
+ if (!isPlainObject$1(provider)) return;
34599
+ const defaults = provider[TAKT_RUNTIME_DEFAULTS_KEY];
34600
+ if (!isPlainObject$1(defaults) || typeof defaults[TAKT_RUNTIME_PROFILE_KEY] !== "string") return;
34601
+ return providerOf(defaults[TAKT_RUNTIME_PROFILE_KEY]);
34602
+ }
34603
+ /**
34604
+ * Lift the runtime profiles' flat `options` bags back into the per-provider
34605
+ * shape the `takt` override uses. Each profile's options belong to that
34606
+ * profile's own provider; when several profiles name the same provider they are
34607
+ * merged in document order, so a later profile wins on a colliding option key.
34608
+ */
34609
+ function collectRuntimeProviderOptions(runtime) {
34610
+ const collected = {};
34611
+ for (const profile of Object.values(runtimeProfiles(runtime))) {
34612
+ if (!isPlainObject$1(profile)) continue;
34613
+ const providerName = profile[TAKT_RUNTIME_PROVIDER_KEY];
34614
+ const options = profile[TAKT_RUNTIME_OPTIONS_KEY];
34615
+ if (typeof providerName !== "string" || providerName.trim() === "" || !hasEntries(options)) continue;
34616
+ const existing = collected[providerName];
34617
+ collected[providerName] = {
34618
+ ...isPlainObject$1(existing) ? existing : {},
34619
+ ...options
34620
+ };
34621
+ }
34622
+ return collected;
34623
+ }
34624
+ /**
34625
+ * Read one `runtime.yaml`. A file that cannot be parsed reports `unparsable`
34626
+ * rather than a document: Takt refuses to start on a broken runtime.yaml, and
34627
+ * treating it as legacy would be the one outcome that writes the
34628
+ * mixed-configuration key into the user's config.yaml.
34629
+ */
34630
+ async function readTaktRuntimeFile({ filePath, logger }) {
34631
+ const fileContent = await readFileContentOrNull(filePath);
34632
+ if (fileContent === null) return;
34633
+ try {
34634
+ return {
34635
+ config: parseSharedConfig({
34636
+ format: "yaml",
34637
+ fileContent,
34638
+ filePath
34639
+ }),
34640
+ unparsable: false
34641
+ };
34642
+ } catch (error) {
34643
+ 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.`);
34644
+ return {
34645
+ config: {},
34646
+ unparsable: true
34647
+ };
34648
+ }
34649
+ }
34650
+ /**
34651
+ * Collapse the project and global `runtime.yaml` into the single document Takt
34652
+ * itself resolves against, matching upstream's loader: `profiles` is a union
34653
+ * with the project definition of a same-named profile replacing the global one,
34654
+ * while `defaults`, `targets` and `auto_routing` are taken from the project file
34655
+ * whole whenever it states them at all (`??`, so a project `targets: {}` masks
34656
+ * the global one rather than merging with it).
34657
+ * https://github.com/nrslib/takt/blob/main/src/infra/config/runtime-provider/loader.ts
34658
+ *
34659
+ * Merging before mode detection matters in both directions: a project file
34660
+ * active only through `targets:` still resolves its provider from the global
34661
+ * file's `defaults`/`profiles`, and a project section that masks the global one
34662
+ * leaves the merged document inactive even though the global file alone was not.
34663
+ */
34664
+ function mergeTaktRuntimeConfigs({ project, global: globalConfig }) {
34665
+ const sectionOf = (config) => {
34666
+ const provider = config?.[TAKT_RUNTIME_PROVIDER_KEY];
34667
+ return isPlainObject$1(provider) ? provider : {};
34668
+ };
34669
+ const projectSection = sectionOf(project);
34670
+ const globalSection = sectionOf(globalConfig);
34671
+ const replaced = (key) => projectSection[key] ?? globalSection[key];
34672
+ const profilesOf = (section) => isPlainObject$1(section[TAKT_RUNTIME_PROFILES_KEY]) ? section[TAKT_RUNTIME_PROFILES_KEY] : void 0;
34673
+ const globalProfiles = profilesOf(globalSection);
34674
+ const projectProfiles = profilesOf(projectSection);
34675
+ const provider = {};
34676
+ if (globalProfiles !== void 0 || projectProfiles !== void 0) provider[TAKT_RUNTIME_PROFILES_KEY] = {
34677
+ ...globalProfiles,
34678
+ ...projectProfiles
34679
+ };
34680
+ for (const key of [
34681
+ TAKT_RUNTIME_DEFAULTS_KEY,
34682
+ TAKT_RUNTIME_TARGETS_KEY,
34683
+ TAKT_RUNTIME_AUTO_ROUTING_KEY
34684
+ ]) {
34685
+ const value = replaced(key);
34686
+ if (value !== void 0) provider[key] = value;
34687
+ }
34688
+ return { [TAKT_RUNTIME_PROVIDER_KEY]: provider };
34689
+ }
34690
+ /**
34691
+ * The runtime provider configuration in force for a generate run: the project
34692
+ * and global `runtime.yaml` merged the way Takt merges them, plus whether the
34693
+ * result puts Takt in runtime mode.
34694
+ *
34695
+ * Both scopes are read whichever scope is being generated: Takt collects legacy
34696
+ * signals from the project and global `config.yaml` alike, so a global
34697
+ * `runtime.yaml` — the one Takt generates on first launch — makes a legacy key
34698
+ * in the project config.yaml a hard failure just the same.
34699
+ */
34700
+ async function readTaktRuntimeConfig({ outputRoot, logger }) {
34701
+ const projectPath = join(outputRoot, TAKT_DIR, TAKT_RUNTIME_CONFIG_FILE_NAME);
34702
+ let globalPath;
34703
+ try {
34704
+ const resolved = join(getHomeDirectory(), TAKT_DIR, TAKT_RUNTIME_CONFIG_FILE_NAME);
34705
+ globalPath = resolved === projectPath ? void 0 : resolved;
34706
+ } catch {}
34707
+ const [project, globalFile] = await Promise.all([readTaktRuntimeFile({
34708
+ filePath: projectPath,
34709
+ logger
34710
+ }), globalPath === void 0 ? Promise.resolve(void 0) : readTaktRuntimeFile({
34711
+ filePath: globalPath,
34712
+ logger
34713
+ })]);
34714
+ const filePaths = [...project === void 0 ? [] : [projectPath], ...globalFile === void 0 || globalPath === void 0 ? [] : [globalPath]];
34715
+ const config = mergeTaktRuntimeConfigs({
34716
+ project: project?.config,
34717
+ global: globalFile?.config
34718
+ });
34719
+ return {
34720
+ filePaths,
34721
+ config,
34722
+ active: project?.unparsable === true || globalFile?.unparsable === true || isRuntimeModeActive(config)
34723
+ };
34724
+ }
33342
34725
  /**
33343
34726
  * Collapse a rulesync permissions config into Takt's single coarse mode.
33344
34727
  *
@@ -34198,6 +35581,57 @@ function asRecord(value) {
34198
35581
  return Object.fromEntries(Object.entries(value));
34199
35582
  }
34200
35583
  /**
35584
+ * The `agent` keys the `zed` override may author. Everything else — above all
35585
+ * `tool_permissions`, which the canonical `permission` block owns end to end —
35586
+ * is refused, so an override can never reach past its own surface and weaken a
35587
+ * canonical deny. The override object is read key by key rather than spread, so
35588
+ * an unlisted key is inert whether or not it is named here — an allowlist, not a
35589
+ * denylist, because `agent` carries blunt instruments of its own (Zed's
35590
+ * `always_allow_tool_actions` would disarm every permission rule at once), and a
35591
+ * verbatim `agent` merge would hand them to the override.
35592
+ *
35593
+ * Every key the patch does not consume is reported, so an unsupported or
35594
+ * misspelled one surfaces as a warning rather than as config that quietly does
35595
+ * nothing. `permission` is exempt: it is the canonical tool-scoped block, and
35596
+ * `RulesyncPermissions.forTarget` has already consumed and stripped it.
35597
+ */
35598
+ const ZED_OVERRIDE_AGENT_KEYS = ["sandbox_permissions", "profiles"];
35599
+ const ZED_CANONICAL_AGENT_KEY = "tool_permissions";
35600
+ const ZED_OVERRIDE_CONSUMED_KEYS = /* @__PURE__ */ new Set([...ZED_OVERRIDE_AGENT_KEYS, "permission"]);
35601
+ /**
35602
+ * Build the `agent` patch fragment carrying the `zed` override's verbatim
35603
+ * blocks. A key the override supplies replaces the existing block wholesale
35604
+ * (Zed reads each as one unit — a deep merge would leave half of a rewritten
35605
+ * sandbox policy behind); a key it omits is left out of the fragment, so the
35606
+ * existing value survives via the caller's spread of `agent`.
35607
+ */
35608
+ function buildZedOverridePatch({ override, logger }) {
35609
+ if (!isPlainObject$1(override)) return {};
35610
+ 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.`);
35611
+ const unsupportedKeys = Object.keys(override).filter((key) => key !== ZED_CANONICAL_AGENT_KEY && !ZED_OVERRIDE_CONSUMED_KEYS.has(key));
35612
+ 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 ")}.`);
35613
+ const patch = {};
35614
+ for (const key of ZED_OVERRIDE_AGENT_KEYS) {
35615
+ const value = override[key];
35616
+ if (isPlainObject$1(value)) patch[key] = value;
35617
+ }
35618
+ return patch;
35619
+ }
35620
+ /**
35621
+ * The write-side inverse: lift `agent.sandbox_permissions` / `agent.profiles`
35622
+ * back into the `zed` override so a hand-written sandbox policy or profile set
35623
+ * round-trips instead of being lost on the next generate. Returns `undefined`
35624
+ * when the settings carry neither, so the override key is omitted.
35625
+ */
35626
+ function extractZedOverride(agent) {
35627
+ const override = {};
35628
+ for (const key of ZED_OVERRIDE_AGENT_KEYS) {
35629
+ const value = agent[key];
35630
+ if (isPlainObject$1(value)) override[key] = value;
35631
+ }
35632
+ return Object.keys(override).length > 0 ? override : void 0;
35633
+ }
35634
+ /**
34201
35635
  * Permissions generator for the Zed editor.
34202
35636
  *
34203
35637
  * Zed maps tool permissions onto `agent.tool_permissions` inside its settings
@@ -34205,6 +35639,11 @@ function asRecord(value) {
34205
35639
  * global). That file is shared with the MCP (`context_servers`) and ignore
34206
35640
  * (`private_files`) features, so reads and writes merge into the existing JSON
34207
35641
  * rather than overwriting it, and the file is never deleted.
35642
+ *
35643
+ * Zed's OS sandbox (`agent.sandbox_permissions`) and its tool-availability
35644
+ * profiles (`agent.profiles`) are separate enforcement layers with no canonical
35645
+ * counterpart; they are authored verbatim through the `zed` override and lifted
35646
+ * back out of the settings on import. See `ZedPermissionsOverrideSchema`.
34208
35647
  */
34209
35648
  var ZedPermissions = class ZedPermissions extends ToolPermissions {
34210
35649
  constructor(params) {
@@ -34283,6 +35722,10 @@ var ZedPermissions = class ZedPermissions extends ToolPermissions {
34283
35722
  existingContent,
34284
35723
  patch: { agent: {
34285
35724
  ...agent,
35725
+ ...buildZedOverridePatch({
35726
+ override: config.zed,
35727
+ logger
35728
+ }),
34286
35729
  tool_permissions: {
34287
35730
  ...toolPermissions,
34288
35731
  ...managedDefault !== void 0 && { default: managedDefault },
@@ -34304,7 +35747,8 @@ var ZedPermissions = class ZedPermissions extends ToolPermissions {
34304
35747
  } catch (error) {
34305
35748
  throw new Error(`Failed to parse Zed permissions content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
34306
35749
  }
34307
- const toolPermissionsRaw = asRecord(settings.agent).tool_permissions;
35750
+ const agent = asRecord(settings.agent);
35751
+ const toolPermissionsRaw = agent.tool_permissions;
34308
35752
  const parsed = ZedToolPermissionsSchema.safeParse(toolPermissionsRaw ?? {});
34309
35753
  const tools = parsed.success ? parsed.data.tools ?? {} : {};
34310
35754
  const globalDefault = parsed.success ? parsed.data.default : void 0;
@@ -34328,7 +35772,10 @@ var ZedPermissions = class ZedPermissions extends ToolPermissions {
34328
35772
  for (const entry of toolPermission.always_deny ?? []) ensure(category)[entry.pattern] = "deny";
34329
35773
  for (const entry of toolPermission.always_confirm ?? []) ensure(category)[entry.pattern] = "ask";
34330
35774
  }
34331
- return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({ permission }, null, 2) });
35775
+ const zedOverride = extractZedOverride(agent);
35776
+ const result = { permission };
35777
+ if (zedOverride !== void 0) result.zed = zedOverride;
35778
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
34332
35779
  }
34333
35780
  validate() {
34334
35781
  return {
@@ -34414,6 +35861,14 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
34414
35861
  supportsImport: true
34415
35862
  }
34416
35863
  }],
35864
+ ["copilotcli", {
35865
+ class: CopilotcliPermissions,
35866
+ meta: {
35867
+ supportsProject: true,
35868
+ supportsGlobal: true,
35869
+ supportsImport: true
35870
+ }
35871
+ }],
34417
35872
  ["cursor", {
34418
35873
  class: CursorPermissions,
34419
35874
  meta: {
@@ -36367,7 +37822,8 @@ function openaiYamlToCodexcliSection(parsed) {
36367
37822
  /**
36368
37823
  * Represents a Codex CLI skill directory.
36369
37824
  * Codex CLI supports skills in both project mode (under $CWD/.agents/skills)
36370
- * and global mode (under $CODEX_HOME/skills, typically ~/.agents/skills).
37825
+ * and global mode (under ~/.agents/skills Rulesync resolves the home
37826
+ * directory rather than $CODEX_HOME).
36371
37827
  */
36372
37828
  var CodexCliSkill = class CodexCliSkill extends ToolSkill {
36373
37829
  constructor({ outputRoot = process.cwd(), relativeDirPath = CODEXCLI_SKILLS_DIR_PATH, dirName, frontmatter, body, otherFiles = [], validate = true, global = false }) {
@@ -38403,6 +39859,145 @@ var KiroIdeSkill = class extends KiroSkill {
38403
39859
  }
38404
39860
  };
38405
39861
  //#endregion
39862
+ //#region src/features/skills/musecode-skill.ts
39863
+ const MusecodeSkillFrontmatterSchema = z.looseObject({
39864
+ name: z.string(),
39865
+ description: z.string()
39866
+ });
39867
+ /**
39868
+ * Represents a Meta Muse Code skill directory.
39869
+ *
39870
+ * Muse Code reads Agent Skills as `<skill-id>/SKILL.md` directories from the
39871
+ * project (`.agents/skills/`) and from the user config
39872
+ * (`$XDG_CONFIG_HOME/muse/skills`, plus `~/.agents/skills`). rulesync emits the
39873
+ * project directory and, at global scope, only the XDG-default
39874
+ * `~/.config/muse/skills` so a skill is written exactly once. Muse Code's
39875
+ * compat scans of repo-local `.codex/skills` and `.claude/skills` belong to
39876
+ * other tools and are not emitted.
39877
+ * @see https://dev.meta.ai/docs/muse-code/extending.md
39878
+ */
39879
+ var MusecodeSkill = class MusecodeSkill extends ToolSkill {
39880
+ constructor({ outputRoot = process.cwd(), relativeDirPath = MUSECODE_SKILLS_DIR_PATH, dirName, frontmatter, body, otherFiles = [], validate = true, global = false }) {
39881
+ super({
39882
+ outputRoot,
39883
+ relativeDirPath,
39884
+ dirName,
39885
+ mainFile: {
39886
+ name: SKILL_FILE_NAME,
39887
+ body,
39888
+ frontmatter: { ...frontmatter }
39889
+ },
39890
+ otherFiles,
39891
+ global
39892
+ });
39893
+ if (validate) {
39894
+ const result = this.validate();
39895
+ if (!result.success) throw result.error;
39896
+ }
39897
+ }
39898
+ static getSettablePaths({ global = false } = {}) {
39899
+ return { relativeDirPath: global ? MUSECODE_GLOBAL_SKILLS_DIR_PATH : MUSECODE_SKILLS_DIR_PATH };
39900
+ }
39901
+ getFrontmatter() {
39902
+ return MusecodeSkillFrontmatterSchema.parse(this.requireMainFileFrontmatter());
39903
+ }
39904
+ getBody() {
39905
+ return this.mainFile?.body ?? "";
39906
+ }
39907
+ validate() {
39908
+ if (!this.mainFile) return {
39909
+ success: false,
39910
+ error: /* @__PURE__ */ new Error(`${this.getDirPath()}: ${SKILL_FILE_NAME} file does not exist`)
39911
+ };
39912
+ const result = MusecodeSkillFrontmatterSchema.safeParse(this.mainFile.frontmatter);
39913
+ if (!result.success) return {
39914
+ success: false,
39915
+ error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${this.getDirPath()}: ${formatError(result.error)}`)
39916
+ };
39917
+ return {
39918
+ success: true,
39919
+ error: null
39920
+ };
39921
+ }
39922
+ toRulesyncSkill() {
39923
+ const frontmatter = this.getFrontmatter();
39924
+ const rulesyncFrontmatter = {
39925
+ name: frontmatter.name,
39926
+ description: frontmatter.description,
39927
+ targets: ["*"]
39928
+ };
39929
+ return new RulesyncSkill({
39930
+ outputRoot: this.outputRoot,
39931
+ relativeDirPath: RULESYNC_SKILLS_RELATIVE_DIR_PATH,
39932
+ dirName: this.getDirName(),
39933
+ frontmatter: rulesyncFrontmatter,
39934
+ body: this.getBody(),
39935
+ otherFiles: this.getOtherFiles(),
39936
+ validate: true,
39937
+ global: this.global
39938
+ });
39939
+ }
39940
+ static fromRulesyncSkill({ outputRoot = process.cwd(), rulesyncSkill, validate = true, global = false }) {
39941
+ const settablePaths = MusecodeSkill.getSettablePaths({ global });
39942
+ const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
39943
+ const musecodeFrontmatter = {
39944
+ name: rulesyncFrontmatter.name,
39945
+ description: rulesyncFrontmatter.description
39946
+ };
39947
+ return new MusecodeSkill({
39948
+ outputRoot,
39949
+ relativeDirPath: settablePaths.relativeDirPath,
39950
+ dirName: rulesyncSkill.getDirName(),
39951
+ frontmatter: musecodeFrontmatter,
39952
+ body: rulesyncSkill.getBody(),
39953
+ otherFiles: rulesyncSkill.getOtherFiles(),
39954
+ validate,
39955
+ global
39956
+ });
39957
+ }
39958
+ static isTargetedByRulesyncSkill(rulesyncSkill) {
39959
+ const targets = rulesyncSkill.getFrontmatter().targets;
39960
+ return targets.includes("*") || targets.includes("musecode");
39961
+ }
39962
+ static async fromDir(params) {
39963
+ const loaded = await this.loadSkillDirContent({
39964
+ ...params,
39965
+ getSettablePaths: MusecodeSkill.getSettablePaths
39966
+ });
39967
+ const result = MusecodeSkillFrontmatterSchema.safeParse(loaded.frontmatter);
39968
+ if (!result.success) {
39969
+ const skillDirPath = join(loaded.outputRoot, loaded.relativeDirPath, loaded.dirName);
39970
+ throw new Error(`Invalid frontmatter in ${join(skillDirPath, SKILL_FILE_NAME)}: ${formatError(result.error)}`);
39971
+ }
39972
+ return new MusecodeSkill({
39973
+ outputRoot: loaded.outputRoot,
39974
+ relativeDirPath: loaded.relativeDirPath,
39975
+ dirName: loaded.dirName,
39976
+ frontmatter: result.data,
39977
+ body: loaded.body,
39978
+ otherFiles: loaded.otherFiles,
39979
+ validate: true,
39980
+ global: loaded.global
39981
+ });
39982
+ }
39983
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, dirName, global = false }) {
39984
+ const settablePaths = MusecodeSkill.getSettablePaths({ global });
39985
+ return new MusecodeSkill({
39986
+ outputRoot,
39987
+ relativeDirPath: relativeDirPath ?? settablePaths.relativeDirPath,
39988
+ dirName,
39989
+ frontmatter: {
39990
+ name: "",
39991
+ description: ""
39992
+ },
39993
+ body: "",
39994
+ otherFiles: [],
39995
+ validate: false,
39996
+ global
39997
+ });
39998
+ }
39999
+ };
40000
+ //#endregion
38406
40001
  //#region src/features/skills/opencode-skill.ts
38407
40002
  const OpenCodeSkillFrontmatterSchema = z.looseObject({
38408
40003
  name: z.string(),
@@ -40231,6 +41826,14 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
40231
41826
  supportsGlobal: true
40232
41827
  }
40233
41828
  }],
41829
+ ["musecode", {
41830
+ class: MusecodeSkill,
41831
+ meta: {
41832
+ supportsProject: true,
41833
+ supportsSimulated: false,
41834
+ supportsGlobal: true
41835
+ }
41836
+ }],
40234
41837
  ["opencode", {
40235
41838
  class: OpenCodeSkill,
40236
41839
  meta: {
@@ -42751,7 +44354,7 @@ var FactorydroidSubagent = class FactorydroidSubagent extends ToolSubagent {
42751
44354
  * `sub_recipes` list, which rulesync never wrote) — so those files were inert.
42752
44355
  * The custom-agent surface is the one Goose actually reads.
42753
44356
  *
42754
- * @see https://block.github.io/goose/docs/guides/context-engineering/custom-agents/
44357
+ * @see https://goose-docs.ai/docs/guides/context-engineering/custom-agents/
42755
44358
  */
42756
44359
  const GooseSubagentFrontmatterSchema = z.looseObject({
42757
44360
  name: z.string(),
@@ -48085,7 +49688,7 @@ var FactorydroidRule = class FactorydroidRule extends ToolRule {
48085
49688
  * touches during a session. The separate `.goose/memories/` tree is the Memory
48086
49689
  * extension's storage and is NOT auto-loaded as session context.
48087
49690
  * (Verified against the official docs:
48088
- * https://block.github.io/goose/docs/guides/context-engineering/using-goosehints/)
49691
+ * https://goose-docs.ai/docs/guides/context-engineering/using-goosehints/)
48089
49692
  *
48090
49693
  * rulesync's topic-based non-root rules have no project subdirectory to map onto,
48091
49694
  * so writing them under `.goose/memories/` made them effectively invisible to
@@ -48096,7 +49699,7 @@ var FactorydroidRule = class FactorydroidRule extends ToolRule {
48096
49699
  * Goose uses plain markdown files (.goosehints) without frontmatter.
48097
49700
  *
48098
49701
  * Global scope emits only `~/.config/goose/.goosehints`. Goose v1.41.0 (PR
48099
- * block/goose#9736) additionally loads the vendor-neutral
49702
+ * aaif-goose/goose#9736) additionally loads the vendor-neutral
48100
49703
  * `~/.agents/AGENTS.md` alongside the config-dir hints, but rulesync
48101
49704
  * deliberately does not emit that shared path from the goose target: the
48102
49705
  * config-dir hints remain fully loaded (no capability loss), and the
@@ -48804,6 +50407,73 @@ var KiroIdeRule = class extends KiroRule {
48804
50407
  }
48805
50408
  };
48806
50409
  //#endregion
50410
+ //#region src/features/rules/musecode-rule.ts
50411
+ var MusecodeRule = class MusecodeRule extends ToolRule {
50412
+ constructor({ fileContent, root, ...rest }) {
50413
+ super({
50414
+ ...rest,
50415
+ fileContent,
50416
+ root: root ?? false
50417
+ });
50418
+ }
50419
+ static getSettablePaths(_options = {}) {
50420
+ return { root: {
50421
+ relativeDirPath: ".",
50422
+ relativeFilePath: MUSECODE_RULE_FILE_NAME
50423
+ } };
50424
+ }
50425
+ static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, validate = true, global = false }) {
50426
+ const { root } = this.getSettablePaths({ global });
50427
+ const relativePath = join(root.relativeDirPath, root.relativeFilePath);
50428
+ const fileContent = await readFileContent(join(outputRoot, relativePath));
50429
+ return new MusecodeRule({
50430
+ outputRoot,
50431
+ relativeDirPath: root.relativeDirPath,
50432
+ relativeFilePath: root.relativeFilePath,
50433
+ fileContent,
50434
+ validate,
50435
+ root: true
50436
+ });
50437
+ }
50438
+ static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
50439
+ const { root } = this.getSettablePaths({ global });
50440
+ const isRoot = rulesyncRule.getFrontmatter().root ?? false;
50441
+ return new MusecodeRule({
50442
+ outputRoot,
50443
+ relativeDirPath: root.relativeDirPath,
50444
+ relativeFilePath: root.relativeFilePath,
50445
+ fileContent: rulesyncRule.getBody(),
50446
+ validate,
50447
+ root: isRoot
50448
+ });
50449
+ }
50450
+ toRulesyncRule() {
50451
+ return this.toRulesyncRuleDefault();
50452
+ }
50453
+ validate() {
50454
+ return {
50455
+ success: true,
50456
+ error: null
50457
+ };
50458
+ }
50459
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
50460
+ return new MusecodeRule({
50461
+ outputRoot,
50462
+ relativeDirPath,
50463
+ relativeFilePath,
50464
+ fileContent: "",
50465
+ validate: false,
50466
+ root: relativeFilePath === "AGENTS.md" && relativeDirPath === "."
50467
+ });
50468
+ }
50469
+ static isTargetedByRulesyncRule(rulesyncRule) {
50470
+ return this.isTargetedByRulesyncRuleDefault({
50471
+ rulesyncRule,
50472
+ toolTarget: "musecode"
50473
+ });
50474
+ }
50475
+ };
50476
+ //#endregion
48807
50477
  //#region src/features/rules/opencode-rule.ts
48808
50478
  var OpenCodeRule = class OpenCodeRule extends ToolRule {
48809
50479
  static getSettablePaths({ global, excludeToolDir } = {}) {
@@ -50384,6 +52054,15 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
50384
52054
  ruleDiscoveryMode: "toon"
50385
52055
  }
50386
52056
  }],
52057
+ ["musecode", {
52058
+ class: MusecodeRule,
52059
+ meta: {
52060
+ extension: "md",
52061
+ supportsGlobal: false,
52062
+ ruleDiscoveryMode: "auto",
52063
+ collisionPolicy: "fold"
52064
+ }
52065
+ }],
50387
52066
  ["opencode", {
50388
52067
  class: OpenCodeRule,
50389
52068
  meta: {
@@ -53043,4 +54722,4 @@ async function importChecksCore(params) {
53043
54722
  //#endregion
53044
54723
  export { JsonLogger as $, RULESYNC_PERMISSIONS_LEGACY_FILE_NAME as $t, RulesyncRuleFrontmatterSchema as A, ALL_TOOL_TARGETS_WITH_WILDCARD as At, RulesyncCheck as B, RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH as Bt, CODEXCLI_DIR as C, removeTempDirectory as Ct, RulesyncSkill as D, writeFileBuffer as Dt, RulesyncSubagentFrontmatterSchema as E, toPosixPath as Et, getRulesyncSourceCandidates as F, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as Ft, SKILL_FILE_NAME as G, RULESYNC_IGNORE_RELATIVE_FILE_PATH as Gt, stringifyFrontmatter as H, RULESYNC_HOOKS_FILE_NAME as Ht, resolveRulesyncSourceWritePath as I, RULESYNC_CHECKS_RELATIVE_DIR_PATH as It, ConfigFileSchema as J, RULESYNC_MCP_LEGACY_FILE_NAME as Jt, ConfigResolver as K, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as Kt, parseJsonc as L, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as Lt, RulesyncMcp as M, ToolTargetSchema as Mt, RulesyncIgnore as N, MAX_FILE_SIZE as Nt, RulesyncSkillFrontmatterSchema as O, writeFileContent as Ot, RulesyncHooks as P, RULESYNC_AIIGNORE_FILE_NAME as Pt, ConsoleLogger as Q, RULESYNC_PERMISSIONS_FILE_NAME as Qt, RulesyncCommand as R, RULESYNC_CONFIG_RELATIVE_FILE_PATH as Rt, CODEXCLI_BASH_RULES_FILE_NAME as S, removeFileStrict as St, RulesyncSubagent as T, runWithDirectoryRollback as Tt, loadYaml as U, RULESYNC_HOOKS_LEGACY_FILE_NAME as Ut, RulesyncCheckFrontmatterSchema as V, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as Vt, SHARED_USER_MANAGED_CONFIG_PATHS as W, RULESYNC_HOOKS_RELATIVE_FILE_PATH as Wt, SourceEntrySchema as X, RULESYNC_MCP_SCHEMA_URL as Xt, GITIGNORE_DESTINATION_KEY as Y, RULESYNC_MCP_RELATIVE_FILE_PATH as Yt, findControlCharacter as Z, RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH as Zt, CLAUDECODE_LOCAL_RULE_FILE_NAME as _, readFileContent as _t, convertFromTool as a, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as an, assertTreeContainsNoSymlinks as at, CLAUDECODE_SKILLS_DIR_PATH as b, removeDirectoryStrict as bt, SubagentsProcessor as c, ALL_FEATURES_WITH_WILDCARD as cn, createTempDirectory as ct, IgnoreProcessor as d, fileExists as dt, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as en, fallbackLogger as et, HooksProcessor as f, findFilesByGlobs as ft, CLAUDECODE_DIR as g, listDirectoryFiles as gt, QWENCODE_LOCAL_RULE_FILE_NAME as h, isSymlink as ht, getProcessorRegistryEntry as i, RULESYNC_SKILLS_RELATIVE_DIR_PATH as in, assertDirectoryIfExists as it, RulesyncPermissions as j, PACKAGING_TOOL_TARGETS as jt, RulesyncRule as k, ALL_TOOL_TARGETS as kt, SkillsProcessor as l, DEPRECATED_FEATURE_REPLACEMENTS as ln, directoryExists as lt, QWENCODE_DIR as m, getHomeDirectory as mt, checkRulesyncDirExists as n, RULESYNC_RELATIVE_DIR_PATH as nn, CLIError as nt, isPackagingToolTarget as o, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as on, assertWritablePathInsideRoot as ot, CommandsProcessor as p, getFileSize as pt, CONFLICTING_TARGET_PAIRS as q, RULESYNC_MCP_FILE_NAME as qt, generate as r, RULESYNC_RULES_RELATIVE_DIR_PATH as rn, ErrorCodes as rt, RulesProcessor as s, ALL_FEATURES as sn, checkPathTraversal as st, importFromTool as t, RULESYNC_PERMISSIONS_SCHEMA_URL as tn, warnOnConflictingFlags as tt, McpProcessor as u, formatError as un, ensureDir as ut, CLAUDECODE_MEMORIES_DIR_NAME as v, readFileContentOrNull as vt, getLocalSkillDirNames as w, resolvePath as wt, ChecksProcessor as x, removeFile as xt, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as y, removeDirectory as yt, RulesyncCommandFrontmatterSchema as z, RULESYNC_CONFIG_SCHEMA_URL as zt };
53045
54724
 
53046
- //# sourceMappingURL=import-BpKoN2US.js.map
54725
+ //# sourceMappingURL=import-Bgkf_jVN.js.map