@oh-my-pi/pi-coding-agent 15.9.0 → 15.9.3

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 (124) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/dist/types/cli/dry-balance-cli.d.ts +104 -0
  3. package/dist/types/cli/update-cli.d.ts +15 -1
  4. package/dist/types/commands/dry-balance.d.ts +31 -0
  5. package/dist/types/config/append-only-context-mode.d.ts +8 -0
  6. package/dist/types/config/model-registry.d.ts +5 -0
  7. package/dist/types/config/models-config-schema.d.ts +18 -0
  8. package/dist/types/config/settings-schema.d.ts +3 -3
  9. package/dist/types/config/settings.d.ts +11 -0
  10. package/dist/types/discovery/helpers.d.ts +1 -0
  11. package/dist/types/exa/mcp-client.d.ts +2 -1
  12. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +2 -3
  13. package/dist/types/hindsight/bank.d.ts +17 -9
  14. package/dist/types/hindsight/mental-models.d.ts +1 -1
  15. package/dist/types/hindsight/state.d.ts +9 -3
  16. package/dist/types/mcp/json-rpc.d.ts +6 -1
  17. package/dist/types/mcp/manager.d.ts +1 -1
  18. package/dist/types/mcp/tool-bridge.d.ts +4 -0
  19. package/dist/types/mnemopi/state.d.ts +2 -2
  20. package/dist/types/modes/components/agent-dashboard.d.ts +1 -0
  21. package/dist/types/modes/components/extensions/extension-dashboard.d.ts +1 -0
  22. package/dist/types/modes/components/plugin-settings.d.ts +40 -8
  23. package/dist/types/modes/components/session-selector.d.ts +8 -3
  24. package/dist/types/modes/components/settings-selector.d.ts +1 -1
  25. package/dist/types/modes/components/transcript-container.d.ts +3 -2
  26. package/dist/types/modes/utils/keybinding-matchers.d.ts +4 -0
  27. package/dist/types/session/agent-session.d.ts +13 -1
  28. package/dist/types/session/auth-storage.d.ts +2 -2
  29. package/dist/types/session/history-storage.d.ts +3 -4
  30. package/dist/types/session/messages.d.ts +1 -0
  31. package/dist/types/session/session-manager.d.ts +1 -0
  32. package/dist/types/slash-commands/types.d.ts +17 -4
  33. package/dist/types/task/types.d.ts +2 -0
  34. package/dist/types/tiny/text.d.ts +17 -0
  35. package/dist/types/tools/index.d.ts +16 -0
  36. package/dist/types/tools/path-utils.d.ts +11 -0
  37. package/dist/types/web/search/providers/base.d.ts +14 -0
  38. package/dist/types/web/search/providers/exa.d.ts +9 -0
  39. package/dist/types/web/search/providers/perplexity.d.ts +2 -2
  40. package/dist/types/web/search/types.d.ts +2 -1
  41. package/package.json +9 -9
  42. package/src/cli/dry-balance-cli.ts +823 -0
  43. package/src/cli/session-picker.ts +1 -0
  44. package/src/cli/update-cli.ts +54 -2
  45. package/src/cli-commands.ts +1 -0
  46. package/src/commands/completions.ts +1 -1
  47. package/src/commands/dry-balance.ts +43 -0
  48. package/src/config/append-only-context-mode.ts +37 -0
  49. package/src/config/model-registry.ts +6 -0
  50. package/src/config/models-config-schema.ts +3 -0
  51. package/src/config/settings-schema.ts +2 -2
  52. package/src/config/settings.ts +38 -0
  53. package/src/discovery/builtin-rules/ts-no-tiny-functions.md +1 -0
  54. package/src/discovery/github.ts +37 -1
  55. package/src/discovery/helpers.ts +3 -1
  56. package/src/exa/mcp-client.ts +11 -5
  57. package/src/extensibility/plugins/legacy-pi-compat.ts +245 -25
  58. package/src/hindsight/backend.ts +184 -35
  59. package/src/hindsight/bank.ts +32 -22
  60. package/src/hindsight/mental-models.ts +1 -1
  61. package/src/hindsight/state.ts +21 -7
  62. package/src/internal-urls/docs-index.generated.ts +5 -5
  63. package/src/internal-urls/omp-protocol.ts +8 -2
  64. package/src/main.ts +4 -2
  65. package/src/mcp/json-rpc.ts +8 -0
  66. package/src/mcp/manager.ts +40 -21
  67. package/src/mcp/render.ts +3 -0
  68. package/src/mcp/tool-bridge.ts +10 -2
  69. package/src/mcp/transports/http.ts +33 -16
  70. package/src/mnemopi/state.ts +4 -4
  71. package/src/modes/acp/acp-agent.ts +168 -3
  72. package/src/modes/components/agent-dashboard.ts +103 -31
  73. package/src/modes/components/extensions/extension-dashboard.ts +56 -10
  74. package/src/modes/components/history-search.ts +128 -14
  75. package/src/modes/components/plugin-settings.ts +270 -36
  76. package/src/modes/components/session-selector.ts +45 -14
  77. package/src/modes/components/settings-selector.ts +1 -1
  78. package/src/modes/components/tips.txt +5 -1
  79. package/src/modes/components/transcript-container.ts +35 -6
  80. package/src/modes/components/tree-selector.ts +29 -2
  81. package/src/modes/controllers/command-controller.ts +4 -3
  82. package/src/modes/controllers/input-controller.ts +18 -7
  83. package/src/modes/controllers/selector-controller.ts +30 -19
  84. package/src/modes/interactive-mode.ts +38 -3
  85. package/src/modes/setup-wizard/scenes/sign-in.ts +27 -7
  86. package/src/modes/utils/keybinding-matchers.ts +10 -0
  87. package/src/prompts/agents/explore.md +1 -0
  88. package/src/prompts/agents/librarian.md +1 -0
  89. package/src/prompts/dry-balance-bench.md +8 -0
  90. package/src/prompts/steering/user-interjection.md +10 -0
  91. package/src/prompts/system/agent-creation-architect.md +1 -26
  92. package/src/prompts/system/system-prompt.md +143 -145
  93. package/src/prompts/system/title-system.md +3 -2
  94. package/src/prompts/tools/browser.md +29 -29
  95. package/src/prompts/tools/render-mermaid.md +2 -2
  96. package/src/sdk.ts +87 -30
  97. package/src/session/agent-session.ts +96 -14
  98. package/src/session/auth-storage.ts +4 -0
  99. package/src/session/history-storage.ts +11 -18
  100. package/src/session/messages.ts +80 -0
  101. package/src/session/session-manager.ts +7 -1
  102. package/src/slash-commands/types.ts +27 -10
  103. package/src/task/executor.ts +6 -2
  104. package/src/task/index.ts +8 -7
  105. package/src/task/types.ts +2 -0
  106. package/src/tiny/text.ts +112 -1
  107. package/src/tools/bash.ts +3 -4
  108. package/src/tools/index.ts +16 -0
  109. package/src/tools/job.ts +3 -3
  110. package/src/tools/memory-recall.ts +1 -1
  111. package/src/tools/memory-reflect.ts +3 -3
  112. package/src/tools/path-utils.ts +21 -0
  113. package/src/tools/search.ts +18 -1
  114. package/src/tools/ssh.ts +26 -10
  115. package/src/tools/write.ts +14 -2
  116. package/src/tui/status-line.ts +15 -4
  117. package/src/utils/file-mentions.ts +7 -107
  118. package/src/utils/title-generator.ts +66 -38
  119. package/src/web/search/index.ts +3 -1
  120. package/src/web/search/provider.ts +1 -1
  121. package/src/web/search/providers/base.ts +17 -0
  122. package/src/web/search/providers/exa.ts +111 -7
  123. package/src/web/search/providers/perplexity.ts +8 -4
  124. package/src/web/search/types.ts +2 -1
@@ -64,6 +64,7 @@ export async function selectSession(
64
64
  loadAllSessions: () => SessionManager.listAll(storage),
65
65
  allSessions: options?.allSessions,
66
66
  startInAllScope: options?.startInAllScope,
67
+ getTerminalRows: () => ui.terminal.rows,
67
68
  },
68
69
  );
69
70
  return selector;
@@ -27,6 +27,33 @@ const PACKAGE = "@oh-my-pi/pi-coding-agent";
27
27
  */
28
28
  const NPM_REGISTRY = "https://registry.npmjs.org/";
29
29
 
30
+ /**
31
+ * Core native addon package. Bumped in lock-step with {@link PACKAGE} so the
32
+ * version sentinel the loader looks up at runtime matches the `.node` on
33
+ * disk; see {@link buildBunInstallArgs} for why this must be installed
34
+ * explicitly rather than inherited as a transitive dependency.
35
+ */
36
+ const NATIVES_PACKAGE = "@oh-my-pi/pi-natives";
37
+
38
+ /**
39
+ * Platform tags the release pipeline publishes as
40
+ * `@oh-my-pi/pi-natives-<tag>` leaves. Mirrors `SUPPORTED_PLATFORMS` in
41
+ * `packages/natives/native/loader-state.js` and `LEAF_TARGETS` in
42
+ * `packages/natives/scripts/gen-npm-packages.ts`; kept here as the local
43
+ * source of truth so the update path stays free of cross-package imports.
44
+ */
45
+ const SUPPORTED_NATIVE_TAGS: ReadonlySet<string> = new Set([
46
+ "linux-x64",
47
+ "linux-arm64",
48
+ "darwin-x64",
49
+ "darwin-arm64",
50
+ "win32-x64",
51
+ ]);
52
+
53
+ function currentNativeTag(): string {
54
+ return `${process.platform}-${process.arch}`;
55
+ }
56
+
30
57
  interface ReleaseInfo {
31
58
  tag: string;
32
59
  version: string;
@@ -319,9 +346,34 @@ export async function replaceBinaryForUpdate(options: BinaryReplacementOptions):
319
346
  *
320
347
  * Together these two flags make `omp update` produce exactly the registry
321
348
  * lookup the version check just performed. See #1686.
349
+ *
350
+ * Also pins {@link NATIVES_PACKAGE} and the platform-specific
351
+ * `@oh-my-pi/pi-natives-<tag>` leaf to `expectedVersion`. `bun install -g`
352
+ * does not reliably refresh transitive `optionalDependencies` when the
353
+ * top-level package is the only one bumped, so the native addon and its
354
+ * version sentinel can drift out of sync with the freshly installed
355
+ * `@oh-my-pi/pi-coding-agent` and the loader aborts at
356
+ * `validateLoadedBindings` on the next launch
357
+ * (`The .node file on disk is from a different release than this loader`).
358
+ * Listing the natives explicitly forces bun to replace them in lock-step.
359
+ * The leaf is added only on tags the release pipeline actually publishes
360
+ * ({@link SUPPORTED_NATIVE_TAGS}) so unsupported platforms still fail with
361
+ * the original "no matching version" message instead of `EBADPLATFORM`.
362
+ * See #1824.
322
363
  */
323
- export function buildBunInstallArgs(expectedVersion: string): string[] {
324
- return ["install", "-g", "--no-cache", `--registry=${NPM_REGISTRY}`, `${PACKAGE}@${expectedVersion}`];
364
+ export function buildBunInstallArgs(expectedVersion: string, nativeTag: string = currentNativeTag()): string[] {
365
+ const args = [
366
+ "install",
367
+ "-g",
368
+ "--no-cache",
369
+ `--registry=${NPM_REGISTRY}`,
370
+ `${PACKAGE}@${expectedVersion}`,
371
+ `${NATIVES_PACKAGE}@${expectedVersion}`,
372
+ ];
373
+ if (SUPPORTED_NATIVE_TAGS.has(nativeTag)) {
374
+ args.push(`${NATIVES_PACKAGE}-${nativeTag}@${expectedVersion}`);
375
+ }
376
+ return args;
325
377
  }
326
378
 
327
379
  /**
@@ -20,6 +20,7 @@ export const commands: CommandEntry[] = [
20
20
  { name: "completions", load: () => import("./commands/completions").then(m => m.default) },
21
21
  { name: "__complete", load: () => import("./commands/complete").then(m => m.default) },
22
22
  { name: "config", load: () => import("./commands/config").then(m => m.default) },
23
+ { name: "dry-balance", load: () => import("./commands/dry-balance").then(m => m.default) },
23
24
  { name: "grep", load: () => import("./commands/grep").then(m => m.default) },
24
25
  { name: "grievances", load: () => import("./commands/grievances").then(m => m.default) },
25
26
  { name: "install", load: () => import("./commands/install").then(m => m.default) },
@@ -51,7 +51,7 @@ export default class Completions extends Command {
51
51
 
52
52
  const config: CliConfig = { bin: APP_NAME, version: VERSION, commands: map };
53
53
  const spec = buildSpec(config, ROOT_COMMAND, aliasMap);
54
- process.stdout.write(generateCompletion(shell, spec));
54
+ await Bun.write(Bun.stdout, generateCompletion(shell, spec));
55
55
  }
56
56
  }
57
57
 
@@ -0,0 +1,43 @@
1
+ import { Args, Command, Flags } from "@oh-my-pi/pi-utils/cli";
2
+ import { runDryBalanceCommand } from "../cli/dry-balance-cli";
3
+
4
+ export default class DryBalance extends Command {
5
+ static description = "Dry-run OAuth account balancing across random session ids";
6
+
7
+ static args = {
8
+ model: Args.string({
9
+ description: "Model selector (provider/model or fuzzy id). Defaults to the configured default model.",
10
+ required: false,
11
+ }),
12
+ };
13
+
14
+ static flags = {
15
+ model: Flags.string({ description: "Model selector (same syntax as --model on omp)" }),
16
+ count: Flags.integer({ description: "Number of random session ids to try", default: 100 }),
17
+ concurrency: Flags.integer({ description: "Maximum concurrent credential resolutions", default: 32 }),
18
+ json: Flags.boolean({ description: "Output JSON" }),
19
+ bench: Flags.boolean({ description: "Send one live benchmark request per OAuth account" }),
20
+ };
21
+
22
+ static examples = [
23
+ "# Dry-run the configured default model with 100 random session ids\n omp dry-balance",
24
+ "# Dry-run a specific model\n omp dry-balance anthropic/claude-sonnet-4-5",
25
+ "# Larger run with bounded concurrency\n omp dry-balance --model openai-codex/gpt-5-codex --count 1000 --concurrency 64",
26
+ "# Benchmark every OAuth account in parallel\n omp dry-balance --bench",
27
+ "# Machine-readable output\n omp dry-balance --json",
28
+ ];
29
+
30
+ async run(): Promise<void> {
31
+ const { args, flags } = await this.parse(DryBalance);
32
+ await runDryBalanceCommand({
33
+ model: args.model,
34
+ flags: {
35
+ model: flags.model,
36
+ count: flags.count,
37
+ concurrency: flags.concurrency,
38
+ json: flags.json,
39
+ bench: flags.bench,
40
+ },
41
+ });
42
+ }
43
+ }
@@ -0,0 +1,37 @@
1
+ /** Provider metadata needed to resolve append-only context mode. */
2
+ export interface AppendOnlyContextModel {
3
+ provider: string;
4
+ baseUrl: string;
5
+ compat?: object;
6
+ }
7
+
8
+ function isXiaomiHost(baseUrl: string): boolean {
9
+ try {
10
+ const host = new URL(baseUrl).hostname;
11
+ return host === "xiaomimimo.com" || host.endsWith(".xiaomimimo.com");
12
+ } catch {
13
+ return false;
14
+ }
15
+ }
16
+
17
+ function shouldAutoEnableAppendOnlyContext(model: AppendOnlyContextModel | null | undefined): boolean {
18
+ if (!model) return false;
19
+ if (model.provider === "deepseek") return true;
20
+ if (isXiaomiHost(model.baseUrl)) return true;
21
+ return !!model.compat && "supportsStore" in model.compat && model.compat.supportsStore === true;
22
+ }
23
+
24
+ /** Resolves whether append-only context should be active for a model and setting. */
25
+ export function shouldEnableAppendOnlyContext(
26
+ setting: "auto" | "on" | "off" | undefined,
27
+ model: AppendOnlyContextModel | null | undefined,
28
+ ): boolean {
29
+ switch (setting ?? "auto") {
30
+ case "on":
31
+ return true;
32
+ case "off":
33
+ return false;
34
+ default:
35
+ return shouldAutoEnableAppendOnlyContext(model);
36
+ }
37
+ }
@@ -547,6 +547,7 @@ function applyModelOverride(model: Model<Api>, override: ModelOverride): Model<A
547
547
  if (override.input !== undefined) result.input = override.input as ("text" | "image")[];
548
548
  if (override.contextWindow !== undefined) result.contextWindow = override.contextWindow;
549
549
  if (override.maxTokens !== undefined) result.maxTokens = override.maxTokens;
550
+ if (override.omitMaxOutputTokens !== undefined) result.omitMaxOutputTokens = override.omitMaxOutputTokens;
550
551
  if (override.contextPromotionTarget !== undefined) result.contextPromotionTarget = override.contextPromotionTarget;
551
552
  if (override.premiumMultiplier !== undefined) result.premiumMultiplier = override.premiumMultiplier;
552
553
  if (override.cost) {
@@ -575,6 +576,7 @@ interface CustomModelDefinitionLike {
575
576
  cost?: { input: number; output: number; cacheRead: number; cacheWrite: number };
576
577
  contextWindow?: number;
577
578
  maxTokens?: number;
579
+ omitMaxOutputTokens?: boolean;
578
580
  headers?: Record<string, string>;
579
581
  compat?: Model<Api>["compat"];
580
582
  contextPromotionTarget?: string;
@@ -597,6 +599,7 @@ type CustomModelOverlay = {
597
599
  cost?: { input: number; output: number; cacheRead: number; cacheWrite: number };
598
600
  contextWindow?: number;
599
601
  maxTokens?: number;
602
+ omitMaxOutputTokens?: boolean;
600
603
  headers?: Record<string, string>;
601
604
  compat?: Model<Api>["compat"];
602
605
  contextPromotionTarget?: string;
@@ -667,6 +670,7 @@ function buildCustomModelOverlay(
667
670
  cost: modelDef.cost,
668
671
  contextWindow: modelDef.contextWindow,
669
672
  maxTokens: modelDef.maxTokens,
673
+ omitMaxOutputTokens: modelDef.omitMaxOutputTokens,
670
674
  headers: mergeCustomModelHeaders(providerHeaders, modelDef.headers, authHeader, providerApiKey),
671
675
  compat: mergeCompat(providerCompat, modelDef.compat),
672
676
  contextPromotionTarget: modelDef.contextPromotionTarget,
@@ -823,6 +827,7 @@ function finalizeCustomModel(model: CustomModelOverlay, options: CustomModelBuil
823
827
  resolvedModel.contextWindow ?? reference?.contextWindow ?? (options.useDefaults ? 128000 : undefined),
824
828
  maxTokens: resolvedModel.maxTokens ?? reference?.maxTokens ?? (options.useDefaults ? 16384 : undefined),
825
829
  headers: resolvedModel.headers,
830
+ omitMaxOutputTokens: resolvedModel.omitMaxOutputTokens ?? reference?.omitMaxOutputTokens,
826
831
  compat: mergeCompat(reference?.compat, resolvedModel.compat),
827
832
  contextPromotionTarget: resolvedModel.contextPromotionTarget,
828
833
  premiumMultiplier: resolvedModel.premiumMultiplier,
@@ -1124,6 +1129,7 @@ export class ModelRegistry {
1124
1129
  cost: customModel.cost ?? existingModel.cost,
1125
1130
  contextWindow: customModel.contextWindow ?? existingModel.contextWindow,
1126
1131
  maxTokens: customModel.maxTokens ?? existingModel.maxTokens,
1132
+ omitMaxOutputTokens: customModel.omitMaxOutputTokens ?? existingModel.omitMaxOutputTokens,
1127
1133
  // Same-id custom definitions replace bundled transport behavior. Provider-level
1128
1134
  // headers/compat were already folded into customModel during parsing; do not
1129
1135
  // re-merge bundled transport metadata here.
@@ -41,6 +41,7 @@ export const OpenAICompatSchema = z.object({
41
41
  openRouterRouting: OpenRouterRoutingSchema.optional(),
42
42
  vercelGatewayRouting: VercelGatewayRoutingSchema.optional(),
43
43
  extraBody: z.record(z.string(), z.unknown()).optional(),
44
+ cacheControlFormat: z.enum(["anthropic"]).optional(),
44
45
  supportsStrictMode: z.boolean().optional(),
45
46
  toolStrictMode: z.enum(["all_strict", "none"]).optional(),
46
47
  });
@@ -92,6 +93,7 @@ const ModelDefinitionSchema = z.object({
92
93
  premiumMultiplier: z.number().optional(),
93
94
  contextWindow: z.number().optional(),
94
95
  maxTokens: z.number().optional(),
96
+ omitMaxOutputTokens: z.boolean().optional(),
95
97
  headers: z.record(z.string(), z.string()).optional(),
96
98
  compat: OpenAICompatSchema.optional(),
97
99
  contextPromotionTarget: z.string().min(1).optional(),
@@ -113,6 +115,7 @@ export const ModelOverrideSchema = z.object({
113
115
  premiumMultiplier: z.number().optional(),
114
116
  contextWindow: z.number().optional(),
115
117
  maxTokens: z.number().optional(),
118
+ omitMaxOutputTokens: z.boolean().optional(),
116
119
  headers: z.record(z.string(), z.string()).optional(),
117
120
  compat: OpenAICompatSchema.optional(),
118
121
  contextPromotionTarget: z.string().min(1).optional(),
@@ -3057,9 +3057,9 @@ export const SETTINGS_SCHEMA = {
3057
3057
  tab: "providers",
3058
3058
  label: "Append-Only Context",
3059
3059
  description:
3060
- "Cache system prompt + tool specs and keep an append-only message log so provider prefix caches (DeepSeek, Anthropic) hit at maximum rate. Auto enables for DeepSeek.",
3060
+ "Cache system prompt + tool specs and keep an append-only message log so provider prefix caches (DeepSeek, Xiaomi/SGLang, Anthropic) hit at maximum rate. Auto enables for known prefix-cache providers.",
3061
3061
  options: [
3062
- { value: "auto", label: "Auto", description: "Enable for DeepSeek (recommended)" },
3062
+ { value: "auto", label: "Auto", description: "Enable for known prefix-cache providers (recommended)" },
3063
3063
  { value: "on", label: "On", description: "Always enable append-only context" },
3064
3064
  { value: "off", label: "Off", description: "Disable append-only context" },
3065
3065
  ],
@@ -907,6 +907,9 @@ const SETTING_HOOKS: Partial<Record<SettingPath, SettingHook<any>>> = {
907
907
  for (const cb of appendOnlyModeCallbacks) cb(value);
908
908
  }
909
909
  },
910
+ "hindsight.bankId": () => fireHindsightScopeChanged(),
911
+ "hindsight.bankIdPrefix": () => fireHindsightScopeChanged(),
912
+ "hindsight.scoping": () => fireHindsightScopeChanged(),
910
913
  };
911
914
  /** Callbacks invoked when `provider.appendOnlyContext` changes at runtime. */
912
915
  const appendOnlyModeCallbacks = new Set<(value: string) => void>();
@@ -923,6 +926,41 @@ export function onAppendOnlyModeChanged(cb: (value: string) => void): () => void
923
926
  };
924
927
  }
925
928
 
929
+ /** Callbacks fired when any `hindsight.bankId` / `bankIdPrefix` / `scoping` value changes. */
930
+ const hindsightScopeCallbacks = new Set<() => void>();
931
+
932
+ function fireHindsightScopeChanged(): void {
933
+ // Snapshot the callback set before invoking — a callback's body is allowed
934
+ // to subscribe a NEW callback (the Hindsight backend re-registers the
935
+ // fresh state's listener on every rebuild). Iterating the live Set would
936
+ // re-invoke those just-added callbacks within the same fire, which spins
937
+ // in place: subscribe → invoke → subscribe → invoke → …
938
+ for (const cb of [...hindsightScopeCallbacks]) {
939
+ try {
940
+ cb();
941
+ } catch (err) {
942
+ logger.warn("Settings: hindsight scope hook failed", { error: String(err) });
943
+ }
944
+ }
945
+ }
946
+
947
+ /**
948
+ * Subscribe to changes in the Hindsight bank-scoping settings. Lets the
949
+ * Hindsight backend rebuild the active `HindsightSessionState` when the
950
+ * operator switches `hindsight.bankId`, `hindsight.bankIdPrefix`, or
951
+ * `hindsight.scoping` mid-session so subsequent retain/recall calls land in
952
+ * the new bank instead of the one selected at session start.
953
+ *
954
+ * Returns an unsubscribe function. The callback receives no arguments — the
955
+ * caller is expected to re-read the relevant settings via `Settings.get`.
956
+ */
957
+ export function onHindsightScopeChanged(cb: () => void): () => void {
958
+ hindsightScopeCallbacks.add(cb);
959
+ return () => {
960
+ hindsightScopeCallbacks.delete(cb);
961
+ };
962
+ }
963
+
926
964
  // ═══════════════════════════════════════════════════════════════════════════
927
965
  // Global Singleton
928
966
  // ═══════════════════════════════════════════════════════════════════════════
@@ -46,5 +46,6 @@ const doubled = value * 2;
46
46
  - Callback identity matters.
47
47
  - Type guard preserves narrowing.
48
48
  - Public API, test seam, or DI boundary needs indirection.
49
+ - Names a non-obvious formula or magic-constant computation that the inlined expression would not explain on its own.
49
50
 
50
51
  If none apply, inline it.
@@ -10,6 +10,7 @@
10
10
  * Capabilities:
11
11
  * - context-files: copilot-instructions.md in .github/
12
12
  * - instructions: *.instructions.md in .github/instructions/ with applyTo frontmatter
13
+ * - skills: <name>/SKILL.md in .github/skills/ (GitHub Agent Skills layout)
13
14
  */
14
15
  import * as path from "node:path";
15
16
  import { parseFrontmatter } from "@oh-my-pi/pi-utils";
@@ -17,9 +18,10 @@ import { registerProvider } from "../capability";
17
18
  import { type ContextFile, contextFileCapability } from "../capability/context-file";
18
19
  import { readFile } from "../capability/fs";
19
20
  import { type Instruction, instructionCapability } from "../capability/instruction";
21
+ import { type Skill, skillCapability } from "../capability/skill";
20
22
  import type { LoadContext, LoadResult, SourceMeta } from "../capability/types";
21
23
 
22
- import { calculateDepth, createSourceMeta, getProjectPath, loadFilesFromDir } from "./helpers";
24
+ import { calculateDepth, createSourceMeta, getProjectPath, loadFilesFromDir, scanSkillsFromDir } from "./helpers";
23
25
 
24
26
  const PROVIDER_ID = "github";
25
27
  const DISPLAY_NAME = "GitHub Copilot";
@@ -97,6 +99,32 @@ function transformInstruction(name: string, content: string, filePath: string, s
97
99
  };
98
100
  }
99
101
 
102
+ // =============================================================================
103
+ // Skills
104
+ // =============================================================================
105
+
106
+ /**
107
+ * Load skills from `.github/skills/<name>/SKILL.md`.
108
+ *
109
+ * GitHub documents this layout for Copilot Agent Skills and matches the
110
+ * non-recursive shape `scanSkillsFromDir` already expects. `requireDescription`
111
+ * is on to match the Agent Skills spec (name + description are mandatory) and
112
+ * the sibling `native`/`omp-plugins` providers.
113
+ *
114
+ * @see https://docs.github.com/en/copilot/how-tos/copilot-on-github/customize-copilot/customize-cloud-agent/add-skills
115
+ */
116
+ async function loadSkills(ctx: LoadContext): Promise<LoadResult<Skill>> {
117
+ const skillsDir = getProjectPath(ctx, "github", "skills");
118
+ if (!skillsDir) return { items: [], warnings: [] };
119
+
120
+ return scanSkillsFromDir(ctx, {
121
+ dir: skillsDir,
122
+ providerId: PROVIDER_ID,
123
+ level: "project",
124
+ requireDescription: true,
125
+ });
126
+ }
127
+
100
128
  // =============================================================================
101
129
  // Provider Registration
102
130
  // =============================================================================
@@ -116,3 +144,11 @@ registerProvider(instructionCapability.id, {
116
144
  priority: PRIORITY,
117
145
  load: loadInstructions,
118
146
  });
147
+
148
+ registerProvider<Skill>(skillCapability.id, {
149
+ id: PROVIDER_ID,
150
+ displayName: DISPLAY_NAME,
151
+ description: "Load skills from .github/skills/*/SKILL.md",
152
+ priority: PRIORITY,
153
+ load: loadSkills,
154
+ });
@@ -212,6 +212,7 @@ export interface ParsedAgentFields {
212
212
  output?: unknown;
213
213
  thinkingLevel?: ThinkingLevel;
214
214
  autoloadSkills?: string[];
215
+ readSummarize?: boolean;
215
216
  blocking?: boolean;
216
217
  }
217
218
 
@@ -265,10 +266,11 @@ export function parseAgentFields(frontmatter: Record<string, unknown>): ParsedAg
265
266
  const thinkingLevel = parseThinkingLevel(rawThinkingLevel);
266
267
  const model = parseModelList(frontmatter.model);
267
268
  const blocking = parseBoolean(frontmatter.blocking);
269
+ const readSummarize = parseBoolean(frontmatter.readSummarize);
268
270
  const autoloadSkills = parseArrayOrCSV(frontmatter.autoloadSkills)
269
271
  ?.map(s => s.trim())
270
272
  .filter(Boolean);
271
- return { name, description, tools, spawns, model, output, thinkingLevel, blocking, autoloadSkills };
273
+ return { name, description, tools, spawns, model, output, thinkingLevel, blocking, autoloadSkills, readSummarize };
272
274
  }
273
275
 
274
276
  async function globIf(
@@ -1,7 +1,7 @@
1
1
  import type { TSchema } from "@oh-my-pi/pi-ai";
2
2
  import { $env, logger } from "@oh-my-pi/pi-utils";
3
3
  import type { CustomTool, CustomToolResult } from "../extensibility/custom-tools/types";
4
- import { callMCP } from "../mcp/json-rpc";
4
+ import { type CallMcpOptions, callMCP } from "../mcp/json-rpc";
5
5
  import type {
6
6
  ExaRenderDetails,
7
7
  ExaSearchResponse,
@@ -105,15 +105,21 @@ export async function callExaTool(
105
105
  toolName: string,
106
106
  args: Record<string, unknown>,
107
107
  apiKey: string | null,
108
+ options?: CallMcpOptions,
108
109
  ): Promise<unknown> {
109
110
  const params = new URLSearchParams();
110
111
  if (apiKey) params.set("exaApiKey", apiKey);
111
112
  params.set("tools", toolName);
112
113
  const url = `https://mcp.exa.ai/mcp?${params.toString()}`;
113
- const response = (await callMCP(url, "tools/call", {
114
- name: toolName,
115
- arguments: args,
116
- })) as MCPCallResponse;
114
+ const response = (await callMCP(
115
+ url,
116
+ "tools/call",
117
+ {
118
+ name: toolName,
119
+ arguments: args,
120
+ },
121
+ options,
122
+ )) as MCPCallResponse;
117
123
 
118
124
  if (response.error) {
119
125
  logger.error("MCP tools/call error", { toolName, args, error: response.error });