diff --git a/src/compaction.ts b/src/compaction.ts index e93152a..4645ced 100644 --- a/src/compaction.ts +++ b/src/compaction.ts @@ -6,12 +6,15 @@ export interface CompactionRuntime { pendingCompactionSource: CompactionSource | null; /** Manual compaction with default settings: clear stale pending on next user prompt (SPEC §16.5, §12.3). */ clearPendingReinjectOnNextUserInput: boolean; + /** Last compact firstKeptEntryId for debug kept-window snapshots (Phase 14). */ + lastCompactionFirstKeptEntryId: string | null; } export function createCompactionRuntime(): CompactionRuntime { return { pendingCompactionSource: null, clearPendingReinjectOnNextUserInput: false, + lastCompactionFirstKeptEntryId: null, }; } diff --git a/src/diag.ts b/src/diag.ts new file mode 100644 index 0000000..37189c6 --- /dev/null +++ b/src/diag.ts @@ -0,0 +1,42 @@ +import type { ExtensionContext, Skill } from "@earendil-works/pi-coding-agent"; +import type { SkillReinjectSettings } from "./settings.js"; +import type { ExtensionState } from "./state.js"; + +export type ReinjectDiagPhase = "session_compact" | "before_agent_start"; + +/** Filter snapshot for debug logging (Phase 14 / B-002). */ +export interface ReinjectDiagSnapshot { + tracked: string[]; + kept: string[]; + registered: string[]; + planned: string[]; + pending: string[]; +} + +export function buildReinjectDiagSnapshot( + state: ExtensionState, + registeredSkills: readonly Pick[], + keptPresent: ReadonlySet, + planned: readonly string[], +): ReinjectDiagSnapshot { + return { + tracked: state.skills.map((skill) => skill.name), + kept: [...keptPresent], + registered: registeredSkills.map((skill) => skill.name), + planned: [...planned], + pending: [...state.pendingReinject], + }; +} + +/** Log reinject filter state when settings.debug is on (Phase 14). */ +export function notifyReinjectDiag( + ctx: ExtensionContext | undefined, + settings: SkillReinjectSettings, + phase: ReinjectDiagPhase, + snapshot: ReinjectDiagSnapshot, +): void { + if (!settings.debug || !ctx?.hasUI) { + return; + } + ctx.ui.notify(`skill-reinject [${phase}]: ${JSON.stringify(snapshot)}`, "info"); +} diff --git a/src/index.ts b/src/index.ts index e1661d5..7378ec5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,7 +14,12 @@ import { markAutoCompactionBeforeCompact, markManualCompactionFromInput, } from "./compaction.js"; +import { buildReinjectDiagSnapshot, notifyReinjectDiag } from "./diag.js"; import { detectSlashSkill, matchReadPathToSkillWhenEnabled, parseSkillBlocksFromText, userMessageText } from "./detect.js"; +import { + getKeptEntries, + skillsPresentInKeptWindow, +} from "./kept.js"; import { applyPendingReinjectAfterCompact, clearPendingReinjectOnUserPrompt, @@ -92,6 +97,11 @@ export default function skillReinject(pi: ExtensionAPI): void { function handleSessionCompact(event: SessionCompactEvent, ctx: ExtensionContext): void { const settings = readSettings(ctx); const skills = resolveRegisteredSkills(ctx.cwd, registeredSkills); + const branch = ctx.sessionManager.getBranch(); + compactionRuntime.lastCompactionFirstKeptEntryId = event.compactionEntry.firstKeptEntryId; + const trackedNames = state.skills.map((skill) => skill.name); + const keptEntries = getKeptEntries(branch, event.compactionEntry.firstKeptEntryId); + const keptPresent = skillsPresentInKeptWindow(keptEntries, trackedNames); const planned = planReinject(state, settings, ctx, event, skills); const shouldReinject = consumeCompactionOnSessionCompact( compactionRuntime, @@ -101,13 +111,19 @@ export default function skillReinject(pi: ExtensionAPI): void { ); const deliveryMode = resolveDeliveryMode(settings, runtime, state.sessionIntegrationOverride); + applyPendingReinjectAfterCompact(state, compactionRuntime, shouldReinject, planned); + notifyReinjectDiag( + ctx, + settings, + "session_compact", + buildReinjectDiagSnapshot(state, skills, keptPresent, planned), + ); + if (deliveryMode === "defer") { - applyPendingReinjectAfterCompact(state, compactionRuntime, shouldReinject, planned); persistState(); return; } - applyPendingReinjectAfterCompact(state, compactionRuntime, shouldReinject, planned); if (!shouldReinject) { persistState(); return; @@ -149,6 +165,24 @@ export default function skillReinject(pi: ExtensionAPI): void { registeredSkills = event.systemPromptOptions.skills ?? registeredSkills; const settings = readSettings(ctx); + const skills = resolveRegisteredSkills(ctx.cwd, registeredSkills); + const trackedNames = state.skills.map((skill) => skill.name); + const keptPresent = + compactionRuntime.lastCompactionFirstKeptEntryId === null + ? new Set() + : skillsPresentInKeptWindow( + getKeptEntries( + ctx.sessionManager.getBranch(), + compactionRuntime.lastCompactionFirstKeptEntryId, + ), + trackedNames, + ); + notifyReinjectDiag( + ctx, + settings, + "before_agent_start", + buildReinjectDiagSnapshot(state, skills, keptPresent, []), + ); const pendingBefore = state.pendingReinject.length; const deferred = tryConsumeDeferredReinject( state, @@ -157,6 +191,9 @@ export default function skillReinject(pi: ExtensionAPI): void { ctx, compactionRuntime, ); + if (deferred) { + compactionRuntime.lastCompactionFirstKeptEntryId = null; + } if (pendingBefore > 0) { persistState(); } diff --git a/src/settings.ts b/src/settings.ts index cb16220..4c8947b 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -26,6 +26,8 @@ export interface SkillReinjectSettings { suffix: string; /** Soft warn threshold; omit for unlimited with default warn above 3 (SPEC §15). */ maxSkills?: number; + /** Verbose reinject filter logging via ui.notify (Phase 14 / B-002). */ + debug: boolean; } /** Defaults from SPEC §7.3 — extension off until explicitly enabled. */ @@ -36,6 +38,7 @@ export const DEFAULT_SKILL_REINJECT_SETTINGS: Readonly = reinjectOnManualCompaction: false, autoCompactIntegration: "auto", suffix: "[skill-reinject] Re-applied after compaction.", + debug: false, }; export function createDefaultSettings(): SkillReinjectSettings { @@ -77,6 +80,9 @@ export function parseSkillReinjectPartial(raw: unknown): PartialSkillReinjectSet if (typeof obj.maxSkills === "number" && Number.isInteger(obj.maxSkills) && obj.maxSkills > 0) { result.maxSkills = obj.maxSkills; } + if (typeof obj.debug === "boolean") { + result.debug = obj.debug; + } return result; } diff --git a/test/diag.test.ts b/test/diag.test.ts new file mode 100644 index 0000000..186e773 --- /dev/null +++ b/test/diag.test.ts @@ -0,0 +1,81 @@ +import { describe, expect, it, vi } from "vitest"; +import { buildReinjectDiagSnapshot, notifyReinjectDiag } from "../src/diag.js"; +import { createDefaultSettings } from "../src/settings.js"; +import { createInitialState } from "../src/state.js"; + +describe("buildReinjectDiagSnapshot", () => { + it("collects tracked, kept, registered, planned, and pending", () => { + const state = createInitialState(); + state.skills.push({ + name: "alpha", + filePath: "/skills/alpha/SKILL.md", + baseDir: "/skills/alpha", + firstSeenAt: 1, + lastSeenAt: 1, + sources: ["slash"], + }); + state.pendingReinject = ["alpha"]; + + expect( + buildReinjectDiagSnapshot( + state, + [{ name: "beta" }], + new Set(["gamma"]), + ["alpha"], + ), + ).toEqual({ + tracked: ["alpha"], + kept: ["gamma"], + registered: ["beta"], + planned: ["alpha"], + pending: ["alpha"], + }); + }); +}); + +describe("notifyReinjectDiag", () => { + it("no-ops when debug is off", () => { + const notify = vi.fn(); + notifyReinjectDiag( + { + hasUI: true, + ui: { notify }, + } as never, + createDefaultSettings(), + "session_compact", + { + tracked: [], + kept: [], + registered: [], + planned: [], + pending: [], + }, + ); + expect(notify).not.toHaveBeenCalled(); + }); + + it("notifies with JSON snapshot when debug is on", () => { + const notify = vi.fn(); + const settings = { ...createDefaultSettings(), debug: true }; + const snapshot = { + tracked: ["a"], + kept: [], + registered: [], + planned: ["a"], + pending: ["a"], + }; + notifyReinjectDiag( + { + hasUI: true, + ui: { notify }, + } as never, + settings, + "before_agent_start", + snapshot, + ); + expect(notify).toHaveBeenCalledWith( + `skill-reinject [before_agent_start]: ${JSON.stringify(snapshot)}`, + "info", + ); + }); +}); diff --git a/test/settings.test.ts b/test/settings.test.ts index fe505d9..5e079c9 100644 --- a/test/settings.test.ts +++ b/test/settings.test.ts @@ -54,6 +54,10 @@ describe("parseSkillReinjectPartial", () => { }), ).toEqual({ enabled: true, suffix: "custom" }); }); + + it("parses debug flag", () => { + expect(parseSkillReinjectPartial({ debug: true })).toEqual({ debug: true }); + }); }); describe("mergeSkillReinjectSettings", () => {