rulesync 16.29.0 → 16.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -444,34 +444,34 @@ const isFeatureValueEnabled = (value) => {
444
444
  const parseCommaSeparatedList = (value) => value.split(",").map((s) => s.trim()).filter(Boolean);
445
445
  //#endregion
446
446
  //#region src/constants/rulesync-paths.ts
447
- const { join: join$307 } = node_path.posix;
447
+ const { join: join$310 } = node_path.posix;
448
448
  const RULESYNC_CONFIG_RELATIVE_FILE_PATH = "rulesync.jsonc";
449
449
  const RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH = "rulesync.local.jsonc";
450
450
  const RULESYNC_RELATIVE_DIR_PATH = ".rulesync";
451
451
  const RULES_FEATURE_SUBDIR = "rules";
452
- const CURATED_RULES_FEATURE_SUBDIR = join$307(RULES_FEATURE_SUBDIR, ".curated");
452
+ const CURATED_RULES_FEATURE_SUBDIR = join$310(RULES_FEATURE_SUBDIR, ".curated");
453
453
  const COMMANDS_FEATURE_SUBDIR = "commands";
454
454
  const SUBAGENTS_FEATURE_SUBDIR = "subagents";
455
455
  const CHECKS_FEATURE_SUBDIR = "checks";
456
456
  const SKILLS_FEATURE_SUBDIR = "skills";
457
- const CURATED_SKILLS_FEATURE_SUBDIR = join$307(SKILLS_FEATURE_SUBDIR, ".curated");
458
- const RULESYNC_RULES_RELATIVE_DIR_PATH = join$307(RULESYNC_RELATIVE_DIR_PATH, RULES_FEATURE_SUBDIR);
459
- const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$307(RULESYNC_RELATIVE_DIR_PATH, CURATED_RULES_FEATURE_SUBDIR);
460
- const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$307(RULESYNC_RELATIVE_DIR_PATH, COMMANDS_FEATURE_SUBDIR);
461
- const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$307(RULESYNC_RELATIVE_DIR_PATH, SUBAGENTS_FEATURE_SUBDIR);
462
- const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$307(RULESYNC_RELATIVE_DIR_PATH, CHECKS_FEATURE_SUBDIR);
463
- const RULESYNC_MCP_RELATIVE_FILE_PATH = join$307(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
464
- const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$307(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
465
- const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$307(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
466
- join$307(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
467
- const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$307(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
468
- const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$307(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
457
+ const CURATED_SKILLS_FEATURE_SUBDIR = join$310(SKILLS_FEATURE_SUBDIR, ".curated");
458
+ const RULESYNC_RULES_RELATIVE_DIR_PATH = join$310(RULESYNC_RELATIVE_DIR_PATH, RULES_FEATURE_SUBDIR);
459
+ const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$310(RULESYNC_RELATIVE_DIR_PATH, CURATED_RULES_FEATURE_SUBDIR);
460
+ const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$310(RULESYNC_RELATIVE_DIR_PATH, COMMANDS_FEATURE_SUBDIR);
461
+ const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$310(RULESYNC_RELATIVE_DIR_PATH, SUBAGENTS_FEATURE_SUBDIR);
462
+ const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$310(RULESYNC_RELATIVE_DIR_PATH, CHECKS_FEATURE_SUBDIR);
463
+ const RULESYNC_MCP_RELATIVE_FILE_PATH = join$310(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
464
+ const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$310(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
465
+ const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$310(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
466
+ join$310(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
467
+ const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$310(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
468
+ const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$310(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
469
469
  const RULESYNC_AIIGNORE_FILE_NAME = ".aiignore";
470
- const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$307(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
470
+ const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$310(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
471
471
  const RULESYNC_IGNORE_RELATIVE_FILE_PATH = ".rulesyncignore";
472
472
  const RULESYNC_OVERVIEW_FILE_NAME = "overview.md";
473
- const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$307(RULESYNC_RELATIVE_DIR_PATH, SKILLS_FEATURE_SUBDIR);
474
- const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$307(RULESYNC_RELATIVE_DIR_PATH, CURATED_SKILLS_FEATURE_SUBDIR);
473
+ const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$310(RULESYNC_RELATIVE_DIR_PATH, SKILLS_FEATURE_SUBDIR);
474
+ const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$310(RULESYNC_RELATIVE_DIR_PATH, CURATED_SKILLS_FEATURE_SUBDIR);
475
475
  const RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync.lock";
476
476
  const RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync-npm.lock.json";
477
477
  const RULESYNC_MCP_FILE_NAME = "mcp.jsonc";
@@ -530,7 +530,8 @@ const rulesProcessorToolTargetTuple = [
530
530
  "zcode",
531
531
  "zed",
532
532
  "zoocode",
533
- "pool"
533
+ "pool",
534
+ "dsh"
534
535
  ];
535
536
  const ignoreProcessorToolTargetTuple = [
536
537
  "aiassistant",
@@ -708,7 +709,8 @@ const skillsProcessorToolTargetTuple = [
708
709
  "devin",
709
710
  "zcode",
710
711
  "zed",
711
- "zoocode"
712
+ "zoocode",
713
+ "dsh"
712
714
  ];
713
715
  const hooksProcessorToolTargetTuple = [
714
716
  "amp",
@@ -4236,10 +4238,12 @@ const PI_HOOK_EVENTS = [
4236
4238
  "sessionEnd",
4237
4239
  "preToolUse",
4238
4240
  "postToolUse",
4241
+ "postToolUseFailure",
4239
4242
  "preModelInvocation",
4240
4243
  "postModelInvocation",
4241
4244
  "beforeSubmitPrompt",
4242
4245
  "stop",
4246
+ "notification",
4243
4247
  "preCompact",
4244
4248
  "postCompact"
4245
4249
  ];
@@ -5111,7 +5115,7 @@ const CANONICAL_TO_KILO_EVENT_NAMES = CANONICAL_TO_OPENCODE_EVENT_NAMES;
5111
5115
  * Stop, this also fires before Pi auto-retries or auto-compacts —
5112
5116
  * `agent_settled` would skip queued follow-ups instead, a pure trade-off).
5113
5117
  * Pi events without a faithful canonical counterpart (e.g. `turn_start`,
5114
- * `agent_settled`) are intentionally unmapped.
5118
+ * `agent_settled`, `ui_prompt_end`) are intentionally unmapped.
5115
5119
  *
5116
5120
  * @see https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/extensions.md
5117
5121
  */
@@ -5120,10 +5124,12 @@ const CANONICAL_TO_PI_EVENT_NAMES = {
5120
5124
  sessionEnd: "session_shutdown",
5121
5125
  preToolUse: "tool_call",
5122
5126
  postToolUse: "tool_result",
5127
+ postToolUseFailure: "tool_result",
5123
5128
  preModelInvocation: "context",
5124
5129
  postModelInvocation: "message_end",
5125
5130
  beforeSubmitPrompt: "input",
5126
5131
  stop: "agent_end",
5132
+ notification: "ui_prompt_start",
5127
5133
  preCompact: "session_before_compact",
5128
5134
  postCompact: "session_compact"
5129
5135
  };
@@ -8007,7 +8013,7 @@ const RulesyncRuleFrontmatterSchema = zod_mini.z.object({
8007
8013
  extends: zod_mini.z.optional(zod_mini.z.string()),
8008
8014
  facet: zod_mini.z.optional(zod_mini.z.enum(["policies", "output-contracts"]))
8009
8015
  })),
8010
- factorydroid: zod_mini.z.optional(zod_mini.z.looseObject({ channel: zod_mini.z.optional(zod_mini.z.enum(["design"])) }))
8016
+ factorydroid: zod_mini.z.optional(zod_mini.z.looseObject({ channel: zod_mini.z.optional(zod_mini.z.enum(["design", "threat-model"])) }))
8011
8017
  });
8012
8018
  /**
8013
8019
  * The `agentsmd.subprojectPath` every consumer should act on, resolved once so
@@ -9221,6 +9227,12 @@ const RulesyncSkillFrontmatterSchema = zod_mini.z.looseObject({
9221
9227
  "disable-model-invocation": zod_mini.z.optional(zod_mini.z.boolean()),
9222
9228
  "user-invocable": zod_mini.z.optional(zod_mini.z.boolean())
9223
9229
  })),
9230
+ dsh: zod_mini.z.optional(zod_mini.z.looseObject({
9231
+ whenToUse: zod_mini.z.optional(zod_mini.z.string()),
9232
+ metadata: zod_mini.z.optional(zod_mini.z.looseObject({})),
9233
+ "disable-model-invocation": zod_mini.z.optional(zod_mini.z.boolean()),
9234
+ "user-invocable": zod_mini.z.optional(zod_mini.z.boolean())
9235
+ })),
9224
9236
  "kimi-code": zod_mini.z.optional(zod_mini.z.looseObject({
9225
9237
  type: zod_mini.z.optional(zod_mini.z.enum([
9226
9238
  "prompt",
@@ -9485,7 +9497,7 @@ async function getLocalSkillDirNames(sourceTree) {
9485
9497
  *
9486
9498
  * The rulesync skill frontmatter exposes a root-level `disable-model-invocation`
9487
9499
  * default that applies to every tool supporting the flag (claudecode, copilot,
9488
- * copilotcli, crush, cursor, zed, pi, qwencode, grokcli, factorydroid). Each tool's own section may override that
9500
+ * copilotcli, crush, cursor, zed, pi, qwencode, grokcli, factorydroid, dsh). Each tool's own section may override that
9489
9501
  * default with a per-target value. A defined section value (including `false`)
9490
9502
  * always wins over the root default.
9491
9503
  *
@@ -9504,7 +9516,7 @@ function resolveDisableModelInvocation({ rootFrontmatter, section }) {
9504
9516
  *
9505
9517
  * The rulesync skill frontmatter exposes a root-level `user-invocable` default
9506
9518
  * that applies to every tool supporting the flag (claudecode, copilot,
9507
- * copilotcli, crush, cursor, qwencode, vibe, grokcli, factorydroid). Each tool's own section may override that default with a
9519
+ * copilotcli, crush, cursor, qwencode, vibe, grokcli, factorydroid, dsh). Each tool's own section may override that default with a
9508
9520
  * per-target value. A defined section value (including `false`) always wins
9509
9521
  * over the root default.
9510
9522
  *
@@ -11106,6 +11118,13 @@ const FACTORYDROID_RULE_FILE_NAME = "AGENTS.md";
11106
11118
  * @see https://docs.factory.ai/cli/configuration/agents-md
11107
11119
  */
11108
11120
  const FACTORYDROID_DESIGN_FILE_NAME = "DESIGN.md";
11121
+ /**
11122
+ * Factory's Security Review threat model, kept inside the tool directory:
11123
+ * "if `.factory/threat-model.md` exists, Droid uses it as the attack-surface
11124
+ * map". Documented only as a repository file, so project scope only.
11125
+ * @see https://docs.factory.ai/software-factory/security-review
11126
+ */
11127
+ const FACTORYDROID_THREAT_MODEL_FILE_NAME = "threat-model.md";
11109
11128
  const FACTORYDROID_MCP_FILE_NAME = "mcp.json";
11110
11129
  const FACTORYDROID_SETTINGS_FILE_NAME = "settings.json";
11111
11130
  const FACTORYDROID_HOOKS_FILE_NAME = "hooks.json";
@@ -11747,7 +11766,10 @@ function parseSharedConfig({ format, fileContent, filePath, invalidRootPolicy =
11747
11766
  throw new Error(`Failed to parse shared config${at}: ${formatError(error)}`, { cause: error });
11748
11767
  }
11749
11768
  if (!isPlainObject$1(sanitized)) {
11750
- if (invalidRootPolicy === "error") throw new Error(`Failed to parse shared config${at}: expected a mapping at the root`);
11769
+ if (invalidRootPolicy === "error") {
11770
+ const reason = /* @__PURE__ */ new Error("expected a mapping at the root");
11771
+ throw new Error(`Failed to parse shared config${at}: ${reason.message}`, { cause: reason });
11772
+ }
11751
11773
  return {};
11752
11774
  }
11753
11775
  return sanitized;
@@ -12749,7 +12771,9 @@ const SHARED_CONFIG_OWNERSHIP = {
12749
12771
  }
12750
12772
  },
12751
12773
  ".augment/settings.json": {
12752
- format: "json",
12774
+ format: "jsonc",
12775
+ invalidRootPolicy: "error",
12776
+ jsoncParseErrors: "error",
12753
12777
  features: {
12754
12778
  mcp: {
12755
12779
  kind: "replace-owned-keys",
@@ -20607,14 +20631,14 @@ var AntigravityPluginHooks = class extends AntigravityIdeHooks {
20607
20631
  * Returns `null` when neither file exists and no `baseFallbackContent` is
20608
20632
  * given, so callers can tell "no settings at this scope" apart from empty ones.
20609
20633
  */
20610
- async function readSettingsWithLocalOverlay({ outputRoot, relativeDirPath, baseFileName, localFileName, toolLabel, baseFallbackContent, sensitiveKeys = [], quiet = false, merge, logger }) {
20634
+ async function readSettingsWithLocalOverlay({ outputRoot, relativeDirPath, baseFileName, localFileName, toolLabel, baseFallbackContent, sensitiveKeys = [], quiet = false, parse = JSON.parse, merge, logger }) {
20611
20635
  const baseContent = await readFileContentOrNull((0, node_path.join)(outputRoot, relativeDirPath, baseFileName)) ?? baseFallbackContent ?? null;
20612
20636
  const localContent = await readFileContentOrNull((0, node_path.join)(outputRoot, relativeDirPath, localFileName));
20613
20637
  if (localContent === null) return baseContent;
20614
20638
  const configPath = (0, node_path.join)(relativeDirPath, localFileName);
20615
20639
  let localParsed;
20616
20640
  try {
20617
- localParsed = JSON.parse(localContent);
20641
+ localParsed = parse(localContent);
20618
20642
  } catch (error) {
20619
20643
  throw new Error(`Failed to parse ${toolLabel} settings at ${configPath}: ${formatError(error)}`, { cause: error });
20620
20644
  }
@@ -20623,7 +20647,7 @@ async function readSettingsWithLocalOverlay({ outputRoot, relativeDirPath, baseF
20623
20647
  if (baseContent !== null) {
20624
20648
  let parsed;
20625
20649
  try {
20626
- parsed = JSON.parse(baseContent);
20650
+ parsed = parse(baseContent);
20627
20651
  } catch {
20628
20652
  return baseContent;
20629
20653
  }
@@ -20682,6 +20706,51 @@ function warnAboutLocalKeys({ localParsed, configPath, toolLabel, sensitiveKeys,
20682
20706
  //#endregion
20683
20707
  //#region src/utils/augmentcode-settings.ts
20684
20708
  /**
20709
+ * The one place that spells out how an AugmentCode settings file is parsed.
20710
+ *
20711
+ * Auggie reads `settings.json` / `settings.local.json` as JSON with Comments —
20712
+ * "The files support JSON with Comments (JSONC), allowing comments and trailing
20713
+ * commas for better documentation." (https://docs.augmentcode.com/cli/config) —
20714
+ * so a bare `JSON.parse` rejects a hand-written file the CLI itself accepts. The
20715
+ * parse is fail-closed: a syntax error or a non-object root throws instead of
20716
+ * yielding a partial document, because every caller either merges its own keys
20717
+ * back into this file or imports permissions from it, and neither may proceed
20718
+ * on a file it could not read in full. An empty file parses as `{}`.
20719
+ *
20720
+ * The generate direction registers the same file as `jsonc` in
20721
+ * `SHARED_CONFIG_OWNERSHIP`, so the in-place patch reads it the same way.
20722
+ *
20723
+ * Throws the bare reason (the parser's own error), without naming the file;
20724
+ * `parseAugmentcodeSettingsDocument` adds that, and so does
20725
+ * `readSettingsWithLocalOverlay` when this is handed over as its `parse`.
20726
+ */
20727
+ function parseAugmentcodeSettingsContent(fileContent) {
20728
+ try {
20729
+ return parseSharedConfig({
20730
+ format: "jsonc",
20731
+ fileContent,
20732
+ invalidRootPolicy: "error",
20733
+ jsoncParseErrors: "error"
20734
+ });
20735
+ } catch (error) {
20736
+ if (error instanceof Error && error.cause !== void 0) throw error.cause;
20737
+ throw error;
20738
+ }
20739
+ }
20740
+ /**
20741
+ * Parse an AugmentCode settings file (`settings.json` / `settings.local.json`)
20742
+ * into a plain object, naming `configPath` in the error when it cannot be read.
20743
+ * See `parseAugmentcodeSettingsContent` for the format and the fail-closed
20744
+ * policy.
20745
+ */
20746
+ function parseAugmentcodeSettingsDocument({ fileContent, configPath }) {
20747
+ try {
20748
+ return parseAugmentcodeSettingsContent(fileContent);
20749
+ } catch (error) {
20750
+ throw new Error(`Failed to parse AugmentCode settings at ${configPath}: ${formatError(error)}`, { cause: error });
20751
+ }
20752
+ }
20753
+ /**
20685
20754
  * Top-level keys AugmentCode *replaces* (higher-precedence wins wholesale)
20686
20755
  * rather than combining across tiers. Everything else combines.
20687
20756
  *
@@ -20756,6 +20825,7 @@ async function readAugmentcodeSettingsWithLocalOverlay({ outputRoot, relativeDir
20756
20825
  localFileName: "settings.local.json",
20757
20826
  toolLabel: "AugmentCode",
20758
20827
  sensitiveKeys: AUGMENTCODE_GUARDRAIL_KEYS,
20828
+ parse: parseAugmentcodeSettingsContent,
20759
20829
  baseFallbackContent,
20760
20830
  merge: combineAugmentSettings,
20761
20831
  logger
@@ -20835,8 +20905,10 @@ var AugmentcodeHooks = class AugmentcodeHooks extends ToolHooks {
20835
20905
  const existingContent = await readFileContentOrNull(filePath) ?? JSON.stringify({}, null, 2);
20836
20906
  let existingHooks = {};
20837
20907
  try {
20838
- const parsed = JSON.parse(existingContent);
20839
- const candidate = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed.hooks : void 0;
20908
+ const candidate = parseAugmentcodeSettingsDocument({
20909
+ fileContent: existingContent,
20910
+ configPath: (0, node_path.join)(paths.relativeDirPath, paths.relativeFilePath)
20911
+ }).hooks;
20840
20912
  if (candidate && typeof candidate === "object" && !Array.isArray(candidate)) existingHooks = candidate;
20841
20913
  } catch {
20842
20914
  existingHooks = {};
@@ -20858,7 +20930,8 @@ var AugmentcodeHooks = class AugmentcodeHooks extends ToolHooks {
20858
20930
  ...preservedHooks,
20859
20931
  ...augmentHooks
20860
20932
  } },
20861
- filePath
20933
+ filePath,
20934
+ logger
20862
20935
  });
20863
20936
  return new AugmentcodeHooks({
20864
20937
  outputRoot,
@@ -20871,7 +20944,10 @@ var AugmentcodeHooks = class AugmentcodeHooks extends ToolHooks {
20871
20944
  toRulesyncHooks({ logger } = {}) {
20872
20945
  let settings;
20873
20946
  try {
20874
- settings = JSON.parse(this.getFileContent());
20947
+ settings = parseAugmentcodeSettingsDocument({
20948
+ fileContent: this.getFileContent(),
20949
+ configPath: (0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())
20950
+ });
20875
20951
  } catch (error) {
20876
20952
  throw new Error(`Failed to parse AugmentCode hooks content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
20877
20953
  }
@@ -24920,6 +24996,15 @@ const PI_TOOL_EVENTS = /* @__PURE__ */ new Set(["tool_call", "tool_result"]);
24920
24996
  */
24921
24997
  const PI_ASSISTANT_MESSAGE_EVENTS = /* @__PURE__ */ new Set(["message_end"]);
24922
24998
  /**
24999
+ * Canonical events whose Pi event also fires when nothing went wrong. Pi's
25000
+ * `tool_result` fires for every finished tool call and exposes `event.isError`,
25001
+ * so a `postToolUseFailure` handler is gated on that flag while a `postToolUse`
25002
+ * handler on the same Pi event runs regardless.
25003
+ *
25004
+ * @see https://github.com/earendil-works/pi/blob/v0.85.1/packages/coding-agent/docs/extensions.md#tool_result
25005
+ */
25006
+ const PI_ERROR_GATED_CANONICAL_EVENTS = /* @__PURE__ */ new Set(["postToolUseFailure"]);
25007
+ /**
24923
25008
  * `tool_call` is Pi's tool gate. Its return contract is
24924
25009
  * `{ block: true, reason?: string, terminate?: boolean }`.
24925
25010
  *
@@ -25065,7 +25150,8 @@ function collectPiHandlers({ effectiveHooks, eventMap }) {
25065
25150
  if (!def.command) continue;
25066
25151
  handlers.push({
25067
25152
  command: def.command,
25068
- matcher: def.matcher ? def.matcher : void 0
25153
+ matcher: def.matcher ? def.matcher : void 0,
25154
+ onlyOnError: PI_ERROR_GATED_CANONICAL_EVENTS.has(canonicalEvent)
25069
25155
  });
25070
25156
  }
25071
25157
  if (handlers.length > 0) {
@@ -25078,10 +25164,13 @@ function collectPiHandlers({ effectiveHooks, eventMap }) {
25078
25164
  }
25079
25165
  function buildCommandLines({ handler, usesToolName, blocking }) {
25080
25166
  const lines = [];
25081
- const gated = usesToolName && Boolean(handler.matcher);
25167
+ const conditions = [];
25168
+ if (handler.onlyOnError) conditions.push("event.isError");
25169
+ if (usesToolName && handler.matcher) conditions.push(`new RegExp(${matcherToEmbeddedLiteral(handler.matcher)}).test(event.toolName)`);
25170
+ const gated = conditions.length > 0;
25082
25171
  const indent = gated ? " " : " ";
25083
25172
  const embeddedCommand = JSON.stringify(handler.command);
25084
- if (gated && handler.matcher) lines.push(` if (new RegExp(${matcherToEmbeddedLiteral(handler.matcher)}).test(event.toolName)) {`);
25173
+ if (gated) lines.push(` if (${conditions.join(" && ")}) {`);
25085
25174
  const onFailure = FAILURE_LINES_BY_MODE[blocking];
25086
25175
  if (onFailure.length > 0) {
25087
25176
  lines.push(`${indent}try {`);
@@ -25099,8 +25188,9 @@ function buildSubscriptionLines(handlerGroups) {
25099
25188
  const blocking = PI_BLOCKING_MODE_BY_EVENT[piEvent] ?? "none";
25100
25189
  const isPromptGate = blocking === "prompt";
25101
25190
  const usesToolName = PI_TOOL_EVENTS.has(piEvent) && handlers.some((h) => h.matcher);
25191
+ const usesErrorFlag = handlers.some((h) => h.onlyOnError);
25102
25192
  const gatesOnAssistant = PI_ASSISTANT_MESSAGE_EVENTS.has(piEvent);
25103
- const params = isPromptGate ? "event, ctx" : usesToolName || gatesOnAssistant || isPromptGate ? "event" : "";
25193
+ const params = isPromptGate ? "event, ctx" : usesToolName || usesErrorFlag || gatesOnAssistant || isPromptGate ? "event" : "";
25104
25194
  lines.push(` pi.on(${JSON.stringify(piEvent)}, async (${params}) => {`);
25105
25195
  if (gatesOnAssistant) lines.push(` if (event.message.role !== "assistant") return;`);
25106
25196
  if (isPromptGate) lines.push(` if (event.source === "extension") return { action: "continue" };`);
@@ -25122,6 +25212,8 @@ function buildSubscriptionLines(handlerGroups) {
25122
25212
  * gate — where a hook command that exits non-zero denies the call with
25123
25213
  * `{ block: true, reason }`, and on `input` — Pi's prompt-submission gate —
25124
25214
  * where a non-zero exit cancels the prompt with `{ action: "handled" }`.
25215
+ * `postToolUse` and `postToolUseFailure` share Pi's `tool_result` event; the
25216
+ * latter's commands run only when `event.isError` is set.
25125
25217
  *
25126
25218
  * @see https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/extensions.md
25127
25219
  */
@@ -25690,12 +25782,29 @@ const VIBE_HOOKS_FILE_NAME = "hooks.toml";
25690
25782
  const VIBE_TOOL_EVENTS = /* @__PURE__ */ new Set(["pre_tool", "post_tool"]);
25691
25783
  const SUPPORTED_VIBE_EVENTS = new Set(VIBE_HOOK_EVENTS);
25692
25784
  /**
25785
+ * Vibe drops a hook whose `command` contains a backslash when it loads
25786
+ * `hooks.toml` (since v2.25.1; `load_hooks_config` at v2.25.3: "Hook '<name>' skipped:
25787
+ * backslash paths are not supported in hook commands. Use forward slashes
25788
+ * instead."), because its exec-based executor tokenizes the command with
25789
+ * `shlex.split`, which eats the backslash. The hook is still written — the
25790
+ * command is the user's to fix — but it is worth a warning, since Vibe itself
25791
+ * only reports the skip as a config issue rather than failing the run.
25792
+ * @see https://github.com/mistralai/mistral-vibe/blob/v2.25.3/vibe/core/hooks/config.py
25793
+ */
25794
+ function warnAboutBackslashCommand({ name, command, logger }) {
25795
+ if (!command.includes("\\")) return;
25796
+ logger?.warn(`the command of hook ${quoteValueForWarning(name)} contains a backslash, which Vibe rejects when it loads hooks.toml — the hook is skipped with "backslash paths are not supported in hook commands". Use forward slashes and avoid backslash escapes so the hook runs.`);
25797
+ }
25798
+ /**
25693
25799
  * Build the flat `[[hooks]]` array for `.vibe/hooks.toml` from a canonical
25694
25800
  * hooks config. Vibe uses a flat array where each entry carries its own event
25695
25801
  * `type`, tool-name `match` glob/regex, and `command`. Only `type: "command"`
25696
- * canonical hooks are emitted (Vibe hooks are always shell commands).
25802
+ * canonical hooks are emitted: a Vibe hook is a command line, which the legacy
25803
+ * backend hands to a shell and the unified harness tokenizes with `shlex.split`
25804
+ * and executes directly (v2.25.1: "Hook commands run without a shell to
25805
+ * prevent injection via hooks.toml").
25697
25806
  */
25698
- function canonicalToVibeHooks(config, toolOverride) {
25807
+ function canonicalToVibeHooks({ config, toolOverride, logger }) {
25699
25808
  const shared = {};
25700
25809
  for (const [event, defs] of Object.entries(config.hooks)) if (SUPPORTED_VIBE_EVENTS.has(event)) shared[event] = defs;
25701
25810
  const effective = {
@@ -25714,6 +25823,11 @@ function canonicalToVibeHooks(config, toolOverride) {
25714
25823
  if ((def.type ?? "command") !== "command") continue;
25715
25824
  if (typeof def.command !== "string") continue;
25716
25825
  const name = typeof def.name === "string" ? def.name : `${vibeEvent}-${index}`;
25826
+ warnAboutBackslashCommand({
25827
+ name,
25828
+ command: def.command,
25829
+ logger
25830
+ });
25717
25831
  const isToolEvent = VIBE_TOOL_EVENTS.has(vibeEvent);
25718
25832
  const entry = {
25719
25833
  name,
@@ -25814,10 +25928,14 @@ var VibeHooks = class VibeHooks extends ToolHooks {
25814
25928
  validate
25815
25929
  });
25816
25930
  }
25817
- static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false }) {
25931
+ static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
25818
25932
  const paths = VibeHooks.getSettablePaths({ global });
25819
25933
  const config = rulesyncHooks.getJson();
25820
- const vibeHooks = canonicalToVibeHooks(config, config.vibe?.hooks);
25934
+ const vibeHooks = canonicalToVibeHooks({
25935
+ config,
25936
+ toolOverride: config.vibe?.hooks,
25937
+ logger
25938
+ });
25821
25939
  const fileContent = smol_toml.stringify(vibeHooks);
25822
25940
  return new VibeHooks({
25823
25941
  outputRoot,
@@ -28750,15 +28868,10 @@ var AntigravityPluginMcp = class extends AntigravityIdeMcp {
28750
28868
  //#endregion
28751
28869
  //#region src/features/mcp/augmentcode-mcp.ts
28752
28870
  function parseAugmentcodeSettings(fileContent, relativeDirPath, relativeFilePath) {
28753
- const configPath = (0, node_path.join)(relativeDirPath, relativeFilePath);
28754
- let parsed;
28755
- try {
28756
- parsed = JSON.parse(fileContent);
28757
- } catch (error) {
28758
- throw new Error(`Failed to parse AugmentCode settings at ${configPath}: ${formatError(error)}`, { cause: error });
28759
- }
28760
- if (!isPlainObject$1(parsed)) throw new Error(`Failed to parse AugmentCode settings at ${configPath}: expected a JSON object`);
28761
- return parsed;
28871
+ return parseAugmentcodeSettingsDocument({
28872
+ fileContent,
28873
+ configPath: (0, node_path.join)(relativeDirPath, relativeFilePath)
28874
+ });
28762
28875
  }
28763
28876
  /**
28764
28877
  * AugmentCode (Auggie CLI) MCP servers.
@@ -28815,7 +28928,7 @@ var AugmentcodeMcp = class AugmentcodeMcp extends ToolMcp {
28815
28928
  global
28816
28929
  });
28817
28930
  }
28818
- static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
28931
+ static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
28819
28932
  const paths = this.getSettablePaths({ global });
28820
28933
  const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
28821
28934
  const existingContent = await readFileContentOrNull(filePath) ?? JSON.stringify({}, null, 2);
@@ -28828,7 +28941,8 @@ var AugmentcodeMcp = class AugmentcodeMcp extends ToolMcp {
28828
28941
  feature: "mcp",
28829
28942
  existingContent,
28830
28943
  patch: { mcpServers: rulesyncMcp.getMcpServers() },
28831
- filePath
28944
+ filePath,
28945
+ logger
28832
28946
  }),
28833
28947
  validate,
28834
28948
  global
@@ -37001,7 +37115,10 @@ var AugmentcodePermissions = class AugmentcodePermissions extends ToolPermission
37001
37115
  const existingContent = await readFileContentOrNull(filePath) ?? "{}";
37002
37116
  let settings;
37003
37117
  try {
37004
- const parsed = JSON.parse(existingContent);
37118
+ const parsed = parseAugmentcodeSettingsDocument({
37119
+ fileContent: existingContent,
37120
+ configPath: (0, node_path.join)(paths.relativeDirPath, paths.relativeFilePath)
37121
+ });
37005
37122
  const result = AugmentSettingsSchema.safeParse(parsed);
37006
37123
  if (!result.success) throw new Error(formatError(result.error));
37007
37124
  settings = result.data;
@@ -37041,7 +37158,8 @@ var AugmentcodePermissions = class AugmentcodePermissions extends ToolPermission
37041
37158
  feature: "permissions",
37042
37159
  existingContent,
37043
37160
  patch: { toolPermissions: [...specialEntries, ...sortedBasic] },
37044
- filePath
37161
+ filePath,
37162
+ logger
37045
37163
  });
37046
37164
  return new AugmentcodePermissions({
37047
37165
  outputRoot,
@@ -37054,7 +37172,10 @@ var AugmentcodePermissions = class AugmentcodePermissions extends ToolPermission
37054
37172
  toRulesyncPermissions() {
37055
37173
  let settings;
37056
37174
  try {
37057
- const parsed = JSON.parse(this.getFileContent());
37175
+ const parsed = parseAugmentcodeSettingsDocument({
37176
+ fileContent: this.getFileContent(),
37177
+ configPath: (0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())
37178
+ });
37058
37179
  const result = AugmentSettingsSchema.safeParse(parsed);
37059
37180
  if (!result.success) throw new Error(formatError(result.error));
37060
37181
  settings = result.data;
@@ -37072,7 +37193,10 @@ var AugmentcodePermissions = class AugmentcodePermissions extends ToolPermission
37072
37193
  }
37073
37194
  validate() {
37074
37195
  try {
37075
- const parsed = JSON.parse(this.fileContent || "{}");
37196
+ const parsed = parseAugmentcodeSettingsDocument({
37197
+ fileContent: this.fileContent || "{}",
37198
+ configPath: (0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())
37199
+ });
37076
37200
  const result = AugmentSettingsSchema.safeParse(parsed);
37077
37201
  if (!result.success) return {
37078
37202
  success: false,
@@ -52287,6 +52411,207 @@ var DevinSkill = class DevinSkill extends ToolSkill {
52287
52411
  }
52288
52412
  };
52289
52413
  //#endregion
52414
+ //#region src/constants/dsh-paths.ts
52415
+ /**
52416
+ * DeepSeek Harness (`dsh`) configuration-layout conventions.
52417
+ *
52418
+ * The harness home is `$DSH_HOME`, which defaults to `~/.dsh`; rulesync writes
52419
+ * only the default location. Project-scoped assets live under a `.dsh/`
52420
+ * directory at the project root.
52421
+ *
52422
+ * Rules: `dsh-agent-instructions` loads the user-global `$DSH_HOME/AGENTS.md`
52423
+ * (root file only — the user-global file has no `.local.md` overlay), then the
52424
+ * project chain of `AGENTS.md` / `CLAUDE.md` files from the project root (the
52425
+ * nearest ancestor holding `.git`) down to the session cwd, broad to specific.
52426
+ * Rulesync emits `AGENTS.md` only; `CLAUDE.md` is the `claudecode` target's
52427
+ * file, and a `CLAUDE.md` duplicating its sibling `AGENTS.md` renders once.
52428
+ *
52429
+ * Skills: `dsh-skill-filesystem` scans `<projectRoot>/.dsh/skills` (rank 100),
52430
+ * `<projectRoot>/.agents/skills` (200), `<dshHome>/skills` (400) and
52431
+ * `<agentsHome>/skills` (500), each holding top-level `<name>/SKILL.md`
52432
+ * bundles or flat `<name>.md` files; nested `SKILL.md` files are deliberately
52433
+ * not discovered. Rulesync writes only the `dsh`-specific roots — the
52434
+ * `.agents/skills` roots are the `agentsskills` target's output.
52435
+ *
52436
+ * @see https://github.com/deepseek-ai/deepseek-harness
52437
+ * @see https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/context/agent-instructions/README.md
52438
+ * @see https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/skill/skill-filesystem/README.md
52439
+ */
52440
+ /**
52441
+ * Project-scoped `.dsh/` directory, and the harness home relative to the home
52442
+ * directory (`$DSH_HOME` default).
52443
+ */
52444
+ const DSH_DIR = ".dsh";
52445
+ /** Skills root, relative to the project root or the harness home. */
52446
+ const DSH_SKILLS_DIR_PATH = (0, node_path.join)(DSH_DIR, "skills");
52447
+ //#endregion
52448
+ //#region src/features/skills/dsh-skill.ts
52449
+ const DshSkillFrontmatterSchema = zod_mini.z.looseObject({
52450
+ name: zod_mini.z.string(),
52451
+ description: zod_mini.z.string(),
52452
+ whenToUse: zod_mini.z.optional(zod_mini.z.string()),
52453
+ metadata: zod_mini.z.optional(zod_mini.z.looseObject({})),
52454
+ "disable-model-invocation": zod_mini.z.optional(zod_mini.z.boolean()),
52455
+ "user-invocable": zod_mini.z.optional(zod_mini.z.boolean())
52456
+ });
52457
+ /**
52458
+ * Represents a DeepSeek Harness skill directory.
52459
+ *
52460
+ * The harness scans `<projectRoot>/.dsh/skills` (project) and
52461
+ * `~/.dsh/skills` (global, the `$DSH_HOME` default) — plus the `.agents/skills`
52462
+ * roots the `agentsskills` target already writes — and discovers only the
52463
+ * top-level entries of each root: a `<name>/SKILL.md` bundle or a flat
52464
+ * `<name>.md` file. Nested `SKILL.md` files below a bundle are deliberately
52465
+ * not discovered, which matches what rulesync emits: one top-level bundle per
52466
+ * skill, supporting files carried inside it as plain files.
52467
+ *
52468
+ * @see https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/skill/skill-filesystem/README.md
52469
+ */
52470
+ var DshSkill = class DshSkill extends ToolSkill {
52471
+ constructor({ outputRoot = process.cwd(), relativeDirPath = DSH_SKILLS_DIR_PATH, dirName, frontmatter, body, otherFiles = [], validate = true, global = false }) {
52472
+ super({
52473
+ outputRoot,
52474
+ relativeDirPath,
52475
+ dirName,
52476
+ mainFile: {
52477
+ name: SKILL_FILE_NAME,
52478
+ body,
52479
+ frontmatter: { ...frontmatter }
52480
+ },
52481
+ otherFiles,
52482
+ global
52483
+ });
52484
+ if (validate) {
52485
+ const result = this.validate();
52486
+ if (!result.success) throw result.error;
52487
+ }
52488
+ }
52489
+ static getSettablePaths({ global: _global = false } = {}) {
52490
+ return { relativeDirPath: DSH_SKILLS_DIR_PATH };
52491
+ }
52492
+ getFrontmatter() {
52493
+ return DshSkillFrontmatterSchema.parse(this.requireMainFileFrontmatter());
52494
+ }
52495
+ getBody() {
52496
+ return this.mainFile?.body ?? "";
52497
+ }
52498
+ validate() {
52499
+ if (this.mainFile === void 0) return {
52500
+ success: false,
52501
+ error: /* @__PURE__ */ new Error(`${this.getDirPath()}: ${SKILL_FILE_NAME} file does not exist`)
52502
+ };
52503
+ const result = DshSkillFrontmatterSchema.safeParse(this.mainFile.frontmatter);
52504
+ if (!result.success) return {
52505
+ success: false,
52506
+ error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${this.getDirPath()}: ${formatError(result.error)}`)
52507
+ };
52508
+ return {
52509
+ success: true,
52510
+ error: null
52511
+ };
52512
+ }
52513
+ toRulesyncSkill() {
52514
+ const frontmatter = this.getFrontmatter();
52515
+ const dshSection = {
52516
+ ...frontmatter.whenToUse !== void 0 && { whenToUse: frontmatter.whenToUse },
52517
+ ...frontmatter.metadata !== void 0 && { metadata: frontmatter.metadata },
52518
+ ...frontmatter["disable-model-invocation"] !== void 0 && { "disable-model-invocation": frontmatter["disable-model-invocation"] },
52519
+ ...frontmatter["user-invocable"] !== void 0 && { "user-invocable": frontmatter["user-invocable"] }
52520
+ };
52521
+ const rulesyncFrontmatter = {
52522
+ name: frontmatter.name,
52523
+ description: frontmatter.description,
52524
+ targets: ["*"],
52525
+ ...Object.keys(dshSection).length > 0 && { dsh: dshSection }
52526
+ };
52527
+ return new RulesyncSkill({
52528
+ outputRoot: this.outputRoot,
52529
+ relativeDirPath: RULESYNC_SKILLS_RELATIVE_DIR_PATH,
52530
+ dirName: this.getDirName(),
52531
+ frontmatter: rulesyncFrontmatter,
52532
+ body: this.getBody(),
52533
+ otherFiles: this.getOtherFiles(),
52534
+ validate: true,
52535
+ global: this.global
52536
+ });
52537
+ }
52538
+ static fromRulesyncSkill({ outputRoot = process.cwd(), rulesyncSkill, validate = true, global = false }) {
52539
+ const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
52540
+ const dshSection = rulesyncFrontmatter.dsh;
52541
+ const disableModelInvocation = resolveDisableModelInvocation({
52542
+ rootFrontmatter: rulesyncFrontmatter,
52543
+ section: dshSection
52544
+ });
52545
+ const userInvocable = resolveUserInvocable({
52546
+ rootFrontmatter: rulesyncFrontmatter,
52547
+ section: dshSection
52548
+ });
52549
+ const metadata = resolveMetadata({
52550
+ rootFrontmatter: rulesyncFrontmatter,
52551
+ section: dshSection
52552
+ });
52553
+ const dshFrontmatter = {
52554
+ name: rulesyncFrontmatter.name,
52555
+ description: rulesyncFrontmatter.description,
52556
+ ...dshSection?.whenToUse !== void 0 && { whenToUse: dshSection.whenToUse },
52557
+ ...metadata !== void 0 && { metadata },
52558
+ ...disableModelInvocation !== void 0 && { "disable-model-invocation": disableModelInvocation },
52559
+ ...userInvocable !== void 0 && { "user-invocable": userInvocable }
52560
+ };
52561
+ const settablePaths = DshSkill.getSettablePaths({ global });
52562
+ return new DshSkill({
52563
+ outputRoot,
52564
+ relativeDirPath: settablePaths.relativeDirPath,
52565
+ dirName: rulesyncSkill.getDirName(),
52566
+ frontmatter: dshFrontmatter,
52567
+ body: rulesyncSkill.getBody(),
52568
+ otherFiles: rulesyncSkill.getOtherFiles(),
52569
+ validate,
52570
+ global
52571
+ });
52572
+ }
52573
+ static isTargetedByRulesyncSkill(rulesyncSkill) {
52574
+ const targets = rulesyncSkill.getFrontmatter().targets;
52575
+ return targets.includes("*") || targets.includes("dsh");
52576
+ }
52577
+ static async fromDir(params) {
52578
+ const loaded = await this.loadSkillDirContent({
52579
+ ...params,
52580
+ getSettablePaths: DshSkill.getSettablePaths
52581
+ });
52582
+ const result = DshSkillFrontmatterSchema.safeParse(loaded.frontmatter);
52583
+ if (!result.success) {
52584
+ const skillDirPath = (0, node_path.join)(loaded.outputRoot, loaded.relativeDirPath, loaded.dirName);
52585
+ throw new Error(`Invalid frontmatter in ${(0, node_path.join)(skillDirPath, SKILL_FILE_NAME)}: ${formatError(result.error)}`);
52586
+ }
52587
+ return new DshSkill({
52588
+ outputRoot: loaded.outputRoot,
52589
+ relativeDirPath: loaded.relativeDirPath,
52590
+ dirName: loaded.dirName,
52591
+ frontmatter: result.data,
52592
+ body: loaded.body,
52593
+ otherFiles: loaded.otherFiles,
52594
+ validate: true,
52595
+ global: loaded.global
52596
+ });
52597
+ }
52598
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, dirName, global = false }) {
52599
+ return new DshSkill({
52600
+ outputRoot,
52601
+ relativeDirPath,
52602
+ dirName,
52603
+ frontmatter: {
52604
+ name: "",
52605
+ description: ""
52606
+ },
52607
+ body: "",
52608
+ otherFiles: [],
52609
+ validate: false,
52610
+ global
52611
+ });
52612
+ }
52613
+ };
52614
+ //#endregion
52290
52615
  //#region src/features/skills/factorydroid-skill.ts
52291
52616
  const FactorydroidSkillFrontmatterSchema = zod_mini.z.looseObject({
52292
52617
  name: zod_mini.z.string(),
@@ -54842,6 +55167,19 @@ const VibeSkillFrontmatterSchema = zod_mini.z.looseObject({
54842
55167
  "allowed-tools": zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())]))
54843
55168
  });
54844
55169
  /**
55170
+ * Skill names Vibe's built-in skills occupy. `SkillManager` treats them as
55171
+ * reserved: a project or user skill whose frontmatter `name` matches one is
55172
+ * skipped at load time with only a debug log ("Skipping skill '<name>' ...
55173
+ * because builtin skill names are reserved"), so a generated skill by that
55174
+ * name is silently never offered. That is the legacy backend, still the
55175
+ * default; the unified harness constructs the manager with
55176
+ * `include_builtins=False` because its built-ins arrive as skills of the
55177
+ * shipped `vibe` plugin under namespaced names, so it reserves nothing.
55178
+ * @see https://github.com/mistralai/mistral-vibe/blob/v2.25.3/vibe/core/skills/builtins/__init__.py
55179
+ * @see https://github.com/mistralai/mistral-vibe/blob/v2.25.3/vibe/core/skills/manager.py
55180
+ */
55181
+ const VIBE_RESERVED_SKILL_NAMES = /* @__PURE__ */ new Set(["vibe", "skill-creator"]);
55182
+ /**
54845
55183
  * Build the Vibe frontmatter from a rulesync skill frontmatter, preferring the
54846
55184
  * dedicated `vibe` section over the shared root-level fields.
54847
55185
  */
@@ -54873,6 +55211,15 @@ function buildVibeFrontmatter(rulesyncFrontmatter) {
54873
55211
  ...vibeSection?.["allowed-tools"] !== void 0 && { "allowed-tools": vibeSection["allowed-tools"] }
54874
55212
  };
54875
55213
  }
55214
+ /**
55215
+ * The skill is still generated under the reserved name — the name is the
55216
+ * user's to change, and a rename here would make it diverge from the other
55217
+ * targets — but Vibe would drop it without a visible word, so say so now.
55218
+ */
55219
+ function warnAboutReservedSkillName({ name, logger }) {
55220
+ if (!VIBE_RESERVED_SKILL_NAMES.has(name)) return;
55221
+ logger?.warn(`Vibe skills: the skill name ${quoteValueForWarning(name)} is reserved for a Vibe built-in skill, so Vibe skips a project or user skill by that name when it loads skills. Rename the skill for it to be available in Vibe.`);
55222
+ }
54876
55223
  var VibeSkill = class VibeSkill extends ToolSkill {
54877
55224
  constructor({ outputRoot = process.cwd(), relativeDirPath = VibeSkill.getSettablePaths().relativeDirPath, dirName, frontmatter, body, otherFiles = [], validate = true, global = false }) {
54878
55225
  super({
@@ -54945,9 +55292,13 @@ var VibeSkill = class VibeSkill extends ToolSkill {
54945
55292
  global: this.global
54946
55293
  });
54947
55294
  }
54948
- static fromRulesyncSkill({ outputRoot = process.cwd(), rulesyncSkill, validate = true, global = false }) {
55295
+ static fromRulesyncSkill({ outputRoot = process.cwd(), rulesyncSkill, validate = true, global = false, logger }) {
54949
55296
  const settablePaths = VibeSkill.getSettablePaths({ global });
54950
55297
  const vibeFrontmatter = buildVibeFrontmatter(rulesyncSkill.getFrontmatter());
55298
+ warnAboutReservedSkillName({
55299
+ name: vibeFrontmatter.name,
55300
+ logger
55301
+ });
54951
55302
  return new VibeSkill({
54952
55303
  outputRoot,
54953
55304
  relativeDirPath: settablePaths.relativeDirPath,
@@ -55827,6 +56178,14 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
55827
56178
  supportsSimulated: false,
55828
56179
  supportsGlobal: true
55829
56180
  }
56181
+ }],
56182
+ ["dsh", {
56183
+ class: DshSkill,
56184
+ meta: {
56185
+ supportsProject: true,
56186
+ supportsSimulated: false,
56187
+ supportsGlobal: true
56188
+ }
55830
56189
  }]
55831
56190
  ]);
55832
56191
  const defaultGetFactory$2 = (target) => {
@@ -57014,13 +57373,26 @@ var AntigravityPluginSubagent = class extends AntigravitySharedSubagent {
57014
57373
  };
57015
57374
  //#endregion
57016
57375
  //#region src/features/subagents/augmentcode-subagent.ts
57376
+ const AugmentcodeToolListSchema = zod_mini.z.union([zod_mini.z.array(zod_mini.z.string()), zod_mini.z.string()]);
57377
+ /**
57378
+ * Normalize a documented tool-list value to the list form: a string is split on
57379
+ * commas, trimmed, and emptied of blank entries; a list is returned as is. A
57380
+ * string that names no tool (`""`, `", ,"`) is treated as unset rather than as
57381
+ * an empty allowlist — the author left the value blank, not the tool set — so
57382
+ * it yields `undefined`; an authored empty list is kept as written.
57383
+ */
57384
+ function normalizeAugmentcodeToolList(value) {
57385
+ if (Array.isArray(value)) return value;
57386
+ const tools = value.split(",").map((tool) => tool.trim()).filter((tool) => tool.length > 0);
57387
+ return tools.length > 0 ? tools : void 0;
57388
+ }
57017
57389
  const AugmentcodeSubagentFrontmatterSchema = zod_mini.z.looseObject({
57018
57390
  name: zod_mini.z.string(),
57019
57391
  description: zod_mini.z.optional(zod_mini.z.string()),
57020
57392
  color: zod_mini.z.optional(zod_mini.z.string()),
57021
57393
  model: zod_mini.z.optional(zod_mini.z.string()),
57022
- tools: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
57023
- disabled_tools: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string()))
57394
+ tools: zod_mini.z.optional(AugmentcodeToolListSchema),
57395
+ disabled_tools: zod_mini.z.optional(AugmentcodeToolListSchema)
57024
57396
  });
57025
57397
  /**
57026
57398
  * AugmentCode (Auggie CLI) subagents.
@@ -57059,14 +57431,24 @@ var AugmentcodeSubagent = class AugmentcodeSubagent extends ToolSubagent {
57059
57431
  return this.body;
57060
57432
  }
57061
57433
  toRulesyncSubagent() {
57062
- const { name, description, ...rest } = this.frontmatter;
57434
+ const { name, description, tools, disabled_tools, ...rest } = this.frontmatter;
57435
+ const normalizedTools = tools === void 0 ? void 0 : normalizeAugmentcodeToolList(tools);
57436
+ const normalizedDisabledTools = disabled_tools === void 0 ? void 0 : normalizeAugmentcodeToolList(disabled_tools);
57437
+ const toolLists = {
57438
+ ...normalizedTools !== void 0 && { tools: normalizedTools },
57439
+ ...normalizedDisabledTools !== void 0 && { disabled_tools: normalizedDisabledTools }
57440
+ };
57441
+ const augmentcodeSection = {
57442
+ ...rest,
57443
+ ...toolLists
57444
+ };
57063
57445
  return new RulesyncSubagent({
57064
57446
  outputRoot: ".",
57065
57447
  frontmatter: {
57066
57448
  targets: ["*"],
57067
57449
  name,
57068
57450
  description,
57069
- ...Object.keys(rest).length > 0 && { augmentcode: rest }
57451
+ ...Object.keys(augmentcodeSection).length > 0 && { augmentcode: augmentcodeSection }
57070
57452
  },
57071
57453
  body: this.body,
57072
57454
  relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH,
@@ -63379,13 +63761,40 @@ const CodebuddyRuleFrontmatterSchema = zod_mini.z.object({
63379
63761
  enabled: zod_mini.z.optional(zod_mini.z.boolean())
63380
63762
  });
63381
63763
  /**
63764
+ * Splits a scalar `paths` value on the commas that separate patterns, leaving
63765
+ * the commas inside a brace group alone: CodeBuddy's memory docs say "You can
63766
+ * also combine multiple patterns with commas" and give
63767
+ * `paths: {src,lib}/**\/*.ts, tests/**\/*.test.ts` as the example, where the
63768
+ * first comma belongs to the brace expansion and the second separates the two
63769
+ * globs. Only a scalar is split — the docs describe the comma form for the
63770
+ * string shape, and a list already separates its patterns.
63771
+ */
63772
+ function splitCodebuddyPathsScalar(paths) {
63773
+ const patterns = [];
63774
+ let current = "";
63775
+ let braceDepth = 0;
63776
+ for (const char of paths) {
63777
+ if (char === "{") braceDepth += 1;
63778
+ else if (char === "}" && braceDepth > 0) braceDepth -= 1;
63779
+ else if (char === "," && braceDepth === 0) {
63780
+ patterns.push(current);
63781
+ current = "";
63782
+ continue;
63783
+ }
63784
+ current += char;
63785
+ }
63786
+ patterns.push(current);
63787
+ return patterns.map((pattern) => pattern.trim());
63788
+ }
63789
+ /**
63382
63790
  * Normalizes the documented `string` / `string[]` shapes of `paths` to the
63383
- * list form the rest of the adapter works with. An empty list and an empty
63384
- * string both mean "no paths".
63791
+ * list form the rest of the adapter works with, splitting a comma-separated
63792
+ * scalar into its patterns (see `splitCodebuddyPathsScalar`). An empty list
63793
+ * and an empty string both mean "no paths".
63385
63794
  */
63386
63795
  function normalizeCodebuddyPaths(paths) {
63387
63796
  if (paths === void 0) return;
63388
- const list = (typeof paths === "string" ? [paths] : paths).filter((path) => path.trim() !== "");
63797
+ const list = (typeof paths === "string" ? splitCodebuddyPathsScalar(paths) : paths).filter((path) => path.trim() !== "");
63389
63798
  return list.length > 0 ? list : void 0;
63390
63799
  }
63391
63800
  /**
@@ -64619,6 +65028,125 @@ var DevinRule = class DevinRule extends ToolRule {
64619
65028
  }
64620
65029
  };
64621
65030
  //#endregion
65031
+ //#region src/features/rules/dsh-rule.ts
65032
+ /**
65033
+ * DeepSeek Harness (`dsh`) rules.
65034
+ *
65035
+ * Project scope writes the root `AGENTS.md` plus nested `<dir>/AGENTS.md`
65036
+ * files — the project chain `dsh-agent-instructions` loads from the `.git`
65037
+ * root down to the session cwd. Global scope writes the user-global
65038
+ * `~/.dsh/AGENTS.md` (`$DSH_HOME` default), which the harness reads as a
65039
+ * single root file with no local overlay, so non-root rules fold into it.
65040
+ *
65041
+ * @see https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/context/agent-instructions/README.md
65042
+ */
65043
+ var DshRule = class DshRule extends ToolRule {
65044
+ static getSettablePaths({ global = false } = {}) {
65045
+ if (global) return { root: {
65046
+ relativeDirPath: DSH_DIR,
65047
+ relativeFilePath: AGENTSMD_RULE_FILE_NAME
65048
+ } };
65049
+ return { root: {
65050
+ relativeDirPath: ".",
65051
+ relativeFilePath: AGENTSMD_RULE_FILE_NAME
65052
+ } };
65053
+ }
65054
+ /**
65055
+ * DeepSeek Harness loads the project chain of `AGENTS.md` files from the
65056
+ * project root (the nearest ancestor holding `.git`) down through the session
65057
+ * cwd, broad to specific, so a per-directory file applies only while working
65058
+ * under that directory. Nested files are therefore a real scoping surface,
65059
+ * not just the root file's overflow.
65060
+ *
65061
+ * The scan mirrors the AGENTS.md standard's nested discovery — same file
65062
+ * name, same exclusions, import-only, project scope — because it discovers
65063
+ * literally the same files.
65064
+ * @see https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/context/agent-instructions/README.md
65065
+ */
65066
+ static getNestedFilePatterns() {
65067
+ return this.buildNestedFilePatterns({ fileName: AGENTSMD_RULE_FILE_NAME });
65068
+ }
65069
+ /**
65070
+ * The subproject directory this rule scopes, or `undefined` for the root file
65071
+ * (project or global).
65072
+ */
65073
+ getSubprojectPath() {
65074
+ return this.getNestedSubprojectPath({ fileName: AGENTSMD_RULE_FILE_NAME });
65075
+ }
65076
+ static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, relativeDirPath: overrideDirPath, validate = true, global = false }) {
65077
+ const { root } = this.getSettablePaths({ global });
65078
+ if (overrideDirPath !== void 0 && overrideDirPath !== root.relativeDirPath && overrideDirPath !== ".") {
65079
+ const fileContent = await readFileContent((0, node_path.join)(outputRoot, overrideDirPath, AGENTSMD_RULE_FILE_NAME));
65080
+ return new DshRule({
65081
+ outputRoot,
65082
+ relativeDirPath: overrideDirPath,
65083
+ relativeFilePath: AGENTSMD_RULE_FILE_NAME,
65084
+ fileContent,
65085
+ validate,
65086
+ root: false
65087
+ });
65088
+ }
65089
+ const fileContent = await readFileContent((0, node_path.join)(outputRoot, root.relativeDirPath, root.relativeFilePath));
65090
+ return new DshRule({
65091
+ outputRoot,
65092
+ relativeDirPath: root.relativeDirPath,
65093
+ relativeFilePath: root.relativeFilePath,
65094
+ fileContent,
65095
+ validate,
65096
+ root: true
65097
+ });
65098
+ }
65099
+ static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
65100
+ const { root } = this.getSettablePaths({ global });
65101
+ const frontmatter = rulesyncRule.getFrontmatter();
65102
+ const isRoot = frontmatter.root ?? false;
65103
+ const subprojectPath = frontmatter.agentsmd?.subprojectPath;
65104
+ if (!global && !isRoot && subprojectPath) return new DshRule({
65105
+ outputRoot,
65106
+ relativeDirPath: (0, node_path.join)(subprojectPath),
65107
+ relativeFilePath: AGENTSMD_RULE_FILE_NAME,
65108
+ fileContent: rulesyncRule.getBody(),
65109
+ validate,
65110
+ root: false
65111
+ });
65112
+ return new DshRule({
65113
+ outputRoot,
65114
+ relativeDirPath: root.relativeDirPath,
65115
+ relativeFilePath: root.relativeFilePath,
65116
+ fileContent: rulesyncRule.getBody(),
65117
+ validate,
65118
+ root: isRoot
65119
+ });
65120
+ }
65121
+ toRulesyncRule() {
65122
+ const subprojectPath = this.getSubprojectPath();
65123
+ if (subprojectPath === void 0) return this.toRulesyncRuleDefault();
65124
+ return this.toRulesyncRuleNestedAgentsmd({ subprojectPath });
65125
+ }
65126
+ validate() {
65127
+ return {
65128
+ success: true,
65129
+ error: null
65130
+ };
65131
+ }
65132
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
65133
+ return new DshRule({
65134
+ outputRoot,
65135
+ relativeDirPath,
65136
+ relativeFilePath,
65137
+ fileContent: "",
65138
+ validate: false,
65139
+ root: relativeFilePath === "AGENTS.md" && (relativeDirPath === "." || relativeDirPath === ".dsh")
65140
+ });
65141
+ }
65142
+ static isTargetedByRulesyncRule(rulesyncRule) {
65143
+ return this.isTargetedByRulesyncRuleDefault({
65144
+ rulesyncRule,
65145
+ toolTarget: "dsh"
65146
+ });
65147
+ }
65148
+ };
65149
+ //#endregion
64622
65150
  //#region src/features/rules/factorydroid-rule.ts
64623
65151
  /**
64624
65152
  * Rule generator for Factory Droid.
@@ -64627,27 +65155,36 @@ var DevinRule = class DevinRule extends ToolRule {
64627
65155
  * (global) as coding guidelines, plus non-root rules referenced from it via
64628
65156
  * `.factory/rules/*.md`.
64629
65157
  *
64630
- * Factory Droid also loads `DESIGN.md` (project only) as a second,
64631
- * independent instruction surface: "Always-on design-system, UX, visual, and
64632
- * interaction guidance", loaded separately from `AGENTS.md`'s coding
64633
- * guidelines. Rulesync emits it from any non-root rule that opts in via a
64634
- * `factorydroid.channel: design` frontmatter block those rule bodies are
64635
- * routed to `DESIGN.md` instead of `AGENTS.md`/`.factory/rules/*.md`, and
64636
- * multiple opted-in rules concatenate in source order. Factory's docs describe
64637
- * `DESIGN.md` at the repository root and in nested subdirectories, like
64638
- * `AGENTS.md`, but document no personal/global home-directory equivalent, so
64639
- * this channel is project scope only.
65158
+ * Factory Droid also loads two further fixed files (project only) as
65159
+ * independent instruction surfaces, which rulesync emits from any non-root
65160
+ * rule that opts in via a `factorydroid.channel` frontmatter key. Opted-in
65161
+ * rule bodies are routed to the channel's file instead of
65162
+ * `AGENTS.md`/`.factory/rules/*.md`, and multiple opted-in rules concatenate
65163
+ * in source order:
65164
+ *
65165
+ * - `design` → `DESIGN.md`: "Always-on design-system, UX, visual, and
65166
+ * interaction guidance", loaded separately from `AGENTS.md`'s coding
65167
+ * guidelines. Factory's docs describe `DESIGN.md` at the repository root and
65168
+ * in nested subdirectories, like `AGENTS.md`, but document no
65169
+ * personal/global home-directory equivalent.
65170
+ * - `threat-model` → `.factory/threat-model.md`: the attack-surface map
65171
+ * Factory's Security Review reads — "if `.factory/threat-model.md` exists,
65172
+ * Droid uses it as the attack-surface map". It is documented only as a
65173
+ * repository file, so it has no global scope either.
65174
+ *
65175
+ * Both channels are therefore project scope only.
64640
65176
  * @see https://docs.factory.ai/cli/configuration/agents-md
65177
+ * @see https://docs.factory.ai/software-factory/security-review
64641
65178
  */
64642
65179
  var FactorydroidRule = class FactorydroidRule extends ToolRule {
64643
- design;
64644
- constructor({ fileContent, root, design = false, ...rest }) {
65180
+ channel;
65181
+ constructor({ fileContent, root, channel, ...rest }) {
64645
65182
  super({
64646
65183
  ...rest,
64647
65184
  fileContent,
64648
65185
  root: root ?? false
64649
65186
  });
64650
- this.design = design;
65187
+ this.channel = channel;
64651
65188
  }
64652
65189
  static getSettablePaths({ global, excludeToolDir } = {}) {
64653
65190
  if (global) return { root: {
@@ -64663,41 +65200,75 @@ var FactorydroidRule = class FactorydroidRule extends ToolRule {
64663
65200
  design: {
64664
65201
  relativeDirPath: ".",
64665
65202
  relativeFilePath: FACTORYDROID_DESIGN_FILE_NAME
65203
+ },
65204
+ threatModel: {
65205
+ relativeDirPath: buildToolPath(FACTORYDROID_DIR, ".", excludeToolDir),
65206
+ relativeFilePath: FACTORYDROID_THREAT_MODEL_FILE_NAME
64666
65207
  }
64667
65208
  };
64668
65209
  }
64669
65210
  /**
65211
+ * The channel files in a fixed order, so that `getExtraFixedFiles` and the
65212
+ * channel lookups below agree on which paths are channels. Empty in global
65213
+ * mode, where neither file has a documented home-directory equivalent.
65214
+ */
65215
+ static getChannelPaths({ global }) {
65216
+ if (global) return [];
65217
+ const paths = this.getSettablePaths({ global });
65218
+ return [{
65219
+ channel: "design",
65220
+ path: paths.design
65221
+ }, {
65222
+ channel: "threat-model",
65223
+ path: paths.threatModel
65224
+ }];
65225
+ }
65226
+ /**
65227
+ * Which channel, if any, owns the given output path. Matching on
65228
+ * `relativeDirPath` too (not just the basename) keeps a non-root rule that
65229
+ * happens to be named `DESIGN.md` or `threat-model.md` under
65230
+ * `.factory/rules/` from being routed to a channel by mistake.
65231
+ */
65232
+ static findChannelByPath({ relativeDirPath, relativeFilePath, global }) {
65233
+ return this.getChannelPaths({ global }).find(({ path }) => relativeDirPath === path.relativeDirPath && relativeFilePath === path.relativeFilePath);
65234
+ }
65235
+ /**
64670
65236
  * Extra fixed files this tool manages beyond the root/non-root rules. The
64671
65237
  * RulesProcessor enumerates these for import and deletion so a stale
64672
- * `DESIGN.md` is cleaned up once no rule opts in anymore. Empty in global
64673
- * mode: `DESIGN.md` has no documented home-directory equivalent.
65238
+ * `DESIGN.md` or `.factory/threat-model.md` is cleaned up once no rule opts
65239
+ * in anymore. Empty in global mode: neither file has a documented
65240
+ * home-directory equivalent.
64674
65241
  */
64675
65242
  static getExtraFixedFiles({ global = false } = {}) {
64676
- if (global) return [];
64677
- return [this.getSettablePaths({ global }).design];
65243
+ return this.getChannelPaths({ global }).map(({ path }) => path);
64678
65244
  }
64679
65245
  /**
64680
- * Factory Droid loads `DESIGN.md` itself, so listing it in the root rule's
64681
- * TOON reference section would double-load the content (and misrepresent it
64682
- * as a rule the model must remember to open).
65246
+ * Factory Droid loads the channel files itself, so listing one in the root
65247
+ * rule's TOON reference section would double-load the content (and
65248
+ * misrepresent it as a rule the model must remember to open).
64683
65249
  */
64684
65250
  isExcludedFromRootReferences() {
64685
- return this.design;
65251
+ return this.channel !== void 0;
64686
65252
  }
64687
65253
  static async fromFile({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, validate = true, global = false }) {
64688
65254
  const paths = this.getSettablePaths({ global });
64689
- const design = !global ? paths.design : void 0;
64690
- if (design !== void 0 && relativeDirPath === design.relativeDirPath && relativeFilePath === design.relativeFilePath) {
64691
- const relativePath = (0, node_path.join)(design.relativeDirPath, design.relativeFilePath);
65255
+ const channelMatch = this.findChannelByPath({
65256
+ relativeDirPath,
65257
+ relativeFilePath,
65258
+ global
65259
+ });
65260
+ if (channelMatch) {
65261
+ const { channel, path } = channelMatch;
65262
+ const relativePath = (0, node_path.join)(path.relativeDirPath, path.relativeFilePath);
64692
65263
  const fileContent = await readFileContent((0, node_path.join)(outputRoot, relativePath));
64693
65264
  return new FactorydroidRule({
64694
65265
  outputRoot,
64695
- relativeDirPath: design.relativeDirPath,
64696
- relativeFilePath: design.relativeFilePath,
65266
+ relativeDirPath: path.relativeDirPath,
65267
+ relativeFilePath: path.relativeFilePath,
64697
65268
  fileContent,
64698
65269
  validate,
64699
65270
  root: false,
64700
- design: true
65271
+ channel
64701
65272
  });
64702
65273
  }
64703
65274
  if (relativeFilePath === paths.root.relativeFilePath) {
@@ -64726,9 +65297,12 @@ var FactorydroidRule = class FactorydroidRule extends ToolRule {
64726
65297
  }
64727
65298
  static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
64728
65299
  const paths = this.getSettablePaths({ global });
64729
- const design = !global ? paths.design : void 0;
64730
- const isDesign = design !== void 0 && relativeDirPath === design.relativeDirPath && relativeFilePath === design.relativeFilePath;
64731
- const isRoot = !isDesign && relativeFilePath === paths.root.relativeFilePath && relativeDirPath === paths.root.relativeDirPath;
65300
+ const channel = this.findChannelByPath({
65301
+ relativeDirPath,
65302
+ relativeFilePath,
65303
+ global
65304
+ })?.channel;
65305
+ const isRoot = channel === void 0 && relativeFilePath === paths.root.relativeFilePath && relativeDirPath === paths.root.relativeDirPath;
64732
65306
  return new FactorydroidRule({
64733
65307
  outputRoot,
64734
65308
  relativeDirPath,
@@ -64736,22 +65310,24 @@ var FactorydroidRule = class FactorydroidRule extends ToolRule {
64736
65310
  fileContent: "",
64737
65311
  validate: false,
64738
65312
  root: isRoot,
64739
- design: isDesign
65313
+ channel
64740
65314
  });
64741
65315
  }
64742
65316
  static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
64743
65317
  const frontmatter = rulesyncRule.getFrontmatter();
64744
65318
  const paths = this.getSettablePaths({ global });
64745
- if (!global && !frontmatter.root && frontmatter.factorydroid?.channel === "design") {
64746
- const { design } = paths;
65319
+ const requestedChannel = frontmatter.factorydroid?.channel;
65320
+ const channelMatch = !global && !frontmatter.root && requestedChannel !== void 0 ? this.getChannelPaths({ global }).find(({ channel }) => channel === requestedChannel) : void 0;
65321
+ if (channelMatch) {
65322
+ const { channel, path } = channelMatch;
64747
65323
  return new FactorydroidRule({
64748
65324
  outputRoot,
64749
- relativeDirPath: design.relativeDirPath,
64750
- relativeFilePath: design.relativeFilePath,
65325
+ relativeDirPath: path.relativeDirPath,
65326
+ relativeFilePath: path.relativeFilePath,
64751
65327
  fileContent: rulesyncRule.getBody(),
64752
65328
  validate,
64753
65329
  root: false,
64754
- design: true
65330
+ channel
64755
65331
  });
64756
65332
  }
64757
65333
  return new FactorydroidRule(this.buildToolRuleParamsAgentsmd({
@@ -64763,14 +65339,14 @@ var FactorydroidRule = class FactorydroidRule extends ToolRule {
64763
65339
  }));
64764
65340
  }
64765
65341
  toRulesyncRule() {
64766
- if (this.design) return new RulesyncRule({
65342
+ if (this.channel !== void 0) return new RulesyncRule({
64767
65343
  outputRoot: process.cwd(),
64768
65344
  relativeDirPath: RULESYNC_RULES_RELATIVE_DIR_PATH,
64769
- relativeFilePath: FACTORYDROID_DESIGN_FILE_NAME,
65345
+ relativeFilePath: this.getRelativeFilePath(),
64770
65346
  frontmatter: {
64771
65347
  root: false,
64772
65348
  targets: ["factorydroid"],
64773
- factorydroid: { channel: "design" }
65349
+ factorydroid: { channel: this.channel }
64774
65350
  },
64775
65351
  body: this.getFileContent()
64776
65352
  });
@@ -67737,6 +68313,15 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
67737
68313
  ruleDiscoveryMode: "auto",
67738
68314
  collisionPolicy: "fold"
67739
68315
  }
68316
+ }],
68317
+ ["dsh", {
68318
+ class: DshRule,
68319
+ meta: {
68320
+ extension: "md",
68321
+ supportsGlobal: true,
68322
+ ruleDiscoveryMode: "auto",
68323
+ collisionPolicy: "fold"
68324
+ }
67740
68325
  }]
67741
68326
  ]);
67742
68327
  const allToolTargetKeys = [...toolRuleFactories.keys()];