@zaganjade/pi-multi-skill 1.0.0 → 1.3.1
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/README.md +453 -17
- package/package.json +9 -3
- package/skill-bundles.example.json +17 -0
- package/skill-bundles.example.yaml +9 -0
- package/src/bmad-auto.ts +99 -0
- package/src/bmad-status.ts +125 -0
- package/src/build.ts +270 -0
- package/src/bundle-status.ts +178 -0
- package/src/bundles.ts +138 -0
- package/src/completions.ts +184 -0
- package/src/conflicts.ts +26 -0
- package/src/discover.ts +161 -0
- package/src/index.ts +287 -345
- package/src/metadata.ts +170 -0
- package/src/parse-args.ts +80 -0
- package/src/registry.ts +46 -0
- package/src/stats.ts +164 -0
- package/src/subagents.ts +75 -0
- package/src/suggestions.ts +70 -0
- package/src/types.ts +64 -0
- package/src/yaml-bundles.ts +97 -0
package/src/metadata.ts
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import {
|
|
3
|
+
parseFrontmatter as parsePiFrontmatter,
|
|
4
|
+
stripFrontmatter as stripPiFrontmatter,
|
|
5
|
+
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import type { EnrichedSkillInfo, LoadMode, SkillInfo, SkillMetadata, SkillOrder } from "./types.ts";
|
|
7
|
+
|
|
8
|
+
const PROCESS_NAMES = new Set([
|
|
9
|
+
"using-superpowers",
|
|
10
|
+
"brainstorming",
|
|
11
|
+
"systematic-debugging",
|
|
12
|
+
"bmad-master",
|
|
13
|
+
"writing-plans",
|
|
14
|
+
"dispatching-parallel-agents",
|
|
15
|
+
"subagent-driven-development",
|
|
16
|
+
"executing-plans",
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
const PLANNING_NAMES = new Set([
|
|
20
|
+
"analyst",
|
|
21
|
+
"pm",
|
|
22
|
+
"architect",
|
|
23
|
+
"ux-designer",
|
|
24
|
+
"scrum-master",
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
function parseFrontmatter(content: string): Record<string, string> {
|
|
28
|
+
const { frontmatter } = parsePiFrontmatter<Record<string, unknown>>(content);
|
|
29
|
+
const fields: Record<string, string> = {};
|
|
30
|
+
for (const [key, value] of Object.entries(frontmatter)) {
|
|
31
|
+
if (value === undefined || value === null) continue;
|
|
32
|
+
fields[key] = String(value);
|
|
33
|
+
}
|
|
34
|
+
return fields;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function stripFrontmatter(content: string): string {
|
|
38
|
+
return stripPiFrontmatter(content);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function parseListField(value: string | undefined): string[] {
|
|
42
|
+
if (!value) return [];
|
|
43
|
+
const trimmed = value.trim();
|
|
44
|
+
if (trimmed.startsWith("[")) {
|
|
45
|
+
try {
|
|
46
|
+
const parsed = JSON.parse(trimmed) as unknown;
|
|
47
|
+
if (Array.isArray(parsed)) {
|
|
48
|
+
return parsed.map(String).filter(Boolean);
|
|
49
|
+
}
|
|
50
|
+
} catch {
|
|
51
|
+
// fall through
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return trimmed
|
|
55
|
+
.split(",")
|
|
56
|
+
.map((s) => s.trim())
|
|
57
|
+
.filter(Boolean);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function parseTokenBudget(value: string | undefined): LoadMode | undefined {
|
|
61
|
+
if (value === "meta" || value === "lazy" || value === "full") return value;
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function extractCommands(body: string): string[] {
|
|
66
|
+
const commands: string[] = [];
|
|
67
|
+
const section = body.match(
|
|
68
|
+
/## Available Commands[\s\S]*?(?=\n## |\n# |$)/i,
|
|
69
|
+
);
|
|
70
|
+
if (!section) return commands;
|
|
71
|
+
|
|
72
|
+
for (const line of section[0].split("\n")) {
|
|
73
|
+
const match = line.match(/\*\*(\/[\w-]+)/);
|
|
74
|
+
if (match) commands.push(match[1]);
|
|
75
|
+
}
|
|
76
|
+
return commands;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function inferType(
|
|
80
|
+
name: string,
|
|
81
|
+
frontmatter: Record<string, string>,
|
|
82
|
+
): SkillMetadata["type"] {
|
|
83
|
+
const explicit = frontmatter.type?.toLowerCase();
|
|
84
|
+
if (explicit === "process" || explicit === "rigid" || explicit === "flexible") {
|
|
85
|
+
return explicit;
|
|
86
|
+
}
|
|
87
|
+
if (PROCESS_NAMES.has(name)) return "process";
|
|
88
|
+
if (name.includes("debug") || name.includes("tdd")) return "rigid";
|
|
89
|
+
return "unknown";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function priorityScore(meta: SkillMetadata): number {
|
|
93
|
+
if (meta.type === "process") return 0;
|
|
94
|
+
if (PROCESS_NAMES.has(meta.name)) return 1;
|
|
95
|
+
if (PLANNING_NAMES.has(meta.name)) return 2;
|
|
96
|
+
if (meta.module === "core") return 1;
|
|
97
|
+
if (meta.module === "bmm") return 3;
|
|
98
|
+
return 4;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function enrichSkill(skill: SkillInfo): EnrichedSkillInfo {
|
|
102
|
+
let rawContent = "";
|
|
103
|
+
try {
|
|
104
|
+
rawContent = readFileSync(skill.filePath, "utf-8");
|
|
105
|
+
} catch {
|
|
106
|
+
rawContent = "";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const frontmatter = parseFrontmatter(rawContent);
|
|
110
|
+
const body = stripFrontmatter(rawContent).trim();
|
|
111
|
+
const name = frontmatter.name || skill.name;
|
|
112
|
+
const description =
|
|
113
|
+
skill.description || frontmatter.description || truncateFirstLine(body);
|
|
114
|
+
|
|
115
|
+
const metadata: SkillMetadata = {
|
|
116
|
+
name,
|
|
117
|
+
description,
|
|
118
|
+
type: inferType(name, frontmatter),
|
|
119
|
+
module: frontmatter.module || "",
|
|
120
|
+
priority: Number.parseInt(frontmatter.priority || "50", 10) || 50,
|
|
121
|
+
commands: extractCommands(body),
|
|
122
|
+
skillId: frontmatter.skill_id || "",
|
|
123
|
+
version: frontmatter.version || "",
|
|
124
|
+
pairsWith: parseListField(frontmatter.pairs_with),
|
|
125
|
+
conflictsWith: parseListField(frontmatter.conflicts_with),
|
|
126
|
+
tokenBudget: parseTokenBudget(frontmatter.token_budget),
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
return { ...skill, name, description, metadata, rawContent, body };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function truncateFirstLine(body: string): string {
|
|
133
|
+
const line = body
|
|
134
|
+
.split("\n")
|
|
135
|
+
.map((l) => l.replace(/^>\s*/, "").trim())
|
|
136
|
+
.find((l) => l.length > 0);
|
|
137
|
+
return line?.slice(0, 120) ?? "";
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function sortSkills(
|
|
141
|
+
skills: EnrichedSkillInfo[],
|
|
142
|
+
order: SkillOrder = "process-first",
|
|
143
|
+
): EnrichedSkillInfo[] {
|
|
144
|
+
if (order === "alpha") {
|
|
145
|
+
return [...skills].sort((a, b) => a.name.localeCompare(b.name));
|
|
146
|
+
}
|
|
147
|
+
if (order === "explicit") return skills;
|
|
148
|
+
|
|
149
|
+
return [...skills].sort((a, b) => {
|
|
150
|
+
const pa = priorityScore(a.metadata);
|
|
151
|
+
const pb = priorityScore(b.metadata);
|
|
152
|
+
if (pa !== pb) return pa - pb;
|
|
153
|
+
if (a.metadata.priority !== b.metadata.priority) {
|
|
154
|
+
return a.metadata.priority - b.metadata.priority;
|
|
155
|
+
}
|
|
156
|
+
return a.name.localeCompare(b.name);
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function truncateDescription(desc: string, maxLen = 120): string {
|
|
161
|
+
if (!desc) return "";
|
|
162
|
+
const lines = desc
|
|
163
|
+
.split("\n")
|
|
164
|
+
.map((l) => l.replace(/^>\s*/, "").trim())
|
|
165
|
+
.filter((l) => l.length > 0);
|
|
166
|
+
if (lines.length === 0) return "";
|
|
167
|
+
let text = lines[0];
|
|
168
|
+
if (lines.length > 1 && text.length < 40) text = `${text} ${lines[1]}`;
|
|
169
|
+
return text.length > maxLen ? `${text.slice(0, maxLen - 1)}…` : text;
|
|
170
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { LoadMode, ParsedSkillsArgs } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
const FLAG_MODES: Record<string, LoadMode> = {
|
|
4
|
+
"--meta": "meta",
|
|
5
|
+
"--full": "full",
|
|
6
|
+
"--lazy": "lazy",
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function parseSkillsArgs(raw: string): ParsedSkillsArgs {
|
|
10
|
+
let text = raw.trim();
|
|
11
|
+
let mode: LoadMode = "full";
|
|
12
|
+
let auto = false;
|
|
13
|
+
let parallel = false;
|
|
14
|
+
|
|
15
|
+
for (const [flag, loadMode] of Object.entries(FLAG_MODES)) {
|
|
16
|
+
if (text.includes(flag)) {
|
|
17
|
+
mode = loadMode;
|
|
18
|
+
text = text.replace(new RegExp(`\\s*${flag}\\b`, "g"), " ").trim();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (/\s--auto\b/.test(text)) {
|
|
23
|
+
auto = true;
|
|
24
|
+
text = text.replace(/\s*--auto\b/g, " ").trim();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (/\s--parallel\b/.test(text)) {
|
|
28
|
+
parallel = true;
|
|
29
|
+
text = text.replace(/\s*--parallel\b/g, " ").trim();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const tokens = text.match(/^(\S+)(?:\s+([\s\S]*))?$/);
|
|
33
|
+
if (!tokens) {
|
|
34
|
+
return {
|
|
35
|
+
skillNames: [],
|
|
36
|
+
mode,
|
|
37
|
+
auto,
|
|
38
|
+
parallel,
|
|
39
|
+
parallelTasks: [],
|
|
40
|
+
instructions: "",
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const skillNames = tokens[1]
|
|
45
|
+
.split(",")
|
|
46
|
+
.map((s) => s.trim())
|
|
47
|
+
.filter((s) => s.length > 0);
|
|
48
|
+
|
|
49
|
+
let instructions = tokens[2]?.trim() ?? "";
|
|
50
|
+
let embeddedCommand: string | undefined;
|
|
51
|
+
let parallelTasks: string[] = [];
|
|
52
|
+
|
|
53
|
+
if (parallel && instructions.includes("|")) {
|
|
54
|
+
parallelTasks = instructions
|
|
55
|
+
.split("|")
|
|
56
|
+
.map((s) => s.trim())
|
|
57
|
+
.filter((s) => s.length > 0);
|
|
58
|
+
instructions = "";
|
|
59
|
+
} else if (parallel && instructions) {
|
|
60
|
+
parallelTasks = [instructions];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const cmdMatch = instructions.match(
|
|
64
|
+
/^(\/[\w-]+(?:\s+[^\n|]+)?)(?:\s+([\s\S]*))?$/,
|
|
65
|
+
);
|
|
66
|
+
if (cmdMatch && !parallel) {
|
|
67
|
+
embeddedCommand = cmdMatch[1].trim();
|
|
68
|
+
instructions = cmdMatch[2]?.trim() ?? "";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
skillNames,
|
|
73
|
+
mode,
|
|
74
|
+
auto,
|
|
75
|
+
parallel,
|
|
76
|
+
parallelTasks,
|
|
77
|
+
embeddedCommand,
|
|
78
|
+
instructions,
|
|
79
|
+
};
|
|
80
|
+
}
|
package/src/registry.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { enrichSkill } from "./metadata.ts";
|
|
5
|
+
import type { SkillInfo } from "./types.ts";
|
|
6
|
+
|
|
7
|
+
function getAgentDir(): string {
|
|
8
|
+
return join(homedir(), ".pi", "agent");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function rebuildSkillIndex(skills: SkillInfo[]): void {
|
|
12
|
+
const index = {
|
|
13
|
+
updatedAt: new Date().toISOString(),
|
|
14
|
+
count: skills.length,
|
|
15
|
+
skills: skills.map((skill) => {
|
|
16
|
+
const enriched = enrichSkill(skill);
|
|
17
|
+
return {
|
|
18
|
+
name: enriched.name,
|
|
19
|
+
description: enriched.metadata.description,
|
|
20
|
+
type: enriched.metadata.type,
|
|
21
|
+
module: enriched.metadata.module,
|
|
22
|
+
priority: enriched.metadata.priority,
|
|
23
|
+
skillId: enriched.metadata.skillId,
|
|
24
|
+
version: enriched.metadata.version,
|
|
25
|
+
pairsWith: enriched.metadata.pairsWith,
|
|
26
|
+
conflictsWith: enriched.metadata.conflictsWith,
|
|
27
|
+
tokenBudget: enriched.metadata.tokenBudget,
|
|
28
|
+
commands: enriched.metadata.commands,
|
|
29
|
+
location: enriched.filePath,
|
|
30
|
+
};
|
|
31
|
+
}),
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const dir = getAgentDir();
|
|
35
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
writeFileSync(
|
|
39
|
+
join(dir, "skill-index.json"),
|
|
40
|
+
`${JSON.stringify(index, null, 2)}\n`,
|
|
41
|
+
"utf-8",
|
|
42
|
+
);
|
|
43
|
+
} catch {
|
|
44
|
+
// Non-fatal — index is optional
|
|
45
|
+
}
|
|
46
|
+
}
|
package/src/stats.ts
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import type { LoadMode } from "./types.ts";
|
|
5
|
+
|
|
6
|
+
export interface ActivationRecord {
|
|
7
|
+
at: string;
|
|
8
|
+
mode: LoadMode;
|
|
9
|
+
skillNames: string[];
|
|
10
|
+
bundles: string[];
|
|
11
|
+
parallel: boolean;
|
|
12
|
+
skillCount: number;
|
|
13
|
+
/** Original /skills args for /skills-last replay. */
|
|
14
|
+
rawArgs?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface MultiSkillStats {
|
|
18
|
+
version: 1;
|
|
19
|
+
activations: number;
|
|
20
|
+
byMode: Record<LoadMode, number>;
|
|
21
|
+
byBundle: Record<string, number>;
|
|
22
|
+
bySkillCount: Record<string, number>;
|
|
23
|
+
recent: ActivationRecord[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function statsPath(): string {
|
|
27
|
+
return join(homedir(), ".pi", "agent", "multi-skill-stats.json");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function emptyStats(): MultiSkillStats {
|
|
31
|
+
return {
|
|
32
|
+
version: 1,
|
|
33
|
+
activations: 0,
|
|
34
|
+
byMode: { full: 0, meta: 0, lazy: 0 },
|
|
35
|
+
byBundle: {},
|
|
36
|
+
bySkillCount: {},
|
|
37
|
+
recent: [],
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function loadStats(): MultiSkillStats {
|
|
42
|
+
const path = statsPath();
|
|
43
|
+
if (!existsSync(path)) return emptyStats();
|
|
44
|
+
try {
|
|
45
|
+
const parsed = JSON.parse(readFileSync(path, "utf-8")) as MultiSkillStats;
|
|
46
|
+
if (parsed.version !== 1) return emptyStats();
|
|
47
|
+
return { ...emptyStats(), ...parsed, recent: parsed.recent ?? [] };
|
|
48
|
+
} catch {
|
|
49
|
+
return emptyStats();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function saveStats(stats: MultiSkillStats): void {
|
|
54
|
+
const dir = join(homedir(), ".pi", "agent");
|
|
55
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
56
|
+
writeFileSync(statsPath(), `${JSON.stringify(stats, null, 2)}\n`, "utf-8");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function recordActivation(
|
|
60
|
+
record: Omit<ActivationRecord, "at">,
|
|
61
|
+
): void {
|
|
62
|
+
const stats = loadStats();
|
|
63
|
+
stats.activations += 1;
|
|
64
|
+
stats.byMode[record.mode] = (stats.byMode[record.mode] ?? 0) + 1;
|
|
65
|
+
|
|
66
|
+
for (const bundle of record.bundles) {
|
|
67
|
+
stats.byBundle[bundle] = (stats.byBundle[bundle] ?? 0) + 1;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const bucket =
|
|
71
|
+
record.skillCount <= 1
|
|
72
|
+
? "1"
|
|
73
|
+
: record.skillCount <= 3
|
|
74
|
+
? "2-3"
|
|
75
|
+
: "4+";
|
|
76
|
+
stats.bySkillCount[bucket] = (stats.bySkillCount[bucket] ?? 0) + 1;
|
|
77
|
+
|
|
78
|
+
stats.recent.unshift({ ...record, at: new Date().toISOString() });
|
|
79
|
+
stats.recent = stats.recent.slice(0, 20);
|
|
80
|
+
|
|
81
|
+
saveStats(stats);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function formatStatsReport(): string {
|
|
85
|
+
const stats = loadStats();
|
|
86
|
+
const lines: string[] = [
|
|
87
|
+
`Multi-skill activations: ${stats.activations}`,
|
|
88
|
+
"",
|
|
89
|
+
"By load mode:",
|
|
90
|
+
` full: ${stats.byMode.full} · meta: ${stats.byMode.meta} · lazy: ${stats.byMode.lazy}`,
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
const bundles = Object.entries(stats.byBundle).sort((a, b) => b[1] - a[1]);
|
|
94
|
+
if (bundles.length > 0) {
|
|
95
|
+
lines.push("", "Top bundles:");
|
|
96
|
+
for (const [name, count] of bundles.slice(0, 8)) {
|
|
97
|
+
lines.push(` @${name}: ${count}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const counts = Object.entries(stats.bySkillCount);
|
|
102
|
+
if (counts.length > 0) {
|
|
103
|
+
lines.push("", "Skills per activation:");
|
|
104
|
+
for (const [bucket, count] of counts) {
|
|
105
|
+
lines.push(` ${bucket} skill(s): ${count}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (stats.recent.length > 0) {
|
|
110
|
+
lines.push("", "Recent:");
|
|
111
|
+
for (const r of stats.recent.slice(0, 5)) {
|
|
112
|
+
const label =
|
|
113
|
+
r.bundles.length > 0
|
|
114
|
+
? r.bundles.map((b) => `@${b}`).join(",")
|
|
115
|
+
: r.skillNames.slice(0, 3).join(",");
|
|
116
|
+
lines.push(
|
|
117
|
+
` ${r.at.slice(0, 19)} · ${r.mode} · ${label}${r.parallel ? " · parallel" : ""}`,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
lines.push("", "Stats file: ~/.pi/agent/multi-skill-stats.json");
|
|
123
|
+
return lines.join("\n");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function getLastActivation(): ActivationRecord | null {
|
|
127
|
+
const stats = loadStats();
|
|
128
|
+
return stats.recent[0] ?? null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const MODE_FLAGS = ["--meta", "--lazy", "--full", "--auto", "--parallel"] as const;
|
|
132
|
+
|
|
133
|
+
/** Replay last activation, optionally overriding load-mode / parallel flags. */
|
|
134
|
+
export function replayLastArgs(overrideArgs: string): string | null {
|
|
135
|
+
const last = getLastActivation();
|
|
136
|
+
if (!last?.rawArgs) return null;
|
|
137
|
+
|
|
138
|
+
let base = last.rawArgs;
|
|
139
|
+
for (const flag of MODE_FLAGS) {
|
|
140
|
+
base = base.replace(new RegExp(`\\s*${flag}\\b`, "g"), " ").trim();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const override = overrideArgs.trim();
|
|
144
|
+
if (!override) return base;
|
|
145
|
+
|
|
146
|
+
const extraFlags: string[] = [];
|
|
147
|
+
if (/\s--meta\b/.test(` ${override} `)) extraFlags.push("--meta");
|
|
148
|
+
else if (/\s--lazy\b/.test(` ${override} `)) extraFlags.push("--lazy");
|
|
149
|
+
else if (/\s--full\b/.test(` ${override} `)) extraFlags.push("--full");
|
|
150
|
+
if (/\s--auto\b/.test(` ${override} `)) extraFlags.push("--auto");
|
|
151
|
+
if (/\s--parallel\b/.test(` ${override} `)) extraFlags.push("--parallel");
|
|
152
|
+
|
|
153
|
+
return extraFlags.length > 0 ? `${base} ${extraFlags.join(" ")}` : base;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function formatLastActivationHint(): string | null {
|
|
157
|
+
const last = getLastActivation();
|
|
158
|
+
if (!last) return null;
|
|
159
|
+
const label =
|
|
160
|
+
last.bundles.length > 0
|
|
161
|
+
? last.bundles.map((b) => `@${b}`).join(",")
|
|
162
|
+
: last.skillNames.slice(0, 4).join(",");
|
|
163
|
+
return `${label} · ${last.mode}${last.parallel ? " · parallel" : ""} · ${last.at.slice(0, 19)}`;
|
|
164
|
+
}
|
package/src/subagents.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
const SUBAGENT_TOOL = "subagent";
|
|
4
|
+
const DEFAULT_PARALLEL_AGENT = "general-purpose";
|
|
5
|
+
|
|
6
|
+
export function hasSubagentTool(pi: ExtensionAPI): boolean {
|
|
7
|
+
try {
|
|
8
|
+
return pi.getAllTools().some((tool) => tool.name === SUBAGENT_TOOL);
|
|
9
|
+
} catch {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function buildParallelDispatchBlock(options: {
|
|
15
|
+
tasks: string[];
|
|
16
|
+
subagentAvailable: boolean;
|
|
17
|
+
}): string {
|
|
18
|
+
const { tasks, subagentAvailable } = options;
|
|
19
|
+
if (tasks.length === 0) return "";
|
|
20
|
+
|
|
21
|
+
const lines: string[] = ["<parallel_dispatch>"];
|
|
22
|
+
|
|
23
|
+
if (subagentAvailable) {
|
|
24
|
+
lines.push(
|
|
25
|
+
"Use the `subagent` tool in PARALLEL mode for independent tasks below.",
|
|
26
|
+
"Call `{ action: \"list\" }` first if you need to pick agents.",
|
|
27
|
+
);
|
|
28
|
+
if (tasks.length === 1) {
|
|
29
|
+
lines.push("");
|
|
30
|
+
lines.push("Suggested invocation:");
|
|
31
|
+
lines.push("```json");
|
|
32
|
+
lines.push(
|
|
33
|
+
JSON.stringify(
|
|
34
|
+
{
|
|
35
|
+
tasks: [{ agent: DEFAULT_PARALLEL_AGENT, task: tasks[0] }],
|
|
36
|
+
},
|
|
37
|
+
null,
|
|
38
|
+
2,
|
|
39
|
+
),
|
|
40
|
+
);
|
|
41
|
+
lines.push("```");
|
|
42
|
+
} else {
|
|
43
|
+
lines.push("");
|
|
44
|
+
lines.push("Suggested invocation:");
|
|
45
|
+
lines.push("```json");
|
|
46
|
+
lines.push(
|
|
47
|
+
JSON.stringify(
|
|
48
|
+
{
|
|
49
|
+
tasks: tasks.map((task) => ({
|
|
50
|
+
agent: DEFAULT_PARALLEL_AGENT,
|
|
51
|
+
task,
|
|
52
|
+
})),
|
|
53
|
+
},
|
|
54
|
+
null,
|
|
55
|
+
2,
|
|
56
|
+
),
|
|
57
|
+
);
|
|
58
|
+
lines.push("```");
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
lines.push(
|
|
62
|
+
"pi-subagents is not installed — execute tasks sequentially in the current session.",
|
|
63
|
+
"Install: `pi install npm:pi-subagents` then `/reload`.",
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
lines.push("");
|
|
68
|
+
lines.push("Tasks:");
|
|
69
|
+
for (const [i, task] of tasks.entries()) {
|
|
70
|
+
lines.push(`${i + 1}. ${task}`);
|
|
71
|
+
}
|
|
72
|
+
lines.push("</parallel_dispatch>");
|
|
73
|
+
|
|
74
|
+
return lines.join("\n");
|
|
75
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { SkillBundle, SkillInfo } from "./types.ts";
|
|
2
|
+
import { assessBundleAvailability } from "./bundle-status.ts";
|
|
3
|
+
|
|
4
|
+
const SUGGESTIONS: Array<{ pattern: RegExp; bundle: string; reason: string }> =
|
|
5
|
+
[
|
|
6
|
+
{
|
|
7
|
+
pattern: /fail(ing|ed)?\s+test|test\s+fail/i,
|
|
8
|
+
bundle: "debug",
|
|
9
|
+
reason: "failing tests detected",
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
pattern: /\bbug\b|\berror\b|\bcrash\b|\bexception\b/i,
|
|
13
|
+
bundle: "debug",
|
|
14
|
+
reason: "bug/error keywords detected",
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
pattern: /\bprd\b|product requirement|tech spec/i,
|
|
18
|
+
bundle: "bmad-planning",
|
|
19
|
+
reason: "planning keywords detected",
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
pattern: /\barchitect|\bapi design|\bdatabase schema/i,
|
|
23
|
+
bundle: "bmad-solutioning",
|
|
24
|
+
reason: "architecture keywords detected",
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
pattern: /\bimplement|\buser story|\bdev-story|\bfeature\b/i,
|
|
28
|
+
bundle: "bmad-build",
|
|
29
|
+
reason: "implementation keywords detected",
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
pattern: /\bnew feature|\bbuild\b|\bcreate\b.+\bcomponent/i,
|
|
33
|
+
bundle: "cc-feature",
|
|
34
|
+
reason: "feature development keywords detected",
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
export function suggestSkillBundle(
|
|
39
|
+
text: string,
|
|
40
|
+
bundles?: Map<string, SkillBundle>,
|
|
41
|
+
availableSkills?: SkillInfo[],
|
|
42
|
+
): {
|
|
43
|
+
bundle: string;
|
|
44
|
+
reason: string;
|
|
45
|
+
} | null {
|
|
46
|
+
const sample = text.slice(0, 500);
|
|
47
|
+
for (const entry of SUGGESTIONS) {
|
|
48
|
+
if (!entry.pattern.test(sample)) continue;
|
|
49
|
+
if (bundles && availableSkills) {
|
|
50
|
+
const bundle = bundles.get(entry.bundle);
|
|
51
|
+
if (bundle) {
|
|
52
|
+
const status = assessBundleAvailability(
|
|
53
|
+
entry.bundle,
|
|
54
|
+
bundle,
|
|
55
|
+
availableSkills,
|
|
56
|
+
);
|
|
57
|
+
if (!status.ready) continue;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return { bundle: entry.bundle, reason: entry.reason };
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function formatSuggestionHint(
|
|
66
|
+
bundle: string,
|
|
67
|
+
reason: string,
|
|
68
|
+
): string {
|
|
69
|
+
return `💡 Suggested: /skills @${bundle} (${reason})`;
|
|
70
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export type LoadMode = "full" | "meta" | "lazy";
|
|
2
|
+
|
|
3
|
+
export type SkillOrder = "process-first" | "explicit" | "alpha";
|
|
4
|
+
|
|
5
|
+
export interface SkillBundle {
|
|
6
|
+
description: string;
|
|
7
|
+
skills: string[];
|
|
8
|
+
order?: SkillOrder;
|
|
9
|
+
default_mode?: LoadMode;
|
|
10
|
+
/** Human-readable dependency label shown in help/autocomplete. */
|
|
11
|
+
requires?: string;
|
|
12
|
+
/** One-line install instructions when bundle skills are missing. */
|
|
13
|
+
install?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface SkillInfo {
|
|
17
|
+
name: string;
|
|
18
|
+
description: string;
|
|
19
|
+
filePath: string;
|
|
20
|
+
baseDir: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface SkillMetadata {
|
|
24
|
+
name: string;
|
|
25
|
+
description: string;
|
|
26
|
+
type: "process" | "rigid" | "flexible" | "unknown";
|
|
27
|
+
module: string;
|
|
28
|
+
priority: number;
|
|
29
|
+
commands: string[];
|
|
30
|
+
skillId: string;
|
|
31
|
+
version: string;
|
|
32
|
+
pairsWith: string[];
|
|
33
|
+
conflictsWith: string[];
|
|
34
|
+
tokenBudget?: LoadMode;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface EnrichedSkillInfo extends SkillInfo {
|
|
38
|
+
metadata: SkillMetadata;
|
|
39
|
+
rawContent: string;
|
|
40
|
+
body: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ParsedSkillsArgs {
|
|
44
|
+
skillNames: string[];
|
|
45
|
+
mode: LoadMode;
|
|
46
|
+
auto: boolean;
|
|
47
|
+
parallel: boolean;
|
|
48
|
+
parallelTasks: string[];
|
|
49
|
+
embeddedCommand?: string;
|
|
50
|
+
instructions: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface BuildOptions {
|
|
54
|
+
mode: LoadMode;
|
|
55
|
+
bundles?: string[];
|
|
56
|
+
bmadStatusBlock?: string;
|
|
57
|
+
parallel: boolean;
|
|
58
|
+
parallelTasks?: string[];
|
|
59
|
+
subagentAvailable?: boolean;
|
|
60
|
+
embeddedCommand?: string;
|
|
61
|
+
instructions?: string;
|
|
62
|
+
skippedDuplicates?: string[];
|
|
63
|
+
conflictWarnings?: string[];
|
|
64
|
+
}
|