ultimate-pi 0.12.0 → 0.13.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/.agents/skills/ccc/SKILL.md +136 -0
- package/.agents/skills/ccc/references/management.md +110 -0
- package/.agents/skills/ccc/references/settings.md +126 -0
- package/.agents/skills/harness-orchestration/SKILL.md +4 -4
- package/.pi/PACKAGING.md +1 -0
- package/.pi/SYSTEM.md +21 -20
- package/.pi/agents/harness/planning/scout-graphify.md +2 -0
- package/.pi/agents/harness/planning/scout-semantic.md +13 -6
- package/.pi/extensions/harness-run-context.ts +5 -0
- package/.pi/extensions/harness-subagents.ts +16 -5
- package/.pi/extensions/lib/harness-cocoindex-refresh.ts +49 -0
- package/.pi/extensions/lib/harness-subagent-policy.ts +5 -1
- package/.pi/extensions/lib/harness-subagents-bridge.ts +9 -63
- package/.pi/harness/docs/adrs/0033-parent-orchestrated-planning.md +1 -1
- package/.pi/prompts/harness-plan.md +10 -5
- package/.pi/prompts/harness-setup.md +15 -11
- package/.pi/scripts/README.md +1 -0
- package/.pi/scripts/harness-cli-verify.sh +24 -14
- package/.pi/scripts/harness-cocoindex-bootstrap.sh +182 -0
- package/.pi/scripts/harness-verify.mjs +10 -0
- package/.pi/skills/ast-grep/SKILL.md +2 -2
- package/CHANGELOG.md +16 -0
- package/THIRD_PARTY_NOTICES.md +7 -0
- package/package.json +3 -2
- package/vendor/pi-subagents/LICENSE +21 -0
- package/vendor/pi-subagents/UPSTREAM_PIN.md +11 -0
- package/vendor/pi-subagents/src/agents.ts +357 -0
- package/vendor/pi-subagents/src/subagents.ts +1463 -0
- package/.agents/skills/ck-search/SKILL.md +0 -99
- package/.agents/skills/obsidian-bases/SKILL.md +0 -299
- package/.agents/skills/obsidian-markdown/SKILL.md +0 -237
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent discovery — upstream pi-subagents + ultimate-pi harness extensions.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as fs from "node:fs";
|
|
6
|
+
import * as path from "node:path";
|
|
7
|
+
import { getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
|
|
9
|
+
export type AgentScope = "user" | "project" | "both";
|
|
10
|
+
|
|
11
|
+
export type AgentSource = "built-in" | "user" | "project" | "package";
|
|
12
|
+
|
|
13
|
+
export interface AgentConfig {
|
|
14
|
+
name: string;
|
|
15
|
+
description: string;
|
|
16
|
+
tools?: string[];
|
|
17
|
+
model?: string;
|
|
18
|
+
thinking?: string;
|
|
19
|
+
maxTurns?: number;
|
|
20
|
+
extensionsOff?: boolean;
|
|
21
|
+
skillsOff?: boolean;
|
|
22
|
+
systemPrompt: string;
|
|
23
|
+
source: AgentSource;
|
|
24
|
+
filePath: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const BUILTIN_TOOL_NAMES = [
|
|
28
|
+
"read",
|
|
29
|
+
"write",
|
|
30
|
+
"edit",
|
|
31
|
+
"bash",
|
|
32
|
+
"grep",
|
|
33
|
+
"find",
|
|
34
|
+
"ls",
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
const BUILT_IN_AGENTS: AgentConfig[] = [
|
|
38
|
+
{
|
|
39
|
+
name: "scout",
|
|
40
|
+
description:
|
|
41
|
+
"Read-only codebase reconnaissance; returns concise findings with paths and evidence.",
|
|
42
|
+
tools: ["read", "grep", "find", "ls", "bash"],
|
|
43
|
+
source: "built-in",
|
|
44
|
+
filePath: "built-in:scout",
|
|
45
|
+
systemPrompt: [
|
|
46
|
+
"You are a scout subagent. Explore the codebase quickly and report grounded findings.",
|
|
47
|
+
"Do not edit files. Prefer read, grep, find, ls, and safe bash inspection commands.",
|
|
48
|
+
"Return concise bullets with exact file paths, symbols, and open questions.",
|
|
49
|
+
].join("\n"),
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: "planner",
|
|
53
|
+
description: "Turns reconnaissance into a lean implementation or migration plan.",
|
|
54
|
+
tools: ["read", "grep", "find", "ls"],
|
|
55
|
+
source: "built-in",
|
|
56
|
+
filePath: "built-in:planner",
|
|
57
|
+
systemPrompt: [
|
|
58
|
+
"You are a planner subagent. Produce executable, verifiable plans only.",
|
|
59
|
+
"Do not modify files. Ground the plan in the repository's actual structure.",
|
|
60
|
+
"Call out assumptions, risks, sequencing, and verification commands.",
|
|
61
|
+
].join("\n"),
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: "reviewer",
|
|
65
|
+
description: "Independent code review and verification agent for completed changes.",
|
|
66
|
+
tools: ["read", "grep", "find", "ls", "bash"],
|
|
67
|
+
source: "built-in",
|
|
68
|
+
filePath: "built-in:reviewer",
|
|
69
|
+
systemPrompt: [
|
|
70
|
+
"You are a reviewer subagent. Review changes adversarially and verify claims.",
|
|
71
|
+
"Do not edit files. Run safe inspection or test commands when useful.",
|
|
72
|
+
"Report PASS, FAIL, or PARTIAL with evidence, commands run, and specific follow-ups.",
|
|
73
|
+
].join("\n"),
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: "worker",
|
|
77
|
+
description: "General-purpose implementation worker with the default Pi tool set.",
|
|
78
|
+
source: "built-in",
|
|
79
|
+
filePath: "built-in:worker",
|
|
80
|
+
systemPrompt: workerSystemPrompt(),
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: "general",
|
|
84
|
+
description: "Alias for worker; kept for model-generated subagent names.",
|
|
85
|
+
source: "built-in",
|
|
86
|
+
filePath: "built-in:general",
|
|
87
|
+
systemPrompt: workerSystemPrompt(),
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: "general-purpose",
|
|
91
|
+
description: "Alias for worker; compatible with common subagent naming conventions.",
|
|
92
|
+
source: "built-in",
|
|
93
|
+
filePath: "built-in:general-purpose",
|
|
94
|
+
systemPrompt: workerSystemPrompt(),
|
|
95
|
+
},
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
function workerSystemPrompt(): string {
|
|
99
|
+
return [
|
|
100
|
+
"You are a focused worker subagent running in an isolated Pi process.",
|
|
101
|
+
"Complete the delegated task directly. Keep scope tight and avoid unrelated changes.",
|
|
102
|
+
"When done, summarize files changed, commands run, and any remaining risks.",
|
|
103
|
+
].join("\n");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface AgentDiscoveryResult {
|
|
107
|
+
agents: AgentConfig[];
|
|
108
|
+
projectAgentsDir: string | null;
|
|
109
|
+
packageAgentsDir: string | null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function isSafeAgentId(id: string): boolean {
|
|
113
|
+
if (!id || id.includes("..") || id.startsWith("/") || id.includes("\\")) {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
return /^[a-zA-Z0-9][a-zA-Z0-9/_-]*$/.test(id);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function parseCsvField(val: unknown): string[] | undefined {
|
|
120
|
+
if (val === undefined || val === null) return undefined;
|
|
121
|
+
const s = String(val).trim();
|
|
122
|
+
if (!s || s === "none") return undefined;
|
|
123
|
+
const items = s
|
|
124
|
+
.split(",")
|
|
125
|
+
.map((t) => t.trim())
|
|
126
|
+
.filter(Boolean);
|
|
127
|
+
return items.length > 0 ? items : undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function csvList(val: unknown, defaults: string[]): string[] {
|
|
131
|
+
if (val === undefined || val === null) return defaults;
|
|
132
|
+
return parseCsvField(val) ?? [];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function extensionsOff(fm: Record<string, unknown>): boolean {
|
|
136
|
+
const v = fm.extensions ?? fm.inherit_extensions;
|
|
137
|
+
return v === false || v === "false" || v === "none";
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function skillsOff(fm: Record<string, unknown>, extensionsDisabled: boolean): boolean {
|
|
141
|
+
if (extensionsDisabled) return true;
|
|
142
|
+
const v = fm.skills ?? fm.inherit_skills;
|
|
143
|
+
return v === false || v === "false" || v === "none";
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function computeEffectiveTools(
|
|
147
|
+
allowed: string[],
|
|
148
|
+
disallowed: string[] | undefined,
|
|
149
|
+
agentName: string,
|
|
150
|
+
): string[] {
|
|
151
|
+
const deny = new Set(
|
|
152
|
+
(disallowed ?? []).map((t) => t.trim()).filter(Boolean),
|
|
153
|
+
);
|
|
154
|
+
deny.add("subagent");
|
|
155
|
+
deny.add("Agent");
|
|
156
|
+
deny.add("get_subagent_result");
|
|
157
|
+
deny.add("steer_subagent");
|
|
158
|
+
deny.add("blackboard");
|
|
159
|
+
if (agentName.startsWith("harness/")) {
|
|
160
|
+
deny.add("subagent");
|
|
161
|
+
}
|
|
162
|
+
return allowed.filter((t) => !deny.has(t));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function agentAllowsMutatingTools(agent: AgentConfig): boolean {
|
|
166
|
+
const tools = agent.tools ?? BUILTIN_TOOL_NAMES;
|
|
167
|
+
return tools.includes("write") || tools.includes("edit");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function walkAgentsDir(
|
|
171
|
+
rootDir: string,
|
|
172
|
+
source: AgentSource,
|
|
173
|
+
out: Map<string, { filePath: string; content: string }>,
|
|
174
|
+
): void {
|
|
175
|
+
if (!fs.existsSync(rootDir)) return;
|
|
176
|
+
|
|
177
|
+
const stack: string[] = [rootDir];
|
|
178
|
+
while (stack.length > 0) {
|
|
179
|
+
const dir = stack.pop()!;
|
|
180
|
+
let entries: fs.Dirent[];
|
|
181
|
+
try {
|
|
182
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
183
|
+
} catch {
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
for (const entry of entries) {
|
|
188
|
+
const full = path.join(dir, entry.name);
|
|
189
|
+
if (entry.isDirectory()) {
|
|
190
|
+
stack.push(full);
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (!entry.name.endsWith(".md")) continue;
|
|
194
|
+
if (!entry.isFile() && !entry.isSymbolicLink()) continue;
|
|
195
|
+
|
|
196
|
+
const rel = path.relative(rootDir, full).replace(/\\/g, "/");
|
|
197
|
+
const id = rel.replace(/\.md$/i, "");
|
|
198
|
+
if (!isSafeAgentId(id)) continue;
|
|
199
|
+
|
|
200
|
+
let content: string;
|
|
201
|
+
try {
|
|
202
|
+
content = fs.readFileSync(full, "utf-8");
|
|
203
|
+
} catch {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
out.set(id, { filePath: full, content });
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function parseMarkdownAgent(
|
|
213
|
+
agentId: string,
|
|
214
|
+
content: string,
|
|
215
|
+
source: AgentSource,
|
|
216
|
+
filePath: string,
|
|
217
|
+
): AgentConfig | null {
|
|
218
|
+
const { frontmatter: fm, body } = parseFrontmatter<Record<string, unknown>>(
|
|
219
|
+
content,
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
const description =
|
|
223
|
+
typeof fm.description === "string" ? fm.description.trim() : "";
|
|
224
|
+
if (!description) return null;
|
|
225
|
+
|
|
226
|
+
const name =
|
|
227
|
+
typeof fm.name === "string" && fm.name.trim() ? fm.name.trim() : agentId;
|
|
228
|
+
|
|
229
|
+
const allowed = csvList(fm.tools, BUILTIN_TOOL_NAMES);
|
|
230
|
+
const disallowed = parseCsvField(fm.disallowed_tools);
|
|
231
|
+
const extOff = extensionsOff(fm);
|
|
232
|
+
const effective = computeEffectiveTools(allowed, disallowed, name);
|
|
233
|
+
|
|
234
|
+
let thinking: string | undefined;
|
|
235
|
+
if (typeof fm.thinking === "string" && fm.thinking.trim()) {
|
|
236
|
+
thinking = fm.thinking.trim();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
let maxTurns: number | undefined;
|
|
240
|
+
if (typeof fm.max_turns === "number" && fm.max_turns > 0) {
|
|
241
|
+
maxTurns = fm.max_turns;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
let systemPrompt = body.trim();
|
|
245
|
+
if (maxTurns) {
|
|
246
|
+
systemPrompt += `\n\nHard limit: complete within ${maxTurns} tool rounds then output your final structured answer.`;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
name,
|
|
251
|
+
description,
|
|
252
|
+
tools: effective.length > 0 ? effective : undefined,
|
|
253
|
+
model: typeof fm.model === "string" ? fm.model : undefined,
|
|
254
|
+
thinking,
|
|
255
|
+
maxTurns,
|
|
256
|
+
extensionsOff: extOff,
|
|
257
|
+
skillsOff: skillsOff(fm, extOff),
|
|
258
|
+
systemPrompt,
|
|
259
|
+
source,
|
|
260
|
+
filePath,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function isDirectory(p: string): boolean {
|
|
265
|
+
try {
|
|
266
|
+
return fs.statSync(p).isDirectory();
|
|
267
|
+
} catch {
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function findNearestProjectAgentsDir(cwd: string): string | null {
|
|
273
|
+
let currentDir = cwd;
|
|
274
|
+
while (true) {
|
|
275
|
+
const candidate = path.join(currentDir, ".pi", "agents");
|
|
276
|
+
if (isDirectory(candidate)) return candidate;
|
|
277
|
+
|
|
278
|
+
const parentDir = path.dirname(currentDir);
|
|
279
|
+
if (parentDir === currentDir) return null;
|
|
280
|
+
currentDir = parentDir;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function resolvePackageAgentsDir(
|
|
285
|
+
cwd: string,
|
|
286
|
+
packageRoot?: string,
|
|
287
|
+
): string | null {
|
|
288
|
+
const fromEnv = process.env.UP_PKG ?? process.env.HARNESS_PKG_ROOT;
|
|
289
|
+
const root = packageRoot ?? fromEnv;
|
|
290
|
+
if (root) {
|
|
291
|
+
const candidate = path.join(root, ".pi", "agents");
|
|
292
|
+
if (isDirectory(candidate)) return candidate;
|
|
293
|
+
}
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export function discoverAgents(
|
|
298
|
+
cwd: string,
|
|
299
|
+
scope: AgentScope,
|
|
300
|
+
packageRoot?: string,
|
|
301
|
+
): AgentDiscoveryResult {
|
|
302
|
+
const userDir = path.join(getAgentDir(), "agents");
|
|
303
|
+
const projectAgentsDir = findNearestProjectAgentsDir(cwd);
|
|
304
|
+
const packageAgentsDir = resolvePackageAgentsDir(cwd, packageRoot);
|
|
305
|
+
|
|
306
|
+
const fileMap = new Map<string, { filePath: string; content: string }>();
|
|
307
|
+
|
|
308
|
+
if (scope !== "project" && packageAgentsDir) {
|
|
309
|
+
walkAgentsDir(packageAgentsDir, "package", fileMap);
|
|
310
|
+
}
|
|
311
|
+
if (scope !== "project") {
|
|
312
|
+
walkAgentsDir(userDir, "user", fileMap);
|
|
313
|
+
}
|
|
314
|
+
if (scope !== "user" && projectAgentsDir) {
|
|
315
|
+
walkAgentsDir(projectAgentsDir, "project", fileMap);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const agentMap = new Map<string, AgentConfig>();
|
|
319
|
+
|
|
320
|
+
for (const agent of BUILT_IN_AGENTS) {
|
|
321
|
+
agentMap.set(agent.name, { ...agent });
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
for (const [id, file] of fileMap) {
|
|
325
|
+
const source: AgentSource =
|
|
326
|
+
file.filePath.includes("/.pi/agents") &&
|
|
327
|
+
packageAgentsDir &&
|
|
328
|
+
file.filePath.startsWith(packageAgentsDir)
|
|
329
|
+
? "package"
|
|
330
|
+
: file.filePath.includes(projectAgentsDir ?? "\0")
|
|
331
|
+
? "project"
|
|
332
|
+
: "user";
|
|
333
|
+
const parsed = parseMarkdownAgent(id, file.content, source, file.filePath);
|
|
334
|
+
if (parsed) agentMap.set(parsed.name, parsed);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return {
|
|
338
|
+
agents: Array.from(agentMap.values()),
|
|
339
|
+
projectAgentsDir,
|
|
340
|
+
packageAgentsDir,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export function formatAgentList(
|
|
345
|
+
agents: AgentConfig[],
|
|
346
|
+
maxItems: number,
|
|
347
|
+
): { text: string; remaining: number } {
|
|
348
|
+
if (agents.length === 0) return { text: "none", remaining: 0 };
|
|
349
|
+
const listed = agents.slice(0, maxItems);
|
|
350
|
+
const remaining = agents.length - listed.length;
|
|
351
|
+
return {
|
|
352
|
+
text: listed
|
|
353
|
+
.map((a) => `${a.name} (${a.source}): ${a.description}`)
|
|
354
|
+
.join("; "),
|
|
355
|
+
remaining,
|
|
356
|
+
};
|
|
357
|
+
}
|