pi-monofold 0.6.2 → 0.8.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 +17 -2
- package/focus-route-override.ts +44 -0
- package/focus-skills.ts +106 -0
- package/index.ts +40 -8
- package/package.json +3 -1
package/focus-preset.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { type MonofoldRouteType, parseDefaultRouteOverride } from "./focus-route-override.js";
|
|
2
|
+
import { parseFocusSkills, resetFocusSkillsWarningState } from "./focus-skills.js";
|
|
1
3
|
import { assertKnownKeys, asStringArray, isRecord, uniqueStrings } from "./validation.js";
|
|
2
4
|
|
|
3
5
|
export type FocusPresetTarget = {
|
|
@@ -8,13 +10,15 @@ export type FocusPreset = {
|
|
|
8
10
|
id: string;
|
|
9
11
|
label: string;
|
|
10
12
|
targets: FocusPresetTarget[];
|
|
13
|
+
focusSkills?: string[];
|
|
14
|
+
defaultRouteOverride?: MonofoldRouteType;
|
|
11
15
|
};
|
|
12
16
|
|
|
13
17
|
export type FocusMatchableWorkspace = {
|
|
14
18
|
tags: string[];
|
|
15
19
|
};
|
|
16
20
|
|
|
17
|
-
const FOCUS_PRESET_KEYS = new Set(["id", "label", "targets"]);
|
|
21
|
+
const FOCUS_PRESET_KEYS = new Set(["id", "label", "targets", "focusSkills", "defaultRouteOverride"]);
|
|
18
22
|
const FOCUS_PRESET_TARGET_KEYS = new Set(["targetTags"]);
|
|
19
23
|
|
|
20
24
|
/** Parses and validates focus preset configuration from YAML/JSON input. */
|
|
@@ -49,9 +53,17 @@ export function parseFocusPresets(value: unknown, label = "focusPresets"): Focus
|
|
|
49
53
|
}
|
|
50
54
|
targets.push({ targetTags });
|
|
51
55
|
}
|
|
56
|
+
const focusSkills = parseFocusSkills(itemLabel, item.focusSkills);
|
|
57
|
+
const defaultRouteOverride = parseDefaultRouteOverride(itemLabel, item.defaultRouteOverride);
|
|
52
58
|
if (seenIds.has(item.id)) throw new Error(`${label} has duplicate preset id: ${item.id}`);
|
|
53
59
|
seenIds.add(item.id);
|
|
54
|
-
presets.push({
|
|
60
|
+
presets.push({
|
|
61
|
+
id: item.id,
|
|
62
|
+
label: item.label,
|
|
63
|
+
targets,
|
|
64
|
+
...(focusSkills !== undefined ? { focusSkills } : {}),
|
|
65
|
+
...(defaultRouteOverride !== undefined ? { defaultRouteOverride } : {}),
|
|
66
|
+
});
|
|
55
67
|
}
|
|
56
68
|
return presets;
|
|
57
69
|
}
|
|
@@ -118,6 +130,7 @@ export function setActiveFocusPresetId(id: string, focusPresets: FocusPreset[] |
|
|
|
118
130
|
}
|
|
119
131
|
activeFocusPresetId = id;
|
|
120
132
|
activeFocusInitialized = true;
|
|
133
|
+
resetFocusSkillsWarningState();
|
|
121
134
|
}
|
|
122
135
|
|
|
123
136
|
export type ActiveFocusPresetPosition = {
|
|
@@ -177,12 +190,14 @@ export function cycleActiveFocusPresetForward(focusPresets: FocusPreset[] | unde
|
|
|
177
190
|
export function clearActiveFocusPresetId(): void {
|
|
178
191
|
activeFocusPresetId = null;
|
|
179
192
|
activeFocusInitialized = true;
|
|
193
|
+
resetFocusSkillsWarningState();
|
|
180
194
|
}
|
|
181
195
|
|
|
182
196
|
/** Resets in-memory session state (for tests and process restart). */
|
|
183
197
|
export function resetActiveFocusSessionState(): void {
|
|
184
198
|
activeFocusPresetId = null;
|
|
185
199
|
activeFocusInitialized = false;
|
|
200
|
+
resetFocusSkillsWarningState();
|
|
186
201
|
}
|
|
187
202
|
|
|
188
203
|
export type TagBasedTargetInput = {
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** Monofold route types available for routed Markdown writes. */
|
|
2
|
+
export const MONOFOLD_ROUTE_TYPES = [
|
|
3
|
+
"default",
|
|
4
|
+
"prd",
|
|
5
|
+
"design",
|
|
6
|
+
"progress",
|
|
7
|
+
"issue",
|
|
8
|
+
"research",
|
|
9
|
+
"decision",
|
|
10
|
+
] as const;
|
|
11
|
+
|
|
12
|
+
export type MonofoldRouteType = (typeof MONOFOLD_ROUTE_TYPES)[number];
|
|
13
|
+
|
|
14
|
+
const ROUTE_TYPE_SET = new Set<string>(MONOFOLD_ROUTE_TYPES);
|
|
15
|
+
|
|
16
|
+
/** Validates a route type string and returns the typed value. */
|
|
17
|
+
export function assertMonofoldRouteType(label: string, value: string): MonofoldRouteType {
|
|
18
|
+
if (!ROUTE_TYPE_SET.has(value)) {
|
|
19
|
+
throw new Error(
|
|
20
|
+
`${label} must be one of: ${MONOFOLD_ROUTE_TYPES.join(", ")} (got: ${value})`,
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
return value as MonofoldRouteType;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Parses an optional defaultRouteOverride from preset config. */
|
|
27
|
+
export function parseDefaultRouteOverride(itemLabel: string, value: unknown): MonofoldRouteType | undefined {
|
|
28
|
+
if (value === undefined) return undefined;
|
|
29
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
30
|
+
throw new Error(`${itemLabel}.defaultRouteOverride must be a non-empty string`);
|
|
31
|
+
}
|
|
32
|
+
return assertMonofoldRouteType(`${itemLabel}.defaultRouteOverride`, value.trim());
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Resolves the effective write route: explicit selection wins over Focus default, then "default". */
|
|
36
|
+
export function resolveWriteRouteType(
|
|
37
|
+
explicitRoute: string | undefined,
|
|
38
|
+
defaultRouteOverride: MonofoldRouteType | undefined,
|
|
39
|
+
): MonofoldRouteType {
|
|
40
|
+
if (explicitRoute !== undefined && explicitRoute !== "") {
|
|
41
|
+
return assertMonofoldRouteType("routeType", explicitRoute);
|
|
42
|
+
}
|
|
43
|
+
return defaultRouteOverride ?? "default";
|
|
44
|
+
}
|
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,14 @@ import {
|
|
|
20
20
|
setActiveFocusPresetId,
|
|
21
21
|
warnZeroTargetMatchesForPreset,
|
|
22
22
|
} from "./focus-preset.js";
|
|
23
|
+
import {
|
|
24
|
+
resolveWriteRouteType,
|
|
25
|
+
type MonofoldRouteType,
|
|
26
|
+
} from "./focus-route-override.js";
|
|
27
|
+
import {
|
|
28
|
+
applyFocusSkillsToSystemPrompt,
|
|
29
|
+
warnMissingFocusSkills,
|
|
30
|
+
} from "./focus-skills.js";
|
|
23
31
|
import {
|
|
24
32
|
buildMonofoldTree,
|
|
25
33
|
readMonofoldFile,
|
|
@@ -37,7 +45,7 @@ import { assertKnownKeys, asStringArray, isRecord, uniqueStrings } from "./valid
|
|
|
37
45
|
type CapabilityTag = "read" | "writeDocs" | "editCode" | "runCommands" | "git";
|
|
38
46
|
type LegacyCapabilityTag = CapabilityTag | "gitCommit" | "gitPush";
|
|
39
47
|
type IntentCategory = "Explore" | "Write" | "Config" | "Git";
|
|
40
|
-
type RouteType =
|
|
48
|
+
type RouteType = MonofoldRouteType;
|
|
41
49
|
|
|
42
50
|
type RouteConfig = {
|
|
43
51
|
path: string;
|
|
@@ -888,7 +896,11 @@ function sendCommandError(pi: ExtensionAPI, command: string, error: unknown, usa
|
|
|
888
896
|
}
|
|
889
897
|
|
|
890
898
|
function formatFocusStatus(position: ActiveFocusPresetPosition): string {
|
|
891
|
-
|
|
899
|
+
const base = `focus: ${position.preset.label} (${position.index + 1}/${position.total}) ${FOCUS_CYCLE_SHORTCUT}`;
|
|
900
|
+
if (position.preset.defaultRouteOverride) {
|
|
901
|
+
return `${base} route:${position.preset.defaultRouteOverride}`;
|
|
902
|
+
}
|
|
903
|
+
return base;
|
|
892
904
|
}
|
|
893
905
|
|
|
894
906
|
function updateFocusStatus(ctx: ExtensionContext | ExtensionCommandContext, loaded: LoadedConfig): void {
|
|
@@ -1022,6 +1034,9 @@ async function buildManifest(loaded: LoadedConfig): Promise<string> {
|
|
|
1022
1034
|
const activeTargetIds = new Set(activeWorkspaces.map(({ workspace }) => workspace.targetId));
|
|
1023
1035
|
if (activeWorkspaces.length > 0) {
|
|
1024
1036
|
lines.push(`Active Focus: ${activePreset.label} (${activePreset.id})`);
|
|
1037
|
+
if (activePreset.defaultRouteOverride) {
|
|
1038
|
+
lines.push(`Default write route override: ${activePreset.defaultRouteOverride} (explicit routeType/--route still wins)`);
|
|
1039
|
+
}
|
|
1025
1040
|
for (const { workspace } of activeWorkspaces) {
|
|
1026
1041
|
lines.push(await buildFullWorkspaceManifestEntry(workspace, " (active)"));
|
|
1027
1042
|
}
|
|
@@ -1042,6 +1057,9 @@ async function buildManifest(loaded: LoadedConfig): Promise<string> {
|
|
|
1042
1057
|
async function buildFocusContextInjection(loaded: LoadedConfig, preset: FocusPreset): Promise<FocusContextInjection> {
|
|
1043
1058
|
const activeWorkspaces = getActiveFocusWorkspaces(loaded, preset);
|
|
1044
1059
|
const lines = ["## Focus Context Injection", "", `Active Focus: ${preset.label} (${preset.id})`];
|
|
1060
|
+
if (preset.defaultRouteOverride) {
|
|
1061
|
+
lines.push(`Default write route override: ${preset.defaultRouteOverride} (explicit routeType/--route still wins)`);
|
|
1062
|
+
}
|
|
1045
1063
|
const notices: string[] = [];
|
|
1046
1064
|
const seenFiles = new Set<string>();
|
|
1047
1065
|
let injectedFileCount = 0;
|
|
@@ -1235,9 +1253,22 @@ export default function piMultiWorkspace(pi: ExtensionAPI) {
|
|
|
1235
1253
|
"warning",
|
|
1236
1254
|
);
|
|
1237
1255
|
}
|
|
1256
|
+
if (activePreset && ctx.hasUI) {
|
|
1257
|
+
warnMissingFocusSkills(
|
|
1258
|
+
activePreset.id,
|
|
1259
|
+
activePreset.focusSkills,
|
|
1260
|
+
_event.systemPromptOptions?.skills,
|
|
1261
|
+
(message) => ctx.ui.notify(message, "warning"),
|
|
1262
|
+
);
|
|
1263
|
+
}
|
|
1264
|
+
const baseSystemPrompt = applyFocusSkillsToSystemPrompt(
|
|
1265
|
+
_event.systemPrompt,
|
|
1266
|
+
_event.systemPromptOptions?.skills,
|
|
1267
|
+
activePreset?.focusSkills,
|
|
1268
|
+
);
|
|
1238
1269
|
return {
|
|
1239
1270
|
systemPrompt:
|
|
1240
|
-
|
|
1271
|
+
baseSystemPrompt +
|
|
1241
1272
|
`
|
|
1242
1273
|
|
|
1243
1274
|
## Pi Monofold
|
|
@@ -1369,7 +1400,7 @@ ${focusInjection.text}` : ""}
|
|
|
1369
1400
|
label: "Workspace Write",
|
|
1370
1401
|
description: "Write a Markdown document to a routed Workspace destination using routeType, title, body, filename, and metadata.",
|
|
1371
1402
|
parameters: Type.Object({
|
|
1372
|
-
routeType: Type.String({ description: "default, prd, design, progress, issue, research, or decision" }),
|
|
1403
|
+
routeType: Type.Optional(Type.String({ description: "default, prd, design, progress, issue, research, or decision; omitted uses active Focus defaultRouteOverride or default" })),
|
|
1373
1404
|
title: Type.String(),
|
|
1374
1405
|
body: Type.String(),
|
|
1375
1406
|
filename: Type.Optional(Type.String()),
|
|
@@ -1380,9 +1411,9 @@ ${focusInjection.text}` : ""}
|
|
|
1380
1411
|
workspaceName: Type.Optional(Type.String()),
|
|
1381
1412
|
}),
|
|
1382
1413
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
1383
|
-
const routeType = params.routeType as RouteType;
|
|
1384
|
-
if (!ROUTE_TYPES.includes(routeType)) throw new Error(`Unknown routeType: ${params.routeType}`);
|
|
1385
1414
|
const loaded = await loadConfig(ctx.cwd);
|
|
1415
|
+
const activePreset = getActiveFocusPreset(loaded);
|
|
1416
|
+
const routeType = resolveWriteRouteType(params.routeType, activePreset?.defaultRouteOverride);
|
|
1386
1417
|
const workspace = await resolveWorkspace(ctx, loaded, {
|
|
1387
1418
|
targetTags: params.targetTags,
|
|
1388
1419
|
targetName: params.targetName,
|
|
@@ -1596,14 +1627,15 @@ ${focusInjection.text}` : ""}
|
|
|
1596
1627
|
const writeCommand = async (args: string, ctx: ExtensionCommandContext) => {
|
|
1597
1628
|
try {
|
|
1598
1629
|
const parsed = parseCommandArgs(args);
|
|
1599
|
-
const routeType = (stringFlag(parsed.flags, "route", "r") ?? parsed.positional[0] ?? "default") as RouteType;
|
|
1600
|
-
if (!ROUTE_TYPES.includes(routeType)) throw new Error(`Unknown routeType: ${routeType}`);
|
|
1601
1630
|
const title = stringFlag(parsed.flags, "title", "t");
|
|
1602
1631
|
const body = stringFlag(parsed.flags, "body", "b");
|
|
1603
1632
|
if (!title) throw new Error("--title is required");
|
|
1604
1633
|
if (!body) throw new Error("--body is required");
|
|
1605
1634
|
|
|
1606
1635
|
const loaded = await loadConfig(ctx.cwd);
|
|
1636
|
+
const activePreset = getActiveFocusPreset(loaded);
|
|
1637
|
+
const explicitRoute = stringFlag(parsed.flags, "route", "r") ?? parsed.positional[0];
|
|
1638
|
+
const routeType = resolveWriteRouteType(explicitRoute, activePreset?.defaultRouteOverride);
|
|
1607
1639
|
const workspace = await resolveWorkspace(ctx, loaded, commandTarget(parsed.flags, ["writeDocs"]));
|
|
1608
1640
|
const route = workspace.normalizedRoutes[routeType] ?? workspace.normalizedRoutes.default;
|
|
1609
1641
|
if (!route) throw new Error(`Workspace has no route for ${routeType} and no default route`);
|
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.8.0",
|
|
15
15
|
"pi": {
|
|
16
16
|
"extensions": [
|
|
17
17
|
"./index.ts"
|
|
@@ -25,6 +25,8 @@
|
|
|
25
25
|
"unknown-path-allows.ts",
|
|
26
26
|
"file-read-preview.ts",
|
|
27
27
|
"focus-preset.ts",
|
|
28
|
+
"focus-route-override.ts",
|
|
29
|
+
"focus-skills.ts",
|
|
28
30
|
"monofold-read-ops.ts",
|
|
29
31
|
"read-caps.ts",
|
|
30
32
|
"validation.ts"
|