rulesync 30.0.0 → 32.0.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.
@@ -453,36 +453,36 @@ const isFeatureValueEnabled = (value) => {
453
453
  const parseCommaSeparatedList = (value) => value.split(",").map((s) => s.trim()).filter(Boolean);
454
454
  //#endregion
455
455
  //#region src/constants/rulesync-paths.ts
456
- const { join: join$416 } = node_path.posix;
456
+ const { join: join$417 } = node_path.posix;
457
457
  const RULESYNC_CONFIG_RELATIVE_FILE_PATH = "rulesync.jsonc";
458
458
  const RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH = "rulesync.local.jsonc";
459
459
  const RULESYNC_USER_CONFIG_DIR_NAME = "rulesync";
460
460
  const RULESYNC_XDG_CONFIG_HOME_DEFAULT_DIR_NAME = ".config";
461
461
  const RULESYNC_RELATIVE_DIR_PATH = ".rulesync";
462
462
  const RULES_FEATURE_SUBDIR = "rules";
463
- const CURATED_RULES_FEATURE_SUBDIR = join$416(RULES_FEATURE_SUBDIR, ".curated");
463
+ const CURATED_RULES_FEATURE_SUBDIR = join$417(RULES_FEATURE_SUBDIR, ".curated");
464
464
  const COMMANDS_FEATURE_SUBDIR = "commands";
465
465
  const SUBAGENTS_FEATURE_SUBDIR = "subagents";
466
466
  const CHECKS_FEATURE_SUBDIR = "checks";
467
467
  const SKILLS_FEATURE_SUBDIR = "skills";
468
- const CURATED_SKILLS_FEATURE_SUBDIR = join$416(SKILLS_FEATURE_SUBDIR, ".curated");
469
- const RULESYNC_RULES_RELATIVE_DIR_PATH = join$416(RULESYNC_RELATIVE_DIR_PATH, RULES_FEATURE_SUBDIR);
470
- const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$416(RULESYNC_RELATIVE_DIR_PATH, CURATED_RULES_FEATURE_SUBDIR);
471
- const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$416(RULESYNC_RELATIVE_DIR_PATH, COMMANDS_FEATURE_SUBDIR);
472
- const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$416(RULESYNC_RELATIVE_DIR_PATH, SUBAGENTS_FEATURE_SUBDIR);
473
- const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$416(RULESYNC_RELATIVE_DIR_PATH, CHECKS_FEATURE_SUBDIR);
474
- const RULESYNC_MCP_RELATIVE_FILE_PATH = join$416(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
475
- const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$416(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
476
- const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$416(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
477
- join$416(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
478
- const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$416(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
479
- const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$416(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
468
+ const CURATED_SKILLS_FEATURE_SUBDIR = join$417(SKILLS_FEATURE_SUBDIR, ".curated");
469
+ const RULESYNC_RULES_RELATIVE_DIR_PATH = join$417(RULESYNC_RELATIVE_DIR_PATH, RULES_FEATURE_SUBDIR);
470
+ const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$417(RULESYNC_RELATIVE_DIR_PATH, CURATED_RULES_FEATURE_SUBDIR);
471
+ const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$417(RULESYNC_RELATIVE_DIR_PATH, COMMANDS_FEATURE_SUBDIR);
472
+ const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$417(RULESYNC_RELATIVE_DIR_PATH, SUBAGENTS_FEATURE_SUBDIR);
473
+ const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$417(RULESYNC_RELATIVE_DIR_PATH, CHECKS_FEATURE_SUBDIR);
474
+ const RULESYNC_MCP_RELATIVE_FILE_PATH = join$417(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
475
+ const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$417(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
476
+ const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$417(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
477
+ join$417(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
478
+ const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$417(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
479
+ const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$417(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
480
480
  const RULESYNC_AIIGNORE_FILE_NAME = ".aiignore";
481
- const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$416(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
481
+ const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$417(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
482
482
  const RULESYNC_IGNORE_RELATIVE_FILE_PATH = ".rulesyncignore";
483
483
  const RULESYNC_OVERVIEW_FILE_NAME = "overview.md";
484
- const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$416(RULESYNC_RELATIVE_DIR_PATH, SKILLS_FEATURE_SUBDIR);
485
- const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$416(RULESYNC_RELATIVE_DIR_PATH, CURATED_SKILLS_FEATURE_SUBDIR);
484
+ const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$417(RULESYNC_RELATIVE_DIR_PATH, SKILLS_FEATURE_SUBDIR);
485
+ const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$417(RULESYNC_RELATIVE_DIR_PATH, CURATED_SKILLS_FEATURE_SUBDIR);
486
486
  const RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync.lock";
487
487
  const RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync-npm.lock.json";
488
488
  const RULESYNC_MCP_FILE_NAME = "mcp.jsonc";
@@ -935,6 +935,7 @@ const checksProcessorToolTargetTuple = [
935
935
  "cursor",
936
936
  "factorydroid",
937
937
  "gitlabduo",
938
+ "goose",
938
939
  "hermesagent",
939
940
  "kilo",
940
941
  "qwencode",
@@ -4844,13 +4845,11 @@ const PI_HOOK_EVENTS = [
4844
4845
  /**
4845
4846
  * Hook events supported by oh-my-pi (`omp`), Pi's fork, through the same
4846
4847
  * generated TypeScript extension. oh-my-pi keeps Pi's names for every event
4847
- * listed here, but it has neither `agent_before_settle` nor `ui_prompt_start`,
4848
- * so `stop` and `notification` are not mapped yet. oh-my-pi's stop surface is
4849
- * `session_stop`, which takes a Claude-compatible `{ decision: "block", reason }`
4850
- * result; it needs its own handler in the generator and is left for a
4851
- * follow-up. See CANONICAL_TO_OMP_EVENT_NAMES.
4848
+ * listed here except `stop`: it has no `agent_before_settle`, so `stop` maps to
4849
+ * its own `session_stop` event instead. It has no `ui_prompt_start` either, so
4850
+ * `notification` is not mapped. See CANONICAL_TO_OMP_EVENT_NAMES.
4852
4851
  *
4853
- * @see https://github.com/can1357/oh-my-pi/blob/40e9368ef0458fd9073329cdff4174895f91bc6b/docs/extensions.md
4852
+ * @see https://github.com/can1357/oh-my-pi/blob/fa5ff4a3a977a28b93d025f4cf50f046e019f18c/docs/extensions.md
4854
4853
  */
4855
4854
  const OMP_HOOK_EVENTS = [
4856
4855
  "sessionStart",
@@ -4861,6 +4860,7 @@ const OMP_HOOK_EVENTS = [
4861
4860
  "preModelInvocation",
4862
4861
  "postModelInvocation",
4863
4862
  "beforeSubmitPrompt",
4863
+ "stop",
4864
4864
  "preCompact",
4865
4865
  "postCompact"
4866
4866
  ];
@@ -6282,9 +6282,10 @@ const CANONICAL_TO_PI_EVENT_NAMES = {
6282
6282
  /**
6283
6283
  * Map canonical camelCase event names to oh-my-pi extension event names. They
6284
6284
  * are Pi's names (see CANONICAL_TO_PI_EVENT_NAMES) for the events oh-my-pi
6285
- * kept. Note that oh-my-pi runs `session_shutdown` handlers with a 2-second
6286
- * budget, and fails a `tool_call` closed when its handler exceeds
6287
- * `extensionHandlers.toolCallTimeoutMs` (30 seconds by default).
6285
+ * kept, plus `session_stop` for `stop`. Note that oh-my-pi runs
6286
+ * `session_shutdown` handlers with a 2-second budget, and fails a `tool_call`
6287
+ * closed when its handler exceeds `extensionHandlers.toolCallTimeoutMs`
6288
+ * (30 seconds by default).
6288
6289
  *
6289
6290
  * @see https://github.com/can1357/oh-my-pi/blob/40e9368ef0458fd9073329cdff4174895f91bc6b/docs/extensions.md
6290
6291
  */
@@ -6297,6 +6298,7 @@ const CANONICAL_TO_OMP_EVENT_NAMES = {
6297
6298
  preModelInvocation: "context",
6298
6299
  postModelInvocation: "message_end",
6299
6300
  beforeSubmitPrompt: "input",
6301
+ stop: "session_stop",
6300
6302
  preCompact: "session_before_compact",
6301
6303
  postCompact: "session_compact"
6302
6304
  };
@@ -12954,10 +12956,16 @@ const SHARED_CONFIG_OWNERSHIP = {
12954
12956
  [CODEWHALE_CONFIG_SHARED_FILE_KEY]: {
12955
12957
  format: "toml",
12956
12958
  invalidRootPolicy: "error",
12957
- features: { hooks: {
12958
- kind: "replace-owned-keys",
12959
- ownedKeys: ["hooks"]
12960
- } }
12959
+ features: {
12960
+ hooks: {
12961
+ kind: "replace-owned-keys",
12962
+ ownedKeys: ["hooks"]
12963
+ },
12964
+ permissions: {
12965
+ kind: "replace-owned-keys",
12966
+ ownedKeys: ["allow_shell"]
12967
+ }
12968
+ }
12961
12969
  },
12962
12970
  ".letta/settings.json": {
12963
12971
  format: "json",
@@ -14221,12 +14229,12 @@ const AmpCheckFrontmatterSchema = zod_mini.z.looseObject({
14221
14229
  * this format continue to apply.
14222
14230
  * @see https://ampcode.com/docs/review
14223
14231
  */
14224
- var AmpCheck = class AmpCheck extends ToolCheck {
14232
+ var AmpCheck = class extends ToolCheck {
14225
14233
  frontmatter;
14226
14234
  body;
14227
14235
  constructor({ frontmatter, body, fileContent, ...rest }) {
14228
14236
  if (rest.validate !== false) {
14229
- const result = AmpCheckFrontmatterSchema.safeParse(frontmatter);
14237
+ const result = new.target.getFrontmatterSchema().safeParse(frontmatter);
14230
14238
  if (!result.success) throw new Error(`Invalid frontmatter in ${(0, node_path.join)(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}`);
14231
14239
  }
14232
14240
  super({
@@ -14236,6 +14244,26 @@ var AmpCheck = class AmpCheck extends ToolCheck {
14236
14244
  this.frontmatter = frontmatter;
14237
14245
  this.body = body;
14238
14246
  }
14247
+ /**
14248
+ * The tool target this check format belongs to, which is also the name of
14249
+ * its tool-specific frontmatter section. Goose reads the same per-file
14250
+ * format with a few extra keys, so `GooseCheck` reuses this class through
14251
+ * these hooks.
14252
+ */
14253
+ static getCheckToolTarget() {
14254
+ return "amp";
14255
+ }
14256
+ /** The frontmatter schema a check file of this tool is validated against. */
14257
+ static getFrontmatterSchema() {
14258
+ return AmpCheckFrontmatterSchema;
14259
+ }
14260
+ /**
14261
+ * Frontmatter keys only this tool reads. Import moves them into the tool's
14262
+ * own section so they are not passed on to every other checks target.
14263
+ */
14264
+ static getToolOnlyKeys() {
14265
+ return [];
14266
+ }
14239
14267
  static getSettablePaths({ global = false } = {}) {
14240
14268
  return { relativeDirPath: global ? AMP_CHECKS_GLOBAL_DIR : AMP_CHECKS_PROJECT_DIR };
14241
14269
  }
@@ -14247,6 +14275,10 @@ var AmpCheck = class AmpCheck extends ToolCheck {
14247
14275
  }
14248
14276
  toRulesyncCheck() {
14249
14277
  const { name: _name, description, "severity-default": severityDefault, tools, ...restFields } = this.frontmatter;
14278
+ const checkClass = this.constructor;
14279
+ const toolOnlyKeys = new Set(checkClass.getToolOnlyKeys());
14280
+ const sharedFields = Object.fromEntries(Object.entries(restFields).filter(([key]) => !toolOnlyKeys.has(key)));
14281
+ const toolSection = Object.fromEntries(Object.entries(restFields).filter(([key]) => toolOnlyKeys.has(key)));
14250
14282
  return new RulesyncCheck({
14251
14283
  outputRoot: ".",
14252
14284
  frontmatter: {
@@ -14254,7 +14286,8 @@ var AmpCheck = class AmpCheck extends ToolCheck {
14254
14286
  ...description !== void 0 && { description },
14255
14287
  ...severityDefault !== void 0 && { severity: severityDefault },
14256
14288
  ...tools !== void 0 && { tools },
14257
- ...restFields
14289
+ ...sharedFields,
14290
+ ...Object.keys(toolSection).length > 0 && { [checkClass.getCheckToolTarget()]: toolSection }
14258
14291
  },
14259
14292
  body: this.body,
14260
14293
  relativeDirPath: RULESYNC_CHECKS_RELATIVE_DIR_PATH,
@@ -14269,22 +14302,24 @@ var AmpCheck = class AmpCheck extends ToolCheck {
14269
14302
  const { targets: _targets, description, severity, tools, ...restFields } = rulesyncFrontmatter;
14270
14303
  const toolTargetKeys = new Set(ALL_TOOL_TARGETS);
14271
14304
  const passthroughFields = Object.fromEntries(Object.entries(restFields).filter(([key]) => !toolTargetKeys.has(key) && key !== "name"));
14272
- const ampSection = this.filterToolSpecificSection(rulesyncFrontmatter.amp ?? {}, ["name"]);
14305
+ const toolTarget = this.getCheckToolTarget();
14306
+ const rawSection = rulesyncFrontmatter[toolTarget];
14307
+ const toolSection = this.filterToolSpecificSection(rawSection !== null && typeof rawSection === "object" && !Array.isArray(rawSection) ? rawSection : {}, ["name"]);
14273
14308
  const rawAmpFrontmatter = {
14274
14309
  name,
14275
14310
  ...description !== void 0 && { description },
14276
14311
  ...severity !== void 0 && { "severity-default": severity },
14277
14312
  ...tools !== void 0 && { tools },
14278
14313
  ...passthroughFields,
14279
- ...ampSection
14314
+ ...toolSection
14280
14315
  };
14281
- const result = AmpCheckFrontmatterSchema.safeParse(rawAmpFrontmatter);
14282
- if (!result.success) throw new Error(`Invalid amp check frontmatter in ${relativeFilePath}: ${formatError(result.error)}`);
14316
+ const result = this.getFrontmatterSchema().safeParse(rawAmpFrontmatter);
14317
+ if (!result.success) throw new Error(`Invalid ${toolTarget} check frontmatter in ${relativeFilePath}: ${formatError(result.error)}`);
14283
14318
  const ampFrontmatter = result.data;
14284
14319
  const body = rulesyncCheck.getBody();
14285
14320
  const fileContent = stringifyFrontmatter(body, ampFrontmatter);
14286
14321
  const paths = this.getSettablePaths({ global });
14287
- return new AmpCheck({
14322
+ return new this({
14288
14323
  outputRoot,
14289
14324
  frontmatter: ampFrontmatter,
14290
14325
  body,
@@ -14299,7 +14334,7 @@ var AmpCheck = class AmpCheck extends ToolCheck {
14299
14334
  success: true,
14300
14335
  error: null
14301
14336
  };
14302
- const result = AmpCheckFrontmatterSchema.safeParse(this.frontmatter);
14337
+ const result = this.constructor.getFrontmatterSchema().safeParse(this.frontmatter);
14303
14338
  if (result.success) return {
14304
14339
  success: true,
14305
14340
  error: null
@@ -14312,7 +14347,7 @@ var AmpCheck = class AmpCheck extends ToolCheck {
14312
14347
  static isTargetedByRulesyncCheck(rulesyncCheck) {
14313
14348
  return this.isTargetedByRulesyncCheckDefault({
14314
14349
  rulesyncCheck,
14315
- toolTarget: "amp"
14350
+ toolTarget: this.getCheckToolTarget()
14316
14351
  });
14317
14352
  }
14318
14353
  static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
@@ -14320,9 +14355,9 @@ var AmpCheck = class AmpCheck extends ToolCheck {
14320
14355
  const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, relativeFilePath);
14321
14356
  const fileContent = await readFileContent(filePath);
14322
14357
  const { frontmatter, body: content } = parseFrontmatter(fileContent, filePath);
14323
- const result = AmpCheckFrontmatterSchema.safeParse(frontmatter);
14358
+ const result = this.getFrontmatterSchema().safeParse(frontmatter);
14324
14359
  if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
14325
- return new AmpCheck({
14360
+ return new this({
14326
14361
  outputRoot,
14327
14362
  relativeDirPath: paths.relativeDirPath,
14328
14363
  relativeFilePath,
@@ -14333,7 +14368,7 @@ var AmpCheck = class AmpCheck extends ToolCheck {
14333
14368
  });
14334
14369
  }
14335
14370
  static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
14336
- return new AmpCheck({
14371
+ return new this({
14337
14372
  outputRoot,
14338
14373
  relativeDirPath,
14339
14374
  relativeFilePath,
@@ -15433,6 +15468,60 @@ var GitlabduoCheck = class GitlabduoCheck extends ToolCheck {
15433
15468
  }
15434
15469
  };
15435
15470
  //#endregion
15471
+ //#region src/constants/goose-paths.ts
15472
+ const GOOSE_DIR = ".goose";
15473
+ const GOOSE_GLOBAL_DIR = (0, node_path.join)(".config", "goose");
15474
+ const GOOSE_RULE_FILE_NAME = ".goosehints";
15475
+ const GOOSE_MCP_FILE_NAME = "config.yaml";
15476
+ const GOOSE_PERMISSIONS_FILE_NAME = "permission.yaml";
15477
+ const GOOSE_HOOKS_DIR_PATH = (0, node_path.join)(".agents", "plugins", "rulesync", "hooks");
15478
+ const GOOSE_HOOKS_FILE_NAME = "hooks.json";
15479
+ const GOOSE_PLUGIN_MCP_DIR_PATH = (0, node_path.join)(".agents", "plugins", "rulesync");
15480
+ const GOOSE_PLUGIN_MCP_FILE_NAME = ".mcp.json";
15481
+ const GOOSE_SKILLS_DIR_PATH = (0, node_path.join)(GOOSE_DIR, "skills");
15482
+ const GOOSE_GLOBAL_SKILLS_DIR_PATH = (0, node_path.join)(GOOSE_GLOBAL_DIR, "skills");
15483
+ const GOOSE_RECIPES_DIR_PATH = (0, node_path.join)(GOOSE_DIR, "recipes");
15484
+ const GOOSE_GLOBAL_RECIPES_DIR_PATH = (0, node_path.join)(GOOSE_GLOBAL_DIR, "recipes");
15485
+ const GOOSE_AGENTS_DIR_PATH = (0, node_path.join)(GOOSE_DIR, "agents");
15486
+ const GOOSE_GLOBAL_AGENTS_DIR_PATH = (0, node_path.join)(GOOSE_GLOBAL_DIR, "agents");
15487
+ const GOOSE_CHECKS_PROJECT_DIR = (0, node_path.join)(".agents", "checks");
15488
+ const GOOSE_CHECKS_GLOBAL_DIR = (0, node_path.join)(GOOSE_GLOBAL_DIR, "checks");
15489
+ //#endregion
15490
+ //#region src/features/checks/goose-check.ts
15491
+ const GooseCheckFrontmatterSchema = zod_mini.z.extend(AmpCheckFrontmatterSchema, {
15492
+ model: zod_mini.z.optional(zod_mini.z.string()),
15493
+ "turn-limit": zod_mini.z.optional(zod_mini.z.int().check(zod_mini.z.nonnegative()))
15494
+ });
15495
+ /**
15496
+ * Represents a Goose review check (`goose review`), written to the user-wide
15497
+ * `~/.config/goose/checks/` (global scope only).
15498
+ *
15499
+ * Goose discovers repository checks in `.agents/checks/*.md`, the same
15500
+ * cross-tool directory the `amp` checks target already writes (Goose's
15501
+ * `tools` and `severity-default` fields exist for parity with Amp's), so the
15502
+ * project scope is left to `amp` rather than having two targets own the same
15503
+ * files. Its global discovery reads `~/.config/goose/checks/` first, then
15504
+ * `~/.config/agents/checks/`. The file format is Amp's, so this class reuses
15505
+ * `AmpCheck` and adds Goose's `model` and `turn-limit`, which live in the
15506
+ * `goose:` section of a rulesync check; `name` is derived from the file name.
15507
+ *
15508
+ * @see https://github.com/aaif-goose/goose/blob/main/crates/goose/src/checks/mod.rs
15509
+ */
15510
+ var GooseCheck = class extends AmpCheck {
15511
+ static getCheckToolTarget() {
15512
+ return "goose";
15513
+ }
15514
+ static getFrontmatterSchema() {
15515
+ return GooseCheckFrontmatterSchema;
15516
+ }
15517
+ static getToolOnlyKeys() {
15518
+ return ["model", "turn-limit"];
15519
+ }
15520
+ static getSettablePaths({ global = false } = {}) {
15521
+ return { relativeDirPath: global ? GOOSE_CHECKS_GLOBAL_DIR : GOOSE_CHECKS_PROJECT_DIR };
15522
+ }
15523
+ };
15524
+ //#endregion
15436
15525
  //#region src/constants/hermesagent-paths.ts
15437
15526
  /**
15438
15527
  * Hermes Agent configuration-layout conventions.
@@ -16161,6 +16250,14 @@ const toolCheckFactories = /* @__PURE__ */ new Map([
16161
16250
  committedOutput: true
16162
16251
  }
16163
16252
  }],
16253
+ ["goose", {
16254
+ class: GooseCheck,
16255
+ meta: {
16256
+ supportsGlobal: true,
16257
+ supportsProject: false,
16258
+ filePattern: "*.md"
16259
+ }
16260
+ }],
16164
16261
  ["hermesagent", {
16165
16262
  class: HermesagentCheck,
16166
16263
  meta: {
@@ -16206,7 +16303,9 @@ const defaultGetFactory$6 = (target) => {
16206
16303
  return factory;
16207
16304
  };
16208
16305
  const allToolTargetKeys$5 = [...toolCheckFactories.keys()];
16209
- const checksProcessorToolTargets = allToolTargetKeys$5;
16306
+ const checksProcessorToolTargets = allToolTargetKeys$5.filter((target) => {
16307
+ return toolCheckFactories.get(target)?.meta.supportsProject !== false;
16308
+ });
16210
16309
  const checksProcessorToolTargetsGlobal = allToolTargetKeys$5.filter((target) => {
16211
16310
  return toolCheckFactories.get(target)?.meta.supportsGlobal ?? false;
16212
16311
  });
@@ -16377,23 +16476,6 @@ var ChecksProcessor = class extends FeatureProcessor {
16377
16476
  }
16378
16477
  };
16379
16478
  //#endregion
16380
- //#region src/constants/goose-paths.ts
16381
- const GOOSE_DIR = ".goose";
16382
- const GOOSE_GLOBAL_DIR = (0, node_path.join)(".config", "goose");
16383
- const GOOSE_RULE_FILE_NAME = ".goosehints";
16384
- const GOOSE_MCP_FILE_NAME = "config.yaml";
16385
- const GOOSE_PERMISSIONS_FILE_NAME = "permission.yaml";
16386
- const GOOSE_HOOKS_DIR_PATH = (0, node_path.join)(".agents", "plugins", "rulesync", "hooks");
16387
- const GOOSE_HOOKS_FILE_NAME = "hooks.json";
16388
- const GOOSE_PLUGIN_MCP_DIR_PATH = (0, node_path.join)(".agents", "plugins", "rulesync");
16389
- const GOOSE_PLUGIN_MCP_FILE_NAME = ".mcp.json";
16390
- const GOOSE_SKILLS_DIR_PATH = (0, node_path.join)(GOOSE_DIR, "skills");
16391
- const GOOSE_GLOBAL_SKILLS_DIR_PATH = (0, node_path.join)(GOOSE_GLOBAL_DIR, "skills");
16392
- const GOOSE_RECIPES_DIR_PATH = (0, node_path.join)(GOOSE_DIR, "recipes");
16393
- const GOOSE_GLOBAL_RECIPES_DIR_PATH = (0, node_path.join)(GOOSE_GLOBAL_DIR, "recipes");
16394
- const GOOSE_AGENTS_DIR_PATH = (0, node_path.join)(GOOSE_DIR, "agents");
16395
- const GOOSE_GLOBAL_AGENTS_DIR_PATH = (0, node_path.join)(GOOSE_GLOBAL_DIR, "agents");
16396
- //#endregion
16397
16479
  //#region src/utils/tool-home.ts
16398
16480
  /**
16399
16481
  * Where the rulesync-side source files of a tool with a home override belong.
@@ -18115,27 +18197,71 @@ const CLINE_HOOKS_GLOBAL_DIR_PATH = (0, node_path.join)("Documents", "Cline", "H
18115
18197
  const CLINE_HOOKS_MANIFEST_FILE_NAME = "rulesync-hooks.json";
18116
18198
  //#endregion
18117
18199
  //#region src/features/commands/cline-command.ts
18200
+ const CLINE_WORKFLOW_FIELD_TYPES = {
18201
+ name: "string",
18202
+ disabled: "boolean",
18203
+ enabled: "boolean"
18204
+ };
18205
+ function sanitizeClineWorkflowFrontmatter({ cline, relativeFilePath, logger }) {
18206
+ if (!isRecord$1(cline)) return {};
18207
+ return Object.fromEntries(Object.entries(cline).filter(([key, value]) => {
18208
+ const expected = Object.hasOwn(CLINE_WORKFLOW_FIELD_TYPES, key) ? CLINE_WORKFLOW_FIELD_TYPES[key] : void 0;
18209
+ if (expected === void 0 || typeof value === expected) return true;
18210
+ warnWithFallback(logger, `Cline commands: dropping cline.${key} from ${relativeFilePath}: Cline rejects the whole workflow unless it is a ${expected}.`);
18211
+ return false;
18212
+ }));
18213
+ }
18214
+ /**
18215
+ * Cline workflows (commands). A workflow may carry YAML frontmatter that Cline
18216
+ * reads: `name` (the slash-command name) and a `disabled` / `enabled` switch
18217
+ * (`parseWorkflowConfigFromMarkdown` in Cline's
18218
+ * `user-instruction-config-loader.ts`). The `cline:` section of a
18219
+ * `.rulesync/commands/*.md` file is written as that frontmatter, and every key
18220
+ * of an imported workflow's frontmatter is lifted back into `cline:`, so
18221
+ * hand-written workflow frontmatter survives an import → generate round trip.
18222
+ * Cline's own UI toggle is kept in Cline's state, not in the file. A workflow
18223
+ * without a `cline:` section is written as a bare body, as before.
18224
+ *
18225
+ * @see https://github.com/cline/cline/blob/main/sdk/packages/core/src/extensions/config/user-instruction-config-loader.ts
18226
+ */
18118
18227
  var ClineCommand = class ClineCommand extends ToolCommand {
18119
18228
  static getSettablePaths({ global } = {}) {
18120
18229
  if (global) return { relativeDirPath: CLINE_COMMANDS_GLOBAL_DIR_PATH };
18121
18230
  return { relativeDirPath: CLINE_COMMANDS_DIR_PATH };
18122
18231
  }
18232
+ /** The workflow's own frontmatter, or an empty object for a bare body. */
18233
+ getClineFrontmatter() {
18234
+ const { frontmatter } = parseFrontmatter(this.getFileContent(), this.getFilePath());
18235
+ return isRecord$1(frontmatter) ? frontmatter : {};
18236
+ }
18123
18237
  toRulesyncCommand() {
18238
+ const cline = this.getClineFrontmatter();
18239
+ const rulesyncFrontmatter = {
18240
+ targets: ["*"],
18241
+ ...Object.keys(cline).length > 0 && { cline }
18242
+ };
18243
+ const body = this.getBody();
18124
18244
  return new RulesyncCommand({
18125
18245
  outputRoot: process.cwd(),
18126
- frontmatter: { targets: ["*"] },
18127
- body: this.getFileContent(),
18246
+ frontmatter: rulesyncFrontmatter,
18247
+ body,
18128
18248
  relativeDirPath: RulesyncCommand.getSettablePaths().relativeDirPath,
18129
18249
  relativeFilePath: this.relativeFilePath,
18130
- fileContent: this.getFileContent(),
18250
+ fileContent: stringifyFrontmatter(body, rulesyncFrontmatter),
18131
18251
  validate: true
18132
18252
  });
18133
18253
  }
18134
- static fromRulesyncCommand({ outputRoot = process.cwd(), rulesyncCommand, validate = true, global = false }) {
18254
+ static fromRulesyncCommand({ outputRoot = process.cwd(), rulesyncCommand, validate = true, global = false, logger }) {
18135
18255
  const paths = this.getSettablePaths({ global });
18256
+ const cline = sanitizeClineWorkflowFrontmatter({
18257
+ cline: rulesyncCommand.getFrontmatter().cline,
18258
+ relativeFilePath: rulesyncCommand.getRelativeFilePath(),
18259
+ logger
18260
+ });
18261
+ const body = rulesyncCommand.getBody();
18136
18262
  return new ClineCommand({
18137
18263
  outputRoot,
18138
- fileContent: rulesyncCommand.getBody(),
18264
+ fileContent: Object.keys(cline).length > 0 ? stringifyFrontmatter(body, cline) : body,
18139
18265
  relativeDirPath: paths.relativeDirPath,
18140
18266
  relativeFilePath: rulesyncCommand.getRelativeFilePath(),
18141
18267
  validate
@@ -18148,7 +18274,7 @@ var ClineCommand = class ClineCommand extends ToolCommand {
18148
18274
  };
18149
18275
  }
18150
18276
  getBody() {
18151
- return this.getFileContent();
18277
+ return parseFrontmatter(this.getFileContent(), this.getFilePath()).body.trim();
18152
18278
  }
18153
18279
  static isTargetedByRulesyncCommand(rulesyncCommand) {
18154
18280
  return this.isTargetedByRulesyncCommandDefault({
@@ -18159,12 +18285,13 @@ var ClineCommand = class ClineCommand extends ToolCommand {
18159
18285
  static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
18160
18286
  const paths = this.getSettablePaths({ global });
18161
18287
  const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, relativeFilePath);
18162
- const { body: content } = parseFrontmatter(await readFileContent(filePath), filePath);
18288
+ const { frontmatter, body } = parseFrontmatter(await readFileContent(filePath), filePath);
18289
+ const cline = isRecord$1(frontmatter) ? frontmatter : {};
18163
18290
  return new ClineCommand({
18164
18291
  outputRoot,
18165
18292
  relativeDirPath: paths.relativeDirPath,
18166
18293
  relativeFilePath,
18167
- fileContent: content.trim(),
18294
+ fileContent: Object.keys(cline).length > 0 ? stringifyFrontmatter(body.trim(), cline) : body.trim(),
18168
18295
  validate
18169
18296
  });
18170
18297
  }
@@ -32737,6 +32864,31 @@ const PI_PROMPT_BLOCKING_EVENT = "input";
32737
32864
  * @see https://github.com/earendil-works/pi/blob/v0.87.1/packages/coding-agent/src/core/extensions/types.ts
32738
32865
  */
32739
32866
  const PI_SETTLE_BLOCKING_EVENT = "agent_before_settle";
32867
+ /**
32868
+ * `session_stop` is oh-my-pi's stop surface (it has no `agent_before_settle`).
32869
+ * It fires when a main-session run is about to settle (never for subagents,
32870
+ * nor for a run the user aborted), and a handler returning
32871
+ * `{ continue: true, additionalContext }` queues one more turn with that text
32872
+ * as hidden model-visible context. As on Pi, only a stop command exiting with
32873
+ * code 2 asks for that, the feedback of every such command is combined, and
32874
+ * any other failure is rethrown for oh-my-pi to report. The generated handler
32875
+ * continues only when `event.stop_hook_active` is false, which oh-my-pi sets
32876
+ * after a continuation and clears once a stop pass lets the run settle, so the
32877
+ * agent is continued at most once per stop (the flag is shared by every
32878
+ * extension, so another extension's continuation also suppresses ours);
32879
+ * oh-my-pi additionally caps these
32880
+ * advisory continuations at 8 in a row. A run that ended on a provider error
32881
+ * (`stopReason: "error"`) is not continued, matching Pi's `completed`-only
32882
+ * gate. The advisory form is used rather than `{ decision: "block", reason }`,
32883
+ * which oh-my-pi keeps enforcing without that cap. oh-my-pi gives
32884
+ * `session_stop` handlers the 30-second extension handler budget: stop
32885
+ * commands running longer time the handler out, the run settles without a
32886
+ * continuation, and the command is not killed.
32887
+ *
32888
+ * @see https://github.com/can1357/oh-my-pi/blob/fa5ff4a3a977a28b93d025f4cf50f046e019f18c/docs/extensions.md
32889
+ * @see https://github.com/can1357/oh-my-pi/blob/fa5ff4a3a977a28b93d025f4cf50f046e019f18c/packages/coding-agent/src/session/agent-session.ts
32890
+ */
32891
+ const OMP_SESSION_STOP_BLOCKING_EVENT = "session_stop";
32740
32892
  const PI_EXTENSION_DIALECT = {
32741
32893
  overrideKey: "pi",
32742
32894
  typesPackage: "@earendil-works/pi-coding-agent",
@@ -32758,7 +32910,8 @@ const OMP_EXTENSION_DIALECT = {
32758
32910
  const PI_BLOCKING_MODE_BY_EVENT = {
32759
32911
  [PI_BLOCKING_EVENT]: "tool",
32760
32912
  [PI_PROMPT_BLOCKING_EVENT]: "prompt",
32761
- [PI_SETTLE_BLOCKING_EVENT]: "settle"
32913
+ [PI_SETTLE_BLOCKING_EVENT]: "settle",
32914
+ [OMP_SESSION_STOP_BLOCKING_EVENT]: "sessionStop"
32762
32915
  };
32763
32916
  /**
32764
32917
  * The body emitted inside a blocking handler's `catch`, per gate. `tool_call`
@@ -32771,7 +32924,8 @@ function failureLinesByMode(dialect) {
32771
32924
  none: [],
32772
32925
  tool: ["return { block: true, reason: toBlockReason(error) };"],
32773
32926
  prompt: ["reportPromptGateFailure(ctx, toBlockReason(error));", dialect.promptCancelLine],
32774
- settle: ["stopHookFailures.push(error);"]
32927
+ settle: ["stopHookFailures.push(error);"],
32928
+ sessionStop: ["stopHookFailures.push(error);"]
32775
32929
  };
32776
32930
  }
32777
32931
  /**
@@ -32863,6 +33017,12 @@ const PROMPT_GATE_HELPER_LINES = [
32863
33017
  " console.error(reason);",
32864
33018
  "}"
32865
33019
  ];
33020
+ /** Helper emitted alongside every stop gate (Pi's settle gate and oh-my-pi's `session_stop`). */
33021
+ const STOP_HOOK_BLOCK_HELPER_LINES = [
33022
+ "function isStopHookBlock(error: unknown): boolean {",
33023
+ " return (error as { code?: unknown } | null)?.code === 2;",
33024
+ "}"
33025
+ ];
32866
33026
  /**
32867
33027
  * Helper emitted alongside the stop hook's settle gate. `event.entries` holds
32868
33028
  * the drafts earlier extensions proposed at this boundary, so they are kept and
@@ -32873,10 +33033,6 @@ const PROMPT_GATE_HELPER_LINES = [
32873
33033
  * appended message is committed, and Pi re-validates the final context itself.
32874
33034
  */
32875
33035
  const STOP_HOOK_HELPER_LINES = [
32876
- "function isStopHookBlock(error: unknown): boolean {",
32877
- " return (error as { code?: unknown } | null)?.code === 2;",
32878
- "}",
32879
- "",
32880
33036
  "function continueAfterStopHook(",
32881
33037
  " event: AgentBeforeSettleEvent,",
32882
33038
  " reason: string,",
@@ -32959,14 +33115,16 @@ function buildSubscriptionLines(handlerGroups, dialect) {
32959
33115
  const blocking = PI_BLOCKING_MODE_BY_EVENT[piEvent] ?? "none";
32960
33116
  const isPromptGate = blocking === "prompt";
32961
33117
  const isSettleGate = blocking === "settle";
33118
+ const isSessionStopGate = blocking === "sessionStop";
33119
+ const collectsStopFailures = isSettleGate || isSessionStopGate;
32962
33120
  const usesToolName = PI_TOOL_EVENTS.has(piEvent) && handlers.some((h) => h.matcher);
32963
33121
  const usesErrorFlag = handlers.some((h) => h.onlyOnError);
32964
33122
  const gatesOnAssistant = PI_ASSISTANT_MESSAGE_EVENTS.has(piEvent);
32965
- const params = isPromptGate ? "event, ctx" : usesToolName || usesErrorFlag || gatesOnAssistant || isPromptGate || isSettleGate ? "event" : "";
33123
+ const params = isPromptGate ? "event, ctx" : usesToolName || usesErrorFlag || gatesOnAssistant || isPromptGate || collectsStopFailures ? "event" : "";
32966
33124
  lines.push(` pi.on(${JSON.stringify(piEvent)}, async (${params}) => {`);
32967
33125
  if (gatesOnAssistant) lines.push(` if (event.message.role !== "assistant") return;`);
32968
33126
  if (isPromptGate) lines.push(` if (event.source === "extension") ${dialect.promptPassLine}`);
32969
- if (isSettleGate) lines.push(" const stopHookFailures: unknown[] = [];");
33127
+ if (collectsStopFailures) lines.push(" const stopHookFailures: unknown[] = [];");
32970
33128
  for (const handler of handlers) lines.push(...buildCommandLines({
32971
33129
  handler,
32972
33130
  usesToolName,
@@ -32982,6 +33140,17 @@ function buildSubscriptionLines(handlerGroups, dialect) {
32982
33140
  lines.push(" }");
32983
33141
  lines.push(" if (stopHookFailures.length > 0) throw stopHookFailures[0];");
32984
33142
  }
33143
+ if (isSessionStopGate) {
33144
+ lines.push(" const blocks = stopHookFailures.filter(isStopHookBlock);");
33145
+ lines.push(" const endedOnError = (event.last_assistant_message as { stopReason?: unknown } | undefined)?.stopReason === \"error\";");
33146
+ lines.push(" if (blocks.length > 0 && !event.stop_hook_active && !endedOnError) {");
33147
+ lines.push(" return {");
33148
+ lines.push(" continue: true,");
33149
+ lines.push(" additionalContext: `Stop hook feedback:\\n${blocks.map(toBlockReason).join(\"\\n\\n\")}`,");
33150
+ lines.push(" };");
33151
+ lines.push(" }");
33152
+ lines.push(" if (stopHookFailures.length > 0) throw stopHookFailures[0];");
33153
+ }
32985
33154
  lines.push(" });");
32986
33155
  if (isSettleGate) {
32987
33156
  lines.push(` pi.on("agent_settled", async () => {`);
@@ -33000,8 +33169,9 @@ function buildSubscriptionLines(handlerGroups, dialect) {
33000
33169
  * `{ block: true, reason }`, and on `input` — Pi's prompt-submission gate —
33001
33170
  * where a non-zero exit cancels the prompt with the dialect's cancel result
33002
33171
  * (`{ action: "handled" }` for Pi, `{ handled: true }` for oh-my-pi), and
33003
- * on `agent_before_settle`, where a stop command exiting with code 2 asks the
33004
- * agent to continue once with the command's output as feedback.
33172
+ * on `agent_before_settle` (Pi) / `session_stop` (oh-my-pi), where a stop
33173
+ * command exiting with code 2 asks the agent to continue once with the
33174
+ * command's output as feedback.
33005
33175
  * `postToolUse` and `postToolUseFailure` share Pi's `tool_result` event; the
33006
33176
  * latter's commands run only when `event.isError` is set.
33007
33177
  *
@@ -33022,7 +33192,8 @@ function generatePiExtensionCode({ config, supportedEvents, eventMap, dialect =
33022
33192
  const subscriptionLines = buildSubscriptionLines(handlerGroups, dialect);
33023
33193
  const hasPromptGate = Boolean(handlerGroups[PI_PROMPT_BLOCKING_EVENT]);
33024
33194
  const hasSettleGate = Boolean(handlerGroups[PI_SETTLE_BLOCKING_EVENT]);
33025
- const needsBlockReasonHelper = Boolean(handlerGroups[PI_BLOCKING_EVENT]) || hasPromptGate || hasSettleGate;
33195
+ const hasSessionStopGate = Boolean(handlerGroups[OMP_SESSION_STOP_BLOCKING_EVENT]);
33196
+ const needsBlockReasonHelper = Boolean(handlerGroups[PI_BLOCKING_EVENT]) || hasPromptGate || hasSettleGate || hasSessionStopGate;
33026
33197
  const lines = ["// Generated by rulesync. Do not edit manually."];
33027
33198
  if (subscriptionLines.length === 0) {
33028
33199
  lines.push("export default function () {}");
@@ -33049,6 +33220,10 @@ function generatePiExtensionCode({ config, supportedEvents, eventMap, dialect =
33049
33220
  lines.push(...PROMPT_GATE_HELPER_LINES);
33050
33221
  lines.push("");
33051
33222
  }
33223
+ if (hasSettleGate || hasSessionStopGate) {
33224
+ lines.push(...STOP_HOOK_BLOCK_HELPER_LINES);
33225
+ lines.push("");
33226
+ }
33052
33227
  if (hasSettleGate) {
33053
33228
  lines.push(...STOP_HOOK_HELPER_LINES);
33054
33229
  lines.push("");
@@ -40544,6 +40719,35 @@ function convertFromCodexFormat(codexMcp) {
40544
40719
  }
40545
40720
  return result;
40546
40721
  }
40722
+ /**
40723
+ * Keys Codex refuses on each transport. Its `McpServerConfig` deserializer
40724
+ * calls `throw_if_set` for these, and the error fails the whole `config.toml`,
40725
+ * not just the one server, so they are dropped with a warning instead of
40726
+ * written. Checked against `codex-rs/config/src/mcp_types.rs` at rust-v0.162.1.
40727
+ */
40728
+ const CODEX_STDIO_REJECTED_KEYS = [
40729
+ "url",
40730
+ "bearer_token_env_var",
40731
+ "bearer_token",
40732
+ "http_headers_helper",
40733
+ "http_headers",
40734
+ "env_http_headers",
40735
+ "oauth",
40736
+ "oauth_resource",
40737
+ "auth"
40738
+ ];
40739
+ const CODEX_HTTP_REJECTED_KEYS = [
40740
+ "args",
40741
+ "env",
40742
+ "env_vars",
40743
+ "cwd",
40744
+ "bearer_token"
40745
+ ];
40746
+ function dropTransportRejectedKeys({ converted, isStdio, serverName }) {
40747
+ const dropped = (isStdio ? CODEX_STDIO_REJECTED_KEYS : CODEX_HTTP_REJECTED_KEYS).filter((key) => converted[key] !== void 0);
40748
+ for (const key of dropped) delete converted[key];
40749
+ if (dropped.length > 0) warnWithFallback(void 0, `[CodexCliMcp] Dropping ${dropped.map((key) => `'${key}'`).join(", ")} from ${isStdio ? "stdio" : "url-based"} MCP server "${serverName}": Codex rejects ${dropped.length === 1 ? "this key" : "these keys"} on a ${isStdio ? "stdio" : "url-based"} server and would fail to load the whole config.toml.`);
40750
+ }
40547
40751
  function convertToCodexFormat(mcpServers) {
40548
40752
  const result = {};
40549
40753
  const originalNames = /* @__PURE__ */ new Map();
@@ -40586,6 +40790,11 @@ function convertToCodexFormat(mcpServers) {
40586
40790
  converted,
40587
40791
  serverName: name
40588
40792
  });
40793
+ dropTransportRejectedKeys({
40794
+ converted,
40795
+ isStdio,
40796
+ serverName: name
40797
+ });
40589
40798
  const previousName = originalNames.get(codexName);
40590
40799
  if (previousName !== void 0) warnWithFallback(void 0, `Codex MCP server name collision: "${previousName}" and "${name}" both normalize to "${codexName}"; "${name}" (processed last) overwrites "${previousName}".`);
40591
40800
  originalNames.set(codexName, name);
@@ -55811,6 +56020,124 @@ function parseCodebuddySettings({ fileContent, relativePath }) {
55811
56020
  }
55812
56021
  }
55813
56022
  //#endregion
56023
+ //#region src/features/permissions/codewhale-project-config.ts
56024
+ /**
56025
+ * The top-level key of the per-project `.codewhale/config.toml` overlay that
56026
+ * hides Codewhale's shell tools. The overlay only tightens: `false` disables
56027
+ * shell for the session, while `true` is ignored with a warning.
56028
+ * @see https://github.com/Hmbown/Codewhale/blob/fead51eeea0fb6038ea3e30923751b1a1c765b25/docs/CONFIGURATION.md#per-project-overlay-485
56029
+ * @see https://github.com/Hmbown/Codewhale/blob/fead51eeea0fb6038ea3e30923751b1a1c765b25/crates/tui/src/lib.rs
56030
+ */
56031
+ const CODEWHALE_ALLOW_SHELL_KEY = "allow_shell";
56032
+ const CODEWHALE_PROJECT_CONFIG_PATHS = {
56033
+ relativeDirPath: CODEWHALE_DIR,
56034
+ relativeFilePath: CODEWHALE_CONFIG_FILE_NAME
56035
+ };
56036
+ /** The project `.codewhale/config.toml` overlay, as written by the permissions feature. */
56037
+ var CodewhaleProjectConfigFile = class extends ToolFile {
56038
+ validate() {
56039
+ return {
56040
+ success: true,
56041
+ error: null
56042
+ };
56043
+ }
56044
+ };
56045
+ /**
56046
+ * Whether the canonical config denies every shell command: a `deny` on the
56047
+ * catch-all `*` (or `**`) pattern of `bash` or of the all-tools `*` category.
56048
+ * Codewhale decides by action before specificity, so such a deny already
56049
+ * blocks every command in global scope too, whatever `allow` rules sit beside
56050
+ * it.
56051
+ */
56052
+ function deniesEveryShellCommand(permission) {
56053
+ return collectShellCommandRules(permission).rules.some(({ pattern, action }) => action === "deny" && isShellCatchAll(pattern));
56054
+ }
56055
+ /** Whether a canonical shell pattern names every command. */
56056
+ function isShellCatchAll(pattern) {
56057
+ const trimmed = pattern.trim();
56058
+ return trimmed === "*" || trimmed === "**";
56059
+ }
56060
+ function projectConfigFilePath(outputRoot) {
56061
+ return (0, node_path.join)(outputRoot, CODEWHALE_PROJECT_CONFIG_PATHS.relativeDirPath, CODEWHALE_PROJECT_CONFIG_PATHS.relativeFilePath);
56062
+ }
56063
+ function parseProjectConfig({ fileContent, filePath }) {
56064
+ return parseSharedConfig({
56065
+ format: "toml",
56066
+ fileContent,
56067
+ filePath,
56068
+ invalidRootPolicy: "error"
56069
+ });
56070
+ }
56071
+ /**
56072
+ * Whether an overlay disables shell (`allow_shell = false`). A file that
56073
+ * cannot be read as TOML is reported and treated as not disabling shell.
56074
+ */
56075
+ function disablesShell({ fileContent, filePath, logger }) {
56076
+ try {
56077
+ return parseProjectConfig({
56078
+ fileContent,
56079
+ filePath
56080
+ })[CODEWHALE_ALLOW_SHELL_KEY] === false;
56081
+ } catch (error) {
56082
+ logger?.warn(`Codewhale permissions: not reading "allow_shell" from ${filePath}, so a shell deny set there is not taken into account: ${formatError(error)}`);
56083
+ return false;
56084
+ }
56085
+ }
56086
+ /**
56087
+ * Whether the project overlay at `outputRoot` disables shell
56088
+ * (`allow_shell = false`), for import.
56089
+ */
56090
+ async function readCodewhaleProjectShellDisabled({ outputRoot, logger }) {
56091
+ const filePath = projectConfigFilePath(outputRoot);
56092
+ const fileContent = await readFileContentOrNull(filePath);
56093
+ if (fileContent === null) return false;
56094
+ return disablesShell({
56095
+ fileContent,
56096
+ filePath,
56097
+ logger
56098
+ });
56099
+ }
56100
+ /**
56101
+ * The project `.codewhale/config.toml` overlay carrying `allow_shell = false`
56102
+ * when the canonical config denies every shell command, or `null` when the
56103
+ * file needs no change.
56104
+ *
56105
+ * Only `allow_shell` is written; every other overlay key (`model`,
56106
+ * `approval_policy`, ...) is kept, and an existing file that is not valid TOML
56107
+ * is an error only when it has to be written. An existing
56108
+ * `allow_shell = false` is never removed: the overlay cannot widen access, and
56109
+ * a hand-written value cannot be told apart from one rulesync wrote, so it is
56110
+ * reported instead.
56111
+ */
56112
+ async function createCodewhaleProjectConfigFile({ outputRoot = process.cwd(), config, logger }) {
56113
+ const filePath = projectConfigFilePath(outputRoot);
56114
+ const existingContent = await readFileContentOrNull(filePath);
56115
+ if (!deniesEveryShellCommand(config.permission)) {
56116
+ if (existingContent !== null && disablesShell({
56117
+ fileContent: existingContent,
56118
+ filePath,
56119
+ logger
56120
+ })) logger?.warn(`Codewhale permissions: keeping "allow_shell = false" in ${filePath}; rulesync does not remove it. Delete it by hand to re-enable shell tools in this project, or add bash: { "*": "deny" } to the rulesync permissions to match it.`);
56121
+ return null;
56122
+ }
56123
+ if (parseProjectConfig({
56124
+ fileContent: existingContent ?? "",
56125
+ filePath
56126
+ })[CODEWHALE_ALLOW_SHELL_KEY] === false) return null;
56127
+ return new CodewhaleProjectConfigFile({
56128
+ outputRoot,
56129
+ ...CODEWHALE_PROJECT_CONFIG_PATHS,
56130
+ fileContent: applySharedConfigPatch({
56131
+ fileKey: sharedConfigFileKey(CODEWHALE_PROJECT_CONFIG_PATHS),
56132
+ feature: "permissions",
56133
+ existingContent: existingContent ?? "",
56134
+ patch: { [CODEWHALE_ALLOW_SHELL_KEY]: false },
56135
+ filePath,
56136
+ logger
56137
+ })
56138
+ });
56139
+ }
56140
+ //#endregion
55814
56141
  //#region src/features/permissions/codewhale-repo-law.ts
55815
56142
  /**
55816
56143
  * Project-scope Codewhale permissions: the enforced `protected_invariants` of
@@ -55961,7 +56288,8 @@ function collectRestrictions({ config, logger }) {
55961
56288
  let skippedAllowCount = 0;
55962
56289
  for (const [category, rules] of Object.entries(config.permission)) {
55963
56290
  if (!REPO_LAW_CATEGORIES.includes(category)) {
55964
- if (Object.keys(rules).length > 0) skippedCategories.push(category);
56291
+ const entries = Object.entries(rules);
56292
+ if ((category === "bash" && entries.some(([pattern, action]) => action === "deny" && isShellCatchAll(pattern)) ? entries.filter(([, action]) => action === "allow") : entries).length > 0) skippedCategories.push(category);
55965
56293
  continue;
55966
56294
  }
55967
56295
  for (const [pattern, action] of Object.entries(rules)) {
@@ -56687,17 +57015,32 @@ async function warnOnShadowedConstitution({ outputRoot, logger }) {
56687
57015
  * Project scope manages the enforced `protected_invariants` of the repo
56688
57016
  * constitution `.codewhale/constitution.json` (see `codewhale-repo-law.ts`):
56689
57017
  * the project `.codewhale/config.toml` overlay cannot carry rules, but repo
56690
- * law can hold writes to protected paths.
57018
+ * law can hold writes to protected paths. A deny on every shell command also
57019
+ * sets `allow_shell = false` in that overlay (see
57020
+ * `codewhale-project-config.ts`), which import reads back here.
56691
57021
  *
56692
57022
  * @see https://github.com/Hmbown/Codewhale/blob/main/docs/CONFIGURATION.md
56693
57023
  * @see https://github.com/Hmbown/Codewhale/blob/main/docs/AUTHORIZATION_ORDER.md
56694
57024
  */
56695
57025
  var CodewhalePermissions = class CodewhalePermissions extends ToolPermissions {
56696
- constructor(params) {
57026
+ /**
57027
+ * Whether the project `.codewhale/config.toml` overlay disables shell
57028
+ * (`allow_shell = false`); read on import in project scope only.
57029
+ */
57030
+ projectShellDisabled;
57031
+ constructor({ projectShellDisabled = false, ...params }) {
56697
57032
  super({
56698
57033
  ...params,
56699
57034
  fileContent: params.fileContent ?? ""
56700
57035
  });
57036
+ this.projectShellDisabled = projectShellDisabled;
57037
+ }
57038
+ /**
57039
+ * The project `.codewhale/config.toml` overlay, where a deny on every shell
57040
+ * command becomes `allow_shell = false` beside the user's other overlay keys.
57041
+ */
57042
+ static getExtraSharedWritePaths(options) {
57043
+ return options?.global ? [] : [{ ...CODEWHALE_PROJECT_CONFIG_PATHS }];
56701
57044
  }
56702
57045
  /**
56703
57046
  * Codewhale's approval card writes to the global file, and the constitution
@@ -56734,7 +57077,11 @@ var CodewhalePermissions = class CodewhalePermissions extends ToolPermissions {
56734
57077
  relativeFilePath: paths.relativeFilePath,
56735
57078
  fileContent,
56736
57079
  validate,
56737
- global
57080
+ global,
57081
+ projectShellDisabled: global ? false : await readCodewhaleProjectShellDisabled({
57082
+ outputRoot,
57083
+ logger
57084
+ })
56738
57085
  });
56739
57086
  }
56740
57087
  static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, logger, global = false }) {
@@ -56796,11 +57143,15 @@ var CodewhalePermissions = class CodewhalePermissions extends ToolPermissions {
56796
57143
  toRulesyncPermissions() {
56797
57144
  const filePath = (0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath());
56798
57145
  if (!this.global) {
56799
- const document = parseCodewhaleConstitution({
57146
+ const permission = codewhaleConstitutionToCanonical(parseCodewhaleConstitution({
56800
57147
  fileContent: this.getFileContent(),
56801
57148
  filePath
56802
- });
56803
- return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({ permission: codewhaleConstitutionToCanonical(document) }, null, 2) });
57149
+ }));
57150
+ if (this.projectShellDisabled) permission.bash = {
57151
+ ...permission.bash,
57152
+ "*": "deny"
57153
+ };
57154
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({ permission }, null, 2) });
56804
57155
  }
56805
57156
  const { permission, nativeRules } = codewhaleRulesToCanonical(partitionRuleRecords(parsePermissionsDocument({
56806
57157
  fileContent: this.getFileContent(),
@@ -67806,10 +68157,15 @@ var TaktPermissions = class TaktPermissions extends ToolPermissions {
67806
68157
  outputRoot,
67807
68158
  logger
67808
68159
  });
68160
+ const inheritedProvider = global ? void 0 : await readGlobalTaktProvider({
68161
+ projectFilePath: filePath,
68162
+ logger
68163
+ });
67809
68164
  const rulesyncJson = rulesyncPermissions.getJson();
67810
68165
  const provider = resolveActiveProvider({
67811
68166
  config,
67812
- runtime: runtime.active ? runtime.config : void 0
68167
+ runtime: runtime.active ? runtime.config : void 0,
68168
+ inheritedProvider
67813
68169
  });
67814
68170
  const mode = deriveTaktPermissionMode(rulesyncJson);
67815
68171
  const override = isPlainObject$1(rulesyncJson.takt) ? rulesyncJson.takt : void 0;
@@ -67919,10 +68275,12 @@ var TaktPermissions = class TaktPermissions extends ToolPermissions {
67919
68275
  * `provider_profiles` (unless that key is the pre-0.68.0 `claude` fallback),
67920
68276
  * else the `claude-sdk` default (Takt 0.68.0+).
67921
68277
  */
67922
- function resolveActiveProvider({ config, runtime }) {
68278
+ function resolveActiveProvider({ config, runtime, inheritedProvider }) {
67923
68279
  const fromRuntime = runtime === void 0 ? void 0 : resolveRuntimeProvider(runtime);
67924
68280
  if (fromRuntime !== void 0) return fromRuntime;
67925
- if (typeof config[TAKT_PROVIDER_KEY] === "string" && config[TAKT_PROVIDER_KEY].trim() !== "") return config[TAKT_PROVIDER_KEY];
68281
+ const local = readProviderName(config);
68282
+ if (local !== void 0) return local;
68283
+ if (inheritedProvider !== void 0) return inheritedProvider;
67926
68284
  const profiles = config[TAKT_PROVIDER_PROFILES_KEY];
67927
68285
  if (isPlainObject$1(profiles)) {
67928
68286
  const keys = Object.keys(profiles);
@@ -67930,6 +68288,43 @@ function resolveActiveProvider({ config, runtime }) {
67930
68288
  }
67931
68289
  return TAKT_DEFAULT_PROVIDER;
67932
68290
  }
68291
+ /**
68292
+ * The top-level `provider:` of a Takt `config.yaml`, when it names one: either
68293
+ * the plain name or the block form `provider: { type: <name>, ... }` Takt also
68294
+ * accepts (`ConfigProviderReference`).
68295
+ */
68296
+ function readProviderName(config) {
68297
+ const raw = config[TAKT_PROVIDER_KEY];
68298
+ const provider = isPlainObject$1(raw) ? raw.type : raw;
68299
+ return typeof provider === "string" && provider.trim() !== "" ? provider : void 0;
68300
+ }
68301
+ /**
68302
+ * The top-level `provider:` of the global `~/.takt/config.yaml`, read for a
68303
+ * project-scope generate. An unresolvable home directory, a missing or
68304
+ * unparsable file, or one that names no provider all mean "nothing inherited".
68305
+ */
68306
+ async function readGlobalTaktProvider({ projectFilePath, logger }) {
68307
+ let globalPath;
68308
+ try {
68309
+ globalPath = (0, node_path.join)(getHomeDirectory(), TAKT_DIR, TAKT_CONFIG_FILE_NAME);
68310
+ } catch {
68311
+ return;
68312
+ }
68313
+ if (globalPath === projectFilePath) return;
68314
+ try {
68315
+ const content = await readFileContentOrNull(globalPath);
68316
+ if (content === null) return;
68317
+ return readProviderName(parseSharedConfig({
68318
+ format: "yaml",
68319
+ fileContent: content,
68320
+ filePath: globalPath,
68321
+ invalidRootPolicy: "error"
68322
+ }));
68323
+ } catch (error) {
68324
+ logger?.warn(`Takt permissions: could not read the provider from ${globalPath} (${formatError(error)}); falling back to the project config alone.`);
68325
+ return;
68326
+ }
68327
+ }
67933
68328
  /** A plain object carrying at least one entry (Takt's "assignment" test). */
67934
68329
  function hasEntries(value) {
67935
68330
  return isPlainObject$1(value) && Object.keys(value).length > 0;
@@ -71685,6 +72080,14 @@ var PermissionsProcessor = class extends FeatureProcessor {
71685
72080
  logger: this.logger,
71686
72081
  global: this.global
71687
72082
  });
72083
+ if (this.toolTarget === "codewhale" && !this.global) {
72084
+ const projectConfigFile = await createCodewhaleProjectConfigFile({
72085
+ outputRoot: this.outputRoot,
72086
+ config: effectivePermissions.getJson(),
72087
+ logger: this.logger
72088
+ });
72089
+ return projectConfigFile ? [toolPermissions, projectConfigFile] : [toolPermissions];
72090
+ }
71688
72091
  if (this.toolTarget !== "codexcli") return [toolPermissions];
71689
72092
  return [toolPermissions, createCodexcliBashRulesFile({
71690
72093
  outputRoot: this.outputRoot,