pi-monofold 0.6.2 → 0.7.0
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.
- package/focus-preset.ts +13 -2
- package/focus-skills.ts +106 -0
- package/index.ts +18 -1
- package/package.json +2 -1
package/focus-preset.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { parseFocusSkills, resetFocusSkillsWarningState } from "./focus-skills.js";
|
|
1
2
|
import { assertKnownKeys, asStringArray, isRecord, uniqueStrings } from "./validation.js";
|
|
2
3
|
|
|
3
4
|
export type FocusPresetTarget = {
|
|
@@ -8,13 +9,14 @@ export type FocusPreset = {
|
|
|
8
9
|
id: string;
|
|
9
10
|
label: string;
|
|
10
11
|
targets: FocusPresetTarget[];
|
|
12
|
+
focusSkills?: string[];
|
|
11
13
|
};
|
|
12
14
|
|
|
13
15
|
export type FocusMatchableWorkspace = {
|
|
14
16
|
tags: string[];
|
|
15
17
|
};
|
|
16
18
|
|
|
17
|
-
const FOCUS_PRESET_KEYS = new Set(["id", "label", "targets"]);
|
|
19
|
+
const FOCUS_PRESET_KEYS = new Set(["id", "label", "targets", "focusSkills"]);
|
|
18
20
|
const FOCUS_PRESET_TARGET_KEYS = new Set(["targetTags"]);
|
|
19
21
|
|
|
20
22
|
/** Parses and validates focus preset configuration from YAML/JSON input. */
|
|
@@ -49,9 +51,15 @@ export function parseFocusPresets(value: unknown, label = "focusPresets"): Focus
|
|
|
49
51
|
}
|
|
50
52
|
targets.push({ targetTags });
|
|
51
53
|
}
|
|
54
|
+
const focusSkills = parseFocusSkills(itemLabel, item.focusSkills);
|
|
52
55
|
if (seenIds.has(item.id)) throw new Error(`${label} has duplicate preset id: ${item.id}`);
|
|
53
56
|
seenIds.add(item.id);
|
|
54
|
-
presets.push({
|
|
57
|
+
presets.push({
|
|
58
|
+
id: item.id,
|
|
59
|
+
label: item.label,
|
|
60
|
+
targets,
|
|
61
|
+
...(focusSkills !== undefined ? { focusSkills } : {}),
|
|
62
|
+
});
|
|
55
63
|
}
|
|
56
64
|
return presets;
|
|
57
65
|
}
|
|
@@ -118,6 +126,7 @@ export function setActiveFocusPresetId(id: string, focusPresets: FocusPreset[] |
|
|
|
118
126
|
}
|
|
119
127
|
activeFocusPresetId = id;
|
|
120
128
|
activeFocusInitialized = true;
|
|
129
|
+
resetFocusSkillsWarningState();
|
|
121
130
|
}
|
|
122
131
|
|
|
123
132
|
export type ActiveFocusPresetPosition = {
|
|
@@ -177,12 +186,14 @@ export function cycleActiveFocusPresetForward(focusPresets: FocusPreset[] | unde
|
|
|
177
186
|
export function clearActiveFocusPresetId(): void {
|
|
178
187
|
activeFocusPresetId = null;
|
|
179
188
|
activeFocusInitialized = true;
|
|
189
|
+
resetFocusSkillsWarningState();
|
|
180
190
|
}
|
|
181
191
|
|
|
182
192
|
/** Resets in-memory session state (for tests and process restart). */
|
|
183
193
|
export function resetActiveFocusSessionState(): void {
|
|
184
194
|
activeFocusPresetId = null;
|
|
185
195
|
activeFocusInitialized = false;
|
|
196
|
+
resetFocusSkillsWarningState();
|
|
186
197
|
}
|
|
187
198
|
|
|
188
199
|
export type TagBasedTargetInput = {
|
package/focus-skills.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { formatSkillsForPrompt, type Skill } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
/** Max skills a single focus preset may auto-load (matches focus context file cap). */
|
|
4
|
+
export const FOCUS_SKILLS_MAX_COUNT = 6;
|
|
5
|
+
|
|
6
|
+
const SKILL_NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
7
|
+
const SKILLS_SECTION_PATTERN = /\n\nThe following skills provide[\s\S]*?<\/available_skills>/;
|
|
8
|
+
|
|
9
|
+
/** Validates a focusSkills entry per Agent Skills name rules. */
|
|
10
|
+
export function assertValidFocusSkillName(label: string, name: string): void {
|
|
11
|
+
if (name.trim() === "") {
|
|
12
|
+
throw new Error(`${label} must not contain empty skill names`);
|
|
13
|
+
}
|
|
14
|
+
if (name.length > 64) {
|
|
15
|
+
throw new Error(`${label} skill name exceeds 64 characters: ${name}`);
|
|
16
|
+
}
|
|
17
|
+
if (!SKILL_NAME_PATTERN.test(name)) {
|
|
18
|
+
throw new Error(`${label} skill name must use lowercase letters, digits, and hyphens only: ${name}`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Parses and validates an optional focusSkills list from preset config. */
|
|
23
|
+
export function parseFocusSkills(itemLabel: string, value: unknown): string[] | undefined {
|
|
24
|
+
if (value === undefined) return undefined;
|
|
25
|
+
if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) {
|
|
26
|
+
throw new Error(`${itemLabel}.focusSkills must be an array of strings`);
|
|
27
|
+
}
|
|
28
|
+
const names: string[] = [];
|
|
29
|
+
const seen = new Set<string>();
|
|
30
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
31
|
+
const raw = value[index]!;
|
|
32
|
+
const nameLabel = `${itemLabel}.focusSkills[${index}]`;
|
|
33
|
+
assertValidFocusSkillName(nameLabel, raw);
|
|
34
|
+
if (seen.has(raw)) {
|
|
35
|
+
throw new Error(`${itemLabel}.focusSkills has duplicate skill name: ${raw}`);
|
|
36
|
+
}
|
|
37
|
+
seen.add(raw);
|
|
38
|
+
names.push(raw);
|
|
39
|
+
}
|
|
40
|
+
if (names.length > FOCUS_SKILLS_MAX_COUNT) {
|
|
41
|
+
throw new Error(`${itemLabel}.focusSkills must contain at most ${FOCUS_SKILLS_MAX_COUNT} skill names`);
|
|
42
|
+
}
|
|
43
|
+
return names;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Removes Pi's default available-skills block from a system prompt, if present. */
|
|
47
|
+
export function stripSkillsSectionFromSystemPrompt(systemPrompt: string): string {
|
|
48
|
+
return systemPrompt.replace(SKILLS_SECTION_PATTERN, "");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Resolves declared focus skill names against Pi's discovered skill inventory. */
|
|
52
|
+
export function resolveFocusSkills(allSkills: Skill[] | undefined, focusSkillNames: string[] | undefined): Skill[] {
|
|
53
|
+
if (!focusSkillNames) return [];
|
|
54
|
+
const byName = new Map((allSkills ?? []).map((skill) => [skill.name, skill]));
|
|
55
|
+
return focusSkillNames.flatMap((name) => {
|
|
56
|
+
const skill = byName.get(name);
|
|
57
|
+
return skill ? [skill] : [];
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Returns focusSkills names that are not present in the discovered inventory. */
|
|
62
|
+
export function findMissingFocusSkills(focusSkillNames: string[] | undefined, allSkills: Skill[] | undefined): string[] {
|
|
63
|
+
if (!focusSkillNames?.length) return [];
|
|
64
|
+
const available = new Set((allSkills ?? []).map((skill) => skill.name));
|
|
65
|
+
return focusSkillNames.filter((name) => !available.has(name));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Replaces the default skills section with only focus-declared skills when configured. */
|
|
69
|
+
export function applyFocusSkillsToSystemPrompt(
|
|
70
|
+
systemPrompt: string,
|
|
71
|
+
allSkills: Skill[] | undefined,
|
|
72
|
+
focusSkillNames: string[] | undefined,
|
|
73
|
+
): string {
|
|
74
|
+
if (focusSkillNames === undefined) return systemPrompt;
|
|
75
|
+
const stripped = stripSkillsSectionFromSystemPrompt(systemPrompt);
|
|
76
|
+
const resolved = resolveFocusSkills(allSkills, focusSkillNames);
|
|
77
|
+
if (resolved.length === 0) return stripped;
|
|
78
|
+
return stripped + formatSkillsForPrompt(resolved);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let warnedFocusSkillsKey: string | null = null;
|
|
82
|
+
|
|
83
|
+
/** Clears per-activation focus skill warning deduplication (for tests and focus changes). */
|
|
84
|
+
export function resetFocusSkillsWarningState(): void {
|
|
85
|
+
warnedFocusSkillsKey = null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Emits one actionable warning per focus activation for missing focusSkills names. */
|
|
89
|
+
export function warnMissingFocusSkills(
|
|
90
|
+
presetId: string,
|
|
91
|
+
focusSkillNames: string[] | undefined,
|
|
92
|
+
allSkills: Skill[] | undefined,
|
|
93
|
+
warn: (message: string) => void,
|
|
94
|
+
): void {
|
|
95
|
+
if (!focusSkillNames?.length) return;
|
|
96
|
+
const missing = findMissingFocusSkills(focusSkillNames, allSkills);
|
|
97
|
+
if (missing.length === 0) return;
|
|
98
|
+
const key = `${presetId}:${missing.join(",")}`;
|
|
99
|
+
if (warnedFocusSkillsKey === key) return;
|
|
100
|
+
warnedFocusSkillsKey = key;
|
|
101
|
+
for (const name of missing) {
|
|
102
|
+
warn(
|
|
103
|
+
`Focus preset "${presetId}" focusSkills: skill "${name}" was not found in Pi's discovered inventory. Check the skill name or install the package, then reload Pi.`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
package/index.ts
CHANGED
|
@@ -20,6 +20,10 @@ import {
|
|
|
20
20
|
setActiveFocusPresetId,
|
|
21
21
|
warnZeroTargetMatchesForPreset,
|
|
22
22
|
} from "./focus-preset.js";
|
|
23
|
+
import {
|
|
24
|
+
applyFocusSkillsToSystemPrompt,
|
|
25
|
+
warnMissingFocusSkills,
|
|
26
|
+
} from "./focus-skills.js";
|
|
23
27
|
import {
|
|
24
28
|
buildMonofoldTree,
|
|
25
29
|
readMonofoldFile,
|
|
@@ -1235,9 +1239,22 @@ export default function piMultiWorkspace(pi: ExtensionAPI) {
|
|
|
1235
1239
|
"warning",
|
|
1236
1240
|
);
|
|
1237
1241
|
}
|
|
1242
|
+
if (activePreset && ctx.hasUI) {
|
|
1243
|
+
warnMissingFocusSkills(
|
|
1244
|
+
activePreset.id,
|
|
1245
|
+
activePreset.focusSkills,
|
|
1246
|
+
_event.systemPromptOptions?.skills,
|
|
1247
|
+
(message) => ctx.ui.notify(message, "warning"),
|
|
1248
|
+
);
|
|
1249
|
+
}
|
|
1250
|
+
const baseSystemPrompt = applyFocusSkillsToSystemPrompt(
|
|
1251
|
+
_event.systemPrompt,
|
|
1252
|
+
_event.systemPromptOptions?.skills,
|
|
1253
|
+
activePreset?.focusSkills,
|
|
1254
|
+
);
|
|
1238
1255
|
return {
|
|
1239
1256
|
systemPrompt:
|
|
1240
|
-
|
|
1257
|
+
baseSystemPrompt +
|
|
1241
1258
|
`
|
|
1242
1259
|
|
|
1243
1260
|
## Pi Monofold
|
package/package.json
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
},
|
|
12
12
|
"description": "Pi extension that folds multiple repositories and folders into a guarded virtual monorepo for AI agents.",
|
|
13
13
|
"type": "module",
|
|
14
|
-
"version": "0.
|
|
14
|
+
"version": "0.7.0",
|
|
15
15
|
"pi": {
|
|
16
16
|
"extensions": [
|
|
17
17
|
"./index.ts"
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"unknown-path-allows.ts",
|
|
26
26
|
"file-read-preview.ts",
|
|
27
27
|
"focus-preset.ts",
|
|
28
|
+
"focus-skills.ts",
|
|
28
29
|
"monofold-read-ops.ts",
|
|
29
30
|
"read-caps.ts",
|
|
30
31
|
"validation.ts"
|