Phase 7: skip missing skills with ui.notify warning on expand.
buildReinjectBlocks checks registration and file presence before expandSkill; warns via ctx.ui when hasUI, no-op in RPC/print mode. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+41
-16
@@ -5,6 +5,7 @@ import type {
|
|||||||
SessionCompactEvent,
|
SessionCompactEvent,
|
||||||
Skill,
|
Skill,
|
||||||
} from "@earendil-works/pi-coding-agent";
|
} from "@earendil-works/pi-coding-agent";
|
||||||
|
import { existsSync } from "node:fs";
|
||||||
import { expandSkill } from "./expand.js";
|
import { expandSkill } from "./expand.js";
|
||||||
import {
|
import {
|
||||||
filterSkillsNeedingReinject,
|
filterSkillsNeedingReinject,
|
||||||
@@ -16,6 +17,13 @@ import type { ExtensionState } from "./state.js";
|
|||||||
|
|
||||||
export const DEFERRED_REINJECT_CUSTOM_TYPE = "skill-reinject:inject";
|
export const DEFERRED_REINJECT_CUSTOM_TYPE = "skill-reinject:inject";
|
||||||
|
|
||||||
|
function notifySkippedSkill(ctx: ExtensionContext | undefined, skillName: string, reason: string): void {
|
||||||
|
if (!ctx?.hasUI) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ctx.ui.notify(`skill-reinject: skipped "${skillName}" — ${reason}`, "warning");
|
||||||
|
}
|
||||||
|
|
||||||
/** Names still registered in resourceLoader (SPEC §5.2). */
|
/** Names still registered in resourceLoader (SPEC §5.2). */
|
||||||
export function registeredSkillNames(skills: readonly Pick<Skill, "name">[]): ReadonlySet<string> {
|
export function registeredSkillNames(skills: readonly Pick<Skill, "name">[]): ReadonlySet<string> {
|
||||||
return new Set(skills.map((skill) => skill.name));
|
return new Set(skills.map((skill) => skill.name));
|
||||||
@@ -66,25 +74,38 @@ export function buildReinjectBlocks(
|
|||||||
state: ExtensionState,
|
state: ExtensionState,
|
||||||
settings: SkillReinjectSettings,
|
settings: SkillReinjectSettings,
|
||||||
registeredSkills: readonly Pick<Skill, "name" | "filePath" | "baseDir">[],
|
registeredSkills: readonly Pick<Skill, "name" | "filePath" | "baseDir">[],
|
||||||
|
ctx?: ExtensionContext,
|
||||||
): string[] {
|
): string[] {
|
||||||
const registeredByName = new Map(registeredSkills.map((skill) => [skill.name, skill]));
|
const registeredByName = new Map(registeredSkills.map((skill) => [skill.name, skill]));
|
||||||
const blocks: string[] = [];
|
const blocks: string[] = [];
|
||||||
for (const name of skillNames) {
|
for (const name of skillNames) {
|
||||||
const tracked = state.skills.find((skill) => skill.name === name);
|
const tracked = state.skills.find((skill) => skill.name === name);
|
||||||
const registered = registeredByName.get(name);
|
if (!tracked) {
|
||||||
if (!tracked || !registered) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
blocks.push(
|
const registered = registeredByName.get(name);
|
||||||
expandSkill(
|
if (!registered) {
|
||||||
{
|
notifySkippedSkill(ctx, name, "no longer registered");
|
||||||
name: tracked.name,
|
continue;
|
||||||
filePath: registered.filePath,
|
}
|
||||||
baseDir: registered.baseDir,
|
if (!existsSync(registered.filePath)) {
|
||||||
},
|
notifySkippedSkill(ctx, name, "SKILL.md not found on disk");
|
||||||
settings.suffix,
|
continue;
|
||||||
),
|
}
|
||||||
);
|
try {
|
||||||
|
blocks.push(
|
||||||
|
expandSkill(
|
||||||
|
{
|
||||||
|
name: tracked.name,
|
||||||
|
filePath: registered.filePath,
|
||||||
|
baseDir: registered.baseDir,
|
||||||
|
},
|
||||||
|
settings.suffix,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
notifySkippedSkill(ctx, name, "SKILL.md not readable");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return blocks;
|
return blocks;
|
||||||
}
|
}
|
||||||
@@ -95,8 +116,9 @@ export function buildDeferredReinjectContent(
|
|||||||
state: ExtensionState,
|
state: ExtensionState,
|
||||||
settings: SkillReinjectSettings,
|
settings: SkillReinjectSettings,
|
||||||
registeredSkills: readonly Pick<Skill, "name" | "filePath" | "baseDir">[],
|
registeredSkills: readonly Pick<Skill, "name" | "filePath" | "baseDir">[],
|
||||||
|
ctx?: ExtensionContext,
|
||||||
): string {
|
): string {
|
||||||
return buildReinjectBlocks(pendingNames, state, settings, registeredSkills).join("\n\n");
|
return buildReinjectBlocks(pendingNames, state, settings, registeredSkills, ctx).join("\n\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Immediate path when agent is idle: first block triggers turn, rest queue as followUp (SPEC §6.5.2). */
|
/** Immediate path when agent is idle: first block triggers turn, rest queue as followUp (SPEC §6.5.2). */
|
||||||
@@ -106,8 +128,9 @@ export function sendImmediateReinjectIdle(
|
|||||||
state: ExtensionState,
|
state: ExtensionState,
|
||||||
settings: SkillReinjectSettings,
|
settings: SkillReinjectSettings,
|
||||||
registeredSkills: readonly Pick<Skill, "name" | "filePath" | "baseDir">[],
|
registeredSkills: readonly Pick<Skill, "name" | "filePath" | "baseDir">[],
|
||||||
|
ctx?: ExtensionContext,
|
||||||
): void {
|
): void {
|
||||||
const blocks = buildReinjectBlocks(skillNames, state, settings, registeredSkills);
|
const blocks = buildReinjectBlocks(skillNames, state, settings, registeredSkills, ctx);
|
||||||
blocks.forEach((block, index) => {
|
blocks.forEach((block, index) => {
|
||||||
pi.sendUserMessage(block, index === 0 ? undefined : { deliverAs: "followUp" });
|
pi.sendUserMessage(block, index === 0 ? undefined : { deliverAs: "followUp" });
|
||||||
});
|
});
|
||||||
@@ -120,8 +143,9 @@ export function sendImmediateReinjectAllFollowUp(
|
|||||||
state: ExtensionState,
|
state: ExtensionState,
|
||||||
settings: SkillReinjectSettings,
|
settings: SkillReinjectSettings,
|
||||||
registeredSkills: readonly Pick<Skill, "name" | "filePath" | "baseDir">[],
|
registeredSkills: readonly Pick<Skill, "name" | "filePath" | "baseDir">[],
|
||||||
|
ctx?: ExtensionContext,
|
||||||
): void {
|
): void {
|
||||||
for (const block of buildReinjectBlocks(skillNames, state, settings, registeredSkills)) {
|
for (const block of buildReinjectBlocks(skillNames, state, settings, registeredSkills, ctx)) {
|
||||||
pi.sendUserMessage(block, { deliverAs: "followUp" });
|
pi.sendUserMessage(block, { deliverAs: "followUp" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -134,12 +158,13 @@ export function tryConsumeDeferredReinject(
|
|||||||
state: ExtensionState,
|
state: ExtensionState,
|
||||||
settings: SkillReinjectSettings,
|
settings: SkillReinjectSettings,
|
||||||
registeredSkills: readonly Pick<Skill, "name" | "filePath" | "baseDir">[],
|
registeredSkills: readonly Pick<Skill, "name" | "filePath" | "baseDir">[],
|
||||||
|
ctx?: ExtensionContext,
|
||||||
): BeforeAgentStartEventResult | undefined {
|
): BeforeAgentStartEventResult | undefined {
|
||||||
if (state.pendingReinject.length === 0) {
|
if (state.pendingReinject.length === 0) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const pendingNames = [...state.pendingReinject];
|
const pendingNames = [...state.pendingReinject];
|
||||||
const content = buildDeferredReinjectContent(pendingNames, state, settings, registeredSkills);
|
const content = buildDeferredReinjectContent(pendingNames, state, settings, registeredSkills, ctx);
|
||||||
if (!content) {
|
if (!content) {
|
||||||
state.pendingReinject = [];
|
state.pendingReinject = [];
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|||||||
Reference in New Issue
Block a user