From 66d9a39a182953e6622051e3a2c5966684d7466f Mon Sep 17 00:00:00 2001 From: GRayHook Date: Wed, 17 Jun 2026 13:12:21 +0700 Subject: [PATCH] =?UTF-8?q?Phase=2012:=20maxSkills=20soft=20warn=20?= =?UTF-8?q?=E2=80=94=20SPEC=20=C2=A715.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Optional maxSkills setting sets the warn threshold; when unset, re-injecting more than three tracked skills emits a one-time UI warning without blocking delivery. Co-authored-by: Cursor --- src/reinject.ts | 24 +++++++++++++++++++ src/settings.ts | 5 ++++ test/reinject-max-skills.test.ts | 40 ++++++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+) create mode 100644 test/reinject-max-skills.test.ts diff --git a/src/reinject.ts b/src/reinject.ts index dd4f9c6..60252b4 100644 --- a/src/reinject.ts +++ b/src/reinject.ts @@ -96,6 +96,29 @@ export function enqueueDeferredReinjectFromCompact( ); } +/** Warn when re-injecting many skills; unlimited by default with soft warn above 3 (SPEC §15). */ +export function maybeWarnManySkills( + skillCount: number, + settings: SkillReinjectSettings, + ctx?: ExtensionContext, +): void { + if (skillCount <= 0) { + return; + } + const threshold = settings.maxSkills ?? 3; + if (skillCount <= threshold) { + return; + } + if (settings.maxSkills !== undefined) { + notifyWarning( + ctx, + `skill-reinject: re-injecting ${skillCount} skills (maxSkills warn threshold: ${settings.maxSkills})`, + ); + return; + } + notifyWarning(ctx, `skill-reinject: re-injecting ${skillCount} tracked skills (soft warn above 3)`); +} + /** Expanded skill-block messages in queue order (SPEC §5.3). */ export function buildReinjectBlocks( skillNames: readonly string[], @@ -104,6 +127,7 @@ export function buildReinjectBlocks( registeredSkills: readonly Pick[], ctx?: ExtensionContext, ): string[] { + maybeWarnManySkills(skillNames.length, settings, ctx); const registeredByName = registeredSkillsByName(registeredSkills, ctx); const blocks: string[] = []; for (const name of skillNames) { diff --git a/src/settings.ts b/src/settings.ts index 5c0c768..cb16220 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -24,6 +24,8 @@ export interface SkillReinjectSettings { reinjectOnManualCompaction: boolean; autoCompactIntegration: AutoCompactIntegration; suffix: string; + /** Soft warn threshold; omit for unlimited with default warn above 3 (SPEC §15). */ + maxSkills?: number; } /** Defaults from SPEC §7.3 — extension off until explicitly enabled. */ @@ -72,6 +74,9 @@ export function parseSkillReinjectPartial(raw: unknown): PartialSkillReinjectSet if (typeof obj.suffix === "string") { result.suffix = obj.suffix; } + if (typeof obj.maxSkills === "number" && Number.isInteger(obj.maxSkills) && obj.maxSkills > 0) { + result.maxSkills = obj.maxSkills; + } return result; } diff --git a/test/reinject-max-skills.test.ts b/test/reinject-max-skills.test.ts new file mode 100644 index 0000000..a162886 --- /dev/null +++ b/test/reinject-max-skills.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, it, vi } from "vitest"; +import { maybeWarnManySkills } from "../src/reinject"; +import { createDefaultSettings } from "../src/settings"; + +describe("maybeWarnManySkills", () => { + it("warns above 3 when maxSkills is unset", () => { + const notify = vi.fn(); + const ctx = { hasUI: true, ui: { notify } } as never; + + maybeWarnManySkills(4, createDefaultSettings(), ctx); + + expect(notify).toHaveBeenCalledWith( + "skill-reinject: re-injecting 4 tracked skills (soft warn above 3)", + "warning", + ); + }); + + it("does not warn at 3 skills when maxSkills is unset", () => { + const notify = vi.fn(); + const ctx = { hasUI: true, ui: { notify } } as never; + + maybeWarnManySkills(3, createDefaultSettings(), ctx); + + expect(notify).not.toHaveBeenCalled(); + }); + + it("uses configured maxSkills threshold", () => { + const notify = vi.fn(); + const ctx = { hasUI: true, ui: { notify } } as never; + const settings = createDefaultSettings(); + settings.maxSkills = 2; + + maybeWarnManySkills(3, settings, ctx); + + expect(notify).toHaveBeenCalledWith( + "skill-reinject: re-injecting 3 skills (maxSkills warn threshold: 2)", + "warning", + ); + }); +});