Phase 14: reinjectNow loose fallback for --skill paths — B-002 now command
resolveReinjectSkillNames includes tracked skills on disk when requireRegistered is false so /skill-reinject now works without resourceLoader entry. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+26
-2
@@ -252,6 +252,31 @@ export function sendImmediateReinjectAllFollowUp(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Names eligible for re-inject: registered, or on disk when requireRegistered is false (Phase 14 / B-002). */
|
||||||
|
export function resolveReinjectSkillNames(
|
||||||
|
state: ExtensionState,
|
||||||
|
settings: SkillReinjectSettings,
|
||||||
|
registeredSkills: readonly Pick<Skill, "name">[],
|
||||||
|
ctx?: ExtensionContext,
|
||||||
|
): string[] {
|
||||||
|
const registered = registeredSkillNames(registeredSkills);
|
||||||
|
const names: string[] = [];
|
||||||
|
for (const tracked of state.skills) {
|
||||||
|
if (registered.has(tracked.name)) {
|
||||||
|
names.push(tracked.name);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (settings.requireRegistered) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (existsSync(tracked.filePath)) {
|
||||||
|
notifyInfo(ctx, `skill-reinject: re-injected "${tracked.name}" from disk`);
|
||||||
|
names.push(tracked.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
/** Force re-inject all tracked registered skills for /skill-reinject now (SPEC §7.1). */
|
/** Force re-inject all tracked registered skills for /skill-reinject now (SPEC §7.1). */
|
||||||
export function reinjectNow(
|
export function reinjectNow(
|
||||||
pi: ExtensionAPI,
|
pi: ExtensionAPI,
|
||||||
@@ -260,8 +285,7 @@ export function reinjectNow(
|
|||||||
ctx: ExtensionContext,
|
ctx: ExtensionContext,
|
||||||
registeredSkills: readonly Pick<Skill, "name" | "filePath" | "baseDir">[],
|
registeredSkills: readonly Pick<Skill, "name" | "filePath" | "baseDir">[],
|
||||||
): void {
|
): void {
|
||||||
const registered = registeredSkillNames(registeredSkills);
|
const skillNames = resolveReinjectSkillNames(state, settings, registeredSkills, ctx);
|
||||||
const skillNames = state.skills.map((skill) => skill.name).filter((name) => registered.has(name));
|
|
||||||
if (skillNames.length === 0) {
|
if (skillNames.length === 0) {
|
||||||
if (ctx.hasUI) {
|
if (ctx.hasUI) {
|
||||||
ctx.ui.notify("skill-reinject: no tracked skills to re-inject", "info");
|
ctx.ui.notify("skill-reinject: no tracked skills to re-inject", "info");
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "fs";
|
||||||
|
import { tmpdir } from "os";
|
||||||
|
import { join } from "path";
|
||||||
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { reinjectNow, resolveReinjectSkillNames } from "../src/reinject";
|
||||||
|
import { createDefaultSettings } from "../src/settings";
|
||||||
|
import { createInitialState, trackSkill } from "../src/state";
|
||||||
|
|
||||||
|
const tempDirs: string[] = [];
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
for (const dir of tempDirs.splice(0)) {
|
||||||
|
rmSync(dir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function tempSkillDir(name: string): { filePath: string; baseDir: string } {
|
||||||
|
const root = mkdtempSync(join(tmpdir(), "pi-skill-reinject-now-"));
|
||||||
|
tempDirs.push(root);
|
||||||
|
const baseDir = join(root, name);
|
||||||
|
mkdirSync(baseDir, { recursive: true });
|
||||||
|
const filePath = join(baseDir, "SKILL.md");
|
||||||
|
writeFileSync(filePath, "# skill\n", "utf8");
|
||||||
|
return { filePath, baseDir };
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("resolveReinjectSkillNames", () => {
|
||||||
|
it("includes loose tracked skill when file exists and requireRegistered is false", () => {
|
||||||
|
const { filePath, baseDir } = tempSkillDir("loose");
|
||||||
|
const state = createInitialState();
|
||||||
|
trackSkill(state, { name: "loose", filePath, baseDir, source: "slash" });
|
||||||
|
|
||||||
|
expect(resolveReinjectSkillNames(state, createDefaultSettings(), [])).toEqual(["loose"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("excludes unregistered skills when requireRegistered is true", () => {
|
||||||
|
const { filePath, baseDir } = tempSkillDir("strict");
|
||||||
|
const state = createInitialState();
|
||||||
|
trackSkill(state, { name: "strict", filePath, baseDir, source: "slash" });
|
||||||
|
const settings = createDefaultSettings();
|
||||||
|
settings.requireRegistered = true;
|
||||||
|
|
||||||
|
expect(resolveReinjectSkillNames(state, settings, [])).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("reinjectNow loose path", () => {
|
||||||
|
it("sends skill block from disk for unregistered tracked skill", () => {
|
||||||
|
const { filePath, baseDir } = tempSkillDir("loose");
|
||||||
|
const state = createInitialState();
|
||||||
|
trackSkill(state, { name: "loose", filePath, baseDir, source: "slash" });
|
||||||
|
const sendUserMessage = vi.fn();
|
||||||
|
const pi = { sendUserMessage } as never;
|
||||||
|
const ctx = { hasUI: false, isIdle: () => true } as never;
|
||||||
|
|
||||||
|
reinjectNow(pi, state, createDefaultSettings(), ctx, []);
|
||||||
|
|
||||||
|
expect(sendUserMessage).toHaveBeenCalledTimes(1);
|
||||||
|
expect(sendUserMessage.mock.calls[0]?.[0]).toContain('<skill name="loose"');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user