@tianhai/pi-workflow-kit 0.4.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/LICENSE +22 -0
- package/README.md +509 -0
- package/ROADMAP.md +16 -0
- package/agents/code-reviewer.md +18 -0
- package/agents/config.ts +5 -0
- package/agents/implementer.md +26 -0
- package/agents/spec-reviewer.md +13 -0
- package/agents/worker.md +17 -0
- package/banner.jpg +0 -0
- package/docs/developer-usage-guide.md +463 -0
- package/docs/oversight-model.md +49 -0
- package/docs/workflow-phases.md +71 -0
- package/extensions/constants.ts +9 -0
- package/extensions/lib/logging.ts +138 -0
- package/extensions/plan-tracker.ts +496 -0
- package/extensions/subagent/agents.ts +144 -0
- package/extensions/subagent/concurrency.ts +52 -0
- package/extensions/subagent/env.ts +47 -0
- package/extensions/subagent/index.ts +1116 -0
- package/extensions/subagent/lifecycle.ts +25 -0
- package/extensions/subagent/timeout.ts +13 -0
- package/extensions/workflow-monitor/debug-monitor.ts +98 -0
- package/extensions/workflow-monitor/git.ts +31 -0
- package/extensions/workflow-monitor/heuristics.ts +58 -0
- package/extensions/workflow-monitor/investigation.ts +52 -0
- package/extensions/workflow-monitor/reference-tool.ts +42 -0
- package/extensions/workflow-monitor/skip-confirmation.ts +19 -0
- package/extensions/workflow-monitor/tdd-monitor.ts +137 -0
- package/extensions/workflow-monitor/test-runner.ts +37 -0
- package/extensions/workflow-monitor/verification-monitor.ts +61 -0
- package/extensions/workflow-monitor/warnings.ts +81 -0
- package/extensions/workflow-monitor/workflow-handler.ts +358 -0
- package/extensions/workflow-monitor/workflow-tracker.ts +231 -0
- package/extensions/workflow-monitor/workflow-transitions.ts +55 -0
- package/extensions/workflow-monitor.ts +885 -0
- package/package.json +49 -0
- package/skills/brainstorming/SKILL.md +70 -0
- package/skills/dispatching-parallel-agents/SKILL.md +194 -0
- package/skills/executing-tasks/SKILL.md +247 -0
- package/skills/receiving-code-review/SKILL.md +196 -0
- package/skills/systematic-debugging/SKILL.md +170 -0
- package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -0
- package/skills/systematic-debugging/condition-based-waiting.md +115 -0
- package/skills/systematic-debugging/defense-in-depth.md +122 -0
- package/skills/systematic-debugging/find-polluter.sh +63 -0
- package/skills/systematic-debugging/reference/rationalizations.md +61 -0
- package/skills/systematic-debugging/root-cause-tracing.md +169 -0
- package/skills/test-driven-development/SKILL.md +266 -0
- package/skills/test-driven-development/reference/examples.md +101 -0
- package/skills/test-driven-development/reference/rationalizations.md +67 -0
- package/skills/test-driven-development/reference/when-stuck.md +33 -0
- package/skills/test-driven-development/testing-anti-patterns.md +299 -0
- package/skills/using-git-worktrees/SKILL.md +231 -0
- package/skills/writing-plans/SKILL.md +149 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent discovery and configuration
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as fs from "node:fs";
|
|
6
|
+
import * as os from "node:os";
|
|
7
|
+
import * as path from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { parseFrontmatter } from "@mariozechner/pi-coding-agent";
|
|
10
|
+
import { log } from "../lib/logging.js";
|
|
11
|
+
|
|
12
|
+
export type AgentScope = "user" | "project" | "both";
|
|
13
|
+
|
|
14
|
+
export interface AgentConfig {
|
|
15
|
+
name: string;
|
|
16
|
+
description: string;
|
|
17
|
+
tools?: string[];
|
|
18
|
+
extensions?: string[];
|
|
19
|
+
model?: string;
|
|
20
|
+
systemPrompt: string;
|
|
21
|
+
source: "user" | "project" | "bundled";
|
|
22
|
+
filePath: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface AgentDiscoveryResult {
|
|
26
|
+
agents: AgentConfig[];
|
|
27
|
+
projectAgentsDir: string | null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function loadAgentsFromDir(dir: string, source: "user" | "project" | "bundled"): AgentConfig[] {
|
|
31
|
+
const agents: AgentConfig[] = [];
|
|
32
|
+
|
|
33
|
+
if (!fs.existsSync(dir)) {
|
|
34
|
+
return agents;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let entries: fs.Dirent[];
|
|
38
|
+
try {
|
|
39
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
40
|
+
} catch (err) {
|
|
41
|
+
log.warn(`Failed to read agents directory: ${dir} — ${err instanceof Error ? err.message : err}`);
|
|
42
|
+
return agents;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
for (const entry of entries) {
|
|
46
|
+
if (!entry.name.endsWith(".md")) continue;
|
|
47
|
+
if (!entry.isFile() && !entry.isSymbolicLink()) continue;
|
|
48
|
+
|
|
49
|
+
const filePath = path.join(dir, entry.name);
|
|
50
|
+
let content: string;
|
|
51
|
+
try {
|
|
52
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
53
|
+
} catch (err) {
|
|
54
|
+
log.warn(`Failed to read agent file: ${filePath} — ${err instanceof Error ? err.message : err}`);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const { frontmatter, body } = parseFrontmatter<Record<string, string>>(content);
|
|
59
|
+
|
|
60
|
+
if (!frontmatter.name || !frontmatter.description) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const tools = frontmatter.tools
|
|
65
|
+
?.split(",")
|
|
66
|
+
.map((t: string) => t.trim())
|
|
67
|
+
.filter(Boolean);
|
|
68
|
+
const extensions = frontmatter.extensions
|
|
69
|
+
?.split(",")
|
|
70
|
+
.map((t: string) => t.trim())
|
|
71
|
+
.filter(Boolean);
|
|
72
|
+
|
|
73
|
+
agents.push({
|
|
74
|
+
name: frontmatter.name,
|
|
75
|
+
description: frontmatter.description,
|
|
76
|
+
tools: tools && tools.length > 0 ? tools : undefined,
|
|
77
|
+
extensions: extensions && extensions.length > 0 ? extensions : undefined,
|
|
78
|
+
model: frontmatter.model,
|
|
79
|
+
systemPrompt: body,
|
|
80
|
+
source,
|
|
81
|
+
filePath,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return agents;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function isDirectory(p: string): boolean {
|
|
89
|
+
try {
|
|
90
|
+
return fs.statSync(p).isDirectory();
|
|
91
|
+
} catch (err) {
|
|
92
|
+
log.debug(`stat failed for ${p}: ${err instanceof Error ? err.message : err}`);
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function findNearestProjectAgentsDir(cwd: string): string | null {
|
|
98
|
+
let currentDir = cwd;
|
|
99
|
+
while (true) {
|
|
100
|
+
const candidate = path.join(currentDir, ".pi", "agents");
|
|
101
|
+
if (isDirectory(candidate)) return candidate;
|
|
102
|
+
|
|
103
|
+
const parentDir = path.dirname(currentDir);
|
|
104
|
+
if (parentDir === currentDir) return null;
|
|
105
|
+
currentDir = parentDir;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryResult {
|
|
110
|
+
const userDir = path.join(os.homedir(), ".pi", "agent", "agents");
|
|
111
|
+
const projectAgentsDir = findNearestProjectAgentsDir(cwd);
|
|
112
|
+
const thisFile = fileURLToPath(import.meta.url);
|
|
113
|
+
const packageRoot = path.resolve(path.dirname(thisFile), "..", "..");
|
|
114
|
+
const bundledAgentsDir = path.join(packageRoot, "agents");
|
|
115
|
+
|
|
116
|
+
const userAgents = scope === "project" ? [] : loadAgentsFromDir(userDir, "user");
|
|
117
|
+
const projectAgents = scope === "user" || !projectAgentsDir ? [] : loadAgentsFromDir(projectAgentsDir, "project");
|
|
118
|
+
const bundledAgents = scope === "user" ? [] : loadAgentsFromDir(bundledAgentsDir, "bundled");
|
|
119
|
+
|
|
120
|
+
const agentMap = new Map<string, AgentConfig>();
|
|
121
|
+
|
|
122
|
+
if (scope === "both") {
|
|
123
|
+
for (const agent of bundledAgents) agentMap.set(agent.name, agent);
|
|
124
|
+
for (const agent of userAgents) agentMap.set(agent.name, agent);
|
|
125
|
+
for (const agent of projectAgents) agentMap.set(agent.name, agent);
|
|
126
|
+
} else if (scope === "user") {
|
|
127
|
+
for (const agent of userAgents) agentMap.set(agent.name, agent);
|
|
128
|
+
} else {
|
|
129
|
+
for (const agent of bundledAgents) agentMap.set(agent.name, agent);
|
|
130
|
+
for (const agent of projectAgents) agentMap.set(agent.name, agent);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return { agents: Array.from(agentMap.values()), projectAgentsDir };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function formatAgentList(agents: AgentConfig[], maxItems: number): { text: string; remaining: number } {
|
|
137
|
+
if (agents.length === 0) return { text: "none", remaining: 0 };
|
|
138
|
+
const listed = agents.slice(0, maxItems);
|
|
139
|
+
const remaining = agents.length - listed.length;
|
|
140
|
+
return {
|
|
141
|
+
text: listed.map((a) => `${a.name} (${a.source}): ${a.description}`).join("; "),
|
|
142
|
+
remaining,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export const DEFAULT_SUBAGENT_CONCURRENCY = 6;
|
|
2
|
+
|
|
3
|
+
export function getSubagentConcurrency(): number {
|
|
4
|
+
const envVal = process.env.PI_SUBAGENT_CONCURRENCY;
|
|
5
|
+
if (envVal) {
|
|
6
|
+
const parsed = Number.parseInt(envVal, 10);
|
|
7
|
+
if (Number.isFinite(parsed)) return Math.max(1, parsed);
|
|
8
|
+
}
|
|
9
|
+
return DEFAULT_SUBAGENT_CONCURRENCY;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class Semaphore {
|
|
13
|
+
private _active = 0;
|
|
14
|
+
private _queue: Array<() => void> = [];
|
|
15
|
+
|
|
16
|
+
constructor(private _limit: number) {}
|
|
17
|
+
|
|
18
|
+
get limit(): number {
|
|
19
|
+
return this._limit;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
get active(): number {
|
|
23
|
+
return this._active;
|
|
24
|
+
}
|
|
25
|
+
get waiting(): number {
|
|
26
|
+
return this._queue.length;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async acquire(): Promise<() => void> {
|
|
30
|
+
if (this._active < this._limit) {
|
|
31
|
+
this._active++;
|
|
32
|
+
return this._createRelease();
|
|
33
|
+
}
|
|
34
|
+
return new Promise<() => void>((resolve) => {
|
|
35
|
+
this._queue.push(() => {
|
|
36
|
+
this._active++;
|
|
37
|
+
resolve(this._createRelease());
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private _createRelease(): () => void {
|
|
43
|
+
let released = false;
|
|
44
|
+
return () => {
|
|
45
|
+
if (released) return;
|
|
46
|
+
released = true;
|
|
47
|
+
this._active--;
|
|
48
|
+
const next = this._queue.shift();
|
|
49
|
+
if (next) next();
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const ALLOWED_PREFIXES = ["PI_", "NODE_", "NPM_", "NVM_", "LC_", "XDG_"];
|
|
2
|
+
|
|
3
|
+
const ALLOWED_EXPLICIT = new Set([
|
|
4
|
+
"PATH",
|
|
5
|
+
"HOME",
|
|
6
|
+
"SHELL",
|
|
7
|
+
"TERM",
|
|
8
|
+
"USER",
|
|
9
|
+
"LOGNAME",
|
|
10
|
+
"TMPDIR",
|
|
11
|
+
"EDITOR",
|
|
12
|
+
"VISUAL",
|
|
13
|
+
"SSH_AUTH_SOCK",
|
|
14
|
+
"COLORTERM",
|
|
15
|
+
"FORCE_COLOR",
|
|
16
|
+
"NO_COLOR",
|
|
17
|
+
"LANG",
|
|
18
|
+
"LANGUAGE",
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
export function buildSubagentEnv(extra?: Record<string, string>): Record<string, string | undefined> {
|
|
22
|
+
const filtered: Record<string, string | undefined> = {};
|
|
23
|
+
|
|
24
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
25
|
+
if (value === undefined) continue;
|
|
26
|
+
if (ALLOWED_EXPLICIT.has(key) || ALLOWED_PREFIXES.some((p) => key.startsWith(p))) {
|
|
27
|
+
filtered[key] = value;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const passthrough = process.env.PI_SUBAGENT_ENV_PASSTHROUGH;
|
|
32
|
+
if (passthrough) {
|
|
33
|
+
for (const name of passthrough
|
|
34
|
+
.split(",")
|
|
35
|
+
.map((s) => s.trim())
|
|
36
|
+
.filter(Boolean)) {
|
|
37
|
+
const val = process.env[name];
|
|
38
|
+
if (val !== undefined) filtered[name] = val;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (extra) {
|
|
43
|
+
Object.assign(filtered, extra);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return filtered;
|
|
47
|
+
}
|