@tranhoangnguyen0310/pi-flow-external 1.0.10-external.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/LICENSE +201 -0
- package/README.md +106 -0
- package/assets/pi-flow.png +0 -0
- package/index.ts +2 -0
- package/package.json +76 -0
- package/scripts/e2e/claude-subagent.mjs +237 -0
- package/scripts/e2e/codex-subagent.mjs +238 -0
- package/scripts/e2e/main-agent-comparison.mjs +967 -0
- package/scripts/e2e/workflow-features.mjs +929 -0
- package/src/core/claude.ts +554 -0
- package/src/core/codex.ts +549 -0
- package/src/core/concurrency.ts +120 -0
- package/src/core/display.ts +14 -0
- package/src/core/model.ts +31 -0
- package/src/core/progress.ts +359 -0
- package/src/core/spawn.ts +297 -0
- package/src/core/spinner.ts +4 -0
- package/src/core/stream.ts +71 -0
- package/src/core/subagent-render.ts +209 -0
- package/src/core/timeout.ts +100 -0
- package/src/pi-subagent.ts +574 -0
- package/src/profiles.ts +180 -0
- package/src/prompts.ts +109 -0
- package/src/subagents/explorer.md +16 -0
- package/src/subagents/general-purpose.md +3 -0
- package/src/types.ts +122 -0
- package/src/workflow/journal.ts +203 -0
- package/src/workflow/registry.ts +275 -0
- package/src/workflow/replay-cache.ts +41 -0
- package/src/workflow/runtime-values.ts +111 -0
- package/src/workflow/runtime.ts +406 -0
- package/src/workflow/script-validation.ts +231 -0
- package/src/workflow/script-worker.ts +376 -0
- package/src/workflow/source.ts +336 -0
- package/src/workflow/structured-output.ts +68 -0
- package/src/workflow/tool.ts +525 -0
- package/src/workflow/types.ts +102 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Real end-to-end smoke for Codex CLI-backed subagents.
|
|
3
|
+
//
|
|
4
|
+
// This creates a temporary custom subagent profile under the selected pi agent
|
|
5
|
+
// dir, then runs a fresh `pi -p` session that is only allowed to use the Agent
|
|
6
|
+
// tool. The selected profile uses `backend: codex`, `model: gpt-5.4-mini`, and
|
|
7
|
+
// `thinking: medium`, so a successful run proves the root pi agent launched a
|
|
8
|
+
// real Codex CLI child through the extension.
|
|
9
|
+
//
|
|
10
|
+
// Usage:
|
|
11
|
+
// node scripts/e2e/codex-subagent.mjs
|
|
12
|
+
// node scripts/e2e/codex-subagent.mjs --root-model openai/gpt-5.4-mini --root-thinking medium --keep
|
|
13
|
+
|
|
14
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
15
|
+
import {
|
|
16
|
+
existsSync,
|
|
17
|
+
mkdirSync,
|
|
18
|
+
readFileSync,
|
|
19
|
+
readdirSync,
|
|
20
|
+
rmSync,
|
|
21
|
+
statSync,
|
|
22
|
+
writeFileSync,
|
|
23
|
+
unlinkSync,
|
|
24
|
+
} from "node:fs";
|
|
25
|
+
import { homedir, tmpdir } from "node:os";
|
|
26
|
+
import path from "node:path";
|
|
27
|
+
import process from "node:process";
|
|
28
|
+
import { fileURLToPath } from "node:url";
|
|
29
|
+
|
|
30
|
+
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
31
|
+
const extensionPath = path.join(repoRoot, "index.ts");
|
|
32
|
+
|
|
33
|
+
function parseArgs(argv) {
|
|
34
|
+
const options = {
|
|
35
|
+
rootModel: "openai/gpt-5.4-mini",
|
|
36
|
+
rootThinking: "medium",
|
|
37
|
+
codexModel: "gpt-5.4-mini",
|
|
38
|
+
codexThinking: "medium",
|
|
39
|
+
agentDir: process.env.PI_CODING_AGENT_DIR || path.join(homedir(), ".pi", "agent"),
|
|
40
|
+
runRoot: path.join(tmpdir(), `pi-codex-subagent-e2e-${Date.now()}`),
|
|
41
|
+
keep: false,
|
|
42
|
+
timeoutMs: 180_000,
|
|
43
|
+
};
|
|
44
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
45
|
+
const arg = argv[i];
|
|
46
|
+
const value = () => {
|
|
47
|
+
const v = argv[i + 1];
|
|
48
|
+
if (v === undefined) throw new Error(`${arg} requires a value`);
|
|
49
|
+
i += 1;
|
|
50
|
+
return v;
|
|
51
|
+
};
|
|
52
|
+
if (arg === "--root-model") options.rootModel = value();
|
|
53
|
+
else if (arg === "--root-thinking") options.rootThinking = value();
|
|
54
|
+
else if (arg === "--codex-model") options.codexModel = value();
|
|
55
|
+
else if (arg === "--codex-thinking") options.codexThinking = value();
|
|
56
|
+
else if (arg === "--agent-dir") options.agentDir = path.resolve(value());
|
|
57
|
+
else if (arg === "--run-root") options.runRoot = path.resolve(value());
|
|
58
|
+
else if (arg === "--timeout-ms") options.timeoutMs = Number(value());
|
|
59
|
+
else if (arg === "--keep") options.keep = true;
|
|
60
|
+
else if (arg === "--help" || arg === "-h") options.help = true;
|
|
61
|
+
else throw new Error(`Unknown option: ${arg}`);
|
|
62
|
+
}
|
|
63
|
+
return options;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function printHelp() {
|
|
67
|
+
console.log(`Usage: node scripts/e2e/codex-subagent.mjs [options]\n\nOptions:\n --root-model <provider/model> pi root model (default: openai/gpt-5.4-mini)\n --root-thinking <level> pi root thinking level (default: medium)\n --codex-model <model> Codex CLI subagent model (default: gpt-5.4-mini)\n --codex-thinking <level> profile thinking level passed to Codex (default: medium)\n --agent-dir <dir> pi agent dir (default: PI_CODING_AGENT_DIR or ~/.pi/agent)\n --run-root <dir> temp run root\n --timeout-ms <ms> pi process timeout (default: 180000)\n --keep keep temp run root and temporary profile\n`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function ensureDir(dir) {
|
|
71
|
+
mkdirSync(dir, { recursive: true });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function shell(cmd, args, options = {}) {
|
|
75
|
+
const result = spawnSync(cmd, args, { encoding: "utf8", ...options });
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function writeFixture(runRoot) {
|
|
80
|
+
const fixture = path.join(runRoot, "fixture");
|
|
81
|
+
ensureDir(fixture);
|
|
82
|
+
writeFileSync(path.join(fixture, "e2e-target.txt"), "gpt-5.4-mini-medium\n", "utf8");
|
|
83
|
+
writeFileSync(path.join(fixture, "README.md"), "# Codex subagent E2E\n\nRead e2e-target.txt and report the token.\n", "utf8");
|
|
84
|
+
shell("git", ["init", "-q"], { cwd: fixture });
|
|
85
|
+
shell("git", ["add", "."], { cwd: fixture });
|
|
86
|
+
shell("git", ["-c", "user.name=pi-flow-e2e", "-c", "user.email=pi-flow-e2e@example.invalid", "commit", "-qm", "fixture"], { cwd: fixture });
|
|
87
|
+
return fixture;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function listFilesRecursive(root) {
|
|
91
|
+
const out = [];
|
|
92
|
+
const walk = (dir) => {
|
|
93
|
+
for (const entry of readdirSync(dir)) {
|
|
94
|
+
const p = path.join(dir, entry);
|
|
95
|
+
const st = statSync(p);
|
|
96
|
+
if (st.isDirectory()) walk(p);
|
|
97
|
+
else out.push(p);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
if (existsSync(root)) walk(root);
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function readAllTextUnder(root) {
|
|
105
|
+
return listFilesRecursive(root)
|
|
106
|
+
.map((file) => {
|
|
107
|
+
try {
|
|
108
|
+
return `\n--- ${file} ---\n${readFileSync(file, "utf8")}`;
|
|
109
|
+
} catch {
|
|
110
|
+
return "";
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
.join("\n");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function runPi({ command, cwd, env, timeoutMs }) {
|
|
117
|
+
return new Promise((resolve) => {
|
|
118
|
+
const child = spawn(command[0], command.slice(1), {
|
|
119
|
+
cwd,
|
|
120
|
+
env,
|
|
121
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
122
|
+
});
|
|
123
|
+
let stdout = "";
|
|
124
|
+
let stderr = "";
|
|
125
|
+
let timedOut = false;
|
|
126
|
+
const timer = setTimeout(() => {
|
|
127
|
+
timedOut = true;
|
|
128
|
+
child.kill("SIGTERM");
|
|
129
|
+
setTimeout(() => child.kill("SIGKILL"), 3000).unref();
|
|
130
|
+
}, timeoutMs);
|
|
131
|
+
child.stdout.setEncoding("utf8");
|
|
132
|
+
child.stderr.setEncoding("utf8");
|
|
133
|
+
child.stdout.on("data", (chunk) => { stdout += chunk; });
|
|
134
|
+
child.stderr.on("data", (chunk) => { stderr += chunk; });
|
|
135
|
+
child.once("error", (error) => {
|
|
136
|
+
clearTimeout(timer);
|
|
137
|
+
resolve({ code: null, signal: null, stdout, stderr, error, timedOut });
|
|
138
|
+
});
|
|
139
|
+
child.once("close", (code, signal) => {
|
|
140
|
+
clearTimeout(timer);
|
|
141
|
+
resolve({ code, signal, stdout, stderr, timedOut });
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function assert(condition, message) {
|
|
147
|
+
if (!condition) throw new Error(message);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function main() {
|
|
151
|
+
const options = parseArgs(process.argv.slice(2));
|
|
152
|
+
if (options.help) {
|
|
153
|
+
printHelp();
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const profileName = `zz-e2e-codex-${Date.now()}`;
|
|
158
|
+
const subagentsDir = path.join(options.agentDir, "subagents");
|
|
159
|
+
const profilePath = path.join(subagentsDir, `${profileName}.md`);
|
|
160
|
+
ensureDir(options.runRoot);
|
|
161
|
+
ensureDir(subagentsDir);
|
|
162
|
+
const fixture = writeFixture(options.runRoot);
|
|
163
|
+
const sessionDir = path.join(options.runRoot, "sessions");
|
|
164
|
+
ensureDir(sessionDir);
|
|
165
|
+
|
|
166
|
+
const profile = `---\ndescription: E2E Codex CLI smoke profile.\nbackend: codex\nmodel: ${options.codexModel}\nthinking: ${options.codexThinking}\n---\n\nYou are a Codex CLI subagent used by pi-flow E2E. Use the repository files to answer exactly what was asked. Do not edit files.\n`;
|
|
167
|
+
writeFileSync(profilePath, profile, "utf8");
|
|
168
|
+
|
|
169
|
+
const promptPath = path.join(options.runRoot, "prompt.md");
|
|
170
|
+
const expected = "CODEX_SUBAGENT_OK:gpt-5.4-mini-medium";
|
|
171
|
+
writeFileSync(promptPath, `You are testing pi-flow Codex CLI backend.\n\nYou MUST call the Agent tool exactly once with subagent_type \"${profileName}\".\nUse description \"Codex CLI smoke\".\nThe subagent prompt must be:\n\nRead e2e-target.txt in the current working directory and reply with exactly this format and nothing else: CODEX_SUBAGENT_OK:<file content without surrounding whitespace>\n\nAfter the Agent result returns, reply with the subagent's exact final token line.\nExpected token line: ${expected}\n`, "utf8");
|
|
172
|
+
|
|
173
|
+
const command = [
|
|
174
|
+
"pi",
|
|
175
|
+
"-p",
|
|
176
|
+
"--mode", "json",
|
|
177
|
+
"--model", options.rootModel,
|
|
178
|
+
"--thinking", options.rootThinking,
|
|
179
|
+
"--session-dir", sessionDir,
|
|
180
|
+
"--no-extensions",
|
|
181
|
+
"--extension", extensionPath,
|
|
182
|
+
"--no-skills",
|
|
183
|
+
"--no-prompt-templates",
|
|
184
|
+
"--no-themes",
|
|
185
|
+
"--no-context-files",
|
|
186
|
+
"--tools", "Agent",
|
|
187
|
+
"--approve",
|
|
188
|
+
`@${promptPath}`,
|
|
189
|
+
];
|
|
190
|
+
|
|
191
|
+
console.log(`Running: ${command.join(" ")}`);
|
|
192
|
+
console.log(`Fixture: ${fixture}`);
|
|
193
|
+
console.log(`Session dir: ${sessionDir}`);
|
|
194
|
+
console.log(`Profile: ${profilePath}`);
|
|
195
|
+
console.log(`Codex profile model/thinking: ${options.codexModel}/${options.codexThinking}`);
|
|
196
|
+
console.log(`Root pi model/thinking: ${options.rootModel}/${options.rootThinking}`);
|
|
197
|
+
|
|
198
|
+
let run;
|
|
199
|
+
try {
|
|
200
|
+
run = await runPi({
|
|
201
|
+
command,
|
|
202
|
+
cwd: fixture,
|
|
203
|
+
env: { ...process.env, PI_CODING_AGENT_DIR: options.agentDir },
|
|
204
|
+
timeoutMs: options.timeoutMs,
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
const transcriptText = `${run.stdout}\n${run.stderr}\n${readAllTextUnder(sessionDir)}`;
|
|
208
|
+
const gitStatus = shell("git", ["status", "--short"], { cwd: fixture });
|
|
209
|
+
|
|
210
|
+
assert(!run.timedOut, `pi timed out after ${options.timeoutMs}ms`);
|
|
211
|
+
assert(run.code === 0, `pi exited with ${run.code}${run.signal ? ` (${run.signal})` : ""}\nSTDOUT:\n${run.stdout}\nSTDERR:\n${run.stderr}`);
|
|
212
|
+
assert(transcriptText.includes(profileName), `session/output did not mention profile ${profileName}`);
|
|
213
|
+
assert(transcriptText.includes("Agent"), "session/output did not record Agent tool usage");
|
|
214
|
+
assert(transcriptText.includes(expected), `expected Codex subagent token not found: ${expected}`);
|
|
215
|
+
assert(gitStatus.status === 0, `git status failed: ${gitStatus.stderr}`);
|
|
216
|
+
assert(gitStatus.stdout.trim() === "", `fixture was modified:\n${gitStatus.stdout}`);
|
|
217
|
+
|
|
218
|
+
console.log("PASS codex subagent E2E");
|
|
219
|
+
console.log(`Observed token: ${expected}`);
|
|
220
|
+
} finally {
|
|
221
|
+
if (!options.keep) {
|
|
222
|
+
try { unlinkSync(profilePath); } catch {}
|
|
223
|
+
rmSync(options.runRoot, { recursive: true, force: true });
|
|
224
|
+
} else {
|
|
225
|
+
console.log(`Kept run root: ${options.runRoot}`);
|
|
226
|
+
console.log(`Kept profile: ${profilePath}`);
|
|
227
|
+
if (run) {
|
|
228
|
+
writeFileSync(path.join(options.runRoot, "stdout.jsonl"), run.stdout ?? "", "utf8");
|
|
229
|
+
writeFileSync(path.join(options.runRoot, "stderr.log"), run.stderr ?? "", "utf8");
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
main().catch((error) => {
|
|
236
|
+
console.error(`FAIL codex subagent E2E: ${error instanceof Error ? error.stack || error.message : String(error)}`);
|
|
237
|
+
process.exitCode = 1;
|
|
238
|
+
});
|