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.
@@ -913,6 +913,7 @@ const checksProcessorToolTargetTuple = [
913
913
  "cursor",
914
914
  "factorydroid",
915
915
  "gitlabduo",
916
+ "goose",
916
917
  "hermesagent",
917
918
  "kilo",
918
919
  "qwencode",
@@ -4822,13 +4823,11 @@ const PI_HOOK_EVENTS = [
4822
4823
  /**
4823
4824
  * Hook events supported by oh-my-pi (`omp`), Pi's fork, through the same
4824
4825
  * generated TypeScript extension. oh-my-pi keeps Pi's names for every event
4825
- * listed here, but it has neither `agent_before_settle` nor `ui_prompt_start`,
4826
- * so `stop` and `notification` are not mapped yet. oh-my-pi's stop surface is
4827
- * `session_stop`, which takes a Claude-compatible `{ decision: "block", reason }`
4828
- * result; it needs its own handler in the generator and is left for a
4829
- * follow-up. See CANONICAL_TO_OMP_EVENT_NAMES.
4826
+ * listed here except `stop`: it has no `agent_before_settle`, so `stop` maps to
4827
+ * its own `session_stop` event instead. It has no `ui_prompt_start` either, so
4828
+ * `notification` is not mapped. See CANONICAL_TO_OMP_EVENT_NAMES.
4830
4829
  *
4831
- * @see https://github.com/can1357/oh-my-pi/blob/40e9368ef0458fd9073329cdff4174895f91bc6b/docs/extensions.md
4830
+ * @see https://github.com/can1357/oh-my-pi/blob/fa5ff4a3a977a28b93d025f4cf50f046e019f18c/docs/extensions.md
4832
4831
  */
4833
4832
  const OMP_HOOK_EVENTS = [
4834
4833
  "sessionStart",
@@ -4839,6 +4838,7 @@ const OMP_HOOK_EVENTS = [
4839
4838
  "preModelInvocation",
4840
4839
  "postModelInvocation",
4841
4840
  "beforeSubmitPrompt",
4841
+ "stop",
4842
4842
  "preCompact",
4843
4843
  "postCompact"
4844
4844
  ];
@@ -6260,9 +6260,10 @@ const CANONICAL_TO_PI_EVENT_NAMES = {
6260
6260
  /**
6261
6261
  * Map canonical camelCase event names to oh-my-pi extension event names. They
6262
6262
  * are Pi's names (see CANONICAL_TO_PI_EVENT_NAMES) for the events oh-my-pi
6263
- * kept. Note that oh-my-pi runs `session_shutdown` handlers with a 2-second
6264
- * budget, and fails a `tool_call` closed when its handler exceeds
6265
- * `extensionHandlers.toolCallTimeoutMs` (30 seconds by default).
6263
+ * kept, plus `session_stop` for `stop`. Note that oh-my-pi runs
6264
+ * `session_shutdown` handlers with a 2-second budget, and fails a `tool_call`
6265
+ * closed when its handler exceeds `extensionHandlers.toolCallTimeoutMs`
6266
+ * (30 seconds by default).
6266
6267
  *
6267
6268
  * @see https://github.com/can1357/oh-my-pi/blob/40e9368ef0458fd9073329cdff4174895f91bc6b/docs/extensions.md
6268
6269
  */
@@ -6275,6 +6276,7 @@ const CANONICAL_TO_OMP_EVENT_NAMES = {
6275
6276
  preModelInvocation: "context",
6276
6277
  postModelInvocation: "message_end",
6277
6278
  beforeSubmitPrompt: "input",
6279
+ stop: "session_stop",
6278
6280
  preCompact: "session_before_compact",
6279
6281
  postCompact: "session_compact"
6280
6282
  };
@@ -12932,10 +12934,16 @@ const SHARED_CONFIG_OWNERSHIP = {
12932
12934
  [CODEWHALE_CONFIG_SHARED_FILE_KEY]: {
12933
12935
  format: "toml",
12934
12936
  invalidRootPolicy: "error",
12935
- features: { hooks: {
12936
- kind: "replace-owned-keys",
12937
- ownedKeys: ["hooks"]
12938
- } }
12937
+ features: {
12938
+ hooks: {
12939
+ kind: "replace-owned-keys",
12940
+ ownedKeys: ["hooks"]
12941
+ },
12942
+ permissions: {
12943
+ kind: "replace-owned-keys",
12944
+ ownedKeys: ["allow_shell"]
12945
+ }
12946
+ }
12939
12947
  },
12940
12948
  ".letta/settings.json": {
12941
12949
  format: "json",
@@ -14199,12 +14207,12 @@ const AmpCheckFrontmatterSchema = z.looseObject({
14199
14207
  * this format continue to apply.
14200
14208
  * @see https://ampcode.com/docs/review
14201
14209
  */
14202
- var AmpCheck = class AmpCheck extends ToolCheck {
14210
+ var AmpCheck = class extends ToolCheck {
14203
14211
  frontmatter;
14204
14212
  body;
14205
14213
  constructor({ frontmatter, body, fileContent, ...rest }) {
14206
14214
  if (rest.validate !== false) {
14207
- const result = AmpCheckFrontmatterSchema.safeParse(frontmatter);
14215
+ const result = new.target.getFrontmatterSchema().safeParse(frontmatter);
14208
14216
  if (!result.success) throw new Error(`Invalid frontmatter in ${join(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}`);
14209
14217
  }
14210
14218
  super({
@@ -14214,6 +14222,26 @@ var AmpCheck = class AmpCheck extends ToolCheck {
14214
14222
  this.frontmatter = frontmatter;
14215
14223
  this.body = body;
14216
14224
  }
14225
+ /**
14226
+ * The tool target this check format belongs to, which is also the name of
14227
+ * its tool-specific frontmatter section. Goose reads the same per-file
14228
+ * format with a few extra keys, so `GooseCheck` reuses this class through
14229
+ * these hooks.
14230
+ */
14231
+ static getCheckToolTarget() {
14232
+ return "amp";
14233
+ }
14234
+ /** The frontmatter schema a check file of this tool is validated against. */
14235
+ static getFrontmatterSchema() {
14236
+ return AmpCheckFrontmatterSchema;
14237
+ }
14238
+ /**
14239
+ * Frontmatter keys only this tool reads. Import moves them into the tool's
14240
+ * own section so they are not passed on to every other checks target.
14241
+ */
14242
+ static getToolOnlyKeys() {
14243
+ return [];
14244
+ }
14217
14245
  static getSettablePaths({ global = false } = {}) {
14218
14246
  return { relativeDirPath: global ? AMP_CHECKS_GLOBAL_DIR : AMP_CHECKS_PROJECT_DIR };
14219
14247
  }
@@ -14225,6 +14253,10 @@ var AmpCheck = class AmpCheck extends ToolCheck {
14225
14253
  }
14226
14254
  toRulesyncCheck() {
14227
14255
  const { name: _name, description, "severity-default": severityDefault, tools, ...restFields } = this.frontmatter;
14256
+ const checkClass = this.constructor;
14257
+ const toolOnlyKeys = new Set(checkClass.getToolOnlyKeys());
14258
+ const sharedFields = Object.fromEntries(Object.entries(restFields).filter(([key]) => !toolOnlyKeys.has(key)));
14259
+ const toolSection = Object.fromEntries(Object.entries(restFields).filter(([key]) => toolOnlyKeys.has(key)));
14228
14260
  return new RulesyncCheck({
14229
14261
  outputRoot: ".",
14230
14262
  frontmatter: {
@@ -14232,7 +14264,8 @@ var AmpCheck = class AmpCheck extends ToolCheck {
14232
14264
  ...description !== void 0 && { description },
14233
14265
  ...severityDefault !== void 0 && { severity: severityDefault },
14234
14266
  ...tools !== void 0 && { tools },
14235
- ...restFields
14267
+ ...sharedFields,
14268
+ ...Object.keys(toolSection).length > 0 && { [checkClass.getCheckToolTarget()]: toolSection }
14236
14269
  },
14237
14270
  body: this.body,
14238
14271
  relativeDirPath: RULESYNC_CHECKS_RELATIVE_DIR_PATH,
@@ -14247,22 +14280,24 @@ var AmpCheck = class AmpCheck extends ToolCheck {
14247
14280
  const { targets: _targets, description, severity, tools, ...restFields } = rulesyncFrontmatter;
14248
14281
  const toolTargetKeys = new Set(ALL_TOOL_TARGETS);
14249
14282
  const passthroughFields = Object.fromEntries(Object.entries(restFields).filter(([key]) => !toolTargetKeys.has(key) && key !== "name"));
14250
- const ampSection = this.filterToolSpecificSection(rulesyncFrontmatter.amp ?? {}, ["name"]);
14283
+ const toolTarget = this.getCheckToolTarget();
14284
+ const rawSection = rulesyncFrontmatter[toolTarget];
14285
+ const toolSection = this.filterToolSpecificSection(rawSection !== null && typeof rawSection === "object" && !Array.isArray(rawSection) ? rawSection : {}, ["name"]);
14251
14286
  const rawAmpFrontmatter = {
14252
14287
  name,
14253
14288
  ...description !== void 0 && { description },
14254
14289
  ...severity !== void 0 && { "severity-default": severity },
14255
14290
  ...tools !== void 0 && { tools },
14256
14291
  ...passthroughFields,
14257
- ...ampSection
14292
+ ...toolSection
14258
14293
  };
14259
- const result = AmpCheckFrontmatterSchema.safeParse(rawAmpFrontmatter);
14260
- if (!result.success) throw new Error(`Invalid amp check frontmatter in ${relativeFilePath}: ${formatError(result.error)}`);
14294
+ const result = this.getFrontmatterSchema().safeParse(rawAmpFrontmatter);
14295
+ if (!result.success) throw new Error(`Invalid ${toolTarget} check frontmatter in ${relativeFilePath}: ${formatError(result.error)}`);
14261
14296
  const ampFrontmatter = result.data;
14262
14297
  const body = rulesyncCheck.getBody();
14263
14298
  const fileContent = stringifyFrontmatter(body, ampFrontmatter);
14264
14299
  const paths = this.getSettablePaths({ global });
14265
- return new AmpCheck({
14300
+ return new this({
14266
14301
  outputRoot,
14267
14302
  frontmatter: ampFrontmatter,
14268
14303
  body,
@@ -14277,7 +14312,7 @@ var AmpCheck = class AmpCheck extends ToolCheck {
14277
14312
  success: true,
14278
14313
  error: null
14279
14314
  };
14280
- const result = AmpCheckFrontmatterSchema.safeParse(this.frontmatter);
14315
+ const result = this.constructor.getFrontmatterSchema().safeParse(this.frontmatter);
14281
14316
  if (result.success) return {
14282
14317
  success: true,
14283
14318
  error: null
@@ -14290,7 +14325,7 @@ var AmpCheck = class AmpCheck extends ToolCheck {
14290
14325
  static isTargetedByRulesyncCheck(rulesyncCheck) {
14291
14326
  return this.isTargetedByRulesyncCheckDefault({
14292
14327
  rulesyncCheck,
14293
- toolTarget: "amp"
14328
+ toolTarget: this.getCheckToolTarget()
14294
14329
  });
14295
14330
  }
14296
14331
  static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
@@ -14298,9 +14333,9 @@ var AmpCheck = class AmpCheck extends ToolCheck {
14298
14333
  const filePath = join(outputRoot, paths.relativeDirPath, relativeFilePath);
14299
14334
  const fileContent = await readFileContent(filePath);
14300
14335
  const { frontmatter, body: content } = parseFrontmatter(fileContent, filePath);
14301
- const result = AmpCheckFrontmatterSchema.safeParse(frontmatter);
14336
+ const result = this.getFrontmatterSchema().safeParse(frontmatter);
14302
14337
  if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
14303
- return new AmpCheck({
14338
+ return new this({
14304
14339
  outputRoot,
14305
14340
  relativeDirPath: paths.relativeDirPath,
14306
14341
  relativeFilePath,
@@ -14311,7 +14346,7 @@ var AmpCheck = class AmpCheck extends ToolCheck {
14311
14346
  });
14312
14347
  }
14313
14348
  static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
14314
- return new AmpCheck({
14349
+ return new this({
14315
14350
  outputRoot,
14316
14351
  relativeDirPath,
14317
14352
  relativeFilePath,
@@ -15411,6 +15446,60 @@ var GitlabduoCheck = class GitlabduoCheck extends ToolCheck {
15411
15446
  }
15412
15447
  };
15413
15448
  //#endregion
15449
+ //#region src/constants/goose-paths.ts
15450
+ const GOOSE_DIR = ".goose";
15451
+ const GOOSE_GLOBAL_DIR = join(".config", "goose");
15452
+ const GOOSE_RULE_FILE_NAME = ".goosehints";
15453
+ const GOOSE_MCP_FILE_NAME = "config.yaml";
15454
+ const GOOSE_PERMISSIONS_FILE_NAME = "permission.yaml";
15455
+ const GOOSE_HOOKS_DIR_PATH = join(".agents", "plugins", "rulesync", "hooks");
15456
+ const GOOSE_HOOKS_FILE_NAME = "hooks.json";
15457
+ const GOOSE_PLUGIN_MCP_DIR_PATH = join(".agents", "plugins", "rulesync");
15458
+ const GOOSE_PLUGIN_MCP_FILE_NAME = ".mcp.json";
15459
+ const GOOSE_SKILLS_DIR_PATH = join(GOOSE_DIR, "skills");
15460
+ const GOOSE_GLOBAL_SKILLS_DIR_PATH = join(GOOSE_GLOBAL_DIR, "skills");
15461
+ const GOOSE_RECIPES_DIR_PATH = join(GOOSE_DIR, "recipes");
15462
+ const GOOSE_GLOBAL_RECIPES_DIR_PATH = join(GOOSE_GLOBAL_DIR, "recipes");
15463
+ const GOOSE_AGENTS_DIR_PATH = join(GOOSE_DIR, "agents");
15464
+ const GOOSE_GLOBAL_AGENTS_DIR_PATH = join(GOOSE_GLOBAL_DIR, "agents");
15465
+ const GOOSE_CHECKS_PROJECT_DIR = join(".agents", "checks");
15466
+ const GOOSE_CHECKS_GLOBAL_DIR = join(GOOSE_GLOBAL_DIR, "checks");
15467
+ //#endregion
15468
+ //#region src/features/checks/goose-check.ts
15469
+ const GooseCheckFrontmatterSchema = z.extend(AmpCheckFrontmatterSchema, {
15470
+ model: z.optional(z.string()),
15471
+ "turn-limit": z.optional(z.int().check(z.nonnegative()))
15472
+ });
15473
+ /**
15474
+ * Represents a Goose review check (`goose review`), written to the user-wide
15475
+ * `~/.config/goose/checks/` (global scope only).
15476
+ *
15477
+ * Goose discovers repository checks in `.agents/checks/*.md`, the same
15478
+ * cross-tool directory the `amp` checks target already writes (Goose's
15479
+ * `tools` and `severity-default` fields exist for parity with Amp's), so the
15480
+ * project scope is left to `amp` rather than having two targets own the same
15481
+ * files. Its global discovery reads `~/.config/goose/checks/` first, then
15482
+ * `~/.config/agents/checks/`. The file format is Amp's, so this class reuses
15483
+ * `AmpCheck` and adds Goose's `model` and `turn-limit`, which live in the
15484
+ * `goose:` section of a rulesync check; `name` is derived from the file name.
15485
+ *
15486
+ * @see https://github.com/aaif-goose/goose/blob/main/crates/goose/src/checks/mod.rs
15487
+ */
15488
+ var GooseCheck = class extends AmpCheck {
15489
+ static getCheckToolTarget() {
15490
+ return "goose";
15491
+ }
15492
+ static getFrontmatterSchema() {
15493
+ return GooseCheckFrontmatterSchema;
15494
+ }
15495
+ static getToolOnlyKeys() {
15496
+ return ["model", "turn-limit"];
15497
+ }
15498
+ static getSettablePaths({ global = false } = {}) {
15499
+ return { relativeDirPath: global ? GOOSE_CHECKS_GLOBAL_DIR : GOOSE_CHECKS_PROJECT_DIR };
15500
+ }
15501
+ };
15502
+ //#endregion
15414
15503
  //#region src/constants/hermesagent-paths.ts
15415
15504
  /**
15416
15505
  * Hermes Agent configuration-layout conventions.
@@ -16139,6 +16228,14 @@ const toolCheckFactories = /* @__PURE__ */ new Map([
16139
16228
  committedOutput: true
16140
16229
  }
16141
16230
  }],
16231
+ ["goose", {
16232
+ class: GooseCheck,
16233
+ meta: {
16234
+ supportsGlobal: true,
16235
+ supportsProject: false,
16236
+ filePattern: "*.md"
16237
+ }
16238
+ }],
16142
16239
  ["hermesagent", {
16143
16240
  class: HermesagentCheck,
16144
16241
  meta: {
@@ -16184,7 +16281,9 @@ const defaultGetFactory$6 = (target) => {
16184
16281
  return factory;
16185
16282
  };
16186
16283
  const allToolTargetKeys$5 = [...toolCheckFactories.keys()];
16187
- const checksProcessorToolTargets = allToolTargetKeys$5;
16284
+ const checksProcessorToolTargets = allToolTargetKeys$5.filter((target) => {
16285
+ return toolCheckFactories.get(target)?.meta.supportsProject !== false;
16286
+ });
16188
16287
  const checksProcessorToolTargetsGlobal = allToolTargetKeys$5.filter((target) => {
16189
16288
  return toolCheckFactories.get(target)?.meta.supportsGlobal ?? false;
16190
16289
  });
@@ -16355,23 +16454,6 @@ var ChecksProcessor = class extends FeatureProcessor {
16355
16454
  }
16356
16455
  };
16357
16456
  //#endregion
16358
- //#region src/constants/goose-paths.ts
16359
- const GOOSE_DIR = ".goose";
16360
- const GOOSE_GLOBAL_DIR = join(".config", "goose");
16361
- const GOOSE_RULE_FILE_NAME = ".goosehints";
16362
- const GOOSE_MCP_FILE_NAME = "config.yaml";
16363
- const GOOSE_PERMISSIONS_FILE_NAME = "permission.yaml";
16364
- const GOOSE_HOOKS_DIR_PATH = join(".agents", "plugins", "rulesync", "hooks");
16365
- const GOOSE_HOOKS_FILE_NAME = "hooks.json";
16366
- const GOOSE_PLUGIN_MCP_DIR_PATH = join(".agents", "plugins", "rulesync");
16367
- const GOOSE_PLUGIN_MCP_FILE_NAME = ".mcp.json";
16368
- const GOOSE_SKILLS_DIR_PATH = join(GOOSE_DIR, "skills");
16369
- const GOOSE_GLOBAL_SKILLS_DIR_PATH = join(GOOSE_GLOBAL_DIR, "skills");
16370
- const GOOSE_RECIPES_DIR_PATH = join(GOOSE_DIR, "recipes");
16371
- const GOOSE_GLOBAL_RECIPES_DIR_PATH = join(GOOSE_GLOBAL_DIR, "recipes");
16372
- const GOOSE_AGENTS_DIR_PATH = join(GOOSE_DIR, "agents");
16373
- const GOOSE_GLOBAL_AGENTS_DIR_PATH = join(GOOSE_GLOBAL_DIR, "agents");
16374
- //#endregion
16375
16457
  //#region src/utils/tool-home.ts
16376
16458
  /**
16377
16459
  * Where the rulesync-side source files of a tool with a home override belong.
@@ -18093,27 +18175,71 @@ const CLINE_HOOKS_GLOBAL_DIR_PATH = join("Documents", "Cline", "Hooks");
18093
18175
  const CLINE_HOOKS_MANIFEST_FILE_NAME = "rulesync-hooks.json";
18094
18176
  //#endregion
18095
18177
  //#region src/features/commands/cline-command.ts
18178
+ const CLINE_WORKFLOW_FIELD_TYPES = {
18179
+ name: "string",
18180
+ disabled: "boolean",
18181
+ enabled: "boolean"
18182
+ };
18183
+ function sanitizeClineWorkflowFrontmatter({ cline, relativeFilePath, logger }) {
18184
+ if (!isRecord$1(cline)) return {};
18185
+ return Object.fromEntries(Object.entries(cline).filter(([key, value]) => {
18186
+ const expected = Object.hasOwn(CLINE_WORKFLOW_FIELD_TYPES, key) ? CLINE_WORKFLOW_FIELD_TYPES[key] : void 0;
18187
+ if (expected === void 0 || typeof value === expected) return true;
18188
+ warnWithFallback(logger, `Cline commands: dropping cline.${key} from ${relativeFilePath}: Cline rejects the whole workflow unless it is a ${expected}.`);
18189
+ return false;
18190
+ }));
18191
+ }
18192
+ /**
18193
+ * Cline workflows (commands). A workflow may carry YAML frontmatter that Cline
18194
+ * reads: `name` (the slash-command name) and a `disabled` / `enabled` switch
18195
+ * (`parseWorkflowConfigFromMarkdown` in Cline's
18196
+ * `user-instruction-config-loader.ts`). The `cline:` section of a
18197
+ * `.rulesync/commands/*.md` file is written as that frontmatter, and every key
18198
+ * of an imported workflow's frontmatter is lifted back into `cline:`, so
18199
+ * hand-written workflow frontmatter survives an import → generate round trip.
18200
+ * Cline's own UI toggle is kept in Cline's state, not in the file. A workflow
18201
+ * without a `cline:` section is written as a bare body, as before.
18202
+ *
18203
+ * @see https://github.com/cline/cline/blob/main/sdk/packages/core/src/extensions/config/user-instruction-config-loader.ts
18204
+ */
18096
18205
  var ClineCommand = class ClineCommand extends ToolCommand {
18097
18206
  static getSettablePaths({ global } = {}) {
18098
18207
  if (global) return { relativeDirPath: CLINE_COMMANDS_GLOBAL_DIR_PATH };
18099
18208
  return { relativeDirPath: CLINE_COMMANDS_DIR_PATH };
18100
18209
  }
18210
+ /** The workflow's own frontmatter, or an empty object for a bare body. */
18211
+ getClineFrontmatter() {
18212
+ const { frontmatter } = parseFrontmatter(this.getFileContent(), this.getFilePath());
18213
+ return isRecord$1(frontmatter) ? frontmatter : {};
18214
+ }
18101
18215
  toRulesyncCommand() {
18216
+ const cline = this.getClineFrontmatter();
18217
+ const rulesyncFrontmatter = {
18218
+ targets: ["*"],
18219
+ ...Object.keys(cline).length > 0 && { cline }
18220
+ };
18221
+ const body = this.getBody();
18102
18222
  return new RulesyncCommand({
18103
18223
  outputRoot: process.cwd(),
18104
- frontmatter: { targets: ["*"] },
18105
- body: this.getFileContent(),
18224
+ frontmatter: rulesyncFrontmatter,
18225
+ body,
18106
18226
  relativeDirPath: RulesyncCommand.getSettablePaths().relativeDirPath,
18107
18227
  relativeFilePath: this.relativeFilePath,
18108
- fileContent: this.getFileContent(),
18228
+ fileContent: stringifyFrontmatter(body, rulesyncFrontmatter),
18109
18229
  validate: true
18110
18230
  });
18111
18231
  }
18112
- static fromRulesyncCommand({ outputRoot = process.cwd(), rulesyncCommand, validate = true, global = false }) {
18232
+ static fromRulesyncCommand({ outputRoot = process.cwd(), rulesyncCommand, validate = true, global = false, logger }) {
18113
18233
  const paths = this.getSettablePaths({ global });
18234
+ const cline = sanitizeClineWorkflowFrontmatter({
18235
+ cline: rulesyncCommand.getFrontmatter().cline,
18236
+ relativeFilePath: rulesyncCommand.getRelativeFilePath(),
18237
+ logger
18238
+ });
18239
+ const body = rulesyncCommand.getBody();
18114
18240
  return new ClineCommand({
18115
18241
  outputRoot,
18116
- fileContent: rulesyncCommand.getBody(),
18242
+ fileContent: Object.keys(cline).length > 0 ? stringifyFrontmatter(body, cline) : body,
18117
18243
  relativeDirPath: paths.relativeDirPath,
18118
18244
  relativeFilePath: rulesyncCommand.getRelativeFilePath(),
18119
18245
  validate
@@ -18126,7 +18252,7 @@ var ClineCommand = class ClineCommand extends ToolCommand {
18126
18252
  };
18127
18253
  }
18128
18254
  getBody() {
18129
- return this.getFileContent();
18255
+ return parseFrontmatter(this.getFileContent(), this.getFilePath()).body.trim();
18130
18256
  }
18131
18257
  static isTargetedByRulesyncCommand(rulesyncCommand) {
18132
18258
  return this.isTargetedByRulesyncCommandDefault({
@@ -18137,12 +18263,13 @@ var ClineCommand = class ClineCommand extends ToolCommand {
18137
18263
  static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
18138
18264
  const paths = this.getSettablePaths({ global });
18139
18265
  const filePath = join(outputRoot, paths.relativeDirPath, relativeFilePath);
18140
- const { body: content } = parseFrontmatter(await readFileContent(filePath), filePath);
18266
+ const { frontmatter, body } = parseFrontmatter(await readFileContent(filePath), filePath);
18267
+ const cline = isRecord$1(frontmatter) ? frontmatter : {};
18141
18268
  return new ClineCommand({
18142
18269
  outputRoot,
18143
18270
  relativeDirPath: paths.relativeDirPath,
18144
18271
  relativeFilePath,
18145
- fileContent: content.trim(),
18272
+ fileContent: Object.keys(cline).length > 0 ? stringifyFrontmatter(body.trim(), cline) : body.trim(),
18146
18273
  validate
18147
18274
  });
18148
18275
  }
@@ -32715,6 +32842,31 @@ const PI_PROMPT_BLOCKING_EVENT = "input";
32715
32842
  * @see https://github.com/earendil-works/pi/blob/v0.87.1/packages/coding-agent/src/core/extensions/types.ts
32716
32843
  */
32717
32844
  const PI_SETTLE_BLOCKING_EVENT = "agent_before_settle";
32845
+ /**
32846
+ * `session_stop` is oh-my-pi's stop surface (it has no `agent_before_settle`).
32847
+ * It fires when a main-session run is about to settle (never for subagents,
32848
+ * nor for a run the user aborted), and a handler returning
32849
+ * `{ continue: true, additionalContext }` queues one more turn with that text
32850
+ * as hidden model-visible context. As on Pi, only a stop command exiting with
32851
+ * code 2 asks for that, the feedback of every such command is combined, and
32852
+ * any other failure is rethrown for oh-my-pi to report. The generated handler
32853
+ * continues only when `event.stop_hook_active` is false, which oh-my-pi sets
32854
+ * after a continuation and clears once a stop pass lets the run settle, so the
32855
+ * agent is continued at most once per stop (the flag is shared by every
32856
+ * extension, so another extension's continuation also suppresses ours);
32857
+ * oh-my-pi additionally caps these
32858
+ * advisory continuations at 8 in a row. A run that ended on a provider error
32859
+ * (`stopReason: "error"`) is not continued, matching Pi's `completed`-only
32860
+ * gate. The advisory form is used rather than `{ decision: "block", reason }`,
32861
+ * which oh-my-pi keeps enforcing without that cap. oh-my-pi gives
32862
+ * `session_stop` handlers the 30-second extension handler budget: stop
32863
+ * commands running longer time the handler out, the run settles without a
32864
+ * continuation, and the command is not killed.
32865
+ *
32866
+ * @see https://github.com/can1357/oh-my-pi/blob/fa5ff4a3a977a28b93d025f4cf50f046e019f18c/docs/extensions.md
32867
+ * @see https://github.com/can1357/oh-my-pi/blob/fa5ff4a3a977a28b93d025f4cf50f046e019f18c/packages/coding-agent/src/session/agent-session.ts
32868
+ */
32869
+ const OMP_SESSION_STOP_BLOCKING_EVENT = "session_stop";
32718
32870
  const PI_EXTENSION_DIALECT = {
32719
32871
  overrideKey: "pi",
32720
32872
  typesPackage: "@earendil-works/pi-coding-agent",
@@ -32736,7 +32888,8 @@ const OMP_EXTENSION_DIALECT = {
32736
32888
  const PI_BLOCKING_MODE_BY_EVENT = {
32737
32889
  [PI_BLOCKING_EVENT]: "tool",
32738
32890
  [PI_PROMPT_BLOCKING_EVENT]: "prompt",
32739
- [PI_SETTLE_BLOCKING_EVENT]: "settle"
32891
+ [PI_SETTLE_BLOCKING_EVENT]: "settle",
32892
+ [OMP_SESSION_STOP_BLOCKING_EVENT]: "sessionStop"
32740
32893
  };
32741
32894
  /**
32742
32895
  * The body emitted inside a blocking handler's `catch`, per gate. `tool_call`
@@ -32749,7 +32902,8 @@ function failureLinesByMode(dialect) {
32749
32902
  none: [],
32750
32903
  tool: ["return { block: true, reason: toBlockReason(error) };"],
32751
32904
  prompt: ["reportPromptGateFailure(ctx, toBlockReason(error));", dialect.promptCancelLine],
32752
- settle: ["stopHookFailures.push(error);"]
32905
+ settle: ["stopHookFailures.push(error);"],
32906
+ sessionStop: ["stopHookFailures.push(error);"]
32753
32907
  };
32754
32908
  }
32755
32909
  /**
@@ -32841,6 +32995,12 @@ const PROMPT_GATE_HELPER_LINES = [
32841
32995
  " console.error(reason);",
32842
32996
  "}"
32843
32997
  ];
32998
+ /** Helper emitted alongside every stop gate (Pi's settle gate and oh-my-pi's `session_stop`). */
32999
+ const STOP_HOOK_BLOCK_HELPER_LINES = [
33000
+ "function isStopHookBlock(error: unknown): boolean {",
33001
+ " return (error as { code?: unknown } | null)?.code === 2;",
33002
+ "}"
33003
+ ];
32844
33004
  /**
32845
33005
  * Helper emitted alongside the stop hook's settle gate. `event.entries` holds
32846
33006
  * the drafts earlier extensions proposed at this boundary, so they are kept and
@@ -32851,10 +33011,6 @@ const PROMPT_GATE_HELPER_LINES = [
32851
33011
  * appended message is committed, and Pi re-validates the final context itself.
32852
33012
  */
32853
33013
  const STOP_HOOK_HELPER_LINES = [
32854
- "function isStopHookBlock(error: unknown): boolean {",
32855
- " return (error as { code?: unknown } | null)?.code === 2;",
32856
- "}",
32857
- "",
32858
33014
  "function continueAfterStopHook(",
32859
33015
  " event: AgentBeforeSettleEvent,",
32860
33016
  " reason: string,",
@@ -32937,14 +33093,16 @@ function buildSubscriptionLines(handlerGroups, dialect) {
32937
33093
  const blocking = PI_BLOCKING_MODE_BY_EVENT[piEvent] ?? "none";
32938
33094
  const isPromptGate = blocking === "prompt";
32939
33095
  const isSettleGate = blocking === "settle";
33096
+ const isSessionStopGate = blocking === "sessionStop";
33097
+ const collectsStopFailures = isSettleGate || isSessionStopGate;
32940
33098
  const usesToolName = PI_TOOL_EVENTS.has(piEvent) && handlers.some((h) => h.matcher);
32941
33099
  const usesErrorFlag = handlers.some((h) => h.onlyOnError);
32942
33100
  const gatesOnAssistant = PI_ASSISTANT_MESSAGE_EVENTS.has(piEvent);
32943
- const params = isPromptGate ? "event, ctx" : usesToolName || usesErrorFlag || gatesOnAssistant || isPromptGate || isSettleGate ? "event" : "";
33101
+ const params = isPromptGate ? "event, ctx" : usesToolName || usesErrorFlag || gatesOnAssistant || isPromptGate || collectsStopFailures ? "event" : "";
32944
33102
  lines.push(` pi.on(${JSON.stringify(piEvent)}, async (${params}) => {`);
32945
33103
  if (gatesOnAssistant) lines.push(` if (event.message.role !== "assistant") return;`);
32946
33104
  if (isPromptGate) lines.push(` if (event.source === "extension") ${dialect.promptPassLine}`);
32947
- if (isSettleGate) lines.push(" const stopHookFailures: unknown[] = [];");
33105
+ if (collectsStopFailures) lines.push(" const stopHookFailures: unknown[] = [];");
32948
33106
  for (const handler of handlers) lines.push(...buildCommandLines({
32949
33107
  handler,
32950
33108
  usesToolName,
@@ -32960,6 +33118,17 @@ function buildSubscriptionLines(handlerGroups, dialect) {
32960
33118
  lines.push(" }");
32961
33119
  lines.push(" if (stopHookFailures.length > 0) throw stopHookFailures[0];");
32962
33120
  }
33121
+ if (isSessionStopGate) {
33122
+ lines.push(" const blocks = stopHookFailures.filter(isStopHookBlock);");
33123
+ lines.push(" const endedOnError = (event.last_assistant_message as { stopReason?: unknown } | undefined)?.stopReason === \"error\";");
33124
+ lines.push(" if (blocks.length > 0 && !event.stop_hook_active && !endedOnError) {");
33125
+ lines.push(" return {");
33126
+ lines.push(" continue: true,");
33127
+ lines.push(" additionalContext: `Stop hook feedback:\\n${blocks.map(toBlockReason).join(\"\\n\\n\")}`,");
33128
+ lines.push(" };");
33129
+ lines.push(" }");
33130
+ lines.push(" if (stopHookFailures.length > 0) throw stopHookFailures[0];");
33131
+ }
32963
33132
  lines.push(" });");
32964
33133
  if (isSettleGate) {
32965
33134
  lines.push(` pi.on("agent_settled", async () => {`);
@@ -32978,8 +33147,9 @@ function buildSubscriptionLines(handlerGroups, dialect) {
32978
33147
  * `{ block: true, reason }`, and on `input` — Pi's prompt-submission gate —
32979
33148
  * where a non-zero exit cancels the prompt with the dialect's cancel result
32980
33149
  * (`{ action: "handled" }` for Pi, `{ handled: true }` for oh-my-pi), and
32981
- * on `agent_before_settle`, where a stop command exiting with code 2 asks the
32982
- * agent to continue once with the command's output as feedback.
33150
+ * on `agent_before_settle` (Pi) / `session_stop` (oh-my-pi), where a stop
33151
+ * command exiting with code 2 asks the agent to continue once with the
33152
+ * command's output as feedback.
32983
33153
  * `postToolUse` and `postToolUseFailure` share Pi's `tool_result` event; the
32984
33154
  * latter's commands run only when `event.isError` is set.
32985
33155
  *
@@ -33000,7 +33170,8 @@ function generatePiExtensionCode({ config, supportedEvents, eventMap, dialect =
33000
33170
  const subscriptionLines = buildSubscriptionLines(handlerGroups, dialect);
33001
33171
  const hasPromptGate = Boolean(handlerGroups[PI_PROMPT_BLOCKING_EVENT]);
33002
33172
  const hasSettleGate = Boolean(handlerGroups[PI_SETTLE_BLOCKING_EVENT]);
33003
- const needsBlockReasonHelper = Boolean(handlerGroups[PI_BLOCKING_EVENT]) || hasPromptGate || hasSettleGate;
33173
+ const hasSessionStopGate = Boolean(handlerGroups[OMP_SESSION_STOP_BLOCKING_EVENT]);
33174
+ const needsBlockReasonHelper = Boolean(handlerGroups[PI_BLOCKING_EVENT]) || hasPromptGate || hasSettleGate || hasSessionStopGate;
33004
33175
  const lines = ["// Generated by rulesync. Do not edit manually."];
33005
33176
  if (subscriptionLines.length === 0) {
33006
33177
  lines.push("export default function () {}");
@@ -33027,6 +33198,10 @@ function generatePiExtensionCode({ config, supportedEvents, eventMap, dialect =
33027
33198
  lines.push(...PROMPT_GATE_HELPER_LINES);
33028
33199
  lines.push("");
33029
33200
  }
33201
+ if (hasSettleGate || hasSessionStopGate) {
33202
+ lines.push(...STOP_HOOK_BLOCK_HELPER_LINES);
33203
+ lines.push("");
33204
+ }
33030
33205
  if (hasSettleGate) {
33031
33206
  lines.push(...STOP_HOOK_HELPER_LINES);
33032
33207
  lines.push("");
@@ -40522,6 +40697,35 @@ function convertFromCodexFormat(codexMcp) {
40522
40697
  }
40523
40698
  return result;
40524
40699
  }
40700
+ /**
40701
+ * Keys Codex refuses on each transport. Its `McpServerConfig` deserializer
40702
+ * calls `throw_if_set` for these, and the error fails the whole `config.toml`,
40703
+ * not just the one server, so they are dropped with a warning instead of
40704
+ * written. Checked against `codex-rs/config/src/mcp_types.rs` at rust-v0.162.1.
40705
+ */
40706
+ const CODEX_STDIO_REJECTED_KEYS = [
40707
+ "url",
40708
+ "bearer_token_env_var",
40709
+ "bearer_token",
40710
+ "http_headers_helper",
40711
+ "http_headers",
40712
+ "env_http_headers",
40713
+ "oauth",
40714
+ "oauth_resource",
40715
+ "auth"
40716
+ ];
40717
+ const CODEX_HTTP_REJECTED_KEYS = [
40718
+ "args",
40719
+ "env",
40720
+ "env_vars",
40721
+ "cwd",
40722
+ "bearer_token"
40723
+ ];
40724
+ function dropTransportRejectedKeys({ converted, isStdio, serverName }) {
40725
+ const dropped = (isStdio ? CODEX_STDIO_REJECTED_KEYS : CODEX_HTTP_REJECTED_KEYS).filter((key) => converted[key] !== void 0);
40726
+ for (const key of dropped) delete converted[key];
40727
+ 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.`);
40728
+ }
40525
40729
  function convertToCodexFormat(mcpServers) {
40526
40730
  const result = {};
40527
40731
  const originalNames = /* @__PURE__ */ new Map();
@@ -40564,6 +40768,11 @@ function convertToCodexFormat(mcpServers) {
40564
40768
  converted,
40565
40769
  serverName: name
40566
40770
  });
40771
+ dropTransportRejectedKeys({
40772
+ converted,
40773
+ isStdio,
40774
+ serverName: name
40775
+ });
40567
40776
  const previousName = originalNames.get(codexName);
40568
40777
  if (previousName !== void 0) warnWithFallback(void 0, `Codex MCP server name collision: "${previousName}" and "${name}" both normalize to "${codexName}"; "${name}" (processed last) overwrites "${previousName}".`);
40569
40778
  originalNames.set(codexName, name);
@@ -55789,6 +55998,124 @@ function parseCodebuddySettings({ fileContent, relativePath }) {
55789
55998
  }
55790
55999
  }
55791
56000
  //#endregion
56001
+ //#region src/features/permissions/codewhale-project-config.ts
56002
+ /**
56003
+ * The top-level key of the per-project `.codewhale/config.toml` overlay that
56004
+ * hides Codewhale's shell tools. The overlay only tightens: `false` disables
56005
+ * shell for the session, while `true` is ignored with a warning.
56006
+ * @see https://github.com/Hmbown/Codewhale/blob/fead51eeea0fb6038ea3e30923751b1a1c765b25/docs/CONFIGURATION.md#per-project-overlay-485
56007
+ * @see https://github.com/Hmbown/Codewhale/blob/fead51eeea0fb6038ea3e30923751b1a1c765b25/crates/tui/src/lib.rs
56008
+ */
56009
+ const CODEWHALE_ALLOW_SHELL_KEY = "allow_shell";
56010
+ const CODEWHALE_PROJECT_CONFIG_PATHS = {
56011
+ relativeDirPath: CODEWHALE_DIR,
56012
+ relativeFilePath: CODEWHALE_CONFIG_FILE_NAME
56013
+ };
56014
+ /** The project `.codewhale/config.toml` overlay, as written by the permissions feature. */
56015
+ var CodewhaleProjectConfigFile = class extends ToolFile {
56016
+ validate() {
56017
+ return {
56018
+ success: true,
56019
+ error: null
56020
+ };
56021
+ }
56022
+ };
56023
+ /**
56024
+ * Whether the canonical config denies every shell command: a `deny` on the
56025
+ * catch-all `*` (or `**`) pattern of `bash` or of the all-tools `*` category.
56026
+ * Codewhale decides by action before specificity, so such a deny already
56027
+ * blocks every command in global scope too, whatever `allow` rules sit beside
56028
+ * it.
56029
+ */
56030
+ function deniesEveryShellCommand(permission) {
56031
+ return collectShellCommandRules(permission).rules.some(({ pattern, action }) => action === "deny" && isShellCatchAll(pattern));
56032
+ }
56033
+ /** Whether a canonical shell pattern names every command. */
56034
+ function isShellCatchAll(pattern) {
56035
+ const trimmed = pattern.trim();
56036
+ return trimmed === "*" || trimmed === "**";
56037
+ }
56038
+ function projectConfigFilePath(outputRoot) {
56039
+ return join(outputRoot, CODEWHALE_PROJECT_CONFIG_PATHS.relativeDirPath, CODEWHALE_PROJECT_CONFIG_PATHS.relativeFilePath);
56040
+ }
56041
+ function parseProjectConfig({ fileContent, filePath }) {
56042
+ return parseSharedConfig({
56043
+ format: "toml",
56044
+ fileContent,
56045
+ filePath,
56046
+ invalidRootPolicy: "error"
56047
+ });
56048
+ }
56049
+ /**
56050
+ * Whether an overlay disables shell (`allow_shell = false`). A file that
56051
+ * cannot be read as TOML is reported and treated as not disabling shell.
56052
+ */
56053
+ function disablesShell({ fileContent, filePath, logger }) {
56054
+ try {
56055
+ return parseProjectConfig({
56056
+ fileContent,
56057
+ filePath
56058
+ })[CODEWHALE_ALLOW_SHELL_KEY] === false;
56059
+ } catch (error) {
56060
+ logger?.warn(`Codewhale permissions: not reading "allow_shell" from ${filePath}, so a shell deny set there is not taken into account: ${formatError(error)}`);
56061
+ return false;
56062
+ }
56063
+ }
56064
+ /**
56065
+ * Whether the project overlay at `outputRoot` disables shell
56066
+ * (`allow_shell = false`), for import.
56067
+ */
56068
+ async function readCodewhaleProjectShellDisabled({ outputRoot, logger }) {
56069
+ const filePath = projectConfigFilePath(outputRoot);
56070
+ const fileContent = await readFileContentOrNull(filePath);
56071
+ if (fileContent === null) return false;
56072
+ return disablesShell({
56073
+ fileContent,
56074
+ filePath,
56075
+ logger
56076
+ });
56077
+ }
56078
+ /**
56079
+ * The project `.codewhale/config.toml` overlay carrying `allow_shell = false`
56080
+ * when the canonical config denies every shell command, or `null` when the
56081
+ * file needs no change.
56082
+ *
56083
+ * Only `allow_shell` is written; every other overlay key (`model`,
56084
+ * `approval_policy`, ...) is kept, and an existing file that is not valid TOML
56085
+ * is an error only when it has to be written. An existing
56086
+ * `allow_shell = false` is never removed: the overlay cannot widen access, and
56087
+ * a hand-written value cannot be told apart from one rulesync wrote, so it is
56088
+ * reported instead.
56089
+ */
56090
+ async function createCodewhaleProjectConfigFile({ outputRoot = process.cwd(), config, logger }) {
56091
+ const filePath = projectConfigFilePath(outputRoot);
56092
+ const existingContent = await readFileContentOrNull(filePath);
56093
+ if (!deniesEveryShellCommand(config.permission)) {
56094
+ if (existingContent !== null && disablesShell({
56095
+ fileContent: existingContent,
56096
+ filePath,
56097
+ logger
56098
+ })) 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.`);
56099
+ return null;
56100
+ }
56101
+ if (parseProjectConfig({
56102
+ fileContent: existingContent ?? "",
56103
+ filePath
56104
+ })[CODEWHALE_ALLOW_SHELL_KEY] === false) return null;
56105
+ return new CodewhaleProjectConfigFile({
56106
+ outputRoot,
56107
+ ...CODEWHALE_PROJECT_CONFIG_PATHS,
56108
+ fileContent: applySharedConfigPatch({
56109
+ fileKey: sharedConfigFileKey(CODEWHALE_PROJECT_CONFIG_PATHS),
56110
+ feature: "permissions",
56111
+ existingContent: existingContent ?? "",
56112
+ patch: { [CODEWHALE_ALLOW_SHELL_KEY]: false },
56113
+ filePath,
56114
+ logger
56115
+ })
56116
+ });
56117
+ }
56118
+ //#endregion
55792
56119
  //#region src/features/permissions/codewhale-repo-law.ts
55793
56120
  /**
55794
56121
  * Project-scope Codewhale permissions: the enforced `protected_invariants` of
@@ -55939,7 +56266,8 @@ function collectRestrictions({ config, logger }) {
55939
56266
  let skippedAllowCount = 0;
55940
56267
  for (const [category, rules] of Object.entries(config.permission)) {
55941
56268
  if (!REPO_LAW_CATEGORIES.includes(category)) {
55942
- if (Object.keys(rules).length > 0) skippedCategories.push(category);
56269
+ const entries = Object.entries(rules);
56270
+ if ((category === "bash" && entries.some(([pattern, action]) => action === "deny" && isShellCatchAll(pattern)) ? entries.filter(([, action]) => action === "allow") : entries).length > 0) skippedCategories.push(category);
55943
56271
  continue;
55944
56272
  }
55945
56273
  for (const [pattern, action] of Object.entries(rules)) {
@@ -56665,17 +56993,32 @@ async function warnOnShadowedConstitution({ outputRoot, logger }) {
56665
56993
  * Project scope manages the enforced `protected_invariants` of the repo
56666
56994
  * constitution `.codewhale/constitution.json` (see `codewhale-repo-law.ts`):
56667
56995
  * the project `.codewhale/config.toml` overlay cannot carry rules, but repo
56668
- * law can hold writes to protected paths.
56996
+ * law can hold writes to protected paths. A deny on every shell command also
56997
+ * sets `allow_shell = false` in that overlay (see
56998
+ * `codewhale-project-config.ts`), which import reads back here.
56669
56999
  *
56670
57000
  * @see https://github.com/Hmbown/Codewhale/blob/main/docs/CONFIGURATION.md
56671
57001
  * @see https://github.com/Hmbown/Codewhale/blob/main/docs/AUTHORIZATION_ORDER.md
56672
57002
  */
56673
57003
  var CodewhalePermissions = class CodewhalePermissions extends ToolPermissions {
56674
- constructor(params) {
57004
+ /**
57005
+ * Whether the project `.codewhale/config.toml` overlay disables shell
57006
+ * (`allow_shell = false`); read on import in project scope only.
57007
+ */
57008
+ projectShellDisabled;
57009
+ constructor({ projectShellDisabled = false, ...params }) {
56675
57010
  super({
56676
57011
  ...params,
56677
57012
  fileContent: params.fileContent ?? ""
56678
57013
  });
57014
+ this.projectShellDisabled = projectShellDisabled;
57015
+ }
57016
+ /**
57017
+ * The project `.codewhale/config.toml` overlay, where a deny on every shell
57018
+ * command becomes `allow_shell = false` beside the user's other overlay keys.
57019
+ */
57020
+ static getExtraSharedWritePaths(options) {
57021
+ return options?.global ? [] : [{ ...CODEWHALE_PROJECT_CONFIG_PATHS }];
56679
57022
  }
56680
57023
  /**
56681
57024
  * Codewhale's approval card writes to the global file, and the constitution
@@ -56712,7 +57055,11 @@ var CodewhalePermissions = class CodewhalePermissions extends ToolPermissions {
56712
57055
  relativeFilePath: paths.relativeFilePath,
56713
57056
  fileContent,
56714
57057
  validate,
56715
- global
57058
+ global,
57059
+ projectShellDisabled: global ? false : await readCodewhaleProjectShellDisabled({
57060
+ outputRoot,
57061
+ logger
57062
+ })
56716
57063
  });
56717
57064
  }
56718
57065
  static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, logger, global = false }) {
@@ -56774,11 +57121,15 @@ var CodewhalePermissions = class CodewhalePermissions extends ToolPermissions {
56774
57121
  toRulesyncPermissions() {
56775
57122
  const filePath = join(this.getRelativeDirPath(), this.getRelativeFilePath());
56776
57123
  if (!this.global) {
56777
- const document = parseCodewhaleConstitution({
57124
+ const permission = codewhaleConstitutionToCanonical(parseCodewhaleConstitution({
56778
57125
  fileContent: this.getFileContent(),
56779
57126
  filePath
56780
- });
56781
- return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({ permission: codewhaleConstitutionToCanonical(document) }, null, 2) });
57127
+ }));
57128
+ if (this.projectShellDisabled) permission.bash = {
57129
+ ...permission.bash,
57130
+ "*": "deny"
57131
+ };
57132
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({ permission }, null, 2) });
56782
57133
  }
56783
57134
  const { permission, nativeRules } = codewhaleRulesToCanonical(partitionRuleRecords(parsePermissionsDocument({
56784
57135
  fileContent: this.getFileContent(),
@@ -67784,10 +68135,15 @@ var TaktPermissions = class TaktPermissions extends ToolPermissions {
67784
68135
  outputRoot,
67785
68136
  logger
67786
68137
  });
68138
+ const inheritedProvider = global ? void 0 : await readGlobalTaktProvider({
68139
+ projectFilePath: filePath,
68140
+ logger
68141
+ });
67787
68142
  const rulesyncJson = rulesyncPermissions.getJson();
67788
68143
  const provider = resolveActiveProvider({
67789
68144
  config,
67790
- runtime: runtime.active ? runtime.config : void 0
68145
+ runtime: runtime.active ? runtime.config : void 0,
68146
+ inheritedProvider
67791
68147
  });
67792
68148
  const mode = deriveTaktPermissionMode(rulesyncJson);
67793
68149
  const override = isPlainObject$1(rulesyncJson.takt) ? rulesyncJson.takt : void 0;
@@ -67897,10 +68253,12 @@ var TaktPermissions = class TaktPermissions extends ToolPermissions {
67897
68253
  * `provider_profiles` (unless that key is the pre-0.68.0 `claude` fallback),
67898
68254
  * else the `claude-sdk` default (Takt 0.68.0+).
67899
68255
  */
67900
- function resolveActiveProvider({ config, runtime }) {
68256
+ function resolveActiveProvider({ config, runtime, inheritedProvider }) {
67901
68257
  const fromRuntime = runtime === void 0 ? void 0 : resolveRuntimeProvider(runtime);
67902
68258
  if (fromRuntime !== void 0) return fromRuntime;
67903
- if (typeof config[TAKT_PROVIDER_KEY] === "string" && config[TAKT_PROVIDER_KEY].trim() !== "") return config[TAKT_PROVIDER_KEY];
68259
+ const local = readProviderName(config);
68260
+ if (local !== void 0) return local;
68261
+ if (inheritedProvider !== void 0) return inheritedProvider;
67904
68262
  const profiles = config[TAKT_PROVIDER_PROFILES_KEY];
67905
68263
  if (isPlainObject$1(profiles)) {
67906
68264
  const keys = Object.keys(profiles);
@@ -67908,6 +68266,43 @@ function resolveActiveProvider({ config, runtime }) {
67908
68266
  }
67909
68267
  return TAKT_DEFAULT_PROVIDER;
67910
68268
  }
68269
+ /**
68270
+ * The top-level `provider:` of a Takt `config.yaml`, when it names one: either
68271
+ * the plain name or the block form `provider: { type: <name>, ... }` Takt also
68272
+ * accepts (`ConfigProviderReference`).
68273
+ */
68274
+ function readProviderName(config) {
68275
+ const raw = config[TAKT_PROVIDER_KEY];
68276
+ const provider = isPlainObject$1(raw) ? raw.type : raw;
68277
+ return typeof provider === "string" && provider.trim() !== "" ? provider : void 0;
68278
+ }
68279
+ /**
68280
+ * The top-level `provider:` of the global `~/.takt/config.yaml`, read for a
68281
+ * project-scope generate. An unresolvable home directory, a missing or
68282
+ * unparsable file, or one that names no provider all mean "nothing inherited".
68283
+ */
68284
+ async function readGlobalTaktProvider({ projectFilePath, logger }) {
68285
+ let globalPath;
68286
+ try {
68287
+ globalPath = join(getHomeDirectory(), TAKT_DIR, TAKT_CONFIG_FILE_NAME);
68288
+ } catch {
68289
+ return;
68290
+ }
68291
+ if (globalPath === projectFilePath) return;
68292
+ try {
68293
+ const content = await readFileContentOrNull(globalPath);
68294
+ if (content === null) return;
68295
+ return readProviderName(parseSharedConfig({
68296
+ format: "yaml",
68297
+ fileContent: content,
68298
+ filePath: globalPath,
68299
+ invalidRootPolicy: "error"
68300
+ }));
68301
+ } catch (error) {
68302
+ logger?.warn(`Takt permissions: could not read the provider from ${globalPath} (${formatError(error)}); falling back to the project config alone.`);
68303
+ return;
68304
+ }
68305
+ }
67911
68306
  /** A plain object carrying at least one entry (Takt's "assignment" test). */
67912
68307
  function hasEntries(value) {
67913
68308
  return isPlainObject$1(value) && Object.keys(value).length > 0;
@@ -71663,6 +72058,14 @@ var PermissionsProcessor = class extends FeatureProcessor {
71663
72058
  logger: this.logger,
71664
72059
  global: this.global
71665
72060
  });
72061
+ if (this.toolTarget === "codewhale" && !this.global) {
72062
+ const projectConfigFile = await createCodewhaleProjectConfigFile({
72063
+ outputRoot: this.outputRoot,
72064
+ config: effectivePermissions.getJson(),
72065
+ logger: this.logger
72066
+ });
72067
+ return projectConfigFile ? [toolPermissions, projectConfigFile] : [toolPermissions];
72068
+ }
71666
72069
  if (this.toolTarget !== "codexcli") return [toolPermissions];
71667
72070
  return [toolPermissions, createCodexcliBashRulesFile({
71668
72071
  outputRoot: this.outputRoot,
@@ -101329,4 +101732,4 @@ async function importChecksCore(params) {
101329
101732
  //#endregion
101330
101733
  export { RulesyncHooks as $, removeFile as $t, groupSpellingsByCaseFoldedIdentity as A, RULESYNC_RELATIVE_DIR_PATH as An, applyFileMode as At, QWENCODE_LOCAL_RULE_FILE_NAME as B, DEPRECATED_FEATURE_REPLACEMENTS as Bn, getFileSize as Bt, COMMANDCODE_SETTINGS_LOCAL_FILE_NAME as C, RULESYNC_IGNORE_RELATIVE_FILE_PATH as Cn, fallbackLogger as Ct, FACTORYDROID_DIR as D, RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH as Dn, withWarnOnceScope as Dt, ChecksProcessor as E, RULESYNC_MCP_SCHEMA_URL as En, resetRunWarningState as Et, CLAUDECODE_LOCAL_RULE_FILE_NAME as F, RULESYNC_USER_CONFIG_DIR_NAME as Fn, createTempDirectory as Ft, RulesyncSubagentFrontmatterSchema as G, quoteForLog as Gn, listDirectoryEntryNames as Gt, AUGMENTCODE_SETTINGS_LOCAL_FILE_NAME as H, truncateText as Hn, isFileNotFoundError as Ht, CLAUDECODE_MEMORIES_DIR_NAME as I, RULESYNC_XDG_CONFIG_HOME_DEFAULT_DIR_NAME as In, directoryExists as It, RulesyncRule as J, stripHiddenCharacters as Jn, pathEscapesRoot as Jt, RulesyncSkill as K, stripControlCharacters as Kn, listFilePathsRecursively as Kt, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as L, parseCommaSeparatedList as Ln, ensureDir as Lt, CODEXCLI_DIR as M, RULESYNC_SKILLS_RELATIVE_DIR_PATH as Mn, assertTreeContainsNoSymlinks as Mt, CLAUDECODE_AGENT_MEMORY_LOCAL_DIR_NAME as N, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as Nn, assertWritablePathInsideRoot as Nt, FACTORYDROID_SETTINGS_LOCAL_FILE_NAME as O, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as On, CLIError as Ot, CLAUDECODE_DIR as P, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as Pn, checkPathTraversal as Pt, RulesyncIgnore as Q, removeDirectoryStrict as Qt, CLAUDECODE_SKILLS_DIR_PATH as R, ALL_FEATURES as Rn, fileExists as Rt, COMMANDCODE_DIR as S, RULESYNC_HOOKS_RELATIVE_FILE_PATH as Sn, WarningCollectingLogger as St, CODEBUDDY_LOCAL_RULE_FILE_NAME as T, RULESYNC_MCP_RELATIVE_FILE_PATH as Tn, withFallbackLoggerTarget as Tt, getLocalSkillDirNames as U, hasDeceptiveHiddenCharacters as Un, isFileSystemError as Ut, AUGMENTCODE_DIR as V, formatError as Vn, getHomeDirectory as Vt, RulesyncSubagent as W, hasEnclosingMarkOutsideKeycap as Wn, isSymlink as Wt, RulesyncPermissions as X, readFileContentOrNull as Xt, RulesyncRuleFrontmatterSchema as Y, readFileContent as Yt, RulesyncMcp as Z, removeDirectory as Zt, LETTACODE_DIR as _, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as _n, SourceEntrySchema as _t, getProcessorRegistryEntry as a, toPosixPath as an, RulesyncCheck as at, CommandsProcessor as b, RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH as bn, ConsoleLogger as bt, RulesProcessor as c, ALL_TOOL_TARGETS as cn, loadYaml as ct, ELLIPSIS_WIDTH as d, ToolTargetSchema as dn, ConfigResolver as dt, removeFileStrict as en, getRulesyncSourceCandidates as et, displayWidthOf as f, CURATED_RULES_FEATURE_SUBDIR as fn, mergeInputRootConfigs as ft, HooksProcessor as g, RULESYNC_CHECKS_RELATIVE_DIR_PATH as gn, GITIGNORE_DESTINATION_KEY as gt, IgnoreProcessor as h, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as hn, ConfigFileSchema as ht, inspectInputRoots as i, splitPathSegments as in, RulesyncCommandFrontmatterSchema as it, CODEXCLI_BASH_RULES_FILE_NAME as j, RULESYNC_RULES_RELATIVE_DIR_PATH as jn, assertDirectoryIfExists as jt, caseFoldIdentity as k, RULESYNC_PERMISSIONS_SCHEMA_URL as kn, ErrorCodes as kt, SubagentsProcessor as l, ALL_TOOL_TARGETS_WITH_WILDCARD as ln, SHARED_USER_MANAGED_CONFIG_PATHS as lt, McpProcessor as m, MAX_FILE_SIZE as mn, CONFLICTING_TARGET_PAIRS as mt, formatSourceLoadFailure as n, resolvePath as nn, parseJsonc as nt, convertFromTool as o, writeFileBuffer as on, RulesyncCheckFrontmatterSchema as ot, shortenToWidth as p, FEATURE_SOURCE_TREE_ENTRIES as pn, resolveEffectiveInputRoots as pt, RulesyncSkillFrontmatterSchema as q, stripControlCharactersKeepingLineFeeds as qn, listSubdirectoryNames as qt, generate as r, runWithDirectoryRollback as rn, RulesyncCommand as rt, isPackagingToolTarget as s, writeFileContent as sn, stringifyFrontmatter as st, importFromTool as t, removeTempDirectory as tn, resolveRulesyncSourceWritePath as tt, SkillsProcessor as u, PACKAGING_TOOL_TARGETS as un, SKILL_FILE_NAME as ut, LETTACODE_SETTINGS_LOCAL_FILE_NAME as v, RULESYNC_CONFIG_RELATIVE_FILE_PATH as vn, assertTargetsFeaturesExclusive as vt, CODEBUDDY_DIR as w, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as wn, warnOnConflictingFlags as wt, REASONIX_LOCAL_RULE_FILE_NAME as x, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as xn, JsonLogger as xt, CRUSH_LOCAL_RULE_FILE_NAME as y, RULESYNC_CONFIG_SCHEMA_URL as yn, findControlCharacter as yt, QWENCODE_DIR as z, ALL_FEATURES_WITH_WILDCARD as zn, findFilesByGlobs as zt };
101331
101734
 
101332
- //# sourceMappingURL=import-DBd2G6Eo.js.map
101735
+ //# sourceMappingURL=import-DgKWnDni.js.map