@cydm/pie 2.1.0 → 2.1.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.
Files changed (37) hide show
  1. package/README.md +6 -3
  2. package/dist/builtin/extensions/ask-user/index.js +1 -1
  3. package/dist/builtin/extensions/computer-use/index.js +3993 -107
  4. package/dist/builtin/extensions/plan-mode/index.js +1 -1
  5. package/dist/builtin/extensions/subagent/index.js +2 -2
  6. package/dist/builtin/extensions/todo/index.js +1 -1
  7. package/dist/builtin/skills/chrome-devtools-axi/LICENSE +21 -0
  8. package/dist/builtin/skills/chrome-devtools-axi/SKILL.md +20 -1
  9. package/dist/builtin/skills/chrome-devtools-axi/chrome-devtools-axi-bridge.js +20034 -0
  10. package/dist/builtin/skills/chrome-devtools-axi/chrome-devtools-axi-cli.js +8 -2
  11. package/dist/builtin/skills/chrome-devtools-axi/chrome-devtools-axi-runtime.js +26 -0
  12. package/dist/builtin/skills/chrome-devtools-axi/chunk-42SGITKJ.js +11 -0
  13. package/dist/builtin/skills/chrome-devtools-axi/chunk-OJ6N23UZ.js +47 -0
  14. package/dist/builtin/skills/chrome-devtools-axi/chunk-UPATXALW.js +215 -0
  15. package/dist/builtin/skills/chrome-devtools-axi/cli-F2VCSVL4.js +4424 -0
  16. package/dist/builtin/skills/skill-creator/SKILL.md +5 -2
  17. package/dist/builtin/skills/skill-creator/eval-viewer/generate_review.mjs +7 -20
  18. package/dist/builtin/skills/skill-creator/eval-viewer/viewer.html +15 -15
  19. package/dist/builtin/skills/skill-creator/scripts/aggregate_benchmark.mjs +20 -19
  20. package/dist/builtin/skills/skill-creator/scripts/generate_report.mjs +3 -3
  21. package/dist/builtin/skills/skill-creator/scripts/improve_description.mjs +12 -8
  22. package/dist/builtin/skills/skill-creator/scripts/package_skill.mjs +1 -0
  23. package/dist/builtin/skills/skill-creator/scripts/pie_runner.mjs +77 -31
  24. package/dist/builtin/skills/skill-creator/scripts/run_eval.mjs +38 -11
  25. package/dist/builtin/skills/skill-creator/scripts/run_loop.mjs +12 -6
  26. package/dist/builtin/skills/skill-creator/scripts/skill_metadata.mjs +12 -1
  27. package/dist/builtin/skills/tui-use/LICENSE +21 -0
  28. package/dist/builtin/skills/tui-use/README.md +13 -3
  29. package/dist/builtin/skills/tui-use/SKILL.md +87 -39
  30. package/dist/builtin/skills/tui-use/tui-use-cli.cjs +10608 -0
  31. package/dist/builtin/skills/tui-use/tui-use-cli.js +39 -0
  32. package/dist/builtin/skills/tui-use/tui-use-daemon.cjs +7916 -0
  33. package/dist/chunks/{chunk-PMPD56N2.js → chunk-LU7RKALH.js} +82 -24
  34. package/dist/chunks/{chunk-G2POWIA5.js → chunk-TJ2F4FL5.js} +64 -16
  35. package/dist/cli.js +7503 -87
  36. package/package.json +26 -18
  37. package/scripts/repair-node-pty.mjs +45 -0
@@ -382,12 +382,15 @@ Save the eval set to the workspace, then run in the background:
382
382
  node <skill-creator-path>/scripts/run_loop.mjs \
383
383
  --eval-set <path-to-trigger-eval.json> \
384
384
  --skill-path <path-to-skill> \
385
- --model <model-id-powering-this-session> \
385
+ --model <profile/modelId> \
386
386
  --max-iterations 5 \
387
387
  --verbose
388
388
  ```
389
389
 
390
- Use the model ID from your system prompt (the one powering the current session) so the triggering test matches what the user actually experiences.
390
+ Use a configured Pie `profile/modelId` so the triggering test matches what the
391
+ user actually experiences. The runner applies selection through JSON-events
392
+ metadata and verifies the actual session model. Unknown models and runtime
393
+ errors fail explicitly; they are not counted as successful negative examples.
391
394
 
392
395
  While it runs, periodically tail the output to give the user updates on which iteration it's on and what the scores look like.
393
396
 
@@ -4,15 +4,14 @@ import path from "node:path";
4
4
  import { createServer } from "node:http";
5
5
  import { readFile, readdir, stat, writeFile } from "node:fs/promises";
6
6
  import { fileURLToPath } from "node:url";
7
- import { execFile } from "node:child_process";
8
- import { promisify } from "node:util";
9
7
  import { openInBrowser } from "../scripts/pie_runner.mjs";
8
+ import { readJsonFile } from "../scripts/skill_metadata.mjs";
10
9
 
11
- const execFileAsync = promisify(execFile);
12
10
  const METADATA_FILES = new Set(["transcript.md", "user_notes.md", "metrics.json"]);
13
11
  const TEXT_EXTENSIONS = new Set([".txt", ".md", ".json", ".csv", ".py", ".js", ".ts", ".tsx", ".jsx", ".yaml", ".yml", ".xml", ".html", ".css", ".sh", ".rb", ".go", ".rs", ".java", ".c", ".cpp", ".h", ".hpp", ".sql", ".r", ".toml", ".mjs"]);
14
12
  const IMAGE_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp"]);
15
13
  const MIME_OVERRIDES = {
14
+ ".pdf": "application/pdf",
16
15
  ".svg": "image/svg+xml",
17
16
  ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
18
17
  ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
@@ -36,7 +35,7 @@ async function pathExists(targetPath) {
36
35
  async function readJsonIfExists(targetPath) {
37
36
  if (!(await pathExists(targetPath))) return null;
38
37
  try {
39
- return JSON.parse(await readFile(targetPath, "utf8"));
38
+ return await readJsonFile(targetPath);
40
39
  } catch {
41
40
  return null;
42
41
  }
@@ -175,18 +174,7 @@ export async function generateHtml({ workspace, skillName, previous = null, benc
175
174
  if (benchmark) {
176
175
  embedded.benchmark = benchmark;
177
176
  }
178
- return template.replace("/*__EMBEDDED_DATA__*/", `const EMBEDDED_DATA = ${JSON.stringify(embedded)};`);
179
- }
180
-
181
- async function killPort(port) {
182
- try {
183
- const { stdout } = await execFileAsync("lsof", ["-ti", `:${port}`], { timeout: 5000 });
184
- for (const pid of stdout.split("\n").map((value) => value.trim()).filter(Boolean)) {
185
- try {
186
- process.kill(Number(pid), "SIGTERM");
187
- } catch {}
188
- }
189
- } catch {}
177
+ return template.replace("/*__EMBEDDED_DATA__*/", () => `const EMBEDDED_DATA = ${JSON.stringify(embedded).replaceAll("<", "\\u003c")};`);
190
178
  }
191
179
 
192
180
  function parseArgs(args) {
@@ -211,10 +199,10 @@ function parseArgs(args) {
211
199
 
212
200
  async function main(argv = process.argv) {
213
201
  const args = parseArgs(argv.slice(2));
214
- const workspace = path.resolve(args._[0] ?? "");
215
- if (!workspace) {
202
+ if (!args._[0]) {
216
203
  throw new Error("Usage: node generate_review.mjs <workspace-path> [--port PORT] [--skill-name NAME] [--static output.html]");
217
204
  }
205
+ const workspace = path.resolve(args._[0]);
218
206
  const skillName = args["skill-name"] ?? path.basename(workspace).replace(/-workspace$/, "");
219
207
  const previous = args["previous-workspace"] ? await loadPreviousIteration(path.resolve(args["previous-workspace"])) : {};
220
208
  const benchmark = args.benchmark ? await readJsonIfExists(path.resolve(args.benchmark)) : null;
@@ -225,7 +213,6 @@ async function main(argv = process.argv) {
225
213
  return;
226
214
  }
227
215
  let port = Number(args.port ?? args.p ?? 3117);
228
- await killPort(port);
229
216
  const feedbackPath = path.join(workspace, "feedback.json");
230
217
  const server = createServer(async (req, res) => {
231
218
  try {
@@ -273,7 +260,7 @@ async function main(argv = process.argv) {
273
260
  port = typeof address === "object" && address ? address.port : port;
274
261
  const url = `http://localhost:${port}`;
275
262
  process.stdout.write(`Eval Viewer\nURL: ${url}\nWorkspace: ${workspace}\nFeedback: ${feedbackPath}\n`);
276
- await openInBrowser(url);
263
+ if (!args["no-open"]) await openInBrowser(url);
277
264
  }
278
265
 
279
266
  const entryPath = fileURLToPath(import.meta.url);
@@ -882,7 +882,7 @@
882
882
  const badgeClass = summary.pass_rate >= 0.8 ? "grade-pass" : summary.pass_rate >= 0.5 ? "" : "grade-fail";
883
883
  html += '<div class="grades-summary">';
884
884
  html += '<span class="grade-badge ' + badgeClass + '">' + passRate + '</span>';
885
- html += '<span>' + (summary.passed || 0) + ' passed, ' + (summary.failed || 0) + ' failed of ' + (summary.total || 0) + '</span>';
885
+ html += '<span>' + escapeHtml(summary.passed || 0) + ' passed, ' + escapeHtml(summary.failed || 0) + ' failed of ' + escapeHtml(summary.total || 0) + '</span>';
886
886
  html += '</div>';
887
887
 
888
888
  // Assertions list
@@ -1099,7 +1099,7 @@
1099
1099
  function escapeHtml(text) {
1100
1100
  const div = document.createElement("div");
1101
1101
  div.textContent = text;
1102
- return div.innerHTML;
1102
+ return div.innerHTML.replaceAll('"', "&quot;").replaceAll("'", "&#39;");
1103
1103
  }
1104
1104
 
1105
1105
  // ---- View switching ----
@@ -1129,16 +1129,16 @@
1129
1129
  html += "<h2 style='font-family: Poppins, sans-serif; margin-bottom: 0.5rem;'>Benchmark Results</h2>";
1130
1130
  html += "<p style='color: var(--text-muted); font-size: 0.875rem; margin-bottom: 1.25rem;'>";
1131
1131
  if (metadata.skill_name) html += "<strong>" + escapeHtml(metadata.skill_name) + "</strong> &mdash; ";
1132
- if (metadata.timestamp) html += metadata.timestamp + " &mdash; ";
1133
- if (metadata.evals_run) html += "Evals: " + metadata.evals_run.join(", ") + " &mdash; ";
1134
- html += (metadata.runs_per_configuration || "?") + " runs per configuration";
1132
+ if (metadata.timestamp) html += escapeHtml(metadata.timestamp) + " &mdash; ";
1133
+ if (metadata.evals_run) html += "Evals: " + escapeHtml(metadata.evals_run.join(", ")) + " &mdash; ";
1134
+ html += escapeHtml(metadata.runs_per_configuration || "?") + " runs per configuration";
1135
1135
  html += "</p>";
1136
1136
 
1137
1137
  // Summary table
1138
1138
  html += '<table class="benchmark-table">';
1139
1139
 
1140
1140
  function fmtStat(stat, pct) {
1141
- if (!stat) return "—";
1141
+ if (!stat || !Number.isFinite(stat.mean) || !Number.isFinite(stat.stddev)) return "—";
1142
1142
  const suffix = pct ? "%" : "";
1143
1143
  const m = pct ? (stat.mean * 100).toFixed(0) : stat.mean.toFixed(1);
1144
1144
  const s = pct ? (stat.stddev * 100).toFixed(0) : stat.stddev.toFixed(1);
@@ -1169,14 +1169,14 @@
1169
1169
  html += "<tr><td><strong>Pass Rate</strong></td>";
1170
1170
  html += "<td>" + fmtStat(a.pass_rate, true) + "</td>";
1171
1171
  html += "<td>" + fmtStat(b.pass_rate, true) + "</td>";
1172
- html += '<td class="' + deltaClass(delta.pass_rate) + '">' + (delta.pass_rate || "—") + "</td></tr>";
1172
+ html += '<td class="' + deltaClass(delta.pass_rate) + '">' + escapeHtml(delta.pass_rate || "—") + "</td></tr>";
1173
1173
 
1174
1174
  // Time (only show row if data exists)
1175
1175
  if (a.time_seconds || b.time_seconds) {
1176
1176
  html += "<tr><td><strong>Time (s)</strong></td>";
1177
1177
  html += "<td>" + fmtStat(a.time_seconds, false) + "</td>";
1178
1178
  html += "<td>" + fmtStat(b.time_seconds, false) + "</td>";
1179
- html += '<td class="' + deltaClass(delta.time_seconds) + '">' + (delta.time_seconds ? delta.time_seconds + "s" : "—") + "</td></tr>";
1179
+ html += '<td class="' + deltaClass(delta.time_seconds) + '">' + escapeHtml(delta.time_seconds ? delta.time_seconds + "s" : "—") + "</td></tr>";
1180
1180
  }
1181
1181
 
1182
1182
  // Tokens (only show row if data exists)
@@ -1184,7 +1184,7 @@
1184
1184
  html += "<tr><td><strong>Tokens</strong></td>";
1185
1185
  html += "<td>" + fmtStat(a.tokens, false) + "</td>";
1186
1186
  html += "<td>" + fmtStat(b.tokens, false) + "</td>";
1187
- html += '<td class="' + deltaClass(delta.tokens) + '">' + (delta.tokens || "—") + "</td></tr>";
1187
+ html += '<td class="' + deltaClass(delta.tokens) + '">' + escapeHtml(delta.tokens || "—") + "</td></tr>";
1188
1188
  }
1189
1189
 
1190
1190
  html += "</tbody></table>";
@@ -1225,11 +1225,11 @@
1225
1225
  const r = run.result || {};
1226
1226
  const prClass = r.pass_rate >= 0.8 ? "benchmark-delta-positive" : r.pass_rate < 0.5 ? "benchmark-delta-negative" : "";
1227
1227
  html += '<tr class="' + rowClass + '">';
1228
- html += "<td>" + configLabel + "</td>";
1229
- html += "<td>" + run.run_number + "</td>";
1230
- html += '<td class="' + prClass + '">' + ((r.pass_rate || 0) * 100).toFixed(0) + "% (" + (r.passed || 0) + "/" + (r.total || 0) + ")</td>";
1228
+ html += "<td>" + escapeHtml(configLabel) + "</td>";
1229
+ html += "<td>" + escapeHtml(run.run_number) + "</td>";
1230
+ html += '<td class="' + prClass + '">' + ((r.pass_rate || 0) * 100).toFixed(0) + "% (" + escapeHtml(r.passed || 0) + "/" + escapeHtml(r.total || 0) + ")</td>";
1231
1231
  if (hasTime) html += "<td>" + (r.time_seconds != null ? r.time_seconds.toFixed(1) : "—") + "</td>";
1232
- if (hasErrors) html += "<td>" + (r.errors || 0) + "</td>";
1232
+ if (hasErrors) html += "<td>" + escapeHtml(r.errors || 0) + "</td>";
1233
1233
  html += "</tr>";
1234
1234
  }
1235
1235
 
@@ -1238,7 +1238,7 @@
1238
1238
  const avgRate = rates.reduce((a, b) => a + b, 0) / rates.length;
1239
1239
  const avgPrClass = avgRate >= 0.8 ? "benchmark-delta-positive" : avgRate < 0.5 ? "benchmark-delta-negative" : "";
1240
1240
  html += '<tr class="benchmark-row-avg ' + rowClass + '">';
1241
- html += "<td>" + configLabel + "</td>";
1241
+ html += "<td>" + escapeHtml(configLabel) + "</td>";
1242
1242
  html += "<td>Avg</td>";
1243
1243
  html += '<td class="' + avgPrClass + '">' + (avgRate * 100).toFixed(0) + "%</td>";
1244
1244
  if (hasTime) {
@@ -1289,7 +1289,7 @@
1289
1289
  if (exp) {
1290
1290
  const cls = exp.passed ? "benchmark-delta-positive" : "benchmark-delta-negative";
1291
1291
  const icon = exp.passed ? "\u2713" : "\u2717";
1292
- html += '<span class="' + cls + '" title="Run ' + run.run_number + ': ' + escapeHtml(exp.evidence || "") + '">' + icon + "</span> ";
1292
+ html += '<span class="' + cls + '" title="Run ' + escapeHtml(run.run_number) + ': ' + escapeHtml(exp.evidence || "") + '">' + icon + "</span> ";
1293
1293
  } else {
1294
1294
  html += "— ";
1295
1295
  }
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import path from "node:path";
4
- import { readdir, readFile, stat, writeFile } from "node:fs/promises";
4
+ import { readdir, stat, writeFile } from "node:fs/promises";
5
5
  import { fileURLToPath } from "node:url";
6
+ import { readJsonFile } from "./skill_metadata.mjs";
6
7
 
7
8
  function round(value, places = 4) {
8
9
  const factor = 10 ** places;
@@ -41,7 +42,7 @@ async function readJsonIfExists(targetPath) {
41
42
  return null;
42
43
  }
43
44
  try {
44
- return JSON.parse(await readFile(targetPath, "utf8"));
45
+ return await readJsonFile(targetPath);
45
46
  } catch {
46
47
  return null;
47
48
  }
@@ -76,10 +77,8 @@ export async function loadRunResults(benchmarkDir) {
76
77
  for (const configName of await listDirectories(evalDir)) {
77
78
  const configDir = path.join(evalDir, configName);
78
79
  const childDirs = await listDirectories(configDir);
79
- const runDirs = childDirs.filter((name) => name.startsWith("run-"));
80
- if (runDirs.length === 0) {
81
- continue;
82
- }
80
+ const nestedRuns = childDirs.filter((name) => name.startsWith("run-"));
81
+ const runDirs = nestedRuns.length > 0 ? nestedRuns : [""];
83
82
  if (!results[configName]) {
84
83
  results[configName] = [];
85
84
  }
@@ -91,19 +90,16 @@ export async function loadRunResults(benchmarkDir) {
91
90
  }
92
91
  const timing = grading.timing ?? {};
93
92
  let timeSeconds = Number(timing.total_duration_seconds ?? 0);
94
- let tokens = 0;
93
+ let tokens = null;
95
94
  const timingJson = await readJsonIfExists(path.join(runDir, "timing.json"));
96
95
  if (timeSeconds === 0 && timingJson) {
97
96
  timeSeconds = Number(timingJson.total_duration_seconds ?? 0);
98
- tokens = Number(timingJson.total_tokens ?? 0);
99
97
  }
98
+ if (Number.isFinite(timingJson?.total_tokens)) tokens = timingJson.total_tokens;
100
99
  const executionMetrics = grading.execution_metrics ?? {};
101
- if (tokens === 0) {
102
- tokens = Number(executionMetrics.output_chars ?? 0);
103
- }
104
100
  results[configName].push({
105
101
  eval_id: evalId,
106
- run_number: Number.parseInt(runDirName.split("-")[1], 10) || 0,
102
+ run_number: Number.parseInt(runDirName.split("-")[1], 10) || 1,
107
103
  pass_rate: Number(grading.summary?.pass_rate ?? 0),
108
104
  passed: Number(grading.summary?.passed ?? 0),
109
105
  failed: Number(grading.summary?.failed ?? 0),
@@ -127,13 +123,15 @@ export async function loadRunResults(benchmarkDir) {
127
123
 
128
124
  export function aggregateResults(results) {
129
125
  const runSummary = {};
130
- const configs = Object.keys(results);
126
+ const configs = Object.keys(results).sort((a, b) => Number(b === "with_skill") - Number(a === "with_skill"));
131
127
  for (const config of configs) {
132
128
  const runs = results[config] ?? [];
133
129
  runSummary[config] = {
134
130
  pass_rate: calculateStats(runs.map((run) => Number(run.pass_rate ?? 0))),
135
131
  time_seconds: calculateStats(runs.map((run) => Number(run.time_seconds ?? 0))),
136
- tokens: calculateStats(runs.map((run) => Number(run.tokens ?? 0))),
132
+ tokens: runs.some((run) => Number.isFinite(run.tokens))
133
+ ? calculateStats(runs.map((run) => run.tokens).filter(Number.isFinite))
134
+ : { mean: null, stddev: null, min: null, max: null },
137
135
  };
138
136
  }
139
137
  const primaryConfig = configs[0];
@@ -143,7 +141,8 @@ export function aggregateResults(results) {
143
141
  runSummary.delta = {
144
142
  pass_rate: `${(primary.pass_rate?.mean ?? 0) - (baseline.pass_rate?.mean ?? 0) >= 0 ? "+" : ""}${round((primary.pass_rate?.mean ?? 0) - (baseline.pass_rate?.mean ?? 0), 2).toFixed(2)}`,
145
143
  time_seconds: `${(primary.time_seconds?.mean ?? 0) - (baseline.time_seconds?.mean ?? 0) >= 0 ? "+" : ""}${round((primary.time_seconds?.mean ?? 0) - (baseline.time_seconds?.mean ?? 0), 1).toFixed(1)}`,
146
- tokens: `${(primary.tokens?.mean ?? 0) - (baseline.tokens?.mean ?? 0) >= 0 ? "+" : ""}${Math.round((primary.tokens?.mean ?? 0) - (baseline.tokens?.mean ?? 0))}`,
144
+ tokens: primary.tokens?.mean != null && baseline.tokens?.mean != null
145
+ ? `${primary.tokens.mean - baseline.tokens.mean >= 0 ? "+" : ""}${Math.round(primary.tokens.mean - baseline.tokens.mean)}` : "—",
147
146
  };
148
147
  return runSummary;
149
148
  }
@@ -152,7 +151,8 @@ export async function generateBenchmark(benchmarkDir, { skillName = "", skillPat
152
151
  const results = await loadRunResults(benchmarkDir);
153
152
  const runSummary = aggregateResults(results);
154
153
  const runs = [];
155
- for (const [configuration, entries] of Object.entries(results)) {
154
+ for (const configuration of Object.keys(runSummary).filter((key) => key !== "delta")) {
155
+ const entries = results[configuration];
156
156
  for (const result of entries) {
157
157
  runs.push({
158
158
  eval_id: result.eval_id,
@@ -174,6 +174,7 @@ export async function generateBenchmark(benchmarkDir, { skillName = "", skillPat
174
174
  }
175
175
  }
176
176
  const evalIds = [...new Set(runs.map((run) => run.eval_id))].sort((a, b) => a - b);
177
+ const counts = new Set(Object.values(results).flatMap((entries) => evalIds.map((id) => entries.filter((run) => run.eval_id === id).length)));
177
178
  return {
178
179
  metadata: {
179
180
  skill_name: skillName || "<skill-name>",
@@ -182,7 +183,7 @@ export async function generateBenchmark(benchmarkDir, { skillName = "", skillPat
182
183
  analyzer_model: "<model-name>",
183
184
  timestamp: new Date().toISOString().replace(/\.\d{3}Z$/, "Z"),
184
185
  evals_run: evalIds,
185
- runs_per_configuration: 3,
186
+ runs_per_configuration: counts.size === 1 ? [...counts][0] : null,
186
187
  },
187
188
  runs,
188
189
  run_summary: runSummary,
@@ -206,7 +207,7 @@ export function generateMarkdown(benchmark) {
206
207
  "",
207
208
  `**Model**: ${metadata.executor_model ?? "<model-name>"}`,
208
209
  `**Date**: ${metadata.timestamp ?? ""}`,
209
- `**Evals**: ${(metadata.evals_run ?? []).join(", ")} (${metadata.runs_per_configuration ?? 0} runs each per configuration)`,
210
+ `**Evals**: ${(metadata.evals_run ?? []).join(", ")} (${metadata.runs_per_configuration ?? "variable"} runs each per configuration)`,
210
211
  "",
211
212
  "## Summary",
212
213
  "",
@@ -214,7 +215,7 @@ export function generateMarkdown(benchmark) {
214
215
  "|--------|------------|---------------|-------|",
215
216
  `| Pass Rate | ${(aSummary.pass_rate?.mean ?? 0) * 100}% ± ${(aSummary.pass_rate?.stddev ?? 0) * 100}% | ${(bSummary.pass_rate?.mean ?? 0) * 100}% ± ${(bSummary.pass_rate?.stddev ?? 0) * 100}% | ${delta.pass_rate ?? "—"} |`,
216
217
  `| Time | ${(aSummary.time_seconds?.mean ?? 0).toFixed?.(1) ?? aSummary.time_seconds?.mean ?? 0}s ± ${(aSummary.time_seconds?.stddev ?? 0).toFixed?.(1) ?? aSummary.time_seconds?.stddev ?? 0}s | ${(bSummary.time_seconds?.mean ?? 0).toFixed?.(1) ?? bSummary.time_seconds?.mean ?? 0}s ± ${(bSummary.time_seconds?.stddev ?? 0).toFixed?.(1) ?? bSummary.time_seconds?.stddev ?? 0}s | ${delta.time_seconds ?? "—"}s |`,
217
- `| Tokens | ${Math.round(aSummary.tokens?.mean ?? 0)} ± ${Math.round(aSummary.tokens?.stddev ?? 0)} | ${Math.round(bSummary.tokens?.mean ?? 0)} ± ${Math.round(bSummary.tokens?.stddev ?? 0)} | ${delta.tokens ?? "—"} |`,
218
+ `| Tokens | ${aSummary.tokens?.mean == null ? "—" : `${Math.round(aSummary.tokens.mean)} ± ${Math.round(aSummary.tokens.stddev)}`} | ${bSummary.tokens?.mean == null ? "—" : `${Math.round(bSummary.tokens.mean)} ± ${Math.round(bSummary.tokens.stddev)}`} | ${delta.tokens ?? "—"} |`,
218
219
  ];
219
220
  if (Array.isArray(benchmark.notes) && benchmark.notes.length > 0) {
220
221
  lines.push("", "## Notes", "");
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import path from "node:path";
4
- import { readFile, writeFile } from "node:fs/promises";
4
+ import { writeFile } from "node:fs/promises";
5
5
  import { fileURLToPath } from "node:url";
6
+ import { readJsonFile } from "./skill_metadata.mjs";
6
7
 
7
8
  function escapeHtml(value) {
8
9
  return String(value ?? "")
@@ -176,8 +177,7 @@ async function main(argv = process.argv) {
176
177
  if (!inputPath) {
177
178
  throw new Error("Usage: node generate_report.mjs <input.json> [-o output.html] [--skill-name name]");
178
179
  }
179
- const raw = inputPath === "-" ? await readStdin() : await readFile(inputPath, "utf8");
180
- const data = JSON.parse(raw);
180
+ const data = inputPath === "-" ? JSON.parse((await readStdin()).replace(/^\uFEFF/, "")) : await readJsonFile(inputPath);
181
181
  const output = generateHtml(data, { skillName: args["skill-name"] ?? "", autoRefresh: false });
182
182
  if (args.output || args.o) {
183
183
  await writeFile(args.output || args.o, output);
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import path from "node:path";
4
- import { mkdir, readFile, writeFile } from "node:fs/promises";
4
+ import { mkdir, writeFile } from "node:fs/promises";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { callPieText } from "./pie_runner.mjs";
7
- import { parseSkillFile } from "./skill_metadata.mjs";
7
+ import { parseSkillFile, readJsonFile, validateFrontmatter } from "./skill_metadata.mjs";
8
8
 
9
9
  export async function improveDescription({
10
10
  skillName,
@@ -100,8 +100,11 @@ I'd encourage you to be creative and mix up the style in different iterations si
100
100
 
101
101
  Please respond with only the new description text in <new_description> tags, nothing else.`;
102
102
 
103
- const transcript = { iteration, prompt };
104
- const text = await callPieText(prompt);
103
+ const transcript = { iteration, prompt, requested_model: model, actual_models: [] };
104
+ const onModelSelection = ({ actualModel }) => {
105
+ if (actualModel && !transcript.actual_models.includes(actualModel)) transcript.actual_models.push(actualModel);
106
+ };
107
+ const text = await callPieText(prompt, { model, onModelSelection });
105
108
  transcript.response = text;
106
109
 
107
110
  let description = parseDescription(text);
@@ -119,7 +122,7 @@ A previous attempt produced this description, which at ${description.length} cha
119
122
  "${description}"
120
123
 
121
124
  Rewrite it to be under 1024 characters while keeping the most important trigger words and intent coverage. Respond with only the new description in <new_description> tags.`;
122
- const shortenText = await callPieText(shortenPrompt);
125
+ const shortenText = await callPieText(shortenPrompt, { model, onModelSelection });
123
126
  const shortened = parseDescription(shortenText);
124
127
  transcript.rewrite_prompt = shortenPrompt;
125
128
  transcript.rewrite_response = shortenText;
@@ -133,7 +136,8 @@ Rewrite it to be under 1024 characters while keeping the most important trigger
133
136
  await mkdir(logDir, { recursive: true });
134
137
  await writeFile(path.join(logDir, `improve_iter_${iteration ?? "unknown"}.json`), JSON.stringify(transcript, null, 2));
135
138
  }
136
-
139
+ const validation = validateFrontmatter({ name: skillName, description });
140
+ if (!validation.valid) throw new Error(`Improved description is invalid: ${validation.message}`);
137
141
  return description;
138
142
  }
139
143
 
@@ -145,8 +149,8 @@ function parseDescription(text) {
145
149
  async function main(argv = process.argv) {
146
150
  const args = parseArgs(argv.slice(2));
147
151
  const skillPath = path.resolve(args["skill-path"]);
148
- const evalResults = JSON.parse(await readFile(args["eval-results"], "utf8"));
149
- const history = args.history ? JSON.parse(await readFile(args.history, "utf8")) : [];
152
+ const evalResults = await readJsonFile(args["eval-results"]);
153
+ const history = args.history ? await readJsonFile(args.history) : [];
150
154
  const { name, content } = await parseSkillFile(skillPath);
151
155
  const currentDescription = evalResults.description;
152
156
 
@@ -81,6 +81,7 @@ export async function packageSkill(skillPath, outputDir) {
81
81
  const files = await collectFiles(resolvedSkillPath, baseDir);
82
82
 
83
83
  for (const file of files) {
84
+ if (path.resolve(file.fullPath) === archivePath) continue;
84
85
  if (shouldExclude(file.arcname)) {
85
86
  process.stdout.write(` Skipped: ${file.arcname}\n`);
86
87
  continue;
@@ -3,6 +3,7 @@
3
3
  import crypto from "node:crypto";
4
4
  import { spawn } from "node:child_process";
5
5
  import fs from "node:fs";
6
+ import os from "node:os";
6
7
  import path from "node:path";
7
8
  import { fileURLToPath } from "node:url";
8
9
 
@@ -61,21 +62,29 @@ export function getPieCliPath() {
61
62
  );
62
63
  }
63
64
 
64
- export async function runPieJsonPrompt(prompt, { cwd = process.cwd(), timeout = 300_000, sessionId = null } = {}) {
65
+ export async function runPieJsonPrompt(prompt, { cwd = process.cwd(), timeout = 300_000, sessionId = null, model = null } = {}) {
66
+ if (model && (typeof model !== "string" || !/^[^/\s]+\/\S+$/.test(model))) {
67
+ throw new Error("Model must use profile/modelId format.");
68
+ }
65
69
  return new Promise((resolve, reject) => {
66
- const args = [getPieCliPath(), prompt, "--json-output"];
67
- if (sessionId) {
68
- args.push("--session-id", sessionId);
69
- }
70
+ const id = sessionId || `skill-run-${uuidFragment()}`;
71
+ const turnId = `skill-turn-${uuidFragment()}`;
72
+ const args = [getPieCliPath(), "--json-events", "--event-detail", "full", "--session-id", id];
70
73
 
71
- const child = spawn("node", args, {
74
+ const child = spawn(process.execPath, args, {
72
75
  cwd,
73
76
  env: { ...process.env },
74
- stdio: ["ignore", "pipe", "pipe"],
77
+ stdio: ["pipe", "pipe", "pipe"],
78
+ windowsHide: true,
75
79
  });
80
+ child.stdout.setEncoding("utf8");
81
+ child.stderr.setEncoding("utf8");
76
82
 
77
- let stdout = "";
83
+ let buffer = "";
78
84
  let stderr = "";
85
+ let text = "";
86
+ let response;
87
+ let turnSettled = false;
79
88
  let settled = false;
80
89
 
81
90
  const cleanup = () => {
@@ -89,55 +98,92 @@ export async function runPieJsonPrompt(prompt, { cwd = process.cwd(), timeout =
89
98
  if (settled) return;
90
99
  settled = true;
91
100
  cleanup();
92
- try {
93
- child.kill("SIGKILL");
94
- } catch {}
95
101
  cb();
96
102
  };
97
103
 
98
- const tryResolveFromStdout = () => {
99
- const lines = stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
100
- for (let i = lines.length - 1; i >= 0; i -= 1) {
101
- const line = lines[i];
102
- if (!line.startsWith("{")) continue;
103
- try {
104
- const parsed = JSON.parse(line);
105
- if (Object.prototype.hasOwnProperty.call(parsed, "ok")) {
106
- finish(() => resolve(parsed));
107
- return true;
108
- }
109
- } catch {}
104
+ const terminate = () => {
105
+ child.kill();
106
+ child.stdin.destroy();
107
+ child.stdout.destroy();
108
+ child.stderr.destroy();
109
+ child.unref();
110
+ };
111
+
112
+ const consume = (line) => {
113
+ let event;
114
+ try { event = JSON.parse(line); } catch { return; }
115
+ if (event.turnId && event.turnId !== turnId) return;
116
+ if (event.type === "display.output" && event.stream === "terminal") text += event.chunk || "";
117
+ if (event.type === "turn.completed") response = { ok: true, text: event.summary ?? text };
118
+ if (event.type === "turn.failed" || event.type === "turn.cancelled") {
119
+ const error = event.error || event.reason || event.type;
120
+ response = { ok: false, error: typeof error === "string" ? error : error.message || JSON.stringify(error) };
110
121
  }
111
- return false;
122
+ if (event.type === "turn.settled") turnSettled = true;
112
123
  };
113
124
 
114
125
  const timer = setTimeout(() => {
126
+ terminate();
115
127
  finish(() => reject(new Error(`pie prompt timed out after ${Math.round(timeout / 1000)}s`)));
116
128
  }, timeout);
117
129
 
118
130
  child.stdout.on("data", (chunk) => {
119
- stdout += String(chunk);
120
- tryResolveFromStdout();
131
+ buffer += String(chunk);
132
+ let newline;
133
+ while ((newline = buffer.indexOf("\n")) >= 0) {
134
+ consume(buffer.slice(0, newline).trim());
135
+ buffer = buffer.slice(newline + 1);
136
+ }
121
137
  });
122
138
  child.stderr.on("data", (chunk) => {
123
- stderr += String(chunk);
139
+ stderr = (stderr + String(chunk)).slice(-8_000);
124
140
  });
125
141
  child.on("error", (error) => {
126
142
  finish(() => reject(error));
127
143
  });
128
144
  child.on("close", (code) => {
129
- if (tryResolveFromStdout()) {
145
+ if (buffer.trim()) consume(buffer.trim());
146
+ if (code !== 0 || !turnSettled || !response) {
147
+ finish(() => reject(new Error(`pie prompt exited ${code} without a settled response\nstderr: ${stderr}`)));
148
+ return;
149
+ }
150
+ const configuredDir = process.env.PIE_CODING_AGENT_DIR || path.join(os.homedir(), ".pie");
151
+ const configDir = configuredDir === "~" ? os.homedir()
152
+ : configuredDir.startsWith("~/") ? path.join(os.homedir(), configuredDir.slice(2)) : configuredDir;
153
+ let actualModel = null;
154
+ try {
155
+ const session = JSON.parse(fs.readFileSync(path.join(configDir, "sessions", `${id}.json`), "utf8"));
156
+ const metadata = session.metadata || {};
157
+ if (metadata.provider && metadata.modelId) actualModel = `${metadata.provider}/${metadata.modelId}`;
158
+ const messages = session.messages || (session.entries || []).map((entry) => entry.message).filter(Boolean);
159
+ const lastAssistant = messages.filter((message) => message.role === "assistant").at(-1);
160
+ if (response.ok && lastAssistant) {
161
+ response.text = typeof lastAssistant.content === "string" ? lastAssistant.content
162
+ : (lastAssistant.content || []).filter((block) => block.type === "text").map((block) => block.text).join("");
163
+ }
164
+ } catch {}
165
+ if (model && response.ok && actualModel !== model) {
166
+ finish(() => reject(new Error(`Requested model ${model}, but session reported ${actualModel || "no model"}.`)));
130
167
  return;
131
168
  }
132
- finish(() => reject(new Error(`pie prompt exited ${code}\nstdout: ${stdout}\nstderr: ${stderr}`)));
169
+ finish(() => resolve({ ...response, sessionId: id, requestedModel: model, actualModel }));
133
170
  });
171
+ child.stdin.end(`${JSON.stringify({
172
+ type: "turn.request",
173
+ sessionId: id,
174
+ turnId,
175
+ prompt,
176
+ cwd,
177
+ ...(model ? { metadata: { model } } : {}),
178
+ })}\n`);
134
179
  });
135
180
  }
136
181
 
137
- export async function callPieText(prompt, { cwd = process.cwd(), timeout = 300_000, sessionId = null } = {}) {
138
- const response = await runPieJsonPrompt(prompt, { cwd, timeout, sessionId });
182
+ export async function callPieText(prompt, { onModelSelection, ...options } = {}) {
183
+ const response = await runPieJsonPrompt(prompt, options);
139
184
  if (!response?.ok) {
140
185
  throw new Error(response?.error || "Pie prompt failed");
141
186
  }
187
+ onModelSelection?.({ requestedModel: response.requestedModel, actualModel: response.actualModel });
142
188
  return String(response.text || "");
143
189
  }