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.
@@ -505,7 +505,8 @@ const rulesProcessorToolTargetTuple = [
505
505
  "zcode",
506
506
  "zed",
507
507
  "zoocode",
508
- "pool"
508
+ "pool",
509
+ "dsh"
509
510
  ];
510
511
  const ignoreProcessorToolTargetTuple = [
511
512
  "aiassistant",
@@ -683,7 +684,8 @@ const skillsProcessorToolTargetTuple = [
683
684
  "devin",
684
685
  "zcode",
685
686
  "zed",
686
- "zoocode"
687
+ "zoocode",
688
+ "dsh"
687
689
  ];
688
690
  const hooksProcessorToolTargetTuple = [
689
691
  "amp",
@@ -4211,10 +4213,12 @@ const PI_HOOK_EVENTS = [
4211
4213
  "sessionEnd",
4212
4214
  "preToolUse",
4213
4215
  "postToolUse",
4216
+ "postToolUseFailure",
4214
4217
  "preModelInvocation",
4215
4218
  "postModelInvocation",
4216
4219
  "beforeSubmitPrompt",
4217
4220
  "stop",
4221
+ "notification",
4218
4222
  "preCompact",
4219
4223
  "postCompact"
4220
4224
  ];
@@ -5086,7 +5090,7 @@ const CANONICAL_TO_KILO_EVENT_NAMES = CANONICAL_TO_OPENCODE_EVENT_NAMES;
5086
5090
  * Stop, this also fires before Pi auto-retries or auto-compacts —
5087
5091
  * `agent_settled` would skip queued follow-ups instead, a pure trade-off).
5088
5092
  * Pi events without a faithful canonical counterpart (e.g. `turn_start`,
5089
- * `agent_settled`) are intentionally unmapped.
5093
+ * `agent_settled`, `ui_prompt_end`) are intentionally unmapped.
5090
5094
  *
5091
5095
  * @see https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/extensions.md
5092
5096
  */
@@ -5095,10 +5099,12 @@ const CANONICAL_TO_PI_EVENT_NAMES = {
5095
5099
  sessionEnd: "session_shutdown",
5096
5100
  preToolUse: "tool_call",
5097
5101
  postToolUse: "tool_result",
5102
+ postToolUseFailure: "tool_result",
5098
5103
  preModelInvocation: "context",
5099
5104
  postModelInvocation: "message_end",
5100
5105
  beforeSubmitPrompt: "input",
5101
5106
  stop: "agent_end",
5107
+ notification: "ui_prompt_start",
5102
5108
  preCompact: "session_before_compact",
5103
5109
  postCompact: "session_compact"
5104
5110
  };
@@ -7982,7 +7988,7 @@ const RulesyncRuleFrontmatterSchema = z.object({
7982
7988
  extends: z.optional(z.string()),
7983
7989
  facet: z.optional(z.enum(["policies", "output-contracts"]))
7984
7990
  })),
7985
- factorydroid: z.optional(z.looseObject({ channel: z.optional(z.enum(["design"])) }))
7991
+ factorydroid: z.optional(z.looseObject({ channel: z.optional(z.enum(["design", "threat-model"])) }))
7986
7992
  });
7987
7993
  /**
7988
7994
  * The `agentsmd.subprojectPath` every consumer should act on, resolved once so
@@ -9196,6 +9202,12 @@ const RulesyncSkillFrontmatterSchema = z.looseObject({
9196
9202
  "disable-model-invocation": z.optional(z.boolean()),
9197
9203
  "user-invocable": z.optional(z.boolean())
9198
9204
  })),
9205
+ dsh: z.optional(z.looseObject({
9206
+ whenToUse: z.optional(z.string()),
9207
+ metadata: z.optional(z.looseObject({})),
9208
+ "disable-model-invocation": z.optional(z.boolean()),
9209
+ "user-invocable": z.optional(z.boolean())
9210
+ })),
9199
9211
  "kimi-code": z.optional(z.looseObject({
9200
9212
  type: z.optional(z.enum([
9201
9213
  "prompt",
@@ -9460,7 +9472,7 @@ async function getLocalSkillDirNames(sourceTree) {
9460
9472
  *
9461
9473
  * The rulesync skill frontmatter exposes a root-level `disable-model-invocation`
9462
9474
  * default that applies to every tool supporting the flag (claudecode, copilot,
9463
- * copilotcli, crush, cursor, zed, pi, qwencode, grokcli, factorydroid). Each tool's own section may override that
9475
+ * copilotcli, crush, cursor, zed, pi, qwencode, grokcli, factorydroid, dsh). Each tool's own section may override that
9464
9476
  * default with a per-target value. A defined section value (including `false`)
9465
9477
  * always wins over the root default.
9466
9478
  *
@@ -9479,7 +9491,7 @@ function resolveDisableModelInvocation({ rootFrontmatter, section }) {
9479
9491
  *
9480
9492
  * The rulesync skill frontmatter exposes a root-level `user-invocable` default
9481
9493
  * that applies to every tool supporting the flag (claudecode, copilot,
9482
- * copilotcli, crush, cursor, qwencode, vibe, grokcli, factorydroid). Each tool's own section may override that default with a
9494
+ * copilotcli, crush, cursor, qwencode, vibe, grokcli, factorydroid, dsh). Each tool's own section may override that default with a
9483
9495
  * per-target value. A defined section value (including `false`) always wins
9484
9496
  * over the root default.
9485
9497
  *
@@ -11081,6 +11093,13 @@ const FACTORYDROID_RULE_FILE_NAME = "AGENTS.md";
11081
11093
  * @see https://docs.factory.ai/cli/configuration/agents-md
11082
11094
  */
11083
11095
  const FACTORYDROID_DESIGN_FILE_NAME = "DESIGN.md";
11096
+ /**
11097
+ * Factory's Security Review threat model, kept inside the tool directory:
11098
+ * "if `.factory/threat-model.md` exists, Droid uses it as the attack-surface
11099
+ * map". Documented only as a repository file, so project scope only.
11100
+ * @see https://docs.factory.ai/software-factory/security-review
11101
+ */
11102
+ const FACTORYDROID_THREAT_MODEL_FILE_NAME = "threat-model.md";
11084
11103
  const FACTORYDROID_MCP_FILE_NAME = "mcp.json";
11085
11104
  const FACTORYDROID_SETTINGS_FILE_NAME = "settings.json";
11086
11105
  const FACTORYDROID_HOOKS_FILE_NAME = "hooks.json";
@@ -11722,7 +11741,10 @@ function parseSharedConfig({ format, fileContent, filePath, invalidRootPolicy =
11722
11741
  throw new Error(`Failed to parse shared config${at}: ${formatError(error)}`, { cause: error });
11723
11742
  }
11724
11743
  if (!isPlainObject$1(sanitized)) {
11725
- if (invalidRootPolicy === "error") throw new Error(`Failed to parse shared config${at}: expected a mapping at the root`);
11744
+ if (invalidRootPolicy === "error") {
11745
+ const reason = /* @__PURE__ */ new Error("expected a mapping at the root");
11746
+ throw new Error(`Failed to parse shared config${at}: ${reason.message}`, { cause: reason });
11747
+ }
11726
11748
  return {};
11727
11749
  }
11728
11750
  return sanitized;
@@ -12724,7 +12746,9 @@ const SHARED_CONFIG_OWNERSHIP = {
12724
12746
  }
12725
12747
  },
12726
12748
  ".augment/settings.json": {
12727
- format: "json",
12749
+ format: "jsonc",
12750
+ invalidRootPolicy: "error",
12751
+ jsoncParseErrors: "error",
12728
12752
  features: {
12729
12753
  mcp: {
12730
12754
  kind: "replace-owned-keys",
@@ -20582,14 +20606,14 @@ var AntigravityPluginHooks = class extends AntigravityIdeHooks {
20582
20606
  * Returns `null` when neither file exists and no `baseFallbackContent` is
20583
20607
  * given, so callers can tell "no settings at this scope" apart from empty ones.
20584
20608
  */
20585
- async function readSettingsWithLocalOverlay({ outputRoot, relativeDirPath, baseFileName, localFileName, toolLabel, baseFallbackContent, sensitiveKeys = [], quiet = false, merge, logger }) {
20609
+ async function readSettingsWithLocalOverlay({ outputRoot, relativeDirPath, baseFileName, localFileName, toolLabel, baseFallbackContent, sensitiveKeys = [], quiet = false, parse = JSON.parse, merge, logger }) {
20586
20610
  const baseContent = await readFileContentOrNull(join(outputRoot, relativeDirPath, baseFileName)) ?? baseFallbackContent ?? null;
20587
20611
  const localContent = await readFileContentOrNull(join(outputRoot, relativeDirPath, localFileName));
20588
20612
  if (localContent === null) return baseContent;
20589
20613
  const configPath = join(relativeDirPath, localFileName);
20590
20614
  let localParsed;
20591
20615
  try {
20592
- localParsed = JSON.parse(localContent);
20616
+ localParsed = parse(localContent);
20593
20617
  } catch (error) {
20594
20618
  throw new Error(`Failed to parse ${toolLabel} settings at ${configPath}: ${formatError(error)}`, { cause: error });
20595
20619
  }
@@ -20598,7 +20622,7 @@ async function readSettingsWithLocalOverlay({ outputRoot, relativeDirPath, baseF
20598
20622
  if (baseContent !== null) {
20599
20623
  let parsed;
20600
20624
  try {
20601
- parsed = JSON.parse(baseContent);
20625
+ parsed = parse(baseContent);
20602
20626
  } catch {
20603
20627
  return baseContent;
20604
20628
  }
@@ -20657,6 +20681,51 @@ function warnAboutLocalKeys({ localParsed, configPath, toolLabel, sensitiveKeys,
20657
20681
  //#endregion
20658
20682
  //#region src/utils/augmentcode-settings.ts
20659
20683
  /**
20684
+ * The one place that spells out how an AugmentCode settings file is parsed.
20685
+ *
20686
+ * Auggie reads `settings.json` / `settings.local.json` as JSON with Comments —
20687
+ * "The files support JSON with Comments (JSONC), allowing comments and trailing
20688
+ * commas for better documentation." (https://docs.augmentcode.com/cli/config) —
20689
+ * so a bare `JSON.parse` rejects a hand-written file the CLI itself accepts. The
20690
+ * parse is fail-closed: a syntax error or a non-object root throws instead of
20691
+ * yielding a partial document, because every caller either merges its own keys
20692
+ * back into this file or imports permissions from it, and neither may proceed
20693
+ * on a file it could not read in full. An empty file parses as `{}`.
20694
+ *
20695
+ * The generate direction registers the same file as `jsonc` in
20696
+ * `SHARED_CONFIG_OWNERSHIP`, so the in-place patch reads it the same way.
20697
+ *
20698
+ * Throws the bare reason (the parser's own error), without naming the file;
20699
+ * `parseAugmentcodeSettingsDocument` adds that, and so does
20700
+ * `readSettingsWithLocalOverlay` when this is handed over as its `parse`.
20701
+ */
20702
+ function parseAugmentcodeSettingsContent(fileContent) {
20703
+ try {
20704
+ return parseSharedConfig({
20705
+ format: "jsonc",
20706
+ fileContent,
20707
+ invalidRootPolicy: "error",
20708
+ jsoncParseErrors: "error"
20709
+ });
20710
+ } catch (error) {
20711
+ if (error instanceof Error && error.cause !== void 0) throw error.cause;
20712
+ throw error;
20713
+ }
20714
+ }
20715
+ /**
20716
+ * Parse an AugmentCode settings file (`settings.json` / `settings.local.json`)
20717
+ * into a plain object, naming `configPath` in the error when it cannot be read.
20718
+ * See `parseAugmentcodeSettingsContent` for the format and the fail-closed
20719
+ * policy.
20720
+ */
20721
+ function parseAugmentcodeSettingsDocument({ fileContent, configPath }) {
20722
+ try {
20723
+ return parseAugmentcodeSettingsContent(fileContent);
20724
+ } catch (error) {
20725
+ throw new Error(`Failed to parse AugmentCode settings at ${configPath}: ${formatError(error)}`, { cause: error });
20726
+ }
20727
+ }
20728
+ /**
20660
20729
  * Top-level keys AugmentCode *replaces* (higher-precedence wins wholesale)
20661
20730
  * rather than combining across tiers. Everything else combines.
20662
20731
  *
@@ -20731,6 +20800,7 @@ async function readAugmentcodeSettingsWithLocalOverlay({ outputRoot, relativeDir
20731
20800
  localFileName: "settings.local.json",
20732
20801
  toolLabel: "AugmentCode",
20733
20802
  sensitiveKeys: AUGMENTCODE_GUARDRAIL_KEYS,
20803
+ parse: parseAugmentcodeSettingsContent,
20734
20804
  baseFallbackContent,
20735
20805
  merge: combineAugmentSettings,
20736
20806
  logger
@@ -20810,8 +20880,10 @@ var AugmentcodeHooks = class AugmentcodeHooks extends ToolHooks {
20810
20880
  const existingContent = await readFileContentOrNull(filePath) ?? JSON.stringify({}, null, 2);
20811
20881
  let existingHooks = {};
20812
20882
  try {
20813
- const parsed = JSON.parse(existingContent);
20814
- const candidate = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed.hooks : void 0;
20883
+ const candidate = parseAugmentcodeSettingsDocument({
20884
+ fileContent: existingContent,
20885
+ configPath: join(paths.relativeDirPath, paths.relativeFilePath)
20886
+ }).hooks;
20815
20887
  if (candidate && typeof candidate === "object" && !Array.isArray(candidate)) existingHooks = candidate;
20816
20888
  } catch {
20817
20889
  existingHooks = {};
@@ -20833,7 +20905,8 @@ var AugmentcodeHooks = class AugmentcodeHooks extends ToolHooks {
20833
20905
  ...preservedHooks,
20834
20906
  ...augmentHooks
20835
20907
  } },
20836
- filePath
20908
+ filePath,
20909
+ logger
20837
20910
  });
20838
20911
  return new AugmentcodeHooks({
20839
20912
  outputRoot,
@@ -20846,7 +20919,10 @@ var AugmentcodeHooks = class AugmentcodeHooks extends ToolHooks {
20846
20919
  toRulesyncHooks({ logger } = {}) {
20847
20920
  let settings;
20848
20921
  try {
20849
- settings = JSON.parse(this.getFileContent());
20922
+ settings = parseAugmentcodeSettingsDocument({
20923
+ fileContent: this.getFileContent(),
20924
+ configPath: join(this.getRelativeDirPath(), this.getRelativeFilePath())
20925
+ });
20850
20926
  } catch (error) {
20851
20927
  throw new Error(`Failed to parse AugmentCode hooks content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
20852
20928
  }
@@ -24895,6 +24971,15 @@ const PI_TOOL_EVENTS = /* @__PURE__ */ new Set(["tool_call", "tool_result"]);
24895
24971
  */
24896
24972
  const PI_ASSISTANT_MESSAGE_EVENTS = /* @__PURE__ */ new Set(["message_end"]);
24897
24973
  /**
24974
+ * Canonical events whose Pi event also fires when nothing went wrong. Pi's
24975
+ * `tool_result` fires for every finished tool call and exposes `event.isError`,
24976
+ * so a `postToolUseFailure` handler is gated on that flag while a `postToolUse`
24977
+ * handler on the same Pi event runs regardless.
24978
+ *
24979
+ * @see https://github.com/earendil-works/pi/blob/v0.85.1/packages/coding-agent/docs/extensions.md#tool_result
24980
+ */
24981
+ const PI_ERROR_GATED_CANONICAL_EVENTS = /* @__PURE__ */ new Set(["postToolUseFailure"]);
24982
+ /**
24898
24983
  * `tool_call` is Pi's tool gate. Its return contract is
24899
24984
  * `{ block: true, reason?: string, terminate?: boolean }`.
24900
24985
  *
@@ -25040,7 +25125,8 @@ function collectPiHandlers({ effectiveHooks, eventMap }) {
25040
25125
  if (!def.command) continue;
25041
25126
  handlers.push({
25042
25127
  command: def.command,
25043
- matcher: def.matcher ? def.matcher : void 0
25128
+ matcher: def.matcher ? def.matcher : void 0,
25129
+ onlyOnError: PI_ERROR_GATED_CANONICAL_EVENTS.has(canonicalEvent)
25044
25130
  });
25045
25131
  }
25046
25132
  if (handlers.length > 0) {
@@ -25053,10 +25139,13 @@ function collectPiHandlers({ effectiveHooks, eventMap }) {
25053
25139
  }
25054
25140
  function buildCommandLines({ handler, usesToolName, blocking }) {
25055
25141
  const lines = [];
25056
- const gated = usesToolName && Boolean(handler.matcher);
25142
+ const conditions = [];
25143
+ if (handler.onlyOnError) conditions.push("event.isError");
25144
+ if (usesToolName && handler.matcher) conditions.push(`new RegExp(${matcherToEmbeddedLiteral(handler.matcher)}).test(event.toolName)`);
25145
+ const gated = conditions.length > 0;
25057
25146
  const indent = gated ? " " : " ";
25058
25147
  const embeddedCommand = JSON.stringify(handler.command);
25059
- if (gated && handler.matcher) lines.push(` if (new RegExp(${matcherToEmbeddedLiteral(handler.matcher)}).test(event.toolName)) {`);
25148
+ if (gated) lines.push(` if (${conditions.join(" && ")}) {`);
25060
25149
  const onFailure = FAILURE_LINES_BY_MODE[blocking];
25061
25150
  if (onFailure.length > 0) {
25062
25151
  lines.push(`${indent}try {`);
@@ -25074,8 +25163,9 @@ function buildSubscriptionLines(handlerGroups) {
25074
25163
  const blocking = PI_BLOCKING_MODE_BY_EVENT[piEvent] ?? "none";
25075
25164
  const isPromptGate = blocking === "prompt";
25076
25165
  const usesToolName = PI_TOOL_EVENTS.has(piEvent) && handlers.some((h) => h.matcher);
25166
+ const usesErrorFlag = handlers.some((h) => h.onlyOnError);
25077
25167
  const gatesOnAssistant = PI_ASSISTANT_MESSAGE_EVENTS.has(piEvent);
25078
- const params = isPromptGate ? "event, ctx" : usesToolName || gatesOnAssistant || isPromptGate ? "event" : "";
25168
+ const params = isPromptGate ? "event, ctx" : usesToolName || usesErrorFlag || gatesOnAssistant || isPromptGate ? "event" : "";
25079
25169
  lines.push(` pi.on(${JSON.stringify(piEvent)}, async (${params}) => {`);
25080
25170
  if (gatesOnAssistant) lines.push(` if (event.message.role !== "assistant") return;`);
25081
25171
  if (isPromptGate) lines.push(` if (event.source === "extension") return { action: "continue" };`);
@@ -25097,6 +25187,8 @@ function buildSubscriptionLines(handlerGroups) {
25097
25187
  * gate — where a hook command that exits non-zero denies the call with
25098
25188
  * `{ block: true, reason }`, and on `input` — Pi's prompt-submission gate —
25099
25189
  * where a non-zero exit cancels the prompt with `{ action: "handled" }`.
25190
+ * `postToolUse` and `postToolUseFailure` share Pi's `tool_result` event; the
25191
+ * latter's commands run only when `event.isError` is set.
25100
25192
  *
25101
25193
  * @see https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/extensions.md
25102
25194
  */
@@ -25665,12 +25757,29 @@ const VIBE_HOOKS_FILE_NAME = "hooks.toml";
25665
25757
  const VIBE_TOOL_EVENTS = /* @__PURE__ */ new Set(["pre_tool", "post_tool"]);
25666
25758
  const SUPPORTED_VIBE_EVENTS = new Set(VIBE_HOOK_EVENTS);
25667
25759
  /**
25760
+ * Vibe drops a hook whose `command` contains a backslash when it loads
25761
+ * `hooks.toml` (since v2.25.1; `load_hooks_config` at v2.25.3: "Hook '<name>' skipped:
25762
+ * backslash paths are not supported in hook commands. Use forward slashes
25763
+ * instead."), because its exec-based executor tokenizes the command with
25764
+ * `shlex.split`, which eats the backslash. The hook is still written — the
25765
+ * command is the user's to fix — but it is worth a warning, since Vibe itself
25766
+ * only reports the skip as a config issue rather than failing the run.
25767
+ * @see https://github.com/mistralai/mistral-vibe/blob/v2.25.3/vibe/core/hooks/config.py
25768
+ */
25769
+ function warnAboutBackslashCommand({ name, command, logger }) {
25770
+ if (!command.includes("\\")) return;
25771
+ 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.`);
25772
+ }
25773
+ /**
25668
25774
  * Build the flat `[[hooks]]` array for `.vibe/hooks.toml` from a canonical
25669
25775
  * hooks config. Vibe uses a flat array where each entry carries its own event
25670
25776
  * `type`, tool-name `match` glob/regex, and `command`. Only `type: "command"`
25671
- * canonical hooks are emitted (Vibe hooks are always shell commands).
25777
+ * canonical hooks are emitted: a Vibe hook is a command line, which the legacy
25778
+ * backend hands to a shell and the unified harness tokenizes with `shlex.split`
25779
+ * and executes directly (v2.25.1: "Hook commands run without a shell to
25780
+ * prevent injection via hooks.toml").
25672
25781
  */
25673
- function canonicalToVibeHooks(config, toolOverride) {
25782
+ function canonicalToVibeHooks({ config, toolOverride, logger }) {
25674
25783
  const shared = {};
25675
25784
  for (const [event, defs] of Object.entries(config.hooks)) if (SUPPORTED_VIBE_EVENTS.has(event)) shared[event] = defs;
25676
25785
  const effective = {
@@ -25689,6 +25798,11 @@ function canonicalToVibeHooks(config, toolOverride) {
25689
25798
  if ((def.type ?? "command") !== "command") continue;
25690
25799
  if (typeof def.command !== "string") continue;
25691
25800
  const name = typeof def.name === "string" ? def.name : `${vibeEvent}-${index}`;
25801
+ warnAboutBackslashCommand({
25802
+ name,
25803
+ command: def.command,
25804
+ logger
25805
+ });
25692
25806
  const isToolEvent = VIBE_TOOL_EVENTS.has(vibeEvent);
25693
25807
  const entry = {
25694
25808
  name,
@@ -25789,10 +25903,14 @@ var VibeHooks = class VibeHooks extends ToolHooks {
25789
25903
  validate
25790
25904
  });
25791
25905
  }
25792
- static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false }) {
25906
+ static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
25793
25907
  const paths = VibeHooks.getSettablePaths({ global });
25794
25908
  const config = rulesyncHooks.getJson();
25795
- const vibeHooks = canonicalToVibeHooks(config, config.vibe?.hooks);
25909
+ const vibeHooks = canonicalToVibeHooks({
25910
+ config,
25911
+ toolOverride: config.vibe?.hooks,
25912
+ logger
25913
+ });
25796
25914
  const fileContent = smolToml.stringify(vibeHooks);
25797
25915
  return new VibeHooks({
25798
25916
  outputRoot,
@@ -28725,15 +28843,10 @@ var AntigravityPluginMcp = class extends AntigravityIdeMcp {
28725
28843
  //#endregion
28726
28844
  //#region src/features/mcp/augmentcode-mcp.ts
28727
28845
  function parseAugmentcodeSettings(fileContent, relativeDirPath, relativeFilePath) {
28728
- const configPath = join(relativeDirPath, relativeFilePath);
28729
- let parsed;
28730
- try {
28731
- parsed = JSON.parse(fileContent);
28732
- } catch (error) {
28733
- throw new Error(`Failed to parse AugmentCode settings at ${configPath}: ${formatError(error)}`, { cause: error });
28734
- }
28735
- if (!isPlainObject$1(parsed)) throw new Error(`Failed to parse AugmentCode settings at ${configPath}: expected a JSON object`);
28736
- return parsed;
28846
+ return parseAugmentcodeSettingsDocument({
28847
+ fileContent,
28848
+ configPath: join(relativeDirPath, relativeFilePath)
28849
+ });
28737
28850
  }
28738
28851
  /**
28739
28852
  * AugmentCode (Auggie CLI) MCP servers.
@@ -28790,7 +28903,7 @@ var AugmentcodeMcp = class AugmentcodeMcp extends ToolMcp {
28790
28903
  global
28791
28904
  });
28792
28905
  }
28793
- static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
28906
+ static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
28794
28907
  const paths = this.getSettablePaths({ global });
28795
28908
  const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
28796
28909
  const existingContent = await readFileContentOrNull(filePath) ?? JSON.stringify({}, null, 2);
@@ -28803,7 +28916,8 @@ var AugmentcodeMcp = class AugmentcodeMcp extends ToolMcp {
28803
28916
  feature: "mcp",
28804
28917
  existingContent,
28805
28918
  patch: { mcpServers: rulesyncMcp.getMcpServers() },
28806
- filePath
28919
+ filePath,
28920
+ logger
28807
28921
  }),
28808
28922
  validate,
28809
28923
  global
@@ -36976,7 +37090,10 @@ var AugmentcodePermissions = class AugmentcodePermissions extends ToolPermission
36976
37090
  const existingContent = await readFileContentOrNull(filePath) ?? "{}";
36977
37091
  let settings;
36978
37092
  try {
36979
- const parsed = JSON.parse(existingContent);
37093
+ const parsed = parseAugmentcodeSettingsDocument({
37094
+ fileContent: existingContent,
37095
+ configPath: join(paths.relativeDirPath, paths.relativeFilePath)
37096
+ });
36980
37097
  const result = AugmentSettingsSchema.safeParse(parsed);
36981
37098
  if (!result.success) throw new Error(formatError(result.error));
36982
37099
  settings = result.data;
@@ -37016,7 +37133,8 @@ var AugmentcodePermissions = class AugmentcodePermissions extends ToolPermission
37016
37133
  feature: "permissions",
37017
37134
  existingContent,
37018
37135
  patch: { toolPermissions: [...specialEntries, ...sortedBasic] },
37019
- filePath
37136
+ filePath,
37137
+ logger
37020
37138
  });
37021
37139
  return new AugmentcodePermissions({
37022
37140
  outputRoot,
@@ -37029,7 +37147,10 @@ var AugmentcodePermissions = class AugmentcodePermissions extends ToolPermission
37029
37147
  toRulesyncPermissions() {
37030
37148
  let settings;
37031
37149
  try {
37032
- const parsed = JSON.parse(this.getFileContent());
37150
+ const parsed = parseAugmentcodeSettingsDocument({
37151
+ fileContent: this.getFileContent(),
37152
+ configPath: join(this.getRelativeDirPath(), this.getRelativeFilePath())
37153
+ });
37033
37154
  const result = AugmentSettingsSchema.safeParse(parsed);
37034
37155
  if (!result.success) throw new Error(formatError(result.error));
37035
37156
  settings = result.data;
@@ -37047,7 +37168,10 @@ var AugmentcodePermissions = class AugmentcodePermissions extends ToolPermission
37047
37168
  }
37048
37169
  validate() {
37049
37170
  try {
37050
- const parsed = JSON.parse(this.fileContent || "{}");
37171
+ const parsed = parseAugmentcodeSettingsDocument({
37172
+ fileContent: this.fileContent || "{}",
37173
+ configPath: join(this.getRelativeDirPath(), this.getRelativeFilePath())
37174
+ });
37051
37175
  const result = AugmentSettingsSchema.safeParse(parsed);
37052
37176
  if (!result.success) return {
37053
37177
  success: false,
@@ -52262,6 +52386,207 @@ var DevinSkill = class DevinSkill extends ToolSkill {
52262
52386
  }
52263
52387
  };
52264
52388
  //#endregion
52389
+ //#region src/constants/dsh-paths.ts
52390
+ /**
52391
+ * DeepSeek Harness (`dsh`) configuration-layout conventions.
52392
+ *
52393
+ * The harness home is `$DSH_HOME`, which defaults to `~/.dsh`; rulesync writes
52394
+ * only the default location. Project-scoped assets live under a `.dsh/`
52395
+ * directory at the project root.
52396
+ *
52397
+ * Rules: `dsh-agent-instructions` loads the user-global `$DSH_HOME/AGENTS.md`
52398
+ * (root file only — the user-global file has no `.local.md` overlay), then the
52399
+ * project chain of `AGENTS.md` / `CLAUDE.md` files from the project root (the
52400
+ * nearest ancestor holding `.git`) down to the session cwd, broad to specific.
52401
+ * Rulesync emits `AGENTS.md` only; `CLAUDE.md` is the `claudecode` target's
52402
+ * file, and a `CLAUDE.md` duplicating its sibling `AGENTS.md` renders once.
52403
+ *
52404
+ * Skills: `dsh-skill-filesystem` scans `<projectRoot>/.dsh/skills` (rank 100),
52405
+ * `<projectRoot>/.agents/skills` (200), `<dshHome>/skills` (400) and
52406
+ * `<agentsHome>/skills` (500), each holding top-level `<name>/SKILL.md`
52407
+ * bundles or flat `<name>.md` files; nested `SKILL.md` files are deliberately
52408
+ * not discovered. Rulesync writes only the `dsh`-specific roots — the
52409
+ * `.agents/skills` roots are the `agentsskills` target's output.
52410
+ *
52411
+ * @see https://github.com/deepseek-ai/deepseek-harness
52412
+ * @see https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/context/agent-instructions/README.md
52413
+ * @see https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/skill/skill-filesystem/README.md
52414
+ */
52415
+ /**
52416
+ * Project-scoped `.dsh/` directory, and the harness home relative to the home
52417
+ * directory (`$DSH_HOME` default).
52418
+ */
52419
+ const DSH_DIR = ".dsh";
52420
+ /** Skills root, relative to the project root or the harness home. */
52421
+ const DSH_SKILLS_DIR_PATH = join(DSH_DIR, "skills");
52422
+ //#endregion
52423
+ //#region src/features/skills/dsh-skill.ts
52424
+ const DshSkillFrontmatterSchema = z.looseObject({
52425
+ name: z.string(),
52426
+ description: z.string(),
52427
+ whenToUse: z.optional(z.string()),
52428
+ metadata: z.optional(z.looseObject({})),
52429
+ "disable-model-invocation": z.optional(z.boolean()),
52430
+ "user-invocable": z.optional(z.boolean())
52431
+ });
52432
+ /**
52433
+ * Represents a DeepSeek Harness skill directory.
52434
+ *
52435
+ * The harness scans `<projectRoot>/.dsh/skills` (project) and
52436
+ * `~/.dsh/skills` (global, the `$DSH_HOME` default) — plus the `.agents/skills`
52437
+ * roots the `agentsskills` target already writes — and discovers only the
52438
+ * top-level entries of each root: a `<name>/SKILL.md` bundle or a flat
52439
+ * `<name>.md` file. Nested `SKILL.md` files below a bundle are deliberately
52440
+ * not discovered, which matches what rulesync emits: one top-level bundle per
52441
+ * skill, supporting files carried inside it as plain files.
52442
+ *
52443
+ * @see https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/skill/skill-filesystem/README.md
52444
+ */
52445
+ var DshSkill = class DshSkill extends ToolSkill {
52446
+ constructor({ outputRoot = process.cwd(), relativeDirPath = DSH_SKILLS_DIR_PATH, dirName, frontmatter, body, otherFiles = [], validate = true, global = false }) {
52447
+ super({
52448
+ outputRoot,
52449
+ relativeDirPath,
52450
+ dirName,
52451
+ mainFile: {
52452
+ name: SKILL_FILE_NAME,
52453
+ body,
52454
+ frontmatter: { ...frontmatter }
52455
+ },
52456
+ otherFiles,
52457
+ global
52458
+ });
52459
+ if (validate) {
52460
+ const result = this.validate();
52461
+ if (!result.success) throw result.error;
52462
+ }
52463
+ }
52464
+ static getSettablePaths({ global: _global = false } = {}) {
52465
+ return { relativeDirPath: DSH_SKILLS_DIR_PATH };
52466
+ }
52467
+ getFrontmatter() {
52468
+ return DshSkillFrontmatterSchema.parse(this.requireMainFileFrontmatter());
52469
+ }
52470
+ getBody() {
52471
+ return this.mainFile?.body ?? "";
52472
+ }
52473
+ validate() {
52474
+ if (this.mainFile === void 0) return {
52475
+ success: false,
52476
+ error: /* @__PURE__ */ new Error(`${this.getDirPath()}: ${SKILL_FILE_NAME} file does not exist`)
52477
+ };
52478
+ const result = DshSkillFrontmatterSchema.safeParse(this.mainFile.frontmatter);
52479
+ if (!result.success) return {
52480
+ success: false,
52481
+ error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${this.getDirPath()}: ${formatError(result.error)}`)
52482
+ };
52483
+ return {
52484
+ success: true,
52485
+ error: null
52486
+ };
52487
+ }
52488
+ toRulesyncSkill() {
52489
+ const frontmatter = this.getFrontmatter();
52490
+ const dshSection = {
52491
+ ...frontmatter.whenToUse !== void 0 && { whenToUse: frontmatter.whenToUse },
52492
+ ...frontmatter.metadata !== void 0 && { metadata: frontmatter.metadata },
52493
+ ...frontmatter["disable-model-invocation"] !== void 0 && { "disable-model-invocation": frontmatter["disable-model-invocation"] },
52494
+ ...frontmatter["user-invocable"] !== void 0 && { "user-invocable": frontmatter["user-invocable"] }
52495
+ };
52496
+ const rulesyncFrontmatter = {
52497
+ name: frontmatter.name,
52498
+ description: frontmatter.description,
52499
+ targets: ["*"],
52500
+ ...Object.keys(dshSection).length > 0 && { dsh: dshSection }
52501
+ };
52502
+ return new RulesyncSkill({
52503
+ outputRoot: this.outputRoot,
52504
+ relativeDirPath: RULESYNC_SKILLS_RELATIVE_DIR_PATH,
52505
+ dirName: this.getDirName(),
52506
+ frontmatter: rulesyncFrontmatter,
52507
+ body: this.getBody(),
52508
+ otherFiles: this.getOtherFiles(),
52509
+ validate: true,
52510
+ global: this.global
52511
+ });
52512
+ }
52513
+ static fromRulesyncSkill({ outputRoot = process.cwd(), rulesyncSkill, validate = true, global = false }) {
52514
+ const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
52515
+ const dshSection = rulesyncFrontmatter.dsh;
52516
+ const disableModelInvocation = resolveDisableModelInvocation({
52517
+ rootFrontmatter: rulesyncFrontmatter,
52518
+ section: dshSection
52519
+ });
52520
+ const userInvocable = resolveUserInvocable({
52521
+ rootFrontmatter: rulesyncFrontmatter,
52522
+ section: dshSection
52523
+ });
52524
+ const metadata = resolveMetadata({
52525
+ rootFrontmatter: rulesyncFrontmatter,
52526
+ section: dshSection
52527
+ });
52528
+ const dshFrontmatter = {
52529
+ name: rulesyncFrontmatter.name,
52530
+ description: rulesyncFrontmatter.description,
52531
+ ...dshSection?.whenToUse !== void 0 && { whenToUse: dshSection.whenToUse },
52532
+ ...metadata !== void 0 && { metadata },
52533
+ ...disableModelInvocation !== void 0 && { "disable-model-invocation": disableModelInvocation },
52534
+ ...userInvocable !== void 0 && { "user-invocable": userInvocable }
52535
+ };
52536
+ const settablePaths = DshSkill.getSettablePaths({ global });
52537
+ return new DshSkill({
52538
+ outputRoot,
52539
+ relativeDirPath: settablePaths.relativeDirPath,
52540
+ dirName: rulesyncSkill.getDirName(),
52541
+ frontmatter: dshFrontmatter,
52542
+ body: rulesyncSkill.getBody(),
52543
+ otherFiles: rulesyncSkill.getOtherFiles(),
52544
+ validate,
52545
+ global
52546
+ });
52547
+ }
52548
+ static isTargetedByRulesyncSkill(rulesyncSkill) {
52549
+ const targets = rulesyncSkill.getFrontmatter().targets;
52550
+ return targets.includes("*") || targets.includes("dsh");
52551
+ }
52552
+ static async fromDir(params) {
52553
+ const loaded = await this.loadSkillDirContent({
52554
+ ...params,
52555
+ getSettablePaths: DshSkill.getSettablePaths
52556
+ });
52557
+ const result = DshSkillFrontmatterSchema.safeParse(loaded.frontmatter);
52558
+ if (!result.success) {
52559
+ const skillDirPath = join(loaded.outputRoot, loaded.relativeDirPath, loaded.dirName);
52560
+ throw new Error(`Invalid frontmatter in ${join(skillDirPath, SKILL_FILE_NAME)}: ${formatError(result.error)}`);
52561
+ }
52562
+ return new DshSkill({
52563
+ outputRoot: loaded.outputRoot,
52564
+ relativeDirPath: loaded.relativeDirPath,
52565
+ dirName: loaded.dirName,
52566
+ frontmatter: result.data,
52567
+ body: loaded.body,
52568
+ otherFiles: loaded.otherFiles,
52569
+ validate: true,
52570
+ global: loaded.global
52571
+ });
52572
+ }
52573
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, dirName, global = false }) {
52574
+ return new DshSkill({
52575
+ outputRoot,
52576
+ relativeDirPath,
52577
+ dirName,
52578
+ frontmatter: {
52579
+ name: "",
52580
+ description: ""
52581
+ },
52582
+ body: "",
52583
+ otherFiles: [],
52584
+ validate: false,
52585
+ global
52586
+ });
52587
+ }
52588
+ };
52589
+ //#endregion
52265
52590
  //#region src/features/skills/factorydroid-skill.ts
52266
52591
  const FactorydroidSkillFrontmatterSchema = z.looseObject({
52267
52592
  name: z.string(),
@@ -54817,6 +55142,19 @@ const VibeSkillFrontmatterSchema = z.looseObject({
54817
55142
  "allowed-tools": z.optional(z.union([z.string(), z.array(z.string())]))
54818
55143
  });
54819
55144
  /**
55145
+ * Skill names Vibe's built-in skills occupy. `SkillManager` treats them as
55146
+ * reserved: a project or user skill whose frontmatter `name` matches one is
55147
+ * skipped at load time with only a debug log ("Skipping skill '<name>' ...
55148
+ * because builtin skill names are reserved"), so a generated skill by that
55149
+ * name is silently never offered. That is the legacy backend, still the
55150
+ * default; the unified harness constructs the manager with
55151
+ * `include_builtins=False` because its built-ins arrive as skills of the
55152
+ * shipped `vibe` plugin under namespaced names, so it reserves nothing.
55153
+ * @see https://github.com/mistralai/mistral-vibe/blob/v2.25.3/vibe/core/skills/builtins/__init__.py
55154
+ * @see https://github.com/mistralai/mistral-vibe/blob/v2.25.3/vibe/core/skills/manager.py
55155
+ */
55156
+ const VIBE_RESERVED_SKILL_NAMES = /* @__PURE__ */ new Set(["vibe", "skill-creator"]);
55157
+ /**
54820
55158
  * Build the Vibe frontmatter from a rulesync skill frontmatter, preferring the
54821
55159
  * dedicated `vibe` section over the shared root-level fields.
54822
55160
  */
@@ -54848,6 +55186,15 @@ function buildVibeFrontmatter(rulesyncFrontmatter) {
54848
55186
  ...vibeSection?.["allowed-tools"] !== void 0 && { "allowed-tools": vibeSection["allowed-tools"] }
54849
55187
  };
54850
55188
  }
55189
+ /**
55190
+ * The skill is still generated under the reserved name — the name is the
55191
+ * user's to change, and a rename here would make it diverge from the other
55192
+ * targets — but Vibe would drop it without a visible word, so say so now.
55193
+ */
55194
+ function warnAboutReservedSkillName({ name, logger }) {
55195
+ if (!VIBE_RESERVED_SKILL_NAMES.has(name)) return;
55196
+ 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.`);
55197
+ }
54851
55198
  var VibeSkill = class VibeSkill extends ToolSkill {
54852
55199
  constructor({ outputRoot = process.cwd(), relativeDirPath = VibeSkill.getSettablePaths().relativeDirPath, dirName, frontmatter, body, otherFiles = [], validate = true, global = false }) {
54853
55200
  super({
@@ -54920,9 +55267,13 @@ var VibeSkill = class VibeSkill extends ToolSkill {
54920
55267
  global: this.global
54921
55268
  });
54922
55269
  }
54923
- static fromRulesyncSkill({ outputRoot = process.cwd(), rulesyncSkill, validate = true, global = false }) {
55270
+ static fromRulesyncSkill({ outputRoot = process.cwd(), rulesyncSkill, validate = true, global = false, logger }) {
54924
55271
  const settablePaths = VibeSkill.getSettablePaths({ global });
54925
55272
  const vibeFrontmatter = buildVibeFrontmatter(rulesyncSkill.getFrontmatter());
55273
+ warnAboutReservedSkillName({
55274
+ name: vibeFrontmatter.name,
55275
+ logger
55276
+ });
54926
55277
  return new VibeSkill({
54927
55278
  outputRoot,
54928
55279
  relativeDirPath: settablePaths.relativeDirPath,
@@ -55802,6 +56153,14 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
55802
56153
  supportsSimulated: false,
55803
56154
  supportsGlobal: true
55804
56155
  }
56156
+ }],
56157
+ ["dsh", {
56158
+ class: DshSkill,
56159
+ meta: {
56160
+ supportsProject: true,
56161
+ supportsSimulated: false,
56162
+ supportsGlobal: true
56163
+ }
55805
56164
  }]
55806
56165
  ]);
55807
56166
  const defaultGetFactory$2 = (target) => {
@@ -56989,13 +57348,26 @@ var AntigravityPluginSubagent = class extends AntigravitySharedSubagent {
56989
57348
  };
56990
57349
  //#endregion
56991
57350
  //#region src/features/subagents/augmentcode-subagent.ts
57351
+ const AugmentcodeToolListSchema = z.union([z.array(z.string()), z.string()]);
57352
+ /**
57353
+ * Normalize a documented tool-list value to the list form: a string is split on
57354
+ * commas, trimmed, and emptied of blank entries; a list is returned as is. A
57355
+ * string that names no tool (`""`, `", ,"`) is treated as unset rather than as
57356
+ * an empty allowlist — the author left the value blank, not the tool set — so
57357
+ * it yields `undefined`; an authored empty list is kept as written.
57358
+ */
57359
+ function normalizeAugmentcodeToolList(value) {
57360
+ if (Array.isArray(value)) return value;
57361
+ const tools = value.split(",").map((tool) => tool.trim()).filter((tool) => tool.length > 0);
57362
+ return tools.length > 0 ? tools : void 0;
57363
+ }
56992
57364
  const AugmentcodeSubagentFrontmatterSchema = z.looseObject({
56993
57365
  name: z.string(),
56994
57366
  description: z.optional(z.string()),
56995
57367
  color: z.optional(z.string()),
56996
57368
  model: z.optional(z.string()),
56997
- tools: z.optional(z.array(z.string())),
56998
- disabled_tools: z.optional(z.array(z.string()))
57369
+ tools: z.optional(AugmentcodeToolListSchema),
57370
+ disabled_tools: z.optional(AugmentcodeToolListSchema)
56999
57371
  });
57000
57372
  /**
57001
57373
  * AugmentCode (Auggie CLI) subagents.
@@ -57034,14 +57406,24 @@ var AugmentcodeSubagent = class AugmentcodeSubagent extends ToolSubagent {
57034
57406
  return this.body;
57035
57407
  }
57036
57408
  toRulesyncSubagent() {
57037
- const { name, description, ...rest } = this.frontmatter;
57409
+ const { name, description, tools, disabled_tools, ...rest } = this.frontmatter;
57410
+ const normalizedTools = tools === void 0 ? void 0 : normalizeAugmentcodeToolList(tools);
57411
+ const normalizedDisabledTools = disabled_tools === void 0 ? void 0 : normalizeAugmentcodeToolList(disabled_tools);
57412
+ const toolLists = {
57413
+ ...normalizedTools !== void 0 && { tools: normalizedTools },
57414
+ ...normalizedDisabledTools !== void 0 && { disabled_tools: normalizedDisabledTools }
57415
+ };
57416
+ const augmentcodeSection = {
57417
+ ...rest,
57418
+ ...toolLists
57419
+ };
57038
57420
  return new RulesyncSubagent({
57039
57421
  outputRoot: ".",
57040
57422
  frontmatter: {
57041
57423
  targets: ["*"],
57042
57424
  name,
57043
57425
  description,
57044
- ...Object.keys(rest).length > 0 && { augmentcode: rest }
57426
+ ...Object.keys(augmentcodeSection).length > 0 && { augmentcode: augmentcodeSection }
57045
57427
  },
57046
57428
  body: this.body,
57047
57429
  relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH,
@@ -63354,13 +63736,40 @@ const CodebuddyRuleFrontmatterSchema = z.object({
63354
63736
  enabled: z.optional(z.boolean())
63355
63737
  });
63356
63738
  /**
63739
+ * Splits a scalar `paths` value on the commas that separate patterns, leaving
63740
+ * the commas inside a brace group alone: CodeBuddy's memory docs say "You can
63741
+ * also combine multiple patterns with commas" and give
63742
+ * `paths: {src,lib}/**\/*.ts, tests/**\/*.test.ts` as the example, where the
63743
+ * first comma belongs to the brace expansion and the second separates the two
63744
+ * globs. Only a scalar is split — the docs describe the comma form for the
63745
+ * string shape, and a list already separates its patterns.
63746
+ */
63747
+ function splitCodebuddyPathsScalar(paths) {
63748
+ const patterns = [];
63749
+ let current = "";
63750
+ let braceDepth = 0;
63751
+ for (const char of paths) {
63752
+ if (char === "{") braceDepth += 1;
63753
+ else if (char === "}" && braceDepth > 0) braceDepth -= 1;
63754
+ else if (char === "," && braceDepth === 0) {
63755
+ patterns.push(current);
63756
+ current = "";
63757
+ continue;
63758
+ }
63759
+ current += char;
63760
+ }
63761
+ patterns.push(current);
63762
+ return patterns.map((pattern) => pattern.trim());
63763
+ }
63764
+ /**
63357
63765
  * Normalizes the documented `string` / `string[]` shapes of `paths` to the
63358
- * list form the rest of the adapter works with. An empty list and an empty
63359
- * string both mean "no paths".
63766
+ * list form the rest of the adapter works with, splitting a comma-separated
63767
+ * scalar into its patterns (see `splitCodebuddyPathsScalar`). An empty list
63768
+ * and an empty string both mean "no paths".
63360
63769
  */
63361
63770
  function normalizeCodebuddyPaths(paths) {
63362
63771
  if (paths === void 0) return;
63363
- const list = (typeof paths === "string" ? [paths] : paths).filter((path) => path.trim() !== "");
63772
+ const list = (typeof paths === "string" ? splitCodebuddyPathsScalar(paths) : paths).filter((path) => path.trim() !== "");
63364
63773
  return list.length > 0 ? list : void 0;
63365
63774
  }
63366
63775
  /**
@@ -64594,6 +65003,125 @@ var DevinRule = class DevinRule extends ToolRule {
64594
65003
  }
64595
65004
  };
64596
65005
  //#endregion
65006
+ //#region src/features/rules/dsh-rule.ts
65007
+ /**
65008
+ * DeepSeek Harness (`dsh`) rules.
65009
+ *
65010
+ * Project scope writes the root `AGENTS.md` plus nested `<dir>/AGENTS.md`
65011
+ * files — the project chain `dsh-agent-instructions` loads from the `.git`
65012
+ * root down to the session cwd. Global scope writes the user-global
65013
+ * `~/.dsh/AGENTS.md` (`$DSH_HOME` default), which the harness reads as a
65014
+ * single root file with no local overlay, so non-root rules fold into it.
65015
+ *
65016
+ * @see https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/context/agent-instructions/README.md
65017
+ */
65018
+ var DshRule = class DshRule extends ToolRule {
65019
+ static getSettablePaths({ global = false } = {}) {
65020
+ if (global) return { root: {
65021
+ relativeDirPath: DSH_DIR,
65022
+ relativeFilePath: AGENTSMD_RULE_FILE_NAME
65023
+ } };
65024
+ return { root: {
65025
+ relativeDirPath: ".",
65026
+ relativeFilePath: AGENTSMD_RULE_FILE_NAME
65027
+ } };
65028
+ }
65029
+ /**
65030
+ * DeepSeek Harness loads the project chain of `AGENTS.md` files from the
65031
+ * project root (the nearest ancestor holding `.git`) down through the session
65032
+ * cwd, broad to specific, so a per-directory file applies only while working
65033
+ * under that directory. Nested files are therefore a real scoping surface,
65034
+ * not just the root file's overflow.
65035
+ *
65036
+ * The scan mirrors the AGENTS.md standard's nested discovery — same file
65037
+ * name, same exclusions, import-only, project scope — because it discovers
65038
+ * literally the same files.
65039
+ * @see https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/context/agent-instructions/README.md
65040
+ */
65041
+ static getNestedFilePatterns() {
65042
+ return this.buildNestedFilePatterns({ fileName: AGENTSMD_RULE_FILE_NAME });
65043
+ }
65044
+ /**
65045
+ * The subproject directory this rule scopes, or `undefined` for the root file
65046
+ * (project or global).
65047
+ */
65048
+ getSubprojectPath() {
65049
+ return this.getNestedSubprojectPath({ fileName: AGENTSMD_RULE_FILE_NAME });
65050
+ }
65051
+ static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, relativeDirPath: overrideDirPath, validate = true, global = false }) {
65052
+ const { root } = this.getSettablePaths({ global });
65053
+ if (overrideDirPath !== void 0 && overrideDirPath !== root.relativeDirPath && overrideDirPath !== ".") {
65054
+ const fileContent = await readFileContent(join(outputRoot, overrideDirPath, AGENTSMD_RULE_FILE_NAME));
65055
+ return new DshRule({
65056
+ outputRoot,
65057
+ relativeDirPath: overrideDirPath,
65058
+ relativeFilePath: AGENTSMD_RULE_FILE_NAME,
65059
+ fileContent,
65060
+ validate,
65061
+ root: false
65062
+ });
65063
+ }
65064
+ const fileContent = await readFileContent(join(outputRoot, root.relativeDirPath, root.relativeFilePath));
65065
+ return new DshRule({
65066
+ outputRoot,
65067
+ relativeDirPath: root.relativeDirPath,
65068
+ relativeFilePath: root.relativeFilePath,
65069
+ fileContent,
65070
+ validate,
65071
+ root: true
65072
+ });
65073
+ }
65074
+ static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
65075
+ const { root } = this.getSettablePaths({ global });
65076
+ const frontmatter = rulesyncRule.getFrontmatter();
65077
+ const isRoot = frontmatter.root ?? false;
65078
+ const subprojectPath = frontmatter.agentsmd?.subprojectPath;
65079
+ if (!global && !isRoot && subprojectPath) return new DshRule({
65080
+ outputRoot,
65081
+ relativeDirPath: join(subprojectPath),
65082
+ relativeFilePath: AGENTSMD_RULE_FILE_NAME,
65083
+ fileContent: rulesyncRule.getBody(),
65084
+ validate,
65085
+ root: false
65086
+ });
65087
+ return new DshRule({
65088
+ outputRoot,
65089
+ relativeDirPath: root.relativeDirPath,
65090
+ relativeFilePath: root.relativeFilePath,
65091
+ fileContent: rulesyncRule.getBody(),
65092
+ validate,
65093
+ root: isRoot
65094
+ });
65095
+ }
65096
+ toRulesyncRule() {
65097
+ const subprojectPath = this.getSubprojectPath();
65098
+ if (subprojectPath === void 0) return this.toRulesyncRuleDefault();
65099
+ return this.toRulesyncRuleNestedAgentsmd({ subprojectPath });
65100
+ }
65101
+ validate() {
65102
+ return {
65103
+ success: true,
65104
+ error: null
65105
+ };
65106
+ }
65107
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
65108
+ return new DshRule({
65109
+ outputRoot,
65110
+ relativeDirPath,
65111
+ relativeFilePath,
65112
+ fileContent: "",
65113
+ validate: false,
65114
+ root: relativeFilePath === "AGENTS.md" && (relativeDirPath === "." || relativeDirPath === ".dsh")
65115
+ });
65116
+ }
65117
+ static isTargetedByRulesyncRule(rulesyncRule) {
65118
+ return this.isTargetedByRulesyncRuleDefault({
65119
+ rulesyncRule,
65120
+ toolTarget: "dsh"
65121
+ });
65122
+ }
65123
+ };
65124
+ //#endregion
64597
65125
  //#region src/features/rules/factorydroid-rule.ts
64598
65126
  /**
64599
65127
  * Rule generator for Factory Droid.
@@ -64602,27 +65130,36 @@ var DevinRule = class DevinRule extends ToolRule {
64602
65130
  * (global) as coding guidelines, plus non-root rules referenced from it via
64603
65131
  * `.factory/rules/*.md`.
64604
65132
  *
64605
- * Factory Droid also loads `DESIGN.md` (project only) as a second,
64606
- * independent instruction surface: "Always-on design-system, UX, visual, and
64607
- * interaction guidance", loaded separately from `AGENTS.md`'s coding
64608
- * guidelines. Rulesync emits it from any non-root rule that opts in via a
64609
- * `factorydroid.channel: design` frontmatter block those rule bodies are
64610
- * routed to `DESIGN.md` instead of `AGENTS.md`/`.factory/rules/*.md`, and
64611
- * multiple opted-in rules concatenate in source order. Factory's docs describe
64612
- * `DESIGN.md` at the repository root and in nested subdirectories, like
64613
- * `AGENTS.md`, but document no personal/global home-directory equivalent, so
64614
- * this channel is project scope only.
65133
+ * Factory Droid also loads two further fixed files (project only) as
65134
+ * independent instruction surfaces, which rulesync emits from any non-root
65135
+ * rule that opts in via a `factorydroid.channel` frontmatter key. Opted-in
65136
+ * rule bodies are routed to the channel's file instead of
65137
+ * `AGENTS.md`/`.factory/rules/*.md`, and multiple opted-in rules concatenate
65138
+ * in source order:
65139
+ *
65140
+ * - `design` → `DESIGN.md`: "Always-on design-system, UX, visual, and
65141
+ * interaction guidance", loaded separately from `AGENTS.md`'s coding
65142
+ * guidelines. Factory's docs describe `DESIGN.md` at the repository root and
65143
+ * in nested subdirectories, like `AGENTS.md`, but document no
65144
+ * personal/global home-directory equivalent.
65145
+ * - `threat-model` → `.factory/threat-model.md`: the attack-surface map
65146
+ * Factory's Security Review reads — "if `.factory/threat-model.md` exists,
65147
+ * Droid uses it as the attack-surface map". It is documented only as a
65148
+ * repository file, so it has no global scope either.
65149
+ *
65150
+ * Both channels are therefore project scope only.
64615
65151
  * @see https://docs.factory.ai/cli/configuration/agents-md
65152
+ * @see https://docs.factory.ai/software-factory/security-review
64616
65153
  */
64617
65154
  var FactorydroidRule = class FactorydroidRule extends ToolRule {
64618
- design;
64619
- constructor({ fileContent, root, design = false, ...rest }) {
65155
+ channel;
65156
+ constructor({ fileContent, root, channel, ...rest }) {
64620
65157
  super({
64621
65158
  ...rest,
64622
65159
  fileContent,
64623
65160
  root: root ?? false
64624
65161
  });
64625
- this.design = design;
65162
+ this.channel = channel;
64626
65163
  }
64627
65164
  static getSettablePaths({ global, excludeToolDir } = {}) {
64628
65165
  if (global) return { root: {
@@ -64638,41 +65175,75 @@ var FactorydroidRule = class FactorydroidRule extends ToolRule {
64638
65175
  design: {
64639
65176
  relativeDirPath: ".",
64640
65177
  relativeFilePath: FACTORYDROID_DESIGN_FILE_NAME
65178
+ },
65179
+ threatModel: {
65180
+ relativeDirPath: buildToolPath(FACTORYDROID_DIR, ".", excludeToolDir),
65181
+ relativeFilePath: FACTORYDROID_THREAT_MODEL_FILE_NAME
64641
65182
  }
64642
65183
  };
64643
65184
  }
64644
65185
  /**
65186
+ * The channel files in a fixed order, so that `getExtraFixedFiles` and the
65187
+ * channel lookups below agree on which paths are channels. Empty in global
65188
+ * mode, where neither file has a documented home-directory equivalent.
65189
+ */
65190
+ static getChannelPaths({ global }) {
65191
+ if (global) return [];
65192
+ const paths = this.getSettablePaths({ global });
65193
+ return [{
65194
+ channel: "design",
65195
+ path: paths.design
65196
+ }, {
65197
+ channel: "threat-model",
65198
+ path: paths.threatModel
65199
+ }];
65200
+ }
65201
+ /**
65202
+ * Which channel, if any, owns the given output path. Matching on
65203
+ * `relativeDirPath` too (not just the basename) keeps a non-root rule that
65204
+ * happens to be named `DESIGN.md` or `threat-model.md` under
65205
+ * `.factory/rules/` from being routed to a channel by mistake.
65206
+ */
65207
+ static findChannelByPath({ relativeDirPath, relativeFilePath, global }) {
65208
+ return this.getChannelPaths({ global }).find(({ path }) => relativeDirPath === path.relativeDirPath && relativeFilePath === path.relativeFilePath);
65209
+ }
65210
+ /**
64645
65211
  * Extra fixed files this tool manages beyond the root/non-root rules. The
64646
65212
  * RulesProcessor enumerates these for import and deletion so a stale
64647
- * `DESIGN.md` is cleaned up once no rule opts in anymore. Empty in global
64648
- * mode: `DESIGN.md` has no documented home-directory equivalent.
65213
+ * `DESIGN.md` or `.factory/threat-model.md` is cleaned up once no rule opts
65214
+ * in anymore. Empty in global mode: neither file has a documented
65215
+ * home-directory equivalent.
64649
65216
  */
64650
65217
  static getExtraFixedFiles({ global = false } = {}) {
64651
- if (global) return [];
64652
- return [this.getSettablePaths({ global }).design];
65218
+ return this.getChannelPaths({ global }).map(({ path }) => path);
64653
65219
  }
64654
65220
  /**
64655
- * Factory Droid loads `DESIGN.md` itself, so listing it in the root rule's
64656
- * TOON reference section would double-load the content (and misrepresent it
64657
- * as a rule the model must remember to open).
65221
+ * Factory Droid loads the channel files itself, so listing one in the root
65222
+ * rule's TOON reference section would double-load the content (and
65223
+ * misrepresent it as a rule the model must remember to open).
64658
65224
  */
64659
65225
  isExcludedFromRootReferences() {
64660
- return this.design;
65226
+ return this.channel !== void 0;
64661
65227
  }
64662
65228
  static async fromFile({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, validate = true, global = false }) {
64663
65229
  const paths = this.getSettablePaths({ global });
64664
- const design = !global ? paths.design : void 0;
64665
- if (design !== void 0 && relativeDirPath === design.relativeDirPath && relativeFilePath === design.relativeFilePath) {
64666
- const relativePath = join(design.relativeDirPath, design.relativeFilePath);
65230
+ const channelMatch = this.findChannelByPath({
65231
+ relativeDirPath,
65232
+ relativeFilePath,
65233
+ global
65234
+ });
65235
+ if (channelMatch) {
65236
+ const { channel, path } = channelMatch;
65237
+ const relativePath = join(path.relativeDirPath, path.relativeFilePath);
64667
65238
  const fileContent = await readFileContent(join(outputRoot, relativePath));
64668
65239
  return new FactorydroidRule({
64669
65240
  outputRoot,
64670
- relativeDirPath: design.relativeDirPath,
64671
- relativeFilePath: design.relativeFilePath,
65241
+ relativeDirPath: path.relativeDirPath,
65242
+ relativeFilePath: path.relativeFilePath,
64672
65243
  fileContent,
64673
65244
  validate,
64674
65245
  root: false,
64675
- design: true
65246
+ channel
64676
65247
  });
64677
65248
  }
64678
65249
  if (relativeFilePath === paths.root.relativeFilePath) {
@@ -64701,9 +65272,12 @@ var FactorydroidRule = class FactorydroidRule extends ToolRule {
64701
65272
  }
64702
65273
  static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
64703
65274
  const paths = this.getSettablePaths({ global });
64704
- const design = !global ? paths.design : void 0;
64705
- const isDesign = design !== void 0 && relativeDirPath === design.relativeDirPath && relativeFilePath === design.relativeFilePath;
64706
- const isRoot = !isDesign && relativeFilePath === paths.root.relativeFilePath && relativeDirPath === paths.root.relativeDirPath;
65275
+ const channel = this.findChannelByPath({
65276
+ relativeDirPath,
65277
+ relativeFilePath,
65278
+ global
65279
+ })?.channel;
65280
+ const isRoot = channel === void 0 && relativeFilePath === paths.root.relativeFilePath && relativeDirPath === paths.root.relativeDirPath;
64707
65281
  return new FactorydroidRule({
64708
65282
  outputRoot,
64709
65283
  relativeDirPath,
@@ -64711,22 +65285,24 @@ var FactorydroidRule = class FactorydroidRule extends ToolRule {
64711
65285
  fileContent: "",
64712
65286
  validate: false,
64713
65287
  root: isRoot,
64714
- design: isDesign
65288
+ channel
64715
65289
  });
64716
65290
  }
64717
65291
  static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
64718
65292
  const frontmatter = rulesyncRule.getFrontmatter();
64719
65293
  const paths = this.getSettablePaths({ global });
64720
- if (!global && !frontmatter.root && frontmatter.factorydroid?.channel === "design") {
64721
- const { design } = paths;
65294
+ const requestedChannel = frontmatter.factorydroid?.channel;
65295
+ const channelMatch = !global && !frontmatter.root && requestedChannel !== void 0 ? this.getChannelPaths({ global }).find(({ channel }) => channel === requestedChannel) : void 0;
65296
+ if (channelMatch) {
65297
+ const { channel, path } = channelMatch;
64722
65298
  return new FactorydroidRule({
64723
65299
  outputRoot,
64724
- relativeDirPath: design.relativeDirPath,
64725
- relativeFilePath: design.relativeFilePath,
65300
+ relativeDirPath: path.relativeDirPath,
65301
+ relativeFilePath: path.relativeFilePath,
64726
65302
  fileContent: rulesyncRule.getBody(),
64727
65303
  validate,
64728
65304
  root: false,
64729
- design: true
65305
+ channel
64730
65306
  });
64731
65307
  }
64732
65308
  return new FactorydroidRule(this.buildToolRuleParamsAgentsmd({
@@ -64738,14 +65314,14 @@ var FactorydroidRule = class FactorydroidRule extends ToolRule {
64738
65314
  }));
64739
65315
  }
64740
65316
  toRulesyncRule() {
64741
- if (this.design) return new RulesyncRule({
65317
+ if (this.channel !== void 0) return new RulesyncRule({
64742
65318
  outputRoot: process.cwd(),
64743
65319
  relativeDirPath: RULESYNC_RULES_RELATIVE_DIR_PATH,
64744
- relativeFilePath: FACTORYDROID_DESIGN_FILE_NAME,
65320
+ relativeFilePath: this.getRelativeFilePath(),
64745
65321
  frontmatter: {
64746
65322
  root: false,
64747
65323
  targets: ["factorydroid"],
64748
- factorydroid: { channel: "design" }
65324
+ factorydroid: { channel: this.channel }
64749
65325
  },
64750
65326
  body: this.getFileContent()
64751
65327
  });
@@ -67712,6 +68288,15 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
67712
68288
  ruleDiscoveryMode: "auto",
67713
68289
  collisionPolicy: "fold"
67714
68290
  }
68291
+ }],
68292
+ ["dsh", {
68293
+ class: DshRule,
68294
+ meta: {
68295
+ extension: "md",
68296
+ supportsGlobal: true,
68297
+ ruleDiscoveryMode: "auto",
68298
+ collisionPolicy: "fold"
68299
+ }
67715
68300
  }]
67716
68301
  ]);
67717
68302
  const allToolTargetKeys = [...toolRuleFactories.keys()];
@@ -70811,4 +71396,4 @@ async function importChecksCore(params) {
70811
71396
  //#endregion
70812
71397
  export { RulesyncCheck as $, ALL_TOOL_TARGETS as $t, CLAUDECODE_SKILLS_DIR_PATH as A, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as An, ensureDir as At, RulesyncSkill as B, quoteForLog as Bn, pathEscapesRoot as Bt, ChecksProcessor as C, RULESYNC_PERMISSIONS_FILE_NAME as Cn, applyFileMode as Ct, CLAUDECODE_LOCAL_RULE_FILE_NAME as D, RULESYNC_RELATIVE_DIR_PATH as Dn, checkPathTraversal as Dt, CLAUDECODE_DIR as E, RULESYNC_PERMISSIONS_SCHEMA_URL as En, assertWritablePathInsideRoot as Et, AUGMENTCODE_DIR as F, DEPRECATED_FEATURE_REPLACEMENTS as Fn, isFileSystemError as Ft, RulesyncMcp as G, removeFile as Gt, RulesyncRule as H, stripControlCharactersKeepingLineFeeds as Hn, readFileContentOrNull as Ht, AUGMENTCODE_SETTINGS_LOCAL_FILE_NAME as I, formatError as In, isSymlink as It, getRulesyncSourceCandidates as J, resolvePath as Jt, RulesyncIgnore as K, removeFileStrict as Kt, getLocalSkillDirNames as L, truncateText as Ln, listDirectoryEntryNames as Lt, FACTORYDROID_SETTINGS_LOCAL_FILE_NAME as M, parseCommaSeparatedList as Mn, getFileSize as Mt, caseFoldIdentity as N, ALL_FEATURES as Nn, getHomeDirectory as Nt, CLAUDECODE_MEMORIES_DIR_NAME as O, RULESYNC_RULES_RELATIVE_DIR_PATH as On, createTempDirectory as Ot, groupSpellingsByCaseFoldedIdentity as P, ALL_FEATURES_WITH_WILDCARD as Pn, isFileNotFoundError as Pt, RulesyncCommandFrontmatterSchema as Q, writeFileContent as Qt, RulesyncSubagent as R, hasDeceptiveHiddenCharacters as Rn, listFilePathsRecursively as Rt, QWENCODE_LOCAL_RULE_FILE_NAME as S, RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH as Sn, ErrorCodes as St, CODEXCLI_DIR as T, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as Tn, assertTreeContainsNoSymlinks as Tt, RulesyncRuleFrontmatterSchema as U, stripHiddenCharacters as Un, removeDirectory as Ut, RulesyncSkillFrontmatterSchema as V, stripControlCharacters as Vn, readFileContent as Vt, RulesyncPermissions as W, removeDirectoryStrict as Wt, parseJsonc as X, toPosixPath as Xt, resolveRulesyncSourceWritePath as Y, runWithDirectoryRollback as Yt, RulesyncCommand as Z, writeFileBuffer as Zt, IgnoreProcessor as _, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as _n, warnOnConflictingFlags as _t, getProcessorRegistryEntry as a, RULESYNC_AIIGNORE_FILE_NAME as an, ConfigResolver as at, CommandsProcessor as b, RULESYNC_MCP_RELATIVE_FILE_PATH as bn, withWarnOnceScope as bt, RulesProcessor as c, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as cn, CONFLICTING_TARGET_PAIRS as ct, CODEBUDDY_DIR as d, RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH as dn, SourceEntrySchema as dt, ALL_TOOL_TARGETS_WITH_WILDCARD as en, RulesyncCheckFrontmatterSchema as et, CODEBUDDY_LOCAL_RULE_FILE_NAME as f, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as fn, findControlCharacter as ft, McpProcessor as g, RULESYNC_IGNORE_RELATIVE_FILE_PATH as gn, fallbackLogger as gt, shortenToWidth as h, RULESYNC_HOOKS_RELATIVE_FILE_PATH as hn, WarningCollectingLogger as ht, inspectInputRoots as i, MAX_FILE_SIZE as in, SKILL_FILE_NAME as it, FACTORYDROID_DIR as j, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as jn, fileExists as jt, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as k, RULESYNC_SKILLS_RELATIVE_DIR_PATH as kn, directoryExists as kt, SubagentsProcessor as l, RULESYNC_CONFIG_RELATIVE_FILE_PATH as ln, ConfigFileSchema as lt, displayWidthOf as m, RULESYNC_HOOKS_LEGACY_FILE_NAME as mn, JsonLogger as mt, formatSourceLoadFailure as n, ToolTargetSchema as nn, loadYaml as nt, convertFromTool as o, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as on, mergeInputRootConfigs as ot, ELLIPSIS_WIDTH as p, RULESYNC_HOOKS_FILE_NAME as pn, ConsoleLogger as pt, RulesyncHooks as q, removeTempDirectory as qt, generate as r, CURATED_RULES_FEATURE_SUBDIR as rn, SHARED_USER_MANAGED_CONFIG_PATHS as rt, isPackagingToolTarget as s, RULESYNC_CHECKS_RELATIVE_DIR_PATH as sn, resolveEffectiveInputRoots as st, importFromTool as t, PACKAGING_TOOL_TARGETS as tn, stringifyFrontmatter as tt, SkillsProcessor as u, RULESYNC_CONFIG_SCHEMA_URL as un, GITIGNORE_DESTINATION_KEY as ut, CRUSH_LOCAL_RULE_FILE_NAME as v, RULESYNC_MCP_FILE_NAME as vn, withFallbackLoggerTarget as vt, CODEXCLI_BASH_RULES_FILE_NAME as w, RULESYNC_PERMISSIONS_LEGACY_FILE_NAME as wn, assertDirectoryIfExists as wt, QWENCODE_DIR as x, RULESYNC_MCP_SCHEMA_URL as xn, CLIError as xt, HooksProcessor as y, RULESYNC_MCP_LEGACY_FILE_NAME as yn, resetRunWarningState as yt, RulesyncSubagentFrontmatterSchema as z, hasEnclosingMarkOutsideKeycap as zn, listSubdirectoryNames as zt };
70813
71398
 
70814
- //# sourceMappingURL=import-DFeuBVD1.js.map
71399
+ //# sourceMappingURL=import-BDlmyTcr.js.map