rulesync 16.35.0 → 16.36.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.
@@ -4078,24 +4078,60 @@ const CONTROL_CHARS = [
4078
4078
  const hasControlChars = (val) => CONTROL_CHARS.some((char) => val.includes(char));
4079
4079
  const safeString = zod_mini.z.pipe(zod_mini.z.string(), zod_mini.z.custom((val) => typeof val === "string" && !hasControlChars(val), "must not contain newline, carriage return, or NUL characters"));
4080
4080
  /**
4081
+ * All canonical hook types — the union of the `type` values accepted by every
4082
+ * supported tool:
4083
+ * - `command` — universal (Claude Code, Codex CLI, Qwen Code, Cursor, …)
4084
+ * - `prompt` — Claude Code, Codex CLI, Qwen Code, Copilot CLI, Cursor, Devin
4085
+ * - `http` — Claude Code, Qwen Code, Copilot CLI, Grok CLI
4086
+ * - `agent` — Claude Code, Codex CLI
4087
+ * - `mcp_tool` — Claude Code
4088
+ * - `function` — Qwen Code (internal Skill-system executor)
4089
+ */
4090
+ const HOOK_TYPES = [
4091
+ "command",
4092
+ "prompt",
4093
+ "http",
4094
+ "agent",
4095
+ "mcp_tool",
4096
+ "function"
4097
+ ];
4098
+ /**
4099
+ * One choice offered by a Kiro `confirm` prompt: `run` decides whether picking
4100
+ * it executes the hook command. The prompt text is written back into JSON,
4101
+ * never embedded in generated code, so it is a plain string.
4102
+ */
4103
+ const HookConfirmOptionSchema = zod_mini.z.looseObject({
4104
+ id: zod_mini.z.string(),
4105
+ label: zod_mini.z.string(),
4106
+ run: zod_mini.z.boolean()
4107
+ });
4108
+ /**
4109
+ * Kiro's confirmation prompt shown before a `Stop`-trigger command hook runs.
4110
+ * `confirmCommand` (optional, inside the block) names a command whose JSON
4111
+ * stdout drives the prompt at run time: `{ "skip": true }` suppresses it, a
4112
+ * replacement `question` / `options` object overrides the static prompt, and
4113
+ * on failure or timeout the static prompt is used. Stored verbatim: Kiro is
4114
+ * the only consumer, and it owns the shape.
4115
+ * https://kiro.dev/docs/hooks/
4116
+ */
4117
+ const HookConfirmSchema = zod_mini.z.looseObject({
4118
+ question: zod_mini.z.string(),
4119
+ options: zod_mini.z.array(HookConfirmOptionSchema),
4120
+ confirmCommand: zod_mini.z.optional(safeString)
4121
+ });
4122
+ /**
4081
4123
  * Canonical hook definition.
4082
4124
  * Used in .rulesync/hooks.jsonc and mapped to tool-specific formats.
4083
4125
  */
4084
4126
  const HookDefinitionSchema = zod_mini.z.looseObject({
4085
4127
  command: zod_mini.z.optional(safeString),
4086
- type: zod_mini.z.optional(zod_mini.z.enum([
4087
- "command",
4088
- "prompt",
4089
- "http",
4090
- "agent",
4091
- "mcp_tool",
4092
- "function"
4093
- ])),
4128
+ type: zod_mini.z.optional(zod_mini.z.enum(HOOK_TYPES)),
4094
4129
  url: zod_mini.z.optional(safeString),
4095
4130
  timeout: zod_mini.z.optional(zod_mini.z.number()),
4096
4131
  cacheTtl: zod_mini.z.optional(zod_mini.z.number().check((0, zod_mini.nonnegative)())),
4097
4132
  matcher: zod_mini.z.optional(safeString),
4098
4133
  enabled: zod_mini.z.optional(zod_mini.z.boolean()),
4134
+ confirm: zod_mini.z.optional(HookConfirmSchema),
4099
4135
  prompt: zod_mini.z.optional(safeString),
4100
4136
  loop_limit: zod_mini.z.optional(zod_mini.z.nullable(zod_mini.z.number())),
4101
4137
  name: zod_mini.z.optional(safeString),
@@ -4373,9 +4409,13 @@ const AMP_HOOK_EVENTS = [
4373
4409
  * the platform the other one owns — see `generateClineHookScript` and
4374
4410
  * `generateClineHookPowerShellScript`.
4375
4411
  *
4376
- * `TaskResume` and `TaskCancel` have no canonical counterpart and stay
4377
- * unmapped rather than being approximated by `sessionEnd` / `stop`, whose
4378
- * semantics differ.
4412
+ * `TaskCancel` is the shape of canonical `stopCancelled` (as Grok CLI's
4413
+ * `StopCancelled` and Codex CLI's `Interrupt` are): both runtimes fire it
4414
+ * *instead of* `TaskComplete` when the run ends aborted — the VS Code
4415
+ * extension from `afterRun` when the result status is `aborted`, the SDK/CLI
4416
+ * as `agent_abort` — and neither lets the script cancel anything. `TaskResume`
4417
+ * has no canonical counterpart and stays unmapped rather than being
4418
+ * approximated by `sessionStart`, whose semantics differ.
4379
4419
  *
4380
4420
  * @see https://github.com/cline/cline/blob/main/apps/vscode/src/core/hooks/utils.ts
4381
4421
  * @see https://github.com/cline/cline/blob/main/sdk/packages/core/src/hooks/hook-file-config.ts
@@ -4389,7 +4429,8 @@ const CLINE_HOOK_EVENTS = [
4389
4429
  "preCompact",
4390
4430
  "notification",
4391
4431
  "taskCompleted",
4392
- "afterError"
4432
+ "afterError",
4433
+ "stopCancelled"
4393
4434
  ];
4394
4435
  /**
4395
4436
  * Hook events supported by GitHub Copilot (cloud coding agent).
@@ -4707,23 +4748,32 @@ const QWENCODE_HOOK_EVENTS = [
4707
4748
  * Hook events supported by Reasonix.
4708
4749
  *
4709
4750
  * Reasonix's `.reasonix/settings.json` (project) / `~/.reasonix/settings.json`
4710
- * (global) documents a ten-event surface (`PreToolUse`, `PostToolUse`,
4711
- * `UserPromptSubmit`, `Stop`, `PostLLMCall`, `SessionStart`, `SessionEnd`,
4712
- * `SubagentStop`, `Notification`, `PreCompact`). All ten have a clean canonical
4751
+ * (global) accepts the thirteen events declared in `internal/hook/hook.go`
4752
+ * (`var Events`): `PreToolUse`, `PostToolUse`, `PostToolUseFailure`,
4753
+ * `PermissionRequest`, `UserPromptSubmit`, `Stop`, `StopFailure`,
4754
+ * `PostLLMCall`, `SessionStart`, `SessionEnd`, `SubagentStop`, `Notification`,
4755
+ * `PreCompact`. The zh-CN desktop hooks page documents only ten of them, so the
4756
+ * source file is the authoritative list. All thirteen have a clean canonical
4713
4757
  * equivalent and are mapped: `PreToolUse`, `PostToolUse`,
4714
- * `UserPromptSubmit` ← `beforeSubmitPrompt`, `Stop`, `SessionStart`,
4715
- * `SessionEnd`, `SubagentStop`, `PostLLMCall` ← `postModelInvocation`,
4716
- * `Notification` ← `notification`, and `PreCompact` ← `preCompact`.
4717
- * `match` (Reasonix's matcher field name) is honored only on
4718
- * `PreToolUse`/`PostToolUse`, matching the canonical `matcher` field's
4719
- * tool-event scoping used by other adapters.
4758
+ * `PostToolUseFailure`, `PermissionRequest`, `UserPromptSubmit` ←
4759
+ * `beforeSubmitPrompt`, `Stop`, `StopFailure`, `SessionStart`, `SessionEnd`,
4760
+ * `SubagentStop`, `PostLLMCall` ← `postModelInvocation`, `Notification` ←
4761
+ * `notification`, and `PreCompact` ← `preCompact`.
4762
+ * `match` (Reasonix's matcher field name) is honored on the four tool-scoped
4763
+ * events (`PreToolUse`, `PostToolUse`, `PostToolUseFailure`,
4764
+ * `PermissionRequest` — `UsesToolMatcher` in `internal/hook/inspect.go`) and
4765
+ * ignored everywhere else.
4766
+ * @see https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/internal/hook/hook.go
4720
4767
  * @see https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/docs/DESKTOP_HOOKS.zh-CN.md
4721
4768
  */
4722
4769
  const REASONIX_HOOK_EVENTS = [
4723
4770
  "preToolUse",
4724
4771
  "postToolUse",
4772
+ "postToolUseFailure",
4773
+ "permissionRequest",
4725
4774
  "beforeSubmitPrompt",
4726
4775
  "stop",
4776
+ "stopFailure",
4727
4777
  "sessionStart",
4728
4778
  "sessionEnd",
4729
4779
  "subagentStop",
@@ -4769,8 +4819,8 @@ const GROKCLI_HOOK_EVENTS = [
4769
4819
  * added in 0.32.0 (`TurnStarted`, `UserPromptQueued`, `TaskStarted`,
4770
4820
  * `SessionHeartbeat`), none of which is mapped onto a canonical rulesync event
4771
4821
  * here. `Interrupt` (fires instead of `Stop` when the user interrupts a turn)
4772
- * does have a canonical shape — `stopCancelled`, which Grok CLI and Codex CLI
4773
- * map — but Kimi keeps it native-only for now, since an existing `kimi-code`
4822
+ * does have a canonical shape — `stopCancelled`, which Grok CLI, Codex CLI and
4823
+ * Cline map — but Kimi keeps it native-only for now, since an existing `kimi-code`
4774
4824
  * override addressing it by name would otherwise double up with a canonical
4775
4825
  * `stopCancelled` block; folding it in is a follow-up. All six are listed in
4776
4826
  * `KIMI_CODE_NATIVE_HOOK_EVENTS` so a per-tool `kimi-code` override can
@@ -5065,8 +5115,8 @@ const CONTINUE_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANO
5065
5115
  /**
5066
5116
  * Hook events supported by Hermes Agent's native Shell Hooks system.
5067
5117
  *
5068
- * Hermes validates hook events against a fixed `VALID_HOOKS` set — 37 entries as
5069
- * of v0.20.2 (`v2026.8.16`); see {@link HERMESAGENT_NATIVE_HOOK_EVENTS} for the
5118
+ * Hermes validates hook events against a fixed `VALID_HOOKS` set — 39 entries as
5119
+ * of v0.21.3 (`v2026.9.14`); see {@link HERMESAGENT_NATIVE_HOOK_EVENTS} for the
5070
5120
  * full list. Only the events with a clean 1:1 canonical equivalent are mapped
5071
5121
  * here. All other native events round-trip through `hermesagent.hooks`.
5072
5122
  * @see https://github.com/NousResearch/hermes-agent/blob/main/website/docs/user-guide/features/hooks.md
@@ -5090,11 +5140,13 @@ const HERMESAGENT_HOOK_EVENTS = [
5090
5140
  * observers, `gateway_platform_event` and `transform_api_error_classification`.
5091
5141
  * Shell hooks are gated on the same set.
5092
5142
  *
5093
- * Verified against the tag `v2026.8.16` (v0.20.2): `VALID_HOOKS` holds 37
5094
- * entries, grown from 23 at `v2026.8.3` (v0.20.0). This list holds 36 of them —
5095
- * see {@link HERMESAGENT_SHELL_UNSUPPORTED_HOOK_EVENTS} for the exclusion.
5143
+ * Verified against the tag `v2026.9.14` (v0.21.3): `VALID_HOOKS` holds 39
5144
+ * entries, grown from 23 at `v2026.8.3` (v0.20.0) and 37 at `v2026.8.16`
5145
+ * (v0.20.2) — v0.21 added `agent_loop_stopped` and `on_room_member_activity`.
5146
+ * This list holds 38 of them — see
5147
+ * {@link HERMESAGENT_SHELL_UNSUPPORTED_HOOK_EVENTS} for the exclusion.
5096
5148
  *
5097
- * @see https://github.com/NousResearch/hermes-agent/blob/v2026.8.16/hermes_cli/plugins.py
5149
+ * @see https://github.com/NousResearch/hermes-agent/blob/v2026.9.14/hermes_cli/plugins.py
5098
5150
  */
5099
5151
  const HERMESAGENT_NATIVE_HOOK_EVENTS = [
5100
5152
  "pre_tool_call",
@@ -5120,8 +5172,10 @@ const HERMESAGENT_NATIVE_HOOK_EVENTS = [
5120
5172
  "subagent_start",
5121
5173
  "subagent_stop",
5122
5174
  "pre_gateway_dispatch",
5175
+ "agent_loop_stopped",
5123
5176
  "pre_approval_request",
5124
5177
  "post_approval_response",
5178
+ "on_room_member_activity",
5125
5179
  "pre_transcription",
5126
5180
  "kanban_task_claimed",
5127
5181
  "kanban_task_completed",
@@ -5144,8 +5198,8 @@ const HERMESAGENT_NATIVE_HOOK_EVENTS = [
5144
5198
  * rulesync only ever writes shell hooks, so these are excluded from
5145
5199
  * {@link HERMESAGENT_NATIVE_HOOK_EVENTS} and warned about with their own message.
5146
5200
  *
5147
- * @see https://github.com/NousResearch/hermes-agent/blob/v2026.8.16/hermes_cli/plugins.py — `SHELL_UNSUPPORTED_HOOKS`
5148
- * @see https://github.com/NousResearch/hermes-agent/blob/v2026.8.16/agent/shell_hooks.py — `_parse_hooks_block`
5201
+ * @see https://github.com/NousResearch/hermes-agent/blob/v2026.9.14/hermes_cli/plugins.py — `SHELL_UNSUPPORTED_HOOKS`
5202
+ * @see https://github.com/NousResearch/hermes-agent/blob/v2026.9.14/agent/shell_hooks.py — `_parse_hooks_block`
5149
5203
  */
5150
5204
  const HERMESAGENT_SHELL_UNSUPPORTED_HOOK_EVENTS = ["transform_api_error_classification"];
5151
5205
  /**
@@ -5446,7 +5500,8 @@ const CANONICAL_TO_CLINE_EVENT_NAMES = {
5446
5500
  preCompact: "PreCompact",
5447
5501
  notification: "Notification",
5448
5502
  taskCompleted: "TaskComplete",
5449
- afterError: "TaskError"
5503
+ afterError: "TaskError",
5504
+ stopCancelled: "TaskCancel"
5450
5505
  };
5451
5506
  /**
5452
5507
  * Map canonical camelCase event names to Copilot camelCase.
@@ -5764,8 +5819,11 @@ const QWENCODE_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANO
5764
5819
  const CANONICAL_TO_REASONIX_EVENT_NAMES = {
5765
5820
  preToolUse: "PreToolUse",
5766
5821
  postToolUse: "PostToolUse",
5822
+ postToolUseFailure: "PostToolUseFailure",
5823
+ permissionRequest: "PermissionRequest",
5767
5824
  beforeSubmitPrompt: "UserPromptSubmit",
5768
5825
  stop: "Stop",
5826
+ stopFailure: "StopFailure",
5769
5827
  sessionStart: "SessionStart",
5770
5828
  sessionEnd: "SessionEnd",
5771
5829
  subagentStop: "SubagentStop",
@@ -7656,11 +7714,15 @@ const KiroPermissionsOverrideSchema = zod_mini.z.looseObject({
7656
7714
  });
7657
7715
  /**
7658
7716
  * Codex CLI's approval-workflow policy. Serialized as a kebab-case string in
7659
- * `.codex/config.toml`. `on-failure` is a legacy alias for `on-request` that
7660
- * Codex still accepts, so it is included so existing configs round-trip. The
7661
- * granular table form (`{ granular = { … } }`) is modeled separately in the
7662
- * override union.
7717
+ * `.codex/config.toml`. `on-failure` is a deprecated alias for `on-request`
7718
+ * that Codex still reads, so it stays accepted (generate warns) so existing
7719
+ * configs round-trip. `untrusted` was retired in Codex 0.149.0 and makes
7720
+ * Codex refuse to start; it stays in the enum only so an existing
7721
+ * `.rulesync/permissions.jsonc` still parses — generate warns and does not
7722
+ * write it, and import does not lift it. The granular table form
7723
+ * (`{ granular = { … } }`) is modeled separately in the override union.
7663
7724
  * @see https://learn.chatgpt.com/docs/config-file/config-reference
7725
+ * @see https://github.com/openai/codex/pull/39630
7664
7726
  */
7665
7727
  const CodexApprovalPolicySchema = zod_mini.z.enum([
7666
7728
  "untrusted",
@@ -7730,11 +7792,12 @@ const CodexBasePermissionProfileSchema = zod_mini.z.enum(CODEX_BASE_PERMISSION_P
7730
7792
  * selected directly via `default_permissions` and skips the managed profile
7731
7793
  * entirely — canonical filesystem/network rules are ignored in that mode.
7732
7794
  * Defaults to `:workspace` when unspecified.
7733
- * - `approval_policy` — `untrusted` | `on-request` (legacy alias `on-failure`) |
7795
+ * - `approval_policy` — `on-request` (deprecated alias `on-failure`) |
7734
7796
  * `never`, or a `{ granular = { … } }` table (kept verbatim; the granular
7735
7797
  * schema has required fields that are brittle to model as typed keys).
7736
7798
  * Defaults to `on-request` when neither the override nor the existing
7737
- * config sets it.
7799
+ * config sets it. `untrusted` (retired in Codex 0.149.0) is still parsed
7800
+ * but skipped with a warning on generate, since Codex refuses to start on it.
7738
7801
  * - `sandbox_mode` — **deprecated.** `read-only` | `workspace-write` |
7739
7802
  * `danger-full-access`, with the sibling `sandbox_workspace_write` table
7740
7803
  * (`network_access`, `writable_roots`, …). Codex has superseded the classic
@@ -9620,7 +9683,8 @@ const RulesyncSkillFrontmatterSchema = zod_mini.z.looseObject({
9620
9683
  })),
9621
9684
  grokcli: zod_mini.z.optional(zod_mini.z.looseObject({
9622
9685
  "disable-model-invocation": zod_mini.z.optional(zod_mini.z.boolean()),
9623
- "user-invocable": zod_mini.z.optional(zod_mini.z.boolean())
9686
+ "user-invocable": zod_mini.z.optional(zod_mini.z.boolean()),
9687
+ paths: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())]))
9624
9688
  })),
9625
9689
  dsh: zod_mini.z.optional(zod_mini.z.looseObject({
9626
9690
  whenToUse: zod_mini.z.optional(zod_mini.z.string()),
@@ -9847,8 +9911,32 @@ var RulesyncSubagent = class RulesyncSubagent extends RulesyncFile {
9847
9911
  });
9848
9912
  }
9849
9913
  };
9850
- //#endregion
9851
- //#region src/features/skills/skills-utils.ts
9914
+ const SKILL_DESCRIPTION_MAX_LENGTH = 1024;
9915
+ const SKILL_NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
9916
+ /**
9917
+ * Check a skill `name` against the length and character rules, phrased for the
9918
+ * `authority` that enforces them (e.g. "the Agent Skills spec" or "Zed"). When
9919
+ * that authority does something with a bad name beyond flagging it — Zed
9920
+ * refuses to load the skill — `consequence` names it so the warning says what
9921
+ * the author is about to lose. An empty name is the caller's concern: what it
9922
+ * means differs per consumer.
9923
+ */
9924
+ function collectSkillNameViolations({ name, authority, consequence }) {
9925
+ const violations = [];
9926
+ if (name.length > 64) violations.push(`\`name\` is ${name.length} characters; ${authority} allows at most 64${consequence === void 0 ? "" : ` and ${consequence}`}`);
9927
+ if (!SKILL_NAME_PATTERN.test(name)) violations.push(`\`name\` ${quoteForLog(name)} must contain only lowercase letters, digits and single hyphens, with no leading, trailing or consecutive hyphens${consequence === void 0 ? "" : `; ${authority} ${consequence}`}`);
9928
+ return violations;
9929
+ }
9930
+ /**
9931
+ * Report each violation as a warning prefixed with the skill file it concerns.
9932
+ * The path is shown POSIX-style and stripped of control characters so a hostile
9933
+ * directory name cannot forge log lines; `warnWithFallback` keeps the report
9934
+ * visible when the caller has no logger to hand.
9935
+ */
9936
+ function warnSkillViolations({ skillPath, violations, logger }) {
9937
+ const shownPath = stripControlCharacters(toPosixPath(skillPath));
9938
+ for (const violation of violations) warnWithFallback(logger, `${shownPath}: ${violation}`);
9939
+ }
9852
9940
  /**
9853
9941
  * Whether a directory on disk can be addressed by its name.
9854
9942
  *
@@ -10672,10 +10760,13 @@ const AmpCheckFrontmatterSchema = zod_mini.z.looseObject({
10672
10760
  /**
10673
10761
  * Represents an Amp code review check.
10674
10762
  *
10675
- * Amp natively reads code review checks as Markdown files with YAML frontmatter,
10676
- * scoped to the project (`.agents/checks/`) and user-wide (`~/.config/amp/checks/`).
10677
- * Each check runs as a per-check subagent during code review.
10678
- * @see https://ampcode.com/manual
10763
+ * Amp reads code review checks as Markdown files with YAML frontmatter,
10764
+ * scoped to the project (`.agents/checks/`) and user-wide (`~/.config/amp/checks/`;
10765
+ * Amp also reads `~/.config/agents/checks/`, which rulesync does not write).
10766
+ * Since the docs rebuild the supported runner is the official
10767
+ * `@amp/review-checks` plugin (`amp review` is deprecated); existing checks in
10768
+ * this format continue to apply.
10769
+ * @see https://ampcode.com/docs/review
10679
10770
  */
10680
10771
  var AmpCheck = class AmpCheck extends ToolCheck {
10681
10772
  frontmatter;
@@ -16915,10 +17006,8 @@ const AgentsSkillsSkillFrontmatterSchema = zod_mini.z.looseObject({
16915
17006
  metadata: zod_mini.z.optional(zod_mini.z.looseObject({})),
16916
17007
  "allowed-tools": zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())]))
16917
17008
  });
16918
- const NAME_MAX_LENGTH = 64;
16919
- const DESCRIPTION_MAX_LENGTH = 1024;
16920
17009
  const COMPATIBILITY_MAX_LENGTH = 500;
16921
- const NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
17010
+ const AGENT_SKILLS_SPEC = "the Agent Skills spec";
16922
17011
  /**
16923
17012
  * Placeholder for an object that has already been encoded once in the same
16924
17013
  * value. YAML anchors let one document reference the same node repeatedly, and
@@ -17082,12 +17171,14 @@ function collectAgentSkillViolations({ frontmatter, dirName, sourceAllowedTools
17082
17171
  const { name, description } = frontmatter;
17083
17172
  if (name.length === 0) violations.push("`name` is required and must not be empty");
17084
17173
  else {
17085
- if (name.length > NAME_MAX_LENGTH) violations.push(`\`name\` is ${name.length} characters; the Agent Skills spec allows at most ${NAME_MAX_LENGTH}`);
17086
- if (!NAME_PATTERN.test(name)) violations.push(`\`name\` "${name}" must contain only lowercase letters, digits and single hyphens, with no leading, trailing or consecutive hyphens`);
17174
+ violations.push(...collectSkillNameViolations({
17175
+ name,
17176
+ authority: AGENT_SKILLS_SPEC
17177
+ }));
17087
17178
  if (name !== dirName) violations.push(`\`name\` "${name}" must match its parent directory name "${dirName}"; conformant clients require them to be equal`);
17088
17179
  }
17089
17180
  if (description.length === 0) violations.push(EMPTY_SKILL_DESCRIPTION_VIOLATION);
17090
- else if (description.length > DESCRIPTION_MAX_LENGTH) violations.push(`\`description\` is ${description.length} characters; the Agent Skills spec allows at most ${DESCRIPTION_MAX_LENGTH}`);
17181
+ else if (description.length > 1024) violations.push(`\`description\` is ${description.length} characters; ${AGENT_SKILLS_SPEC} allows at most ${SKILL_DESCRIPTION_MAX_LENGTH}`);
17091
17182
  const { compatibility } = frontmatter;
17092
17183
  if (typeof compatibility !== "string" && compatibility !== void 0) violations.push("`compatibility` must be a string; the Agent Skills spec does not allow a mapping here");
17093
17184
  else if (compatibility !== void 0 && compatibility.length > COMPATIBILITY_MAX_LENGTH) violations.push(`\`compatibility\` is ${compatibility.length} characters; the Agent Skills spec allows at most ${COMPATIBILITY_MAX_LENGTH}`);
@@ -17205,16 +17296,19 @@ var AgentsSkillsSkill = class AgentsSkillsSkill extends ToolSkill {
17205
17296
  * directory rather than a same-named project path.
17206
17297
  */
17207
17298
  static reportSpecViolations({ outputRoot, relativeDirPath, dirName, frontmatter, sourceAllowedTools, logger }) {
17208
- const skillPath = agentSkillFilePath({
17209
- outputRoot,
17210
- relativeDirPath,
17211
- dirName
17299
+ warnSkillViolations({
17300
+ skillPath: agentSkillFilePath({
17301
+ outputRoot,
17302
+ relativeDirPath,
17303
+ dirName
17304
+ }),
17305
+ violations: collectAgentSkillViolations({
17306
+ frontmatter,
17307
+ dirName,
17308
+ sourceAllowedTools
17309
+ }),
17310
+ logger
17212
17311
  });
17213
- for (const violation of collectAgentSkillViolations({
17214
- frontmatter,
17215
- dirName,
17216
- sourceAllowedTools
17217
- })) warnWithFallback(logger, `${stripControlCharacters(toPosixPath(skillPath))}: ${violation}`);
17218
17312
  }
17219
17313
  static isTargetedByRulesyncSkill(rulesyncSkill) {
17220
17314
  const targets = rulesyncSkill.getFrontmatter().targets;
@@ -17854,6 +17948,15 @@ const KiloCommandFrontmatterSchema = zod_mini.z.looseObject({
17854
17948
  model: zod_mini.z.optional(zod_mini.z.string()),
17855
17949
  variant: zod_mini.z.optional(zod_mini.z.string())
17856
17950
  });
17951
+ /**
17952
+ * Command names Kilo keeps for itself. Its loader throws on a custom command
17953
+ * file named `goal.md` in either the project or the global directory ("The
17954
+ * /goal command is reserved for session goals. Rename the custom command."),
17955
+ * and a throw there aborts the whole command scan — every other custom
17956
+ * command becomes unreachable too, not only the reserved one.
17957
+ * https://github.com/Kilo-Org/kilocode/blob/v7.6.2/packages/opencode/src/command/index.ts
17958
+ */
17959
+ const KILO_RESERVED_COMMAND_NAMES = /* @__PURE__ */ new Set(["goal"]);
17857
17960
  var KiloCommand = class KiloCommand extends ToolCommand {
17858
17961
  frontmatter;
17859
17962
  body;
@@ -17896,6 +17999,20 @@ var KiloCommand = class KiloCommand extends ToolCommand {
17896
17999
  validate: true
17897
18000
  });
17898
18001
  }
18002
+ /**
18003
+ * Refuse to write a command Kilo reserves (see
18004
+ * {@link KILO_RESERVED_COMMAND_NAMES}). The name Kilo loads is the file
18005
+ * stem, and the processor calls this on the flattened path, so `goal.md`
18006
+ * is refused at any nesting depth under the basename naming and only at the
18007
+ * top level under the path naming, where `git/goal.md` becomes `git-goal`.
18008
+ * Import is untouched: a `goal.md` already in `.kilo/commands/` is read as
18009
+ * it is, since the reservation is Kilo's to enforce on its own files.
18010
+ */
18011
+ static getWriteBlockReason({ rulesyncCommand }) {
18012
+ const stem = (0, node_path.basename)(rulesyncCommand.getRelativeFilePath(), ".md");
18013
+ if (!KILO_RESERVED_COMMAND_NAMES.has(stem)) return null;
18014
+ return `Kilo reserves the /${stem} command for session goals and refuses to load a custom command by that name — it would abort Kilo's whole command scan, making every other custom command unreachable too. Rename the command, or exclude it from this target with \`targets\`.`;
18015
+ }
17899
18016
  static fromRulesyncCommand({ outputRoot = process.cwd(), rulesyncCommand, validate = true, global = false }) {
17900
18017
  const rulesyncFrontmatter = rulesyncCommand.getFrontmatter();
17901
18018
  const kiloFields = rulesyncFrontmatter.kilo ?? {};
@@ -20118,6 +20235,14 @@ var CommandsProcessor = class extends FeatureProcessor {
20118
20235
  if (!factory.class.isTargetedByRulesyncCommand(rulesyncCommand)) return null;
20119
20236
  const originalRelativePath = rulesyncCommand.getRelativeFilePath();
20120
20237
  const commandToConvert = factory.meta.supportsSubdirectory ? rulesyncCommand : this.flattenRelativeFilePath(rulesyncCommand);
20238
+ const writeBlockReason = factory.class.getWriteBlockReason?.({
20239
+ rulesyncCommand: commandToConvert,
20240
+ global: this.global
20241
+ });
20242
+ if (writeBlockReason !== void 0 && writeBlockReason !== null) {
20243
+ this.logger.warn(`Skipping command ${quoteForLog(originalRelativePath)} for '${this.toolTarget}': ${writeBlockReason}`);
20244
+ return null;
20245
+ }
20121
20246
  if (!factory.meta.supportsSubdirectory) {
20122
20247
  const flattenedPath = commandToConvert.getRelativeFilePath();
20123
20248
  const firstOrigin = flattenedPathOrigins.get(flattenedPath);
@@ -20987,6 +21112,72 @@ function isSupportedHookType({ type, converterConfig }) {
20987
21112
  return converterConfig.supportedHookTypes?.has(type ?? "command") ?? true;
20988
21113
  }
20989
21114
  /**
21115
+ * Whether a keyword field applies to this hook, warning about the reason when
21116
+ * it does not. Shared by both directions: the field is unusable on the tool
21117
+ * side either way, so the value is dropped rather than carried into a config
21118
+ * the tool would ignore or refuse.
21119
+ */
21120
+ function isKeywordFieldApplicable({ spec, hookType, eventName, direction, warn }) {
21121
+ const { canonical, tool, commandOnly, events } = spec;
21122
+ const field = direction === "generated" ? canonical : tool;
21123
+ if (!isFieldApplicable({
21124
+ commandOnly,
21125
+ hookType
21126
+ })) {
21127
+ warn?.(`Dropping "${field}" from a "${hookType}" hook on "${eventName}": this tool documents "${tool}" on "command" hooks only, so it is not ${direction}.`);
21128
+ return false;
21129
+ }
21130
+ if (events !== void 0 && !events.has(eventName)) {
21131
+ warn?.(`Dropping "${field}" from a "${hookType}" hook on "${eventName}": this tool honours "${tool}" on ${[...events].map((event) => `"${event}"`).join(", ")} only, so it is not ${direction}.`);
21132
+ return false;
21133
+ }
21134
+ return true;
21135
+ }
21136
+ /**
21137
+ * Emit the keyword passthrough fields: a canonical `true` becomes the tool's
21138
+ * keyword, while `false` is omitted as the tool's own default. A `true` the
21139
+ * tool would not honour on this hook is warned about, like the other kinds;
21140
+ * a `false` there is dropped silently, since omitting it changes nothing.
21141
+ */
21142
+ function emitKeywordFields({ def, hookType, eventName, fields, warn }) {
21143
+ const emitted = {};
21144
+ for (const spec of fields) {
21145
+ if (def[spec.canonical] !== true) continue;
21146
+ if (isKeywordFieldApplicable({
21147
+ spec,
21148
+ hookType,
21149
+ eventName,
21150
+ direction: "generated",
21151
+ warn
21152
+ })) emitted[spec.tool] = spec.trueValue;
21153
+ }
21154
+ return emitted;
21155
+ }
21156
+ /**
21157
+ * Import the keyword passthrough fields back into canonical booleans,
21158
+ * reversing {@link emitKeywordFields}. Only the two documented keywords are
21159
+ * read; anything else is warned about and dropped, so a typo in a hand-written
21160
+ * tool file never lands in `.rulesync/hooks.*` as a boolean it did not mean.
21161
+ */
21162
+ function importKeywordFields({ h, hookType, eventName, fields, warn }) {
21163
+ const imported = {};
21164
+ for (const spec of fields) {
21165
+ const value = h[spec.tool];
21166
+ if (value === void 0) continue;
21167
+ if (!isKeywordFieldApplicable({
21168
+ spec,
21169
+ hookType,
21170
+ eventName,
21171
+ direction: "imported",
21172
+ warn
21173
+ })) continue;
21174
+ if (value === spec.trueValue) imported[spec.canonical] = true;
21175
+ else if (spec.falseValue !== void 0 && value === spec.falseValue) imported[spec.canonical] = false;
21176
+ else warn?.(`Dropping "${spec.tool}" from an imported "${hookType}" hook on "${eventName}": ${quoteValueForWarning(value)} is not a value this tool documents for it.`);
21177
+ }
21178
+ return imported;
21179
+ }
21180
+ /**
20990
21181
  * Emit every per-hook passthrough kind for one canonical definition.
20991
21182
  */
20992
21183
  function emitAllPassthroughFields({ def, hookType, eventName, converterConfig, warn }) {
@@ -20999,6 +21190,13 @@ function emitAllPassthroughFields({ def, hookType, eventName, converterConfig, w
20999
21190
  isValid: isBooleanValue,
21000
21191
  warn
21001
21192
  }),
21193
+ ...emitKeywordFields({
21194
+ def,
21195
+ hookType,
21196
+ eventName,
21197
+ fields: converterConfig.keywordPassthroughFields ?? [],
21198
+ warn
21199
+ }),
21002
21200
  ...emitPassthroughFields({
21003
21201
  def,
21004
21202
  hookType,
@@ -21327,7 +21525,7 @@ function importTypePayloadFields({ h, hookType, warn }) {
21327
21525
  * Import every per-hook passthrough kind for one tool hook record, reversing
21328
21526
  * the emit side in {@link buildToolHooks}.
21329
21527
  */
21330
- function importAllPassthroughFields({ h, hookType, converterConfig, warn }) {
21528
+ function importAllPassthroughFields({ h, hookType, eventName, converterConfig, warn }) {
21331
21529
  return {
21332
21530
  ...importPassthroughFields({
21333
21531
  h,
@@ -21336,6 +21534,13 @@ function importAllPassthroughFields({ h, hookType, converterConfig, warn }) {
21336
21534
  isValid: isBooleanValue,
21337
21535
  warn
21338
21536
  }),
21537
+ ...importKeywordFields({
21538
+ h,
21539
+ hookType,
21540
+ eventName,
21541
+ fields: converterConfig.keywordPassthroughFields ?? [],
21542
+ warn
21543
+ }),
21339
21544
  ...importPassthroughFields({
21340
21545
  h,
21341
21546
  hookType,
@@ -21381,7 +21586,7 @@ function importAllPassthroughFields({ h, hookType, converterConfig, warn }) {
21381
21586
  /**
21382
21587
  * Convert a single tool hook record into a canonical hook definition.
21383
21588
  */
21384
- function toolHookToCanonical({ h, rawEntry, converterConfig, warn }) {
21589
+ function toolHookToCanonical({ h, rawEntry, eventName, converterConfig, warn }) {
21385
21590
  const hookType = resolveImportedHookType({
21386
21591
  value: h.type,
21387
21592
  converterConfig
@@ -21429,6 +21634,7 @@ function toolHookToCanonical({ h, rawEntry, converterConfig, warn }) {
21429
21634
  ...importAllPassthroughFields({
21430
21635
  h,
21431
21636
  hookType,
21637
+ eventName,
21432
21638
  converterConfig,
21433
21639
  warn
21434
21640
  }),
@@ -21515,7 +21721,7 @@ function describeHookSkipReason({ h, rawEntry, hookType, converterConfig }) {
21515
21721
  /**
21516
21722
  * Convert a single tool matcher entry into canonical hook definitions.
21517
21723
  */
21518
- function toolMatcherEntryToCanonical({ rawEntry, converterConfig, warn }) {
21724
+ function toolMatcherEntryToCanonical({ rawEntry, eventName, converterConfig, warn }) {
21519
21725
  const hookDefs = rawEntry.hooks ?? [];
21520
21726
  const groupSkipReason = describeGroupSkipReason({
21521
21727
  rawEntry,
@@ -21543,6 +21749,7 @@ function toolMatcherEntryToCanonical({ rawEntry, converterConfig, warn }) {
21543
21749
  definitions.push(toolHookToCanonical({
21544
21750
  h,
21545
21751
  rawEntry,
21752
+ eventName,
21546
21753
  converterConfig,
21547
21754
  warn
21548
21755
  }));
@@ -21590,6 +21797,7 @@ function toolHooksToCanonical({ hooks, converterConfig, logger }) {
21590
21797
  if (!isToolMatcherEntry(rawEntry)) continue;
21591
21798
  defs.push(...toolMatcherEntryToCanonical({
21592
21799
  rawEntry,
21800
+ eventName,
21593
21801
  converterConfig,
21594
21802
  warn
21595
21803
  }));
@@ -25210,7 +25418,15 @@ const GOOSE_CONVERTER_CONFIG = {
25210
25418
  toolToCanonicalEventNames: GOOSE_TO_CANONICAL_EVENT_NAMES,
25211
25419
  projectDirVar: "",
25212
25420
  supportedHookTypes: /* @__PURE__ */ new Set(["command"]),
25213
- wildcardMatcherMeansAll: true
25421
+ wildcardMatcherMeansAll: true,
25422
+ keywordPassthroughFields: [{
25423
+ canonical: "failClosed",
25424
+ tool: "on_failure",
25425
+ trueValue: "block",
25426
+ falseValue: "allow",
25427
+ commandOnly: true,
25428
+ events: /* @__PURE__ */ new Set(["preToolUse"])
25429
+ }]
25214
25430
  };
25215
25431
  /**
25216
25432
  * Represents a Goose lifecycle hooks file.
@@ -25519,7 +25735,7 @@ function canonicalToHermesHooks({ config, toolOverrideHooks, logger }) {
25519
25735
  for (const [nativeEvent, definitions] of Object.entries(toolOverrideHooks ?? {})) {
25520
25736
  if (HERMESAGENT_CANONICAL_EVENTS.has(nativeEvent)) continue;
25521
25737
  if (HERMESAGENT_SHELL_UNSUPPORTED_EVENTS.has(nativeEvent)) logger?.warn(`Hermes hook event "${nativeEvent}" is Python-plugin-only; Hermes Agent refuses it for shell hooks, so the generated entry will never run.`);
25522
- else if (!HERMESAGENT_NATIVE_EVENTS.has(nativeEvent)) logger?.warn(`Hermes hook event "${nativeEvent}" is not documented by Hermes Agent v0.20.2; preserving it for forward compatibility.`);
25738
+ else if (!HERMESAGENT_NATIVE_EVENTS.has(nativeEvent)) logger?.warn(`Hermes hook event "${nativeEvent}" is not documented by Hermes Agent v0.21.3; preserving it for forward compatibility.`);
25523
25739
  setHermesHookEntries({
25524
25740
  result,
25525
25741
  event: nativeEvent,
@@ -26399,7 +26615,8 @@ const KiroIdeHookEntrySchema = zod_mini.z.looseObject({
26399
26615
  matcher: zod_mini.z.optional(zod_mini.z.string()),
26400
26616
  action: zod_mini.z.optional(KiroIdeHookActionSchema),
26401
26617
  timeout: zod_mini.z.optional(zod_mini.z.number()),
26402
- enabled: zod_mini.z.optional(zod_mini.z.boolean())
26618
+ enabled: zod_mini.z.optional(zod_mini.z.boolean()),
26619
+ confirm: zod_mini.z.optional(HookConfirmSchema)
26403
26620
  });
26404
26621
  const KiroIdeHooksFileSchema = zod_mini.z.looseObject({
26405
26622
  version: zod_mini.z.optional(zod_mini.z.string()),
@@ -26438,7 +26655,8 @@ function buildKiroIdeEntriesForEvent(trigger, definitions) {
26438
26655
  ...def.matcher !== void 0 && def.matcher !== null && def.matcher !== "" && { matcher: def.matcher },
26439
26656
  action,
26440
26657
  ...def.timeout !== void 0 && def.timeout !== null && def.timeout >= 0 && { timeout: def.timeout },
26441
- enabled: def.enabled ?? true
26658
+ enabled: def.enabled ?? true,
26659
+ ...def.confirm !== void 0 && { confirm: def.confirm }
26442
26660
  });
26443
26661
  }
26444
26662
  return entries;
@@ -26480,6 +26698,14 @@ function canonicalToKiroIdeHooks(config) {
26480
26698
  }
26481
26699
  return entries;
26482
26700
  }
26701
+ /**
26702
+ * Copies the optional per-hook fields (`enabled`, `confirm`) back onto the
26703
+ * canonical definition.
26704
+ */
26705
+ function copyOptionalHookFields({ entry, def }) {
26706
+ if (entry.enabled === false) def.enabled = false;
26707
+ if (entry.confirm !== void 0 && entry.confirm !== null) def.confirm = omitPrototypePollutionKeysDeep(entry.confirm);
26708
+ }
26483
26709
  function kiroIdeHooksToCanonical(entries) {
26484
26710
  const canonical = {};
26485
26711
  for (const entry of entries) {
@@ -26503,7 +26729,10 @@ function kiroIdeHooksToCanonical(entries) {
26503
26729
  if (entry.description !== void 0 && entry.description !== null) def.description = entry.description;
26504
26730
  if (entry.matcher !== void 0 && entry.matcher !== null && entry.matcher !== "") def.matcher = entry.matcher;
26505
26731
  if (entry.timeout !== void 0 && entry.timeout !== null) def.timeout = entry.timeout;
26506
- if (entry.enabled === false) def.enabled = false;
26732
+ copyOptionalHookFields({
26733
+ entry,
26734
+ def
26735
+ });
26507
26736
  const list = lookupOwn({
26508
26737
  record: canonical,
26509
26738
  key: eventName
@@ -27510,10 +27739,16 @@ var QwencodeHooks = class QwencodeHooks extends ToolHooks {
27510
27739
  //#endregion
27511
27740
  //#region src/features/hooks/reasonix-hooks.ts
27512
27741
  /**
27513
- * Only PreToolUse/PostToolUse honor the `match` field (an anchored regex
27514
- * against the tool name); it is ignored on every other event.
27742
+ * Only the tool-scoped events honor the `match` field (an anchored regex
27743
+ * against the tool name); it is ignored on every other event. Mirrors
27744
+ * `UsesToolMatcher` in upstream `internal/hook/inspect.go`.
27515
27745
  */
27516
- const REASONIX_MATCHER_EVENTS = /* @__PURE__ */ new Set(["PreToolUse", "PostToolUse"]);
27746
+ const REASONIX_MATCHER_EVENTS = /* @__PURE__ */ new Set([
27747
+ "PreToolUse",
27748
+ "PostToolUse",
27749
+ "PostToolUseFailure",
27750
+ "PermissionRequest"
27751
+ ]);
27517
27752
  const SUPPORTED_REASONIX_EVENTS = new Set(REASONIX_HOOK_EVENTS);
27518
27753
  /**
27519
27754
  * Convert canonical hooks config to the Reasonix `hooks` object.
@@ -27595,9 +27830,11 @@ function reasonixHooksToCanonical(hooks) {
27595
27830
  * Reasonix hooks live in a Claude-Code-style but standalone JSON file —
27596
27831
  * `.reasonix/settings.json` (project) or `~/.reasonix/settings.json`
27597
27832
  * (global) — separate from the `[permissions]`/`[[plugins]]` TOML config.
27598
- * All ten upstream events have a clean canonical equivalent and are mapped:
27599
- * PreToolUse/PostToolUse/UserPromptSubmit/Stop plus SessionStart/SessionEnd/
27600
- * SubagentStop/PostLLMCall/Notification/PreCompact (see REASONIX_HOOK_EVENTS).
27833
+ * All thirteen upstream events have a clean canonical equivalent and are mapped:
27834
+ * PreToolUse/PostToolUse/PostToolUseFailure/PermissionRequest/UserPromptSubmit/
27835
+ * Stop/StopFailure plus SessionStart/SessionEnd/SubagentStop/PostLLMCall/
27836
+ * Notification/PreCompact (see REASONIX_HOOK_EVENTS).
27837
+ * @see https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/internal/hook/hook.go
27601
27838
  * @see https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/docs/DESKTOP_HOOKS.zh-CN.md
27602
27839
  */
27603
27840
  var ReasonixHooks = class ReasonixHooks extends ToolHooks {
@@ -28423,6 +28660,23 @@ const PER_HOOK_ENABLED_TARGETS = /* @__PURE__ */ new Set([
28423
28660
  "kiro-ide",
28424
28661
  "zcode"
28425
28662
  ]);
28663
+ /** The targets whose hooks format carries the Kiro `confirm` prompt. */
28664
+ const PER_HOOK_CONFIRM_TARGETS = /* @__PURE__ */ new Set(["kiro-cli", "kiro-ide"]);
28665
+ /**
28666
+ * Names the shared events that carry a hook whose canonical definition
28667
+ * satisfies `predicate`, excluding events the target skips altogether (those
28668
+ * are already reported as skipped and produce no output, so warning about them
28669
+ * again would contradict that message).
28670
+ */
28671
+ function emittedEventsWithHook(params) {
28672
+ const { factory, sharedHooks, effectiveHooks, predicate } = params;
28673
+ const skippedEvents = new Set(unsupportedEventNames({
28674
+ factory,
28675
+ sharedHooks,
28676
+ effectiveHooks
28677
+ }));
28678
+ return Object.entries(sharedHooks).filter(([event, defs]) => !skippedEvents.has(event) && defs.some((def) => predicate(def))).map(([event]) => event);
28679
+ }
28426
28680
  const toolHooksFactories = /* @__PURE__ */ new Map([
28427
28681
  ["amp", {
28428
28682
  class: AmpHooks,
@@ -28993,15 +29247,11 @@ var HooksProcessor = class extends FeatureProcessor {
28993
29247
  }
28994
29248
  for (const [hookType, events] of unsupportedTypeToEvents) this.logger.warn(`Skipped ${hookType}-type hook(s) for ${this.toolTarget} (not supported): ${Array.from(events).join(", ")}`);
28995
29249
  }
28996
- if (!PER_HOOK_ENABLED_TARGETS.has(this.toolTarget)) {
28997
- const skippedEvents = new Set(unsupportedEventNames({
28998
- factory,
28999
- sharedHooks,
29000
- effectiveHooks
29001
- }));
29002
- const eventsWithDisabledHooks = Object.entries(sharedHooks).filter(([event, defs]) => !skippedEvents.has(event) && defs.some((def) => def.enabled === false)).map(([event]) => event);
29003
- if (eventsWithDisabledHooks.length > 0) this.logger.warn(`Emitting "enabled: false" hook(s) as active for ${this.toolTarget} (only the kiro-cli / kiro-ide / zcode hooks formats support the flag): ${eventsWithDisabledHooks.join(", ")}`);
29004
- }
29250
+ this.warnAboutDroppedPerHookFields({
29251
+ factory,
29252
+ sharedHooks,
29253
+ effectiveHooks
29254
+ });
29005
29255
  const eventsWithUnsupportedMatcher = unsupportedMatcherEventNames({
29006
29256
  factory,
29007
29257
  effectiveHooks
@@ -29031,6 +29281,33 @@ var HooksProcessor = class extends FeatureProcessor {
29031
29281
  if (auxiliaryFiles && auxiliaryFiles.length > 0) result.push(...auxiliaryFiles);
29032
29282
  return result;
29033
29283
  }
29284
+ /**
29285
+ * Warns about canonical per-hook fields that this target's hooks format
29286
+ * cannot express, so a hook does not silently change behavior on generation.
29287
+ * Only canonical definitions are considered: a tool-native field inside an
29288
+ * override block is passed through verbatim and honored by that tool.
29289
+ */
29290
+ warnAboutDroppedPerHookFields(params) {
29291
+ const { factory, sharedHooks, effectiveHooks } = params;
29292
+ if (!PER_HOOK_ENABLED_TARGETS.has(this.toolTarget)) {
29293
+ const eventsWithDisabledHooks = emittedEventsWithHook({
29294
+ factory,
29295
+ sharedHooks,
29296
+ effectiveHooks,
29297
+ predicate: (def) => def.enabled === false
29298
+ });
29299
+ if (eventsWithDisabledHooks.length > 0) this.logger.warn(`Emitting "enabled: false" hook(s) as active for ${this.toolTarget} (only the kiro-cli / kiro-ide / zcode hooks formats support the flag): ${eventsWithDisabledHooks.join(", ")}`);
29300
+ }
29301
+ if (!PER_HOOK_CONFIRM_TARGETS.has(this.toolTarget)) {
29302
+ const eventsWithConfirmHooks = emittedEventsWithHook({
29303
+ factory,
29304
+ sharedHooks,
29305
+ effectiveHooks,
29306
+ predicate: (def) => def.confirm !== void 0
29307
+ });
29308
+ if (eventsWithConfirmHooks.length > 0) this.logger.warn(`Emitting hook(s) without their "confirm" prompt for ${this.toolTarget} (only the kiro-cli / kiro-ide hooks formats support it): ${eventsWithConfirmHooks.join(", ")}`);
29309
+ }
29310
+ }
29034
29311
  async convertToolFilesToRulesyncFiles(toolFiles) {
29035
29312
  const hooks = toolFiles.filter((f) => f instanceof ToolHooks);
29036
29313
  const logger = withToolTargetPrefix({
@@ -34832,28 +35109,60 @@ function resolveHermesTimeout(config) {
34832
35109
  if (typeof config.networkTimeout === "number") return config.networkTimeout;
34833
35110
  }
34834
35111
  /**
34835
- * Copies the advanced Hermes-recognized per-server fields that have no canonical
34836
- * alias — `auth` (`oauth` for OAuth 2.1/PKCE), mTLS `client_cert` (string PEM
34837
- * path, or `[cert, key]`/`[cert, key, password]` list) and `client_key`,
34838
- * `connect_timeout` (seconds), `supports_parallel_tool_calls`,
34839
- * `keepalive_interval`, `elicitation`, `trust`, and `identity_header` — verbatim
34840
- * from `source` to `target`. Field names are identical on both sides (the
34841
- * canonical `McpServerSchema` is a `looseObject`), so this serves export and
34842
- * import alike. See the Hermes mcp-config-reference.
35112
+ * String-valued `oauth` keys Hermes reads (`tools/mcp_oauth.py`,
35113
+ * `tools/mcp_oauth_device.py`, v0.21.3): the callback overrides, the
35114
+ * pre-registered client, the CIMD document override, the DCR registration
35115
+ * fields, the token-request `user_agent`, the login `flow` (`device` for
35116
+ * RFC 8628) and the space-separated `scope` string sent on registration and
35117
+ * device authorization.
35118
+ */
35119
+ const HERMES_OAUTH_STRING_KEYS = [
35120
+ "redirect_uri",
35121
+ "redirect_host",
35122
+ "client_id",
35123
+ "client_secret",
35124
+ "client_name",
35125
+ "client_metadata_url",
35126
+ "token_endpoint_auth_method",
35127
+ "application_type",
35128
+ "user_agent",
35129
+ "flow",
35130
+ "scope"
35131
+ ];
35132
+ /**
35133
+ * Copies the `oauth` mapping of a server, keeping only the keys Hermes reads.
35134
+ *
35135
+ * Hermes reads a single `scope` string, never a `scopes` list; earlier Rulesync
35136
+ * versions wrote `scopes` regardless, which Hermes silently ignored. A `scopes`
35137
+ * list is therefore folded into `scope` (space-separated, the OAuth wire form)
35138
+ * when no `scope` is given, in both directions, so an authored list finally
35139
+ * reaches the authorization server and an old `config.yaml` imports cleanly.
34843
35140
  */
34844
35141
  function copyHermesOauth(source) {
34845
35142
  if (!isRecord$1(source)) return;
34846
35143
  const oauth = {};
34847
- for (const key of [
34848
- "redirect_uri",
34849
- "redirect_host",
34850
- "client_id",
34851
- "client_secret"
34852
- ]) if (typeof source[key] === "string") oauth[key] = source[key];
34853
- if (typeof source.redirect_port === "number") oauth.redirect_port = source.redirect_port;
34854
- if (isStringArray$2(source.scopes)) oauth.scopes = source.scopes;
35144
+ for (const key of HERMES_OAUTH_STRING_KEYS) if (typeof source[key] === "string") oauth[key] = source[key];
35145
+ if (typeof oauth.scope !== "string" && isStringArray$2(source.scopes)) oauth.scope = source.scopes.join(" ");
35146
+ for (const key of ["redirect_port", "timeout"]) if (typeof source[key] === "number") oauth[key] = source[key];
35147
+ if (typeof source.cimd === "boolean") oauth.cimd = source.cimd;
34855
35148
  return Object.keys(oauth).length > 0 ? oauth : void 0;
34856
35149
  }
35150
+ const HERMES_OPAQUE_MAPPING_KEYS = [
35151
+ "sampling",
35152
+ "elicitation",
35153
+ "identity_header",
35154
+ "lifecycle"
35155
+ ];
35156
+ /**
35157
+ * Copies the advanced Hermes-recognized per-server fields that have no canonical
35158
+ * alias — `auth` (`oauth` for OAuth 2.1/PKCE), mTLS `client_cert` (string PEM
35159
+ * path, or `[cert, key]`/`[cert, key, password]` list) and `client_key`,
35160
+ * `connect_timeout` (seconds), `supports_parallel_tool_calls`, `protocol`,
35161
+ * `lazy`, `keepalive_interval`, `elicitation`, `trust`, and `identity_header` —
35162
+ * verbatim from `source` to `target`. Field names are identical on both sides
35163
+ * (the canonical `McpServerSchema` is a `looseObject`), so this serves export
35164
+ * and import alike. See the Hermes mcp-config-reference.
35165
+ */
34857
35166
  function copyHermesAdvancedFields(source, target) {
34858
35167
  let copied = false;
34859
35168
  if (typeof source.auth === "string") {
@@ -34885,6 +35194,14 @@ function copyHermesAdvancedFields(source, target) {
34885
35194
  target[key] = source[key];
34886
35195
  copied = true;
34887
35196
  }
35197
+ if (typeof source.protocol === "string") {
35198
+ target.protocol = source.protocol;
35199
+ copied = true;
35200
+ }
35201
+ if (typeof source.lazy === "boolean") {
35202
+ target.lazy = source.lazy;
35203
+ copied = true;
35204
+ }
34888
35205
  if (typeof source.ssl_verify === "boolean" || typeof source.ssl_verify === "string") {
34889
35206
  target.ssl_verify = source.ssl_verify;
34890
35207
  copied = true;
@@ -34893,25 +35210,20 @@ function copyHermesAdvancedFields(source, target) {
34893
35210
  target.skip_preflight = source.skip_preflight;
34894
35211
  copied = true;
34895
35212
  }
34896
- if (isPlainObject$1(source.sampling)) {
34897
- target.sampling = omitPrototypePollutionKeys(structuredClone(source.sampling));
34898
- copied = true;
34899
- }
34900
35213
  if (typeof source.keepalive_interval === "number") {
34901
35214
  target.keepalive_interval = source.keepalive_interval;
34902
35215
  copied = true;
34903
35216
  }
34904
- if (isPlainObject$1(source.elicitation)) {
34905
- target.elicitation = omitPrototypePollutionKeys(structuredClone(source.elicitation));
34906
- copied = true;
34907
- }
34908
35217
  if (typeof source.trust === "string") {
34909
35218
  target.trust = source.trust;
34910
35219
  copied = true;
34911
35220
  }
34912
- if (isPlainObject$1(source.identity_header)) {
34913
- target.identity_header = omitPrototypePollutionKeys(structuredClone(source.identity_header));
34914
- copied = true;
35221
+ for (const key of HERMES_OPAQUE_MAPPING_KEYS) {
35222
+ const mapping = source[key];
35223
+ if (isPlainObject$1(mapping)) {
35224
+ target[key] = omitPrototypePollutionKeys(structuredClone(mapping));
35225
+ copied = true;
35226
+ }
34915
35227
  }
34916
35228
  return copied;
34917
35229
  }
@@ -35695,6 +36007,7 @@ function toKimiCodeServer({ name, server, logger }) {
35695
36007
  "headers",
35696
36008
  "bearerTokenEnvVar",
35697
36009
  "enabled",
36010
+ "deferred",
35698
36011
  "startupTimeoutMs",
35699
36012
  "toolTimeoutMs",
35700
36013
  "enabledTools",
@@ -36352,7 +36665,12 @@ const OpencodeMcpRemoteServerSchema = zod_mini.z.looseObject({
36352
36665
  headers: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.string())),
36353
36666
  enabled: zod_mini.z._default(zod_mini.z.boolean(), true)
36354
36667
  });
36355
- const OPENCODE_PASSTHROUGH_SERVER_FIELDS = ["timeout", "oauth"];
36668
+ const OPENCODE_PASSTHROUGH_SERVER_FIELDS = [
36669
+ "timeout",
36670
+ "oauth",
36671
+ "protocol",
36672
+ "codemode"
36673
+ ];
36356
36674
  const OPENCODE_MCP_TRANSPORT_KEYS = [
36357
36675
  ...Object.keys(OpencodeMcpLocalServerSchema.def.shape),
36358
36676
  ...Object.keys(OpencodeMcpRemoteServerSchema.def.shape),
@@ -36494,7 +36812,7 @@ function warnAboutToggleDroppedKeys(serverName, serverConfig, logger) {
36494
36812
  * - env -> environment
36495
36813
  * - disabled -> enabled (inverted)
36496
36814
  * - enabledTools/disabledTools -> top-level tools map (with server name prefix)
36497
- * - OpenCode-supported extras (timeout, oauth) -> passed through verbatim
36815
+ * - OpenCode-supported extras (timeout, oauth, protocol, codemode) -> passed through verbatim
36498
36816
  */
36499
36817
  function convertServerToOpencodeFormat(serverName, serverConfig, existingEntry, logger) {
36500
36818
  const serverRecord = serverConfig;
@@ -39487,7 +39805,7 @@ var ToolPermissions = class extends ToolFile {
39487
39805
  * builtin of that name). It lives in the shared Amp settings file:
39488
39806
  * `.amp/settings.json` (project) and `~/.config/amp/settings.json` (global).
39489
39807
  *
39490
- * Reference: https://ampcode.com/manual ("amp.tools.disable").
39808
+ * Reference: https://ampcode.com/docs/cli/settings ("amp.tools.disable").
39491
39809
  */
39492
39810
  const AMP_TOOLS_DISABLE_KEY = "amp.tools.disable";
39493
39811
  /**
@@ -39498,19 +39816,26 @@ const AMP_TOOLS_DISABLE_KEY = "amp.tools.disable";
39498
39816
  * glob. Rules are evaluated **first-match-wins**. It lives in the same shared
39499
39817
  * Amp settings file as `amp.tools.disable`.
39500
39818
  *
39501
- * `amp.permissions` is Amp's documented legacy/backwards-compat surface — it
39502
- * remains functional and is the only place to express `allow`/`ask` and
39503
- * argument-specific `reject` rules (the simpler `amp.tools.disable` array can
39504
- * only disable whole tools).
39819
+ * `amp.permissions` is Amp's legacy/backwards-compat surface — it remains
39820
+ * functional in the shipped CLI and is the only place to express `allow`/`ask`
39821
+ * and argument-specific `reject` rules (the simpler `amp.tools.disable` array
39822
+ * can only disable whole tools). The web docs no longer describe it (the
39823
+ * permissions section only points at custom plugins), but the shipped CLI
39824
+ * still lists it in its built-in settings reference (`amp --help`) and
39825
+ * evaluates it through `amp permissions list|test`.
39505
39826
  *
39506
- * Reference: https://ampcode.com/manual ("amp.permissions").
39827
+ * Reference: https://ampcode.com/docs/tools#permissions and
39828
+ * https://ampcode.com/docs/customize/plugins#example-plugin-permissions.
39507
39829
  */
39508
39830
  const AMP_PERMISSIONS_KEY = "amp.permissions";
39509
39831
  /**
39510
39832
  * The `amp.guardedFiles.allowlist` array (file globs allowed without
39511
39833
  * confirmation), `amp.dangerouslyAllowAll` boolean (disable all confirmation),
39512
39834
  * and `amp.mcpPermissions` array — sibling settings authored through the `amp`
39513
- * permissions override. Reference: https://ampcode.com/manual.
39835
+ * permissions override. Only `amp.mcpPermissions` is still on the web docs
39836
+ * (https://ampcode.com/docs/cli/settings); the other two, like
39837
+ * `amp.permissions`, survive only in the CLI's built-in settings reference
39838
+ * (see `AMP_PERMISSIONS_KEY`).
39514
39839
  */
39515
39840
  const AMP_GUARDED_FILES_ALLOWLIST_KEY = "amp.guardedFiles.allowlist";
39516
39841
  const AMP_DANGEROUSLY_ALLOW_ALL_KEY = "amp.dangerouslyAllowAll";
@@ -42839,6 +43164,8 @@ const CODEX_DANGER_FULL_ACCESS_BASELINE = ":danger-full-access";
42839
43164
  const CODEX_EXTENDABLE_BASELINES = new Set(CODEX_EXTENDABLE_BASELINE_PROFILES);
42840
43165
  const CODEX_DEFAULT_APPROVAL_POLICY = "on-request";
42841
43166
  const CODEX_DEFAULT_APPROVALS_REVIEWER = "auto_review";
43167
+ const CODEX_RETIRED_APPROVAL_POLICY = "untrusted";
43168
+ const CODEX_DEPRECATED_APPROVAL_POLICY = "on-failure";
42842
43169
  const CODEX_GLOB_SCAN_MAX_DEPTH = 8;
42843
43170
  const CODEX_MINIMAL_KEY = ":minimal";
42844
43171
  const CODEX_GIT_WRITE_RULES = { ".git/**": "write" };
@@ -42962,6 +43289,10 @@ var CodexcliPermissions = class CodexcliPermissions extends ToolPermissions {
42962
43289
  domainsHadUnknown
42963
43290
  });
42964
43291
  const override = extractCodexcliOverride(table);
43292
+ if (override.approval_policy === CODEX_RETIRED_APPROVAL_POLICY) {
43293
+ warnWithFallback(void 0, `${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())} sets approval_policy = "${CODEX_RETIRED_APPROVAL_POLICY}", which was retired in Codex 0.149.0 and makes Codex refuse to start. It was not imported; remove it from the file, or mark the project untrusted in your user config instead ([projects."<path>"] trust_level = "${CODEX_RETIRED_APPROVAL_POLICY}").`);
43294
+ delete override.approval_policy;
43295
+ }
42965
43296
  if (typeof profile?.extends === "string" && CODEX_EXTENDABLE_BASELINES.has(profile.extends)) override.base_permission_profile = profile.extends;
42966
43297
  if (defaultProfile === CODEX_DANGER_FULL_ACCESS_BASELINE) override.base_permission_profile = CODEX_DANGER_FULL_ACCESS_BASELINE;
42967
43298
  const result = Object.keys(override).length > 0 ? {
@@ -43250,6 +43581,10 @@ function computeCodexcliOverridePatch({ existing, override, logger }) {
43250
43581
  continue;
43251
43582
  }
43252
43583
  if (value === void 0) continue;
43584
+ if (key === "approval_policy" && !isWritableApprovalPolicy({
43585
+ value,
43586
+ logger
43587
+ })) continue;
43253
43588
  if (key === "sandbox_mode" || key === "sandbox_workspace_write") logger?.warn(`Codex CLI permission override key "${key}" is deprecated. Codex prioritizes the legacy sandbox settings over permission profiles when both are present, so it disables the generated "${RULESYNC_PROFILE_NAME}" permissions profile. Use "base_permission_profile" and the shared "permission" block instead.`);
43254
43589
  const existingValue = existing[key];
43255
43590
  patch[key] = isPlainObject(existingValue) && isPlainObject(value) ? {
@@ -43257,12 +43592,37 @@ function computeCodexcliOverridePatch({ existing, override, logger }) {
43257
43592
  ...value
43258
43593
  } : value;
43259
43594
  }
43595
+ fillCodexcliDefaults({
43596
+ existing,
43597
+ patch,
43598
+ logger
43599
+ });
43600
+ return patch;
43601
+ }
43602
+ function fillCodexcliDefaults({ existing, patch, logger }) {
43260
43603
  const defaults = {
43261
43604
  approval_policy: CODEX_DEFAULT_APPROVAL_POLICY,
43262
43605
  approvals_reviewer: CODEX_DEFAULT_APPROVALS_REVIEWER
43263
43606
  };
43264
- for (const [key, value] of Object.entries(defaults)) if (patch[key] === void 0 && existing[key] === void 0) patch[key] = value;
43265
- return patch;
43607
+ for (const [key, value] of Object.entries(defaults)) {
43608
+ if (patch[key] !== void 0) continue;
43609
+ if (existing[key] === void 0) {
43610
+ patch[key] = value;
43611
+ continue;
43612
+ }
43613
+ if (key === "approval_policy" && existing[key] === CODEX_RETIRED_APPROVAL_POLICY) {
43614
+ logger?.warn(`The existing Codex CLI config.toml sets approval_policy = "${CODEX_RETIRED_APPROVAL_POLICY}", which was retired in Codex 0.149.0 and makes Codex refuse to start, so it was replaced with "${value}". To keep the strict behavior, mark the project untrusted in your user config instead ([projects."<path>"] trust_level = "${CODEX_RETIRED_APPROVAL_POLICY}").`);
43615
+ patch[key] = value;
43616
+ }
43617
+ }
43618
+ }
43619
+ function isWritableApprovalPolicy({ value, logger }) {
43620
+ if (value === CODEX_RETIRED_APPROVAL_POLICY) {
43621
+ logger?.warn(`Codex CLI permission override "approval_policy": "${CODEX_RETIRED_APPROVAL_POLICY}" was retired in Codex 0.149.0 and makes Codex refuse to start, so it was not written. Remove it from the override; to keep the strict behavior, mark the project untrusted in your user config instead ([projects."<path>"] trust_level = "${CODEX_RETIRED_APPROVAL_POLICY}").`);
43622
+ return false;
43623
+ }
43624
+ if (value === CODEX_DEPRECATED_APPROVAL_POLICY) logger?.warn(`Codex CLI permission override "approval_policy": "${CODEX_DEPRECATED_APPROVAL_POLICY}" is deprecated; Codex reads it as "${CODEX_DEFAULT_APPROVAL_POLICY}". Use "${CODEX_DEFAULT_APPROVAL_POLICY}" instead.`);
43625
+ return true;
43266
43626
  }
43267
43627
  function extractCodexcliOverride(table) {
43268
43628
  const override = {};
@@ -51714,6 +52074,18 @@ const TAKT_RUNTIME_TARGETS_KEY = "targets";
51714
52074
  const TAKT_RUNTIME_AUTO_ROUTING_KEY = "auto_routing";
51715
52075
  const TAKT_RUNTIME_PROFILE_KEY = "profile";
51716
52076
  const TAKT_RUNTIME_OPTIONS_KEY = "options";
52077
+ const TAKT_RUNTIME_ASSIGNMENTS_KEY = "assignments";
52078
+ const TAKT_RUNTIME_LADDER_KEY = "ladder";
52079
+ const TAKT_RUNTIME_EXTENDS_KEY = "extends";
52080
+ const TAKT_RUNTIME_ROUTER_PROFILE_KEY = "router_profile";
52081
+ const TAKT_RUNTIME_POOLS_KEY = "pools";
52082
+ const TAKT_RUNTIME_CANDIDATES_KEY = "candidates";
52083
+ const TAKT_RUNTIME_FALLBACK_PROFILE_KEY = "fallback_profile";
52084
+ /** The one `targets` map that routes companion reviewers rather than agents. */
52085
+ const TAKT_RUNTIME_COMPANIONS_TARGET_KEY = "companions";
52086
+ const TAKT_RUNTIME_COMPANION_KEY = "companion";
52087
+ const TAKT_RUNTIME_ENABLED_KEY = "enabled";
52088
+ const TAKT_DEFAULT_COMPANION_ENABLED = false;
51717
52089
  const TAKT_SECURITY_POLICIES = {
51718
52090
  workflow_arpeggio: [
51719
52091
  "custom_data_source_modules",
@@ -51967,21 +52339,156 @@ function runtimeProfiles(runtime) {
51967
52339
  const profiles = provider[TAKT_RUNTIME_PROFILES_KEY];
51968
52340
  return isPlainObject$1(profiles) ? profiles : {};
51969
52341
  }
52342
+ /** A `targets` map (`personas`, `tags`, …) with at least one non-empty entry. */
52343
+ function hasTargetContent(targets) {
52344
+ return isPlainObject$1(targets) && Object.values(targets).some(hasEntries);
52345
+ }
52346
+ /** The profile names an assignment points at: `profile` plus every `ladder` rung. */
52347
+ function assignmentProfileNames(assignment) {
52348
+ if (!isPlainObject$1(assignment)) return [];
52349
+ const names = [];
52350
+ const profile = assignment[TAKT_RUNTIME_PROFILE_KEY];
52351
+ if (typeof profile === "string") names.push(profile);
52352
+ const ladder = assignment[TAKT_RUNTIME_LADDER_KEY];
52353
+ if (Array.isArray(ladder)) names.push(...ladder.filter((rung) => typeof rung === "string"));
52354
+ return names;
52355
+ }
52356
+ /** The assignments under a `targets` map, split by whether they route companions. */
52357
+ function targetAssignments(targets, { companions }) {
52358
+ if (!isPlainObject$1(targets)) return [];
52359
+ return Object.entries(targets).filter(([name]) => name === TAKT_RUNTIME_COMPANIONS_TARGET_KEY === companions).flatMap(([, targetMap]) => isPlainObject$1(targetMap) ? Object.values(targetMap) : []);
52360
+ }
52361
+ /**
52362
+ * Record the profiles a `defaults` + `targets` pair (the top-level provider
52363
+ * section or one named assignment set) reaches, keeping companion roots apart
52364
+ * from the roots agents use.
52365
+ */
52366
+ function addAssignmentSetRoots({ set, companionRoots, agentRoots }) {
52367
+ for (const name of assignmentProfileNames(set[TAKT_RUNTIME_DEFAULTS_KEY])) agentRoots.add(name);
52368
+ const targets = set[TAKT_RUNTIME_TARGETS_KEY];
52369
+ for (const assignment of targetAssignments(targets, { companions: false })) for (const name of assignmentProfileNames(assignment)) agentRoots.add(name);
52370
+ for (const assignment of targetAssignments(targets, { companions: true })) for (const name of assignmentProfileNames(assignment)) companionRoots.add(name);
52371
+ }
52372
+ /** The profiles `auto_routing` reaches: the router plus every pool candidate/fallback. */
52373
+ function addAutoRoutingRoots(autoRouting, roots) {
52374
+ if (!isPlainObject$1(autoRouting)) return;
52375
+ const routerProfile = autoRouting[TAKT_RUNTIME_ROUTER_PROFILE_KEY];
52376
+ if (typeof routerProfile === "string") roots.add(routerProfile);
52377
+ const pools = autoRouting[TAKT_RUNTIME_POOLS_KEY];
52378
+ if (!isPlainObject$1(pools)) return;
52379
+ for (const pool of Object.values(pools)) {
52380
+ if (!isPlainObject$1(pool)) continue;
52381
+ const candidates = pool[TAKT_RUNTIME_CANDIDATES_KEY];
52382
+ for (const candidate of Array.isArray(candidates) ? candidates : []) for (const name of assignmentProfileNames(candidate)) roots.add(name);
52383
+ const fallback = pool[TAKT_RUNTIME_FALLBACK_PROFILE_KEY];
52384
+ if (typeof fallback === "string") roots.add(fallback);
52385
+ }
52386
+ }
52387
+ /** Every profile reachable from `roots` by following `extends`. */
52388
+ function profileClosure(profiles, roots) {
52389
+ const closure = /* @__PURE__ */ new Set();
52390
+ const visit = (name) => {
52391
+ if (closure.has(name)) return;
52392
+ closure.add(name);
52393
+ const profile = profiles[name];
52394
+ const parent = isPlainObject$1(profile) ? profile[TAKT_RUNTIME_EXTENDS_KEY] : void 0;
52395
+ if (typeof parent === "string") visit(parent);
52396
+ };
52397
+ for (const root of roots) visit(root);
52398
+ return closure;
52399
+ }
52400
+ /** The `targets` map minus its `companions` entry (`undefined` when not a map). */
52401
+ function withoutCompanionTargets(targets) {
52402
+ if (!isPlainObject$1(targets)) return;
52403
+ const { [TAKT_RUNTIME_COMPANIONS_TARGET_KEY]: _companions, ...remaining } = targets;
52404
+ return remaining;
52405
+ }
52406
+ /**
52407
+ * The named assignment sets with their companion targets removed; a set left
52408
+ * with neither `defaults` nor a non-empty `targets` map is dropped, and
52409
+ * `undefined` means none survived.
52410
+ */
52411
+ function withoutCompanionAssignments(assignments) {
52412
+ if (!isPlainObject$1(assignments)) return;
52413
+ const remaining = {};
52414
+ for (const [name, set] of Object.entries(assignments)) {
52415
+ if (!isPlainObject$1(set)) continue;
52416
+ const defaults = set[TAKT_RUNTIME_DEFAULTS_KEY];
52417
+ const targets = withoutCompanionTargets(set[TAKT_RUNTIME_TARGETS_KEY]);
52418
+ const keepTargets = hasTargetContent(targets);
52419
+ if (defaults === void 0 && !keepTargets) continue;
52420
+ remaining[name] = {
52421
+ ...defaults === void 0 ? {} : { [TAKT_RUNTIME_DEFAULTS_KEY]: defaults },
52422
+ ...keepTargets ? { [TAKT_RUNTIME_TARGETS_KEY]: targets } : {}
52423
+ };
52424
+ }
52425
+ return Object.keys(remaining).length > 0 ? remaining : void 0;
52426
+ }
52427
+ /** Whether the section (or any of its named assignment sets) routes companions at all. */
52428
+ function hasCompanionTargets(section) {
52429
+ const routesCompanions = (set) => {
52430
+ const targets = isPlainObject$1(set) ? set[TAKT_RUNTIME_TARGETS_KEY] : void 0;
52431
+ return isPlainObject$1(targets) && targets[TAKT_RUNTIME_COMPANIONS_TARGET_KEY] !== void 0;
52432
+ };
52433
+ const assignments = section[TAKT_RUNTIME_ASSIGNMENTS_KEY];
52434
+ return routesCompanions(section) || isPlainObject$1(assignments) && Object.values(assignments).some(routesCompanions);
52435
+ }
52436
+ /**
52437
+ * The `provider:` section as Takt itself evaluates it, mirroring upstream's
52438
+ * `getEffectiveProviderSection`: unless `companion.enabled` is `true`
52439
+ * (companions are opt-in), the `targets.companions` maps — top-level and inside
52440
+ * every named assignment set — are dropped, along with each profile that only a
52441
+ * companion target reaches through `extends`. A profile an agent assignment
52442
+ * (`defaults`, the other `targets` maps, `auto_routing`) also reaches is kept.
52443
+ * https://github.com/nrslib/takt/blob/main/src/infra/config/runtime-provider/schema.ts
52444
+ */
52445
+ function effectiveRuntimeProviderSection(runtime) {
52446
+ const section = runtime[TAKT_RUNTIME_PROVIDER_KEY];
52447
+ if (!isPlainObject$1(section)) return;
52448
+ const companion = runtime[TAKT_RUNTIME_COMPANION_KEY];
52449
+ if (((isPlainObject$1(companion) ? companion[TAKT_RUNTIME_ENABLED_KEY] : void 0) ?? TAKT_DEFAULT_COMPANION_ENABLED) === true || !hasCompanionTargets(section)) return section;
52450
+ const profiles = runtimeProfiles(runtime);
52451
+ const companionRoots = /* @__PURE__ */ new Set();
52452
+ const agentRoots = /* @__PURE__ */ new Set();
52453
+ addAssignmentSetRoots({
52454
+ set: section,
52455
+ companionRoots,
52456
+ agentRoots
52457
+ });
52458
+ const assignments = section[TAKT_RUNTIME_ASSIGNMENTS_KEY];
52459
+ for (const set of isPlainObject$1(assignments) ? Object.values(assignments) : []) if (isPlainObject$1(set)) addAssignmentSetRoots({
52460
+ set,
52461
+ companionRoots,
52462
+ agentRoots
52463
+ });
52464
+ addAutoRoutingRoots(section[TAKT_RUNTIME_AUTO_ROUTING_KEY], agentRoots);
52465
+ const companionClosure = profileClosure(profiles, companionRoots);
52466
+ const agentClosure = profileClosure(profiles, agentRoots);
52467
+ const effective = {
52468
+ ...section,
52469
+ [TAKT_RUNTIME_PROFILES_KEY]: Object.fromEntries(Object.entries(profiles).filter(([name]) => !companionClosure.has(name) || agentClosure.has(name)))
52470
+ };
52471
+ for (const [key, value] of [[TAKT_RUNTIME_TARGETS_KEY, withoutCompanionTargets(section[TAKT_RUNTIME_TARGETS_KEY])], [TAKT_RUNTIME_ASSIGNMENTS_KEY, withoutCompanionAssignments(assignments)]]) if (value === void 0) delete effective[key];
52472
+ else effective[key] = value;
52473
+ return effective;
52474
+ }
51970
52475
  /**
51971
52476
  * Whether a parsed `runtime.yaml` puts Takt into runtime mode, mirroring
51972
- * upstream's mode detection: the `provider:` section must carry an actual
51973
- * assignment — a non-empty `defaults`, `profiles` or `auto_routing`, or a
51974
- * `targets` map with at least one non-empty nested map. The file existing is not
51975
- * enough, and empty nested maps (`defaults: {}`, `targets: { personas: {} }`)
51976
- * must not flip the mode.
52477
+ * upstream's mode detection: the effective `provider:` section (see
52478
+ * `effectiveRuntimeProviderSection` — companion-only routing does not count
52479
+ * while companions are disabled) must carry an actual assignment — a non-empty
52480
+ * `defaults`, `profiles` or `auto_routing`, a `targets` map with at least one
52481
+ * non-empty nested map, or a named assignment set with a non-empty `defaults`
52482
+ * or such a `targets` map. The file existing is not enough, and empty nested
52483
+ * maps (`defaults: {}`, `targets: { personas: {} }`) must not flip the mode.
51977
52484
  * https://github.com/nrslib/takt/blob/main/src/infra/config/runtime-provider/mode.ts
51978
52485
  */
51979
52486
  function isRuntimeModeActive(runtime) {
51980
- const provider = runtime[TAKT_RUNTIME_PROVIDER_KEY];
51981
- if (!isPlainObject$1(provider)) return false;
51982
- if (hasEntries(provider[TAKT_RUNTIME_DEFAULTS_KEY]) || hasEntries(provider[TAKT_RUNTIME_PROFILES_KEY]) || hasEntries(provider[TAKT_RUNTIME_AUTO_ROUTING_KEY])) return true;
51983
- const targets = provider[TAKT_RUNTIME_TARGETS_KEY];
51984
- return isPlainObject$1(targets) && Object.values(targets).some(hasEntries);
52487
+ const section = effectiveRuntimeProviderSection(runtime);
52488
+ if (section === void 0) return false;
52489
+ const assignments = section[TAKT_RUNTIME_ASSIGNMENTS_KEY];
52490
+ const hasAssignments = isPlainObject$1(assignments) && Object.values(assignments).some((set) => isPlainObject$1(set) && (hasEntries(set[TAKT_RUNTIME_DEFAULTS_KEY]) || hasTargetContent(set[TAKT_RUNTIME_TARGETS_KEY])));
52491
+ return hasEntries(section[TAKT_RUNTIME_DEFAULTS_KEY]) || hasEntries(section[TAKT_RUNTIME_PROFILES_KEY]) || hasEntries(section[TAKT_RUNTIME_AUTO_ROUTING_KEY]) || hasTargetContent(section[TAKT_RUNTIME_TARGETS_KEY]) || hasAssignments;
51985
52492
  }
51986
52493
  /** The provider named by the runtime profile Takt would use by default. */
51987
52494
  function resolveRuntimeProvider(runtime) {
@@ -52004,10 +52511,13 @@ function resolveRuntimeProvider(runtime) {
52004
52511
  * shape the `takt` override uses. Each profile's options belong to that
52005
52512
  * profile's own provider; when several profiles name the same provider they are
52006
52513
  * merged in document order, so a later profile wins on a colliding option key.
52514
+ * Profiles Takt itself ignores (companion-only ones while companions are
52515
+ * disabled) are skipped, as they never reach an agent.
52007
52516
  */
52008
52517
  function collectRuntimeProviderOptions(runtime) {
52009
52518
  const collected = {};
52010
- for (const profile of Object.values(runtimeProfiles(runtime))) {
52519
+ const profiles = effectiveRuntimeProviderSection(runtime)?.[TAKT_RUNTIME_PROFILES_KEY];
52520
+ for (const profile of Object.values(isPlainObject$1(profiles) ? profiles : {})) {
52011
52521
  if (!isPlainObject$1(profile)) continue;
52012
52522
  const providerName = profile[TAKT_RUNTIME_PROVIDER_KEY];
52013
52523
  const options = profile[TAKT_RUNTIME_OPTIONS_KEY];
@@ -52046,13 +52556,20 @@ async function readTaktRuntimeFile({ filePath, logger }) {
52046
52556
  };
52047
52557
  }
52048
52558
  }
52559
+ /** A map-valued key of a `provider:` section, or `undefined` when it is not a map. */
52560
+ function runtimeSectionMap(section, key) {
52561
+ return isPlainObject$1(section[key]) ? section[key] : void 0;
52562
+ }
52049
52563
  /**
52050
52564
  * Collapse the project and global `runtime.yaml` into the single document Takt
52051
- * itself resolves against, matching upstream's loader: `profiles` is a union
52052
- * with the project definition of a same-named profile replacing the global one,
52053
- * while `defaults`, `targets` and `auto_routing` are taken from the project file
52054
- * whole whenever it states them at all (`??`, so a project `targets: {}` masks
52055
- * the global one rather than merging with it).
52565
+ * itself resolves against, matching upstream's loader: `profiles` and
52566
+ * `assignments` are unions with the project definition of a same-named entry
52567
+ * replacing the global one, while `defaults`, `targets` and `auto_routing` are
52568
+ * taken from the project file whole whenever it states them at all (`??`, so a
52569
+ * project `targets: {}` masks the global one rather than merging with it). The
52570
+ * top-level `companion.enabled` — which decides whether companion routing
52571
+ * counts for mode detection — is carried as upstream carries it: unset when
52572
+ * neither file states it, otherwise on only when neither file turns it off.
52056
52573
  * https://github.com/nrslib/takt/blob/main/src/infra/config/runtime-provider/loader.ts
52057
52574
  *
52058
52575
  * Merging before mode detection matters in both directions: a project file
@@ -52068,14 +52585,15 @@ function mergeTaktRuntimeConfigs({ project, global: globalConfig }) {
52068
52585
  const projectSection = sectionOf(project);
52069
52586
  const globalSection = sectionOf(globalConfig);
52070
52587
  const replaced = (key) => projectSection[key] ?? globalSection[key];
52071
- const profilesOf = (section) => isPlainObject$1(section[TAKT_RUNTIME_PROFILES_KEY]) ? section[TAKT_RUNTIME_PROFILES_KEY] : void 0;
52072
- const globalProfiles = profilesOf(globalSection);
52073
- const projectProfiles = profilesOf(projectSection);
52074
52588
  const provider = {};
52075
- if (globalProfiles !== void 0 || projectProfiles !== void 0) provider[TAKT_RUNTIME_PROFILES_KEY] = {
52076
- ...globalProfiles,
52077
- ...projectProfiles
52078
- };
52589
+ for (const key of [TAKT_RUNTIME_PROFILES_KEY, TAKT_RUNTIME_ASSIGNMENTS_KEY]) {
52590
+ const globalMap = runtimeSectionMap(globalSection, key);
52591
+ const projectMap = runtimeSectionMap(projectSection, key);
52592
+ if (globalMap !== void 0 || projectMap !== void 0) provider[key] = {
52593
+ ...globalMap,
52594
+ ...projectMap
52595
+ };
52596
+ }
52079
52597
  for (const key of [
52080
52598
  TAKT_RUNTIME_DEFAULTS_KEY,
52081
52599
  TAKT_RUNTIME_TARGETS_KEY,
@@ -52084,7 +52602,17 @@ function mergeTaktRuntimeConfigs({ project, global: globalConfig }) {
52084
52602
  const value = replaced(key);
52085
52603
  if (value !== void 0) provider[key] = value;
52086
52604
  }
52087
- return { [TAKT_RUNTIME_PROVIDER_KEY]: provider };
52605
+ const enabledOf = (config) => {
52606
+ const companion = config?.[TAKT_RUNTIME_COMPANION_KEY];
52607
+ return isPlainObject$1(companion) ? companion[TAKT_RUNTIME_ENABLED_KEY] : void 0;
52608
+ };
52609
+ const globalEnabled = enabledOf(globalConfig);
52610
+ const projectEnabled = enabledOf(project);
52611
+ const companion = globalEnabled === void 0 && projectEnabled === void 0 ? void 0 : { [TAKT_RUNTIME_ENABLED_KEY]: globalEnabled !== false && projectEnabled !== false };
52612
+ return {
52613
+ [TAKT_RUNTIME_PROVIDER_KEY]: provider,
52614
+ ...companion === void 0 ? {} : { [TAKT_RUNTIME_COMPANION_KEY]: companion }
52615
+ };
52088
52616
  }
52089
52617
  /**
52090
52618
  * The runtime provider configuration in force for a generate run: the project
@@ -59753,7 +60281,8 @@ const GrokcliSkillFrontmatterSchema = zod_mini.z.looseObject({
59753
60281
  name: zod_mini.z.string(),
59754
60282
  description: zod_mini.z.string(),
59755
60283
  "user-invocable": zod_mini.z.optional(zod_mini.z.boolean()),
59756
- "disable-model-invocation": zod_mini.z.optional(zod_mini.z.boolean())
60284
+ "disable-model-invocation": zod_mini.z.optional(zod_mini.z.boolean()),
60285
+ paths: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())]))
59757
60286
  });
59758
60287
  /**
59759
60288
  * Represents a Grok Build skill directory.
@@ -59812,7 +60341,8 @@ var GrokcliSkill = class GrokcliSkill extends ToolSkill {
59812
60341
  const frontmatter = this.getFrontmatter();
59813
60342
  const grokcliSection = {
59814
60343
  ...frontmatter["user-invocable"] !== void 0 && { "user-invocable": frontmatter["user-invocable"] },
59815
- ...frontmatter["disable-model-invocation"] !== void 0 && { "disable-model-invocation": frontmatter["disable-model-invocation"] }
60344
+ ...frontmatter["disable-model-invocation"] !== void 0 && { "disable-model-invocation": frontmatter["disable-model-invocation"] },
60345
+ ...frontmatter.paths !== void 0 && { paths: frontmatter.paths }
59816
60346
  };
59817
60347
  const rulesyncFrontmatter = {
59818
60348
  name: frontmatter.name,
@@ -59847,7 +60377,8 @@ var GrokcliSkill = class GrokcliSkill extends ToolSkill {
59847
60377
  name: rulesyncFrontmatter.name,
59848
60378
  description: rulesyncFrontmatter.description,
59849
60379
  ...resolvedUserInvocable !== void 0 && { "user-invocable": resolvedUserInvocable },
59850
- ...resolvedDisableModelInvocation !== void 0 && { "disable-model-invocation": resolvedDisableModelInvocation }
60380
+ ...resolvedDisableModelInvocation !== void 0 && { "disable-model-invocation": resolvedDisableModelInvocation },
60381
+ ...grokcliSection?.paths !== void 0 && { paths: grokcliSection.paths }
59851
60382
  };
59852
60383
  return new GrokcliSkill({
59853
60384
  outputRoot,
@@ -62498,7 +63029,10 @@ var WarpSkill = class WarpSkill extends ToolSkill {
62498
63029
  }
62499
63030
  }
62500
63031
  static getSettablePaths({ global: _global = false } = {}) {
62501
- return { relativeDirPath: WARP_SKILLS_DIR_PATH };
63032
+ return {
63033
+ relativeDirPath: WARP_SKILLS_DIR_PATH,
63034
+ importOnlySkillRoots: [AGENTSMD_SKILLS_DIR_PATH]
63035
+ };
62502
63036
  }
62503
63037
  getFrontmatter() {
62504
63038
  return WarpSkillFrontmatterSchema.parse(this.requireMainFileFrontmatter());
@@ -62562,12 +63096,14 @@ var WarpSkill = class WarpSkill extends ToolSkill {
62562
63096
  return targets.includes("*") || targets.includes("warp");
62563
63097
  }
62564
63098
  /**
62565
- * Commands are emitted into this same skills tree as `<slug>/SKILL.md`
62566
- * (see `WarpCommand`), so a directory matching a current rulesync command
62567
- * slug is owned by the commands feature: it must not be imported as a
62568
- * skill nor deleted as an orphan skill.
63099
+ * Commands are emitted into the `.warp/skills/` tree as `<slug>/SKILL.md`
63100
+ * (see `WarpCommand`), so a directory there matching a current rulesync
63101
+ * command slug is owned by the commands feature: it must not be imported as
63102
+ * a skill nor deleted as an orphan skill. The import-only `.agents/skills/`
63103
+ * root never receives commands, so a same-named skill in it stays a skill.
62569
63104
  */
62570
- static async isDirOwned({ dirName, inputRoots }) {
63105
+ static async isDirOwned({ relativeDirPath, dirName, inputRoots }) {
63106
+ if (relativeDirPath !== WARP_SKILLS_DIR_PATH) return true;
62571
63107
  return !await rulesyncCommandSlugExists({
62572
63108
  inputRoots,
62573
63109
  dirName
@@ -62780,6 +63316,25 @@ const ZedSkillFrontmatterSchema = zod_mini.z.looseObject({
62780
63316
  description: zod_mini.z.string(),
62781
63317
  "disable-model-invocation": zod_mini.z.optional(zod_mini.z.boolean())
62782
63318
  });
63319
+ const ZED = "Zed";
63320
+ const ZED_LOAD_FAILURE = "does not load the skill otherwise";
63321
+ /**
63322
+ * Collect the rules Zed enforces on a skill's frontmatter that the loose
63323
+ * schema above does not — `name` rules first, then `description`. Returned as
63324
+ * warnings rather than thrown: the canonical skill is shared with every other
63325
+ * target, so a name Zed rejects must not stop the generate run for the rest.
63326
+ */
63327
+ function collectZedSkillViolations({ name, description }) {
63328
+ const violations = [];
63329
+ if (name.length === 0) violations.push("`name` must not be empty; Zed does not load a skill without one");
63330
+ else violations.push(...collectSkillNameViolations({
63331
+ name,
63332
+ authority: ZED,
63333
+ consequence: ZED_LOAD_FAILURE
63334
+ }));
63335
+ if (description.length > 1024) violations.push(`\`description\` is ${description.length} characters; ${ZED} loads the skill but warns past ${SKILL_DESCRIPTION_MAX_LENGTH}`);
63336
+ return violations;
63337
+ }
62783
63338
  /**
62784
63339
  * Represents a Zed agent skill directory.
62785
63340
  * Skills are stored under .agents/skills/ (project) or ~/.agents/skills/ (global)
@@ -62848,9 +63403,10 @@ var ZedSkill = class ZedSkill extends ToolSkill {
62848
63403
  global: this.global
62849
63404
  });
62850
63405
  }
62851
- static fromRulesyncSkill({ outputRoot = process.cwd(), rulesyncSkill, validate = true, global = false }) {
63406
+ static fromRulesyncSkill({ outputRoot = process.cwd(), rulesyncSkill, validate = true, global = false, logger }) {
62852
63407
  const settablePaths = ZedSkill.getSettablePaths({ global });
62853
63408
  const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
63409
+ const dirName = rulesyncSkill.getDirName();
62854
63410
  const zedSection = rulesyncFrontmatter.zed;
62855
63411
  const resolvedDisableModelInvocation = resolveDisableModelInvocation({
62856
63412
  rootFrontmatter: rulesyncFrontmatter,
@@ -62862,10 +63418,17 @@ var ZedSkill = class ZedSkill extends ToolSkill {
62862
63418
  ...zedSection,
62863
63419
  ...resolvedDisableModelInvocation !== void 0 && { "disable-model-invocation": resolvedDisableModelInvocation }
62864
63420
  };
63421
+ ZedSkill.reportSkillViolations({
63422
+ outputRoot,
63423
+ relativeDirPath: settablePaths.relativeDirPath,
63424
+ dirName,
63425
+ frontmatter: zedFrontmatter,
63426
+ logger
63427
+ });
62865
63428
  return new ZedSkill({
62866
63429
  outputRoot,
62867
63430
  relativeDirPath: settablePaths.relativeDirPath,
62868
- dirName: rulesyncSkill.getDirName(),
63431
+ dirName,
62869
63432
  frontmatter: zedFrontmatter,
62870
63433
  body: rulesyncSkill.getBody(),
62871
63434
  otherFiles: rulesyncSkill.getOtherFiles(),
@@ -62873,6 +63436,18 @@ var ZedSkill = class ZedSkill extends ToolSkill {
62873
63436
  global
62874
63437
  });
62875
63438
  }
63439
+ /**
63440
+ * Warn about every rule Zed enforces on the skill about to be written. The
63441
+ * reported path includes `outputRoot` so a global-scope skill points at the
63442
+ * file under the home directory rather than a same-named project path.
63443
+ */
63444
+ static reportSkillViolations({ outputRoot, relativeDirPath, dirName, frontmatter, logger }) {
63445
+ warnSkillViolations({
63446
+ skillPath: (0, node_path.join)(outputRoot, relativeDirPath, dirName, SKILL_FILE_NAME),
63447
+ violations: collectZedSkillViolations(frontmatter),
63448
+ logger
63449
+ });
63450
+ }
62876
63451
  static isTargetedByRulesyncSkill(rulesyncSkill) {
62877
63452
  const targets = rulesyncSkill.getFrontmatter().targets;
62878
63453
  return targets.includes("*") || targets.includes("zed");