@oh-my-pi/pi-coding-agent 15.6.0 → 15.7.1

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.
Files changed (140) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/types/capability/rule-buckets.d.ts +30 -0
  3. package/dist/types/capability/rule.d.ts +7 -0
  4. package/dist/types/cli/completion-gen.d.ts +80 -0
  5. package/dist/types/commands/complete.d.ts +6 -0
  6. package/dist/types/commands/completions.d.ts +13 -0
  7. package/dist/types/commands/setup.d.ts +10 -1
  8. package/dist/types/config/settings-schema.d.ts +170 -10
  9. package/dist/types/discovery/builtin-defaults.d.ts +1 -0
  10. package/dist/types/discovery/builtin-rules/index.d.ts +7 -0
  11. package/dist/types/discovery/index.d.ts +1 -0
  12. package/dist/types/edit/hashline/block-resolver.d.ts +9 -0
  13. package/dist/types/edit/hashline/index.d.ts +1 -0
  14. package/dist/types/eval/py/kernel.d.ts +3 -0
  15. package/dist/types/eval/py/runtime.d.ts +11 -1
  16. package/dist/types/export/html/template.generated.d.ts +1 -1
  17. package/dist/types/main.d.ts +1 -0
  18. package/dist/types/modes/components/index.d.ts +1 -0
  19. package/dist/types/modes/components/segment-track.d.ts +22 -0
  20. package/dist/types/modes/components/welcome.d.ts +21 -0
  21. package/dist/types/modes/interactive-mode.d.ts +3 -2
  22. package/dist/types/modes/setup-wizard/index.d.ts +16 -0
  23. package/dist/types/modes/setup-wizard/scenes/glyph.d.ts +2 -0
  24. package/dist/types/modes/setup-wizard/scenes/outro.d.ts +2 -0
  25. package/dist/types/modes/setup-wizard/scenes/providers.d.ts +2 -0
  26. package/dist/types/modes/setup-wizard/scenes/sign-in.d.ts +19 -0
  27. package/dist/types/modes/setup-wizard/scenes/splash.d.ts +11 -0
  28. package/dist/types/modes/setup-wizard/scenes/theme.d.ts +2 -0
  29. package/dist/types/modes/setup-wizard/scenes/types.d.ts +43 -0
  30. package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +19 -0
  31. package/dist/types/modes/setup-wizard/wizard-overlay.d.ts +14 -0
  32. package/dist/types/modes/theme/shimmer.d.ts +2 -0
  33. package/dist/types/modes/theme/theme.d.ts +11 -0
  34. package/dist/types/modes/types.d.ts +5 -1
  35. package/dist/types/tiny/device.d.ts +78 -0
  36. package/dist/types/tiny/dtype.d.ts +85 -0
  37. package/dist/types/tiny/models.d.ts +6 -6
  38. package/dist/types/tiny/text.d.ts +15 -0
  39. package/dist/types/tiny/title-client.d.ts +8 -0
  40. package/dist/types/tools/bash.d.ts +0 -1
  41. package/dist/types/tools/eval.d.ts +1 -1
  42. package/dist/types/tools/index.d.ts +0 -1
  43. package/dist/types/tui/code-cell.d.ts +2 -0
  44. package/dist/types/tui/output-block.d.ts +17 -0
  45. package/package.json +9 -9
  46. package/src/capability/rule-buckets.ts +64 -0
  47. package/src/capability/rule.ts +8 -0
  48. package/src/cli/completion-gen.ts +550 -0
  49. package/src/cli/setup-cli.ts +5 -3
  50. package/src/cli-commands.ts +2 -0
  51. package/src/cli.ts +1 -7
  52. package/src/commands/complete.ts +66 -0
  53. package/src/commands/completions.ts +60 -0
  54. package/src/commands/setup.ts +29 -4
  55. package/src/config/settings-schema.ts +70 -11
  56. package/src/discovery/builtin-defaults.ts +39 -0
  57. package/src/discovery/builtin-rules/index.ts +48 -0
  58. package/src/discovery/builtin-rules/rs-box-leak.md +48 -0
  59. package/src/discovery/builtin-rules/rs-future-prelude.md +23 -0
  60. package/src/discovery/builtin-rules/rs-lazylock.md +51 -0
  61. package/src/discovery/builtin-rules/rs-match-ergonomics.md +67 -0
  62. package/src/discovery/builtin-rules/rs-parking-lot.md +44 -0
  63. package/src/discovery/builtin-rules/rs-result-type.md +19 -0
  64. package/src/discovery/builtin-rules/ts-bare-catch.md +38 -0
  65. package/src/discovery/builtin-rules/ts-import-type.md +42 -0
  66. package/src/discovery/builtin-rules/ts-no-any.md +56 -0
  67. package/src/discovery/builtin-rules/ts-no-dynamic-import.md +39 -0
  68. package/src/discovery/builtin-rules/ts-no-return-type.md +45 -0
  69. package/src/discovery/builtin-rules/ts-no-tiny-functions.md +50 -0
  70. package/src/discovery/builtin-rules/ts-promise-with-resolvers.md +65 -0
  71. package/src/discovery/builtin-rules/ts-set-map.md +28 -0
  72. package/src/discovery/index.ts +1 -0
  73. package/src/edit/hashline/block-resolver.ts +14 -0
  74. package/src/edit/hashline/diff.ts +4 -1
  75. package/src/edit/hashline/execute.ts +2 -1
  76. package/src/edit/hashline/index.ts +1 -0
  77. package/src/eval/py/kernel.ts +37 -15
  78. package/src/eval/py/runtime.ts +57 -28
  79. package/src/export/html/template.generated.ts +1 -1
  80. package/src/export/html/template.js +0 -12
  81. package/src/export/ttsr.ts +2 -0
  82. package/src/internal-urls/docs-index.generated.ts +7 -8
  83. package/src/main.ts +18 -1
  84. package/src/modes/components/hook-selector.ts +15 -17
  85. package/src/modes/components/index.ts +1 -0
  86. package/src/modes/components/segment-track.ts +52 -0
  87. package/src/modes/components/tips.txt +2 -1
  88. package/src/modes/components/tool-execution.ts +5 -1
  89. package/src/modes/components/welcome.ts +47 -42
  90. package/src/modes/controllers/input-controller.ts +12 -21
  91. package/src/modes/interactive-mode.ts +17 -5
  92. package/src/modes/setup-wizard/index.ts +88 -0
  93. package/src/modes/setup-wizard/scenes/glyph.ts +96 -0
  94. package/src/modes/setup-wizard/scenes/outro.ts +35 -0
  95. package/src/modes/setup-wizard/scenes/providers.ts +69 -0
  96. package/src/modes/setup-wizard/scenes/sign-in.ts +193 -0
  97. package/src/modes/setup-wizard/scenes/splash.ts +201 -0
  98. package/src/modes/setup-wizard/scenes/theme.ts +299 -0
  99. package/src/modes/setup-wizard/scenes/types.ts +48 -0
  100. package/src/modes/setup-wizard/scenes/web-search.ts +128 -0
  101. package/src/modes/setup-wizard/wizard-overlay.ts +275 -0
  102. package/src/modes/theme/shimmer.ts +5 -0
  103. package/src/modes/theme/theme.ts +44 -20
  104. package/src/modes/types.ts +6 -1
  105. package/src/prompts/system/orchestrate-notice.md +1 -1
  106. package/src/prompts/tools/read.md +4 -0
  107. package/src/sdk.ts +5 -15
  108. package/src/slash-commands/builtin-registry.ts +8 -0
  109. package/src/tiny/device.ts +117 -0
  110. package/src/tiny/dtype.ts +101 -0
  111. package/src/tiny/models.ts +7 -6
  112. package/src/tiny/text.ts +36 -1
  113. package/src/tiny/title-client.ts +58 -3
  114. package/src/tiny/worker.ts +93 -29
  115. package/src/tools/bash.ts +16 -13
  116. package/src/tools/eval.ts +9 -4
  117. package/src/tools/index.ts +0 -11
  118. package/src/tools/read.ts +1 -0
  119. package/src/tools/renderers.ts +0 -2
  120. package/src/tui/code-cell.ts +6 -1
  121. package/src/tui/output-block.ts +199 -38
  122. package/dist/types/tools/recipe/index.d.ts +0 -46
  123. package/dist/types/tools/recipe/render.d.ts +0 -36
  124. package/dist/types/tools/recipe/runner.d.ts +0 -60
  125. package/dist/types/tools/recipe/runners/cargo.d.ts +0 -16
  126. package/dist/types/tools/recipe/runners/index.d.ts +0 -2
  127. package/dist/types/tools/recipe/runners/just.d.ts +0 -2
  128. package/dist/types/tools/recipe/runners/make.d.ts +0 -2
  129. package/dist/types/tools/recipe/runners/pkg.d.ts +0 -2
  130. package/dist/types/tools/recipe/runners/task.d.ts +0 -2
  131. package/src/prompts/tools/recipe.md +0 -16
  132. package/src/tools/recipe/index.ts +0 -81
  133. package/src/tools/recipe/render.ts +0 -19
  134. package/src/tools/recipe/runner.ts +0 -219
  135. package/src/tools/recipe/runners/cargo.ts +0 -131
  136. package/src/tools/recipe/runners/index.ts +0 -8
  137. package/src/tools/recipe/runners/just.ts +0 -73
  138. package/src/tools/recipe/runners/make.ts +0 -101
  139. package/src/tools/recipe/runners/pkg.ts +0 -167
  140. package/src/tools/recipe/runners/task.ts +0 -72
@@ -0,0 +1,66 @@
1
+ /**
2
+ * `omp __complete <kind> [-- <prefix>]` — dynamic completion candidates.
3
+ *
4
+ * Hidden helper invoked by the generated shell completion scripts to resolve
5
+ * values that can't be baked into the script: the live model catalog and
6
+ * on-disk sessions. Output is one `value\tdescription` line per candidate
7
+ * (tab-separated); shells that show descriptions parse the tab, bash uses the
8
+ * first field. The import surface is kept deliberately narrow so a TAB press
9
+ * doesn't pay for the full agent boot.
10
+ */
11
+ import { type GeneratedProvider, getBundledModels, getBundledProviders } from "@oh-my-pi/pi-ai/models";
12
+ import { Command } from "@oh-my-pi/pi-utils/cli";
13
+ import { SessionManager } from "../session/session-manager";
14
+
15
+ export default class Complete extends Command {
16
+ static hidden = true;
17
+ static strict = false;
18
+
19
+ async run(): Promise<void> {
20
+ const argv = this.argv.filter(token => token !== "--");
21
+ const kind = argv[0];
22
+ const prefix = argv.length > 1 ? argv[argv.length - 1] : "";
23
+ if (kind === "models") {
24
+ completeModels(prefix);
25
+ } else if (kind === "sessions") {
26
+ await completeSessions(prefix);
27
+ }
28
+ }
29
+ }
30
+
31
+ /** Strip control chars that would corrupt the tab-separated line protocol. */
32
+ function clean(text: string): string {
33
+ return text.replace(/[\t\r\n]+/g, " ").trim();
34
+ }
35
+
36
+ function completeModels(prefix: string): void {
37
+ const needle = prefix.toLowerCase();
38
+ const seen = new Set<string>();
39
+ const lines: string[] = [];
40
+ for (const provider of getBundledProviders()) {
41
+ for (const model of getBundledModels(provider as GeneratedProvider)) {
42
+ // Offer both the fully-qualified `provider/id` and the bare `id`
43
+ // (matches the fuzzy resolution `--model` accepts).
44
+ const candidates = [`${model.provider}/${model.id}`, model.id];
45
+ for (const candidate of candidates) {
46
+ if (seen.has(candidate)) continue;
47
+ seen.add(candidate);
48
+ if (needle && !candidate.toLowerCase().includes(needle)) continue;
49
+ lines.push(`${candidate}\t${model.provider}`);
50
+ }
51
+ }
52
+ }
53
+ lines.sort();
54
+ if (lines.length > 0) process.stdout.write(`${lines.join("\n")}\n`);
55
+ }
56
+
57
+ async function completeSessions(prefix: string): Promise<void> {
58
+ const sessions = await SessionManager.list(process.cwd());
59
+ const lines: string[] = [];
60
+ for (const session of sessions) {
61
+ if (prefix && !session.id.startsWith(prefix)) continue;
62
+ const label = clean(session.title ?? session.firstMessage ?? "").slice(0, 72);
63
+ lines.push(`${session.id}\t${label}`);
64
+ }
65
+ if (lines.length > 0) process.stdout.write(`${lines.join("\n")}\n`);
66
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * `omp completions <bash|zsh|fish>` — print a shell completion script.
3
+ *
4
+ * The script is derived entirely from the declarative command/flag metadata
5
+ * (see `cli/completion-gen.ts`), so it never drifts from the actual CLI surface.
6
+ */
7
+ import { APP_NAME, VERSION } from "@oh-my-pi/pi-utils";
8
+ import { Args, type CliConfig, Command, type CommandCtor } from "@oh-my-pi/pi-utils/cli";
9
+ import { buildSpec, generateCompletion, type Shell } from "../cli/completion-gen";
10
+ import { commands } from "../cli-commands";
11
+
12
+ /** Entry name of the default command whose flags become top-level completions. */
13
+ const ROOT_COMMAND = "launch";
14
+ const SHELLS = ["bash", "zsh", "fish"] as const;
15
+
16
+ export default class Completions extends Command {
17
+ static description = "Print a shell completion script (bash, zsh, or fish)";
18
+
19
+ static args = {
20
+ shell: Args.string({
21
+ description: "Target shell",
22
+ required: true,
23
+ options: SHELLS,
24
+ }),
25
+ };
26
+
27
+ static examples = [
28
+ `# zsh — eval at startup, or write to a file in $fpath\n eval "$(${APP_NAME} completions zsh)"`,
29
+ `# bash\n eval "$(${APP_NAME} completions bash)"`,
30
+ `# fish\n ${APP_NAME} completions fish > ~/.config/fish/completions/${APP_NAME}.fish`,
31
+ ];
32
+
33
+ async run(): Promise<void> {
34
+ const shell = this.argv[0];
35
+ if (!isShell(shell)) {
36
+ process.stderr.write(`Usage: ${APP_NAME} completions <${SHELLS.join("|")}>\n`);
37
+ process.exitCode = 1;
38
+ return;
39
+ }
40
+
41
+ // Load every command class so we can read its static flag/arg descriptors,
42
+ // and collect aliases from both the registration table and the class.
43
+ const loaded = await Promise.all(commands.map(async entry => ({ entry, Cmd: await entry.load() })));
44
+ const map = new Map<string, CommandCtor>();
45
+ const aliasMap = new Map<string, readonly string[]>();
46
+ for (const { entry, Cmd } of loaded) {
47
+ map.set(entry.name, Cmd);
48
+ const merged = new Set<string>([...(Cmd.aliases ?? []), ...(entry.aliases ?? [])]);
49
+ aliasMap.set(entry.name, [...merged]);
50
+ }
51
+
52
+ const config: CliConfig = { bin: APP_NAME, version: VERSION, commands: map };
53
+ const spec = buildSpec(config, ROOT_COMMAND, aliasMap);
54
+ process.stdout.write(generateCompletion(shell, spec));
55
+ }
56
+ }
57
+
58
+ function isShell(value: string | undefined): value is Shell {
59
+ return value === "bash" || value === "zsh" || value === "fish";
60
+ }
@@ -1,18 +1,39 @@
1
1
  /**
2
- * Install dependencies for optional features.
2
+ * Run onboarding setup or install dependencies for optional features.
3
3
  */
4
4
  import { Args, Command, Flags, renderCommandHelp } from "@oh-my-pi/pi-utils/cli";
5
+ import { parseArgs } from "../cli/args";
5
6
  import { runSetupCommand, type SetupCommandArgs, type SetupComponent } from "../cli/setup-cli";
7
+ import { runRootCommand } from "../main";
6
8
  import { initTheme } from "../modes/theme/theme";
7
9
 
8
10
  const COMPONENTS: SetupComponent[] = ["python", "stt"];
9
11
 
12
+ export interface OnboardingSetupDependencies {
13
+ runRoot?: typeof runRootCommand;
14
+ stdinIsTTY?: boolean;
15
+ stdoutIsTTY?: boolean;
16
+ writeStderr?: (text: string) => void;
17
+ exit?: (code: number) => never;
18
+ }
19
+
20
+ export async function runOnboardingSetup(deps: OnboardingSetupDependencies = {}): Promise<void> {
21
+ const stdinIsTTY = deps.stdinIsTTY ?? process.stdin.isTTY;
22
+ const stdoutIsTTY = deps.stdoutIsTTY ?? process.stdout.isTTY;
23
+ if (!stdinIsTTY || !stdoutIsTTY) {
24
+ (deps.writeStderr ?? (text => process.stderr.write(text)))("omp setup requires an interactive TTY.\n");
25
+ (deps.exit ?? process.exit)(1);
26
+ return;
27
+ }
28
+ await (deps.runRoot ?? runRootCommand)(parseArgs([]), [], { forceSetupWizard: true });
29
+ }
30
+
10
31
  export default class Setup extends Command {
11
- static description = "Install dependencies for optional features";
32
+ static description = "Run onboarding setup or install dependencies for optional features";
12
33
 
13
34
  static args = {
14
35
  component: Args.string({
15
- description: "Component to install",
36
+ description: "Optional component to install",
16
37
  required: false,
17
38
  options: COMPONENTS,
18
39
  }),
@@ -26,7 +47,11 @@ export default class Setup extends Command {
26
47
  async run(): Promise<void> {
27
48
  const { args, flags } = await this.parse(Setup);
28
49
  if (!args.component) {
29
- renderCommandHelp("omp", "setup", Setup);
50
+ if (flags.check || flags.json) {
51
+ renderCommandHelp("omp", "setup", Setup);
52
+ return;
53
+ }
54
+ await runOnboardingSetup();
30
55
  return;
31
56
  }
32
57
  const cmd: SetupCommandArgs = {
@@ -1,6 +1,16 @@
1
1
  import { THINKING_EFFORTS } from "@oh-my-pi/pi-ai";
2
2
  import { TASK_SIMPLE_MODES } from "../task/simple-mode";
3
3
  import { getThinkingLevelMetadata } from "../thinking";
4
+ import {
5
+ TINY_MODEL_DEVICE_DEFAULT,
6
+ TINY_MODEL_DEVICE_SETTING_OPTIONS,
7
+ TINY_MODEL_DEVICE_SETTING_VALUES,
8
+ } from "../tiny/device";
9
+ import {
10
+ TINY_MODEL_DTYPE_DEFAULT,
11
+ TINY_MODEL_DTYPE_SETTING_OPTIONS,
12
+ TINY_MODEL_DTYPE_SETTING_VALUES,
13
+ } from "../tiny/dtype";
4
14
  import {
5
15
  ONLINE_MEMORY_MODEL_KEY,
6
16
  ONLINE_TINY_TITLE_MODEL_KEY,
@@ -243,6 +253,7 @@ export const SETTINGS_SCHEMA = {
243
253
  // General settings (no UI)
244
254
  // ────────────────────────────────────────────────────────────────────────
245
255
  lastChangelogVersion: { type: "string", default: undefined },
256
+ setupVersion: { type: "number", default: 0 },
246
257
 
247
258
  // Auth broker — credentials proxied through a remote `omp auth-broker serve`
248
259
  // host. Hidden from the UI; populate via env vars or hand-edited config.yml.
@@ -999,6 +1010,16 @@ export const SETTINGS_SCHEMA = {
999
1010
  },
1000
1011
  },
1001
1012
 
1013
+ "startup.setupWizard": {
1014
+ type: "boolean",
1015
+ default: true,
1016
+ ui: {
1017
+ tab: "interaction",
1018
+ label: "Setup Wizard",
1019
+ description: "Show newly added onboarding steps once per setup version",
1020
+ },
1021
+ },
1022
+
1002
1023
  "startup.checkUpdate": {
1003
1024
  type: "boolean",
1004
1025
  default: true,
@@ -1694,6 +1715,26 @@ export const SETTINGS_SCHEMA = {
1694
1715
  },
1695
1716
  },
1696
1717
 
1718
+ "ttsr.builtinRules": {
1719
+ type: "boolean",
1720
+ default: true,
1721
+ ui: {
1722
+ tab: "context",
1723
+ label: "Builtin Rules",
1724
+ description: "Load the default rules shipped with the agent (override individually with ttsr.disabledRules)",
1725
+ },
1726
+ },
1727
+
1728
+ "ttsr.disabledRules": {
1729
+ type: "array",
1730
+ default: [] as string[],
1731
+ ui: {
1732
+ tab: "context",
1733
+ label: "Disabled Rules",
1734
+ description: "Rule names to ignore entirely (applies to bundled defaults and your own rules)",
1735
+ },
1736
+ },
1737
+
1697
1738
  // ────────────────────────────────────────────────────────────────────────
1698
1739
  // Editing
1699
1740
  // ────────────────────────────────────────────────────────────────────────
@@ -2025,7 +2066,7 @@ export const SETTINGS_SCHEMA = {
2025
2066
  value: "write",
2026
2067
  label: "Write",
2027
2068
  description:
2028
- "Auto-approve read-only and write tools; require confirmation for exec tools such as bash, eval, browser, task, recipe, and ssh.",
2069
+ "Auto-approve read-only and write tools; require confirmation for exec tools such as bash, eval, browser, task, and ssh.",
2029
2070
  },
2030
2071
  {
2031
2072
  value: "yolo",
@@ -2203,16 +2244,6 @@ export const SETTINGS_SCHEMA = {
2203
2244
  description: "Enable the tts tool for xAI Grok Voice speech synthesis",
2204
2245
  },
2205
2246
  },
2206
- "recipe.enabled": {
2207
- type: "boolean",
2208
- default: true,
2209
- ui: {
2210
- tab: "tools",
2211
- label: "Recipe",
2212
- description:
2213
- "Enable the recipe tool when a justfile / package.json / Cargo.toml / Makefile / Taskfile is present",
2214
- },
2215
- },
2216
2247
 
2217
2248
  "inspect_image.enabled": {
2218
2249
  type: "boolean",
@@ -2915,6 +2946,30 @@ export const SETTINGS_SCHEMA = {
2915
2946
  options: TINY_TITLE_MODEL_OPTIONS,
2916
2947
  },
2917
2948
  },
2949
+ "providers.tinyModelDevice": {
2950
+ type: "enum",
2951
+ values: TINY_MODEL_DEVICE_SETTING_VALUES,
2952
+ default: TINY_MODEL_DEVICE_DEFAULT,
2953
+ ui: {
2954
+ tab: "providers",
2955
+ label: "Tiny Model Device",
2956
+ description:
2957
+ "ONNX execution provider for local tiny models (titles + memory). Default picks DirectML on Windows, CUDA on Linux x64, CPU elsewhere. The PI_TINY_DEVICE env var overrides this.",
2958
+ options: TINY_MODEL_DEVICE_SETTING_OPTIONS,
2959
+ },
2960
+ },
2961
+ "providers.tinyModelDtype": {
2962
+ type: "enum",
2963
+ values: TINY_MODEL_DTYPE_SETTING_VALUES,
2964
+ default: TINY_MODEL_DTYPE_DEFAULT,
2965
+ ui: {
2966
+ tab: "providers",
2967
+ label: "Tiny Model Precision",
2968
+ description:
2969
+ "ONNX quantization/precision for local tiny models. Default uses each model's shipped dtype (q4); lower precision is faster, higher is more faithful. The PI_TINY_DTYPE env var overrides this.",
2970
+ options: TINY_MODEL_DTYPE_SETTING_OPTIONS,
2971
+ },
2972
+ },
2918
2973
  "providers.memoryModel": {
2919
2974
  type: "enum",
2920
2975
  values: TINY_MEMORY_MODEL_VALUES,
@@ -3298,6 +3353,10 @@ export interface TtsrSettings {
3298
3353
  interruptMode: "never" | "prose-only" | "tool-only" | "always";
3299
3354
  repeatMode: "once" | "after-gap";
3300
3355
  repeatGap: number;
3356
+ /** Bucketing-only (read by bucketRules, not the TtsrManager). */
3357
+ builtinRules?: boolean;
3358
+ /** Bucketing-only (read by bucketRules, not the TtsrManager). */
3359
+ disabledRules?: string[];
3301
3360
  }
3302
3361
 
3303
3362
  export interface ExaSettings {
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Builtin Defaults Provider
3
+ *
4
+ * Ships a curated set of default rules (mostly TTSR conventions) embedded into
5
+ * the binary. Registered at the lowest priority so any user/project/tool rule
6
+ * with the same `name` overrides the bundled copy (first-wins dedup by name).
7
+ *
8
+ * Users disable bundled rules three ways:
9
+ * - flip `ttsr.builtinRules` off (drops the whole set),
10
+ * - list a name in `ttsr.disabledRules` (drops one rule),
11
+ * - define a same-named rule in any higher-priority source (overrides it).
12
+ * The first two are enforced in `bucketRules` (see capability/rule-buckets.ts).
13
+ */
14
+ import { registerProvider } from "../capability";
15
+ import { BUILTIN_DEFAULTS_PROVIDER_ID, type Rule, ruleCapability } from "../capability/rule";
16
+ import type { LoadContext, LoadResult } from "../capability/types";
17
+ import { BUILTIN_RULE_SOURCES } from "./builtin-rules";
18
+ import { buildRuleFromMarkdown, createSourceMeta } from "./helpers";
19
+
20
+ const DISPLAY_NAME = "Builtin Defaults";
21
+ // Lowest priority: every other rule provider wins a name conflict.
22
+ const PRIORITY = 1;
23
+
24
+ async function loadRules(_ctx: LoadContext): Promise<LoadResult<Rule>> {
25
+ const items = BUILTIN_RULE_SOURCES.map(({ name, content }) => {
26
+ const virtualPath = `${BUILTIN_DEFAULTS_PROVIDER_ID}:${name}.md`;
27
+ const source = createSourceMeta(BUILTIN_DEFAULTS_PROVIDER_ID, virtualPath, "user");
28
+ return buildRuleFromMarkdown(name, content, virtualPath, source, { ruleName: name });
29
+ });
30
+ return { items };
31
+ }
32
+
33
+ registerProvider<Rule>(ruleCapability.id, {
34
+ id: BUILTIN_DEFAULTS_PROVIDER_ID,
35
+ displayName: DISPLAY_NAME,
36
+ description: "Default rules shipped with the agent (disable via ttsr.builtinRules / ttsr.disabledRules)",
37
+ priority: PRIORITY,
38
+ load: loadRules,
39
+ });
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Bundled default rules shipped with the coding agent.
3
+ *
4
+ * Each markdown source is embedded via `with { type: "text" }` so it survives
5
+ * `bun build --compile` (the compiled binary ships no loose rule files; only
6
+ * the embedded text). The native source/tarball installs read the same modules.
7
+ *
8
+ * Registered by the lowest-priority `builtin-defaults` rule provider so any
9
+ * user/project/tool rule with the same name overrides the bundled copy.
10
+ */
11
+ import rsBoxLeak from "./rs-box-leak.md" with { type: "text" };
12
+ import rsFuturePrelude from "./rs-future-prelude.md" with { type: "text" };
13
+ import rsLazylock from "./rs-lazylock.md" with { type: "text" };
14
+ import rsMatchErgonomics from "./rs-match-ergonomics.md" with { type: "text" };
15
+ import rsParkingLot from "./rs-parking-lot.md" with { type: "text" };
16
+ import rsResultType from "./rs-result-type.md" with { type: "text" };
17
+ import tsBareCatch from "./ts-bare-catch.md" with { type: "text" };
18
+ import tsImportType from "./ts-import-type.md" with { type: "text" };
19
+ import tsNoAny from "./ts-no-any.md" with { type: "text" };
20
+ import tsNoDynamicImport from "./ts-no-dynamic-import.md" with { type: "text" };
21
+ import tsNoReturnType from "./ts-no-return-type.md" with { type: "text" };
22
+ import tsNoTinyFunctions from "./ts-no-tiny-functions.md" with { type: "text" };
23
+ import tsPromiseWithResolvers from "./ts-promise-with-resolvers.md" with { type: "text" };
24
+ import tsSetMap from "./ts-set-map.md" with { type: "text" };
25
+
26
+ /** A bundled rule's stable name and raw markdown (frontmatter + body). */
27
+ export interface BuiltinRuleSource {
28
+ name: string;
29
+ content: string;
30
+ }
31
+
32
+ /** All bundled default rules, ordered by name. */
33
+ export const BUILTIN_RULE_SOURCES: readonly BuiltinRuleSource[] = [
34
+ { name: "rs-box-leak", content: rsBoxLeak },
35
+ { name: "rs-future-prelude", content: rsFuturePrelude },
36
+ { name: "rs-lazylock", content: rsLazylock },
37
+ { name: "rs-match-ergonomics", content: rsMatchErgonomics },
38
+ { name: "rs-parking-lot", content: rsParkingLot },
39
+ { name: "rs-result-type", content: rsResultType },
40
+ { name: "ts-bare-catch", content: tsBareCatch },
41
+ { name: "ts-import-type", content: tsImportType },
42
+ { name: "ts-no-any", content: tsNoAny },
43
+ { name: "ts-no-dynamic-import", content: tsNoDynamicImport },
44
+ { name: "ts-no-return-type", content: tsNoReturnType },
45
+ { name: "ts-no-tiny-functions", content: tsNoTinyFunctions },
46
+ { name: "ts-promise-with-resolvers", content: tsPromiseWithResolvers },
47
+ { name: "ts-set-map", content: tsSetMap },
48
+ ];
@@ -0,0 +1,48 @@
1
+ ---
2
+ description: Never use Box::leak - it intentionally leaks memory
3
+ condition: "Box::leak"
4
+ scope: "tool:edit(*.rs), tool:write(*.rs)"
5
+ ---
6
+
7
+ Never use `Box::leak` to satisfy a lifetime. It intentionally leaks the allocation for the rest of the process.
8
+
9
+ ## Why
10
+
11
+ - The allocation is never freed.
12
+ - It hides ownership bugs.
13
+ - It turns lifetime errors into process lifetime growth.
14
+ - It makes tests pass while production memory grows.
15
+
16
+ ## Use instead
17
+
18
+ | Need | Use |
19
+ | --- | --- |
20
+ | Shared async/thread data | `Arc<T>` or owned values |
21
+ | Global lazy state | `LazyLock<T>` or `OnceLock<T>` |
22
+ | Text escaping a scope | `String` / `Arc<str>` |
23
+ | `'static` callback | `move` closure with owned captures |
24
+ | FFI pointer | Explicit owner that frees on drop |
25
+
26
+ ## Examples
27
+
28
+ ```rust
29
+ // Bad — leaking to manufacture 'static.
30
+ fn label(id: u64) -> &'static str {
31
+ Box::leak(Box::new(format!("item_{id}")))
32
+ }
33
+
34
+ // Good — return owned data.
35
+ fn label(id: u64) -> String {
36
+ format!("item_{id}")
37
+ }
38
+
39
+ // Bad — leaking before spawn.
40
+ let state = Box::leak(Box::new(state));
41
+ tokio::spawn(async move { use_state(state) });
42
+
43
+ // Good — share owned state.
44
+ let state = Arc::new(state);
45
+ tokio::spawn(async move { use_state(&state) });
46
+ ```
47
+
48
+ If `Box::leak` looks necessary, fix ownership instead.
@@ -0,0 +1,23 @@
1
+ ---
2
+ description: Use Future not std::future::Future - it's in the prelude
3
+ condition: "std::future::Future"
4
+ scope: "tool:edit(*.rs), tool:write(*.rs)"
5
+ ---
6
+
7
+ Use `Future` directly instead of `std::future::Future` in type positions.
8
+
9
+ Rust 2024 includes `Future` in the standard prelude. Older editions can import it once with `use std::future::Future;`. Repeating the fully qualified path makes signatures harder to read without adding safety.
10
+
11
+ ## Examples
12
+
13
+ ```rust
14
+ // Bad — fully qualified in every signature.
15
+ fn fetch() -> impl std::future::Future<Output = Result<Data>> { ... }
16
+ fn poll(fut: Pin<&mut dyn std::future::Future<Output = i32>>) { ... }
17
+
18
+ // Good — use the prelude or one import.
19
+ fn fetch() -> impl Future<Output = Result<Data>> { ... }
20
+ fn poll(fut: Pin<&mut dyn Future<Output = i32>>) { ... }
21
+ ```
22
+
23
+ Pre-2024 edition? Add `use std::future::Future;` at the top.
@@ -0,0 +1,51 @@
1
+ ---
2
+ description: Prefer std::sync::LazyLock over OnceLock and once_cell
3
+ condition:
4
+ - "once_cell::"
5
+ - "OnceLock::new"
6
+ scope: "tool:edit(*.rs), tool:write(*.rs)"
7
+ ---
8
+
9
+ Prefer `std::sync::LazyLock` over `OnceLock` and the `once_cell` crate when the initializer is known at declaration time.
10
+
11
+ `LazyLock` stores the cell and initializer together. There is no separate `init()` function, no repeated `get_or_init`, and no missing initialization path.
12
+
13
+ ## once_cell → std
14
+
15
+ ```rust
16
+ // Before
17
+ use once_cell::sync::Lazy;
18
+ static CONFIG: Lazy<String> = Lazy::new(|| "value".to_string());
19
+
20
+ // After
21
+ use std::sync::LazyLock;
22
+ static CONFIG: LazyLock<String> = LazyLock::new(|| "value".to_string());
23
+ ```
24
+
25
+ ## OnceLock → LazyLock
26
+
27
+ ```rust
28
+ // Before — fixed initializer hidden in accessor.
29
+ use std::sync::OnceLock;
30
+ static SETTINGS: OnceLock<Settings> = OnceLock::new();
31
+ fn settings() -> &'static Settings {
32
+ SETTINGS.get_or_init(Settings::load)
33
+ }
34
+
35
+ // After — initializer lives with the static.
36
+ use std::sync::LazyLock;
37
+ static SETTINGS: LazyLock<Settings> = LazyLock::new(Settings::load);
38
+ ```
39
+
40
+ ## Keep OnceLock when runtime input is required
41
+
42
+ ```rust
43
+ use std::sync::OnceLock;
44
+ static DATABASE: OnceLock<Database> = OnceLock::new();
45
+
46
+ fn init_database(url: &str) {
47
+ let _ = DATABASE.set(Database::connect(url));
48
+ }
49
+ ```
50
+
51
+ Do not add `once_cell` for new code. Use the standard library equivalent.
@@ -0,0 +1,67 @@
1
+ ---
2
+ description: Use match ergonomics instead of ref/ref mut patterns
3
+ condition:
4
+ - "\\(ref mut "
5
+ - "\\(ref [a-z_]"
6
+ scope: "tool:edit(*.rs), tool:write(*.rs)"
7
+ ---
8
+
9
+ Use match ergonomics instead of explicit `ref` / `ref mut` patterns. Borrow the scrutinee and let bindings receive references.
10
+
11
+ ## Shared references
12
+
13
+ ```rust
14
+ // Before
15
+ match value {
16
+ Some(ref item) => println!("{item}"),
17
+ None => {}
18
+ }
19
+
20
+ // After
21
+ match &value {
22
+ Some(item) => println!("{item}"),
23
+ None => {}
24
+ }
25
+
26
+ if let Some(item) = &value {
27
+ println!("{item}");
28
+ }
29
+ ```
30
+
31
+ ## Mutable references
32
+
33
+ ```rust
34
+ // Before
35
+ match value {
36
+ Some(ref mut item) => *item += 1,
37
+ None => {}
38
+ }
39
+
40
+ // After
41
+ match &mut value {
42
+ Some(item) => *item += 1,
43
+ None => {}
44
+ }
45
+
46
+ if let Some(item) = &mut value {
47
+ *item += 1;
48
+ }
49
+ ```
50
+
51
+ ## Result
52
+
53
+ ```rust
54
+ // Before
55
+ match result {
56
+ Ok(ref data) => println!("{data}"),
57
+ Err(ref err) => eprintln!("{err}"),
58
+ }
59
+
60
+ // After
61
+ match &result {
62
+ Ok(data) => println!("{data}"),
63
+ Err(err) => eprintln!("{err}"),
64
+ }
65
+ ```
66
+
67
+ Modern Rust rarely needs `ref` in patterns. Borrow the value being matched.
@@ -0,0 +1,44 @@
1
+ ---
2
+ description: Use parking_lot instead of std::sync for Mutex/RwLock
3
+ condition:
4
+ - "\\.lock\\(\\)\\.unwrap\\(\\)"
5
+ - "\\.read\\(\\)\\.unwrap\\(\\)"
6
+ - "\\.write\\(\\)\\.unwrap\\(\\)"
7
+ scope: "tool:edit(*.rs), tool:write(*.rs)"
8
+ ---
9
+
10
+ Use `parking_lot::{Mutex, RwLock}` instead of `std::sync::{Mutex, RwLock}` when code immediately unwraps lock results.
11
+
12
+ ## Why
13
+
14
+ - `lock()`, `read()`, and `write()` return guards directly.
15
+ - No poisoning error path to unwrap.
16
+ - Guards are smaller and faster in common contention cases.
17
+ - The call site shows locking, not error handling boilerplate.
18
+
19
+ ## Migration
20
+
21
+ ```rust
22
+ // Before
23
+ use std::sync::Mutex;
24
+ let data = Mutex::new(Vec::new());
25
+ let guard = data.lock().unwrap();
26
+
27
+ // After
28
+ use parking_lot::Mutex;
29
+ let data = Mutex::new(Vec::new());
30
+ let guard = data.lock();
31
+ ```
32
+
33
+ ## Equivalents
34
+
35
+ | std::sync | parking_lot |
36
+ | --- | --- |
37
+ | `Mutex<T>` | `Mutex<T>` |
38
+ | `RwLock<T>` | `RwLock<T>` |
39
+ | `Condvar` | `Condvar` |
40
+ | `Once` | `Once` |
41
+
42
+ ## Keep async locks async
43
+
44
+ Use `tokio::sync::Mutex` / `tokio::sync::RwLock` when a guard is held across `.await` or the lock belongs to async coordination.
@@ -0,0 +1,19 @@
1
+ ---
2
+ description: Result type aliases must include a defaulted error type parameter
3
+ condition: "type\\s+Result<[A-Za-z_]\\w*>\\s*="
4
+ scope: "tool:edit(*.rs), tool:write(*.rs)"
5
+ ---
6
+
7
+ `Result` aliases must expose the error type as a defaulted parameter.
8
+
9
+ ```rust
10
+ pub type Result<T, E = anyhow::Error> = std::result::Result<T, E>;
11
+ ```
12
+
13
+ Never write:
14
+
15
+ ```rust
16
+ type Result<T> = std::result::Result<T, anyhow::Error>;
17
+ ```
18
+
19
+ The default keeps common call sites short while preserving escape hatches for precise errors.