@cydm/pie 2.6.4 → 2.6.6

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.
@@ -1,9 +1,9 @@
1
1
  import { createRequire as __createRequire } from "node:module"; const require = __createRequire(import.meta.url);
2
2
  import {
3
3
  createAskUserCapability
4
- } from "../../../chunks/chunk-3AOMWS2Z.js";
5
- import "../../../chunks/chunk-YEUBQBOB.js";
6
- import "../../../chunks/chunk-OCEYHWVX.js";
4
+ } from "../../../chunks/chunk-H6BAM2N4.js";
5
+ import "../../../chunks/chunk-KEARZX6F.js";
6
+ import "../../../chunks/chunk-G5B6D35K.js";
7
7
  import "../../../chunks/chunk-XXGYUYXI.js";
8
8
  import "../../../chunks/chunk-LZQXQOPJ.js";
9
9
 
@@ -24461,6 +24461,97 @@ function redactSecrets(text, secrets) {
24461
24461
  }
24462
24462
  return sanitized;
24463
24463
  }
24464
+ function encodedSecretVariants(secrets) {
24465
+ const variants = [...secrets];
24466
+ for (const secret of secrets) {
24467
+ try {
24468
+ const base642 = Buffer.from(secret, "utf-8").toString("base64");
24469
+ if (base642.length >= 4) {
24470
+ variants.push(base642);
24471
+ const base64url2 = Buffer.from(secret, "utf-8").toString("base64url");
24472
+ if (base64url2 !== base642 && base64url2.length >= 4) variants.push(base64url2);
24473
+ }
24474
+ } catch {
24475
+ }
24476
+ try {
24477
+ const urlEncoded = encodeURIComponent(secret);
24478
+ if (urlEncoded !== secret && urlEncoded.length >= 4) {
24479
+ variants.push(urlEncoded);
24480
+ const urlLowerHex = urlEncoded.replace(/%[0-9A-F]{2}/g, (m) => `%${m.slice(1).toLowerCase()}`);
24481
+ if (urlLowerHex !== urlEncoded) variants.push(urlLowerHex);
24482
+ }
24483
+ } catch {
24484
+ }
24485
+ }
24486
+ return variants;
24487
+ }
24488
+ function unicodeEscapeAll(text) {
24489
+ let out = "";
24490
+ for (let index = 0; index < text.length; index++) {
24491
+ out += `\\u${text.charCodeAt(index).toString(16).padStart(4, "0")}`;
24492
+ }
24493
+ return out;
24494
+ }
24495
+ function unicodeEscapeVariants(secrets) {
24496
+ const variants = [];
24497
+ for (const secret of secrets) {
24498
+ const escaped = unicodeEscapeAll(secret);
24499
+ if (escaped !== secret && escaped.length >= 4) {
24500
+ variants.push(escaped);
24501
+ const escapedUpper = escaped.replace(/[0-9a-f]{4}/g, (m) => m.toUpperCase());
24502
+ if (escapedUpper !== escaped) variants.push(escapedUpper);
24503
+ }
24504
+ }
24505
+ return variants;
24506
+ }
24507
+ function hexSecretVariants(secrets) {
24508
+ const variants = [];
24509
+ for (const secret of secrets) {
24510
+ if (secret.length < 6) continue;
24511
+ try {
24512
+ const hex = Buffer.from(secret, "utf-8").toString("hex");
24513
+ if (hex.length >= 12) variants.push(hex);
24514
+ } catch {
24515
+ }
24516
+ }
24517
+ return variants;
24518
+ }
24519
+ var MIN_SECRET_FRAGMENT = 8;
24520
+ function redactSecretFragments(text, secrets) {
24521
+ const candidates = secrets.filter((secret) => secret.length >= MIN_SECRET_FRAGMENT);
24522
+ if (candidates.length === 0 || text.length < MIN_SECRET_FRAGMENT) return text;
24523
+ const grams = /* @__PURE__ */ new Map();
24524
+ for (const secret of candidates) {
24525
+ for (let offset = 0; offset + MIN_SECRET_FRAGMENT <= secret.length; offset++) {
24526
+ const gram = secret.slice(offset, offset + MIN_SECRET_FRAGMENT);
24527
+ const bucket = grams.get(gram);
24528
+ if (bucket) bucket.push({ secret, offset });
24529
+ else grams.set(gram, [{ secret, offset }]);
24530
+ }
24531
+ }
24532
+ let out = "";
24533
+ let index = 0;
24534
+ while (index < text.length) {
24535
+ const bucket = grams.get(text.slice(index, index + MIN_SECRET_FRAGMENT));
24536
+ let best = 0;
24537
+ if (bucket) {
24538
+ for (const { secret, offset } of bucket) {
24539
+ let length = MIN_SECRET_FRAGMENT;
24540
+ const maxLength = Math.min(secret.length - offset, text.length - index);
24541
+ while (length < maxLength && text[index + length] === secret[offset + length]) length += 1;
24542
+ if (length > best) best = length;
24543
+ }
24544
+ }
24545
+ if (best > 0) {
24546
+ out += "[redacted]";
24547
+ index += best;
24548
+ } else {
24549
+ out += text[index];
24550
+ index += 1;
24551
+ }
24552
+ }
24553
+ return out;
24554
+ }
24464
24555
  function redactLeadingSecretSuffix(text, secrets) {
24465
24556
  if (secrets.length === 0 || text.length === 0) return text;
24466
24557
  for (const secret of secrets) {
@@ -24483,13 +24574,56 @@ function redactTrailingSecretPrefix(text, secrets) {
24483
24574
  }
24484
24575
  return text;
24485
24576
  }
24577
+ function redactAllSecrets(text, secrets) {
24578
+ const encoded = encodedSecretVariants(secrets);
24579
+ const wholeVariants = [...encoded, ...hexSecretVariants(secrets), ...unicodeEscapeVariants(secrets)];
24580
+ return redactTrailingSecretPrefix(
24581
+ redactLeadingSecretSuffix(
24582
+ redactCaseInsensitiveEncodedSecrets(
24583
+ redactSecretFragments(redactSecrets(text, wholeVariants), encoded),
24584
+ secrets
24585
+ ),
24586
+ encoded
24587
+ ),
24588
+ encoded
24589
+ );
24590
+ }
24591
+ function redactCaseInsensitiveEncodedSecrets(text, secrets) {
24592
+ const escapeRegExpLiteral = (literal2) => literal2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
24593
+ let sanitized = text;
24594
+ for (const secret of secrets) {
24595
+ const patterns = [];
24596
+ try {
24597
+ const urlEncoded = encodeURIComponent(secret);
24598
+ if (urlEncoded !== secret && urlEncoded.length >= 4 && /%[0-9A-F]{2}/i.test(urlEncoded)) {
24599
+ patterns.push(urlEncoded);
24600
+ }
24601
+ } catch {
24602
+ }
24603
+ try {
24604
+ if (secret.length >= 6) {
24605
+ const hex = Buffer.from(secret, "utf-8").toString("hex");
24606
+ if (hex.length >= 12) patterns.push(hex);
24607
+ }
24608
+ } catch {
24609
+ }
24610
+ const escaped = unicodeEscapeAll(secret);
24611
+ if (escaped !== secret && escaped.length >= 4) patterns.push(escaped);
24612
+ for (const pattern of patterns) {
24613
+ sanitized = sanitized.replace(new RegExp(escapeRegExpLiteral(pattern), "gi"), "[redacted]");
24614
+ }
24615
+ }
24616
+ return sanitized;
24617
+ }
24486
24618
  function redactStderrTail(tail, secrets) {
24487
- return redactTrailingSecretPrefix(redactLeadingSecretSuffix(redactSecrets(tail, secrets), secrets), secrets);
24619
+ return redactAllSecrets(tail, secrets);
24488
24620
  }
24489
24621
  function sanitizeDiagnostic(text, secrets) {
24490
- const sanitized = redactLeadingSecretSuffix(redactSecrets(text, secrets), secrets);
24622
+ let sanitized = redactAllSecrets(text, secrets);
24491
24623
  if (sanitized.length > DIAGNOSTIC_TEXT_LIMIT) {
24492
- return `${sanitized.slice(0, DIAGNOSTIC_TEXT_LIMIT)}\u2026[truncated ${sanitized.length - DIAGNOSTIC_TEXT_LIMIT} chars]`;
24624
+ const omitted = sanitized.length - DIAGNOSTIC_TEXT_LIMIT;
24625
+ sanitized = redactAllSecrets(sanitized.slice(0, DIAGNOSTIC_TEXT_LIMIT), secrets);
24626
+ return `${sanitized}\u2026[truncated ${omitted} chars]`;
24493
24627
  }
24494
24628
  return sanitized;
24495
24629
  }
@@ -24589,6 +24723,17 @@ var McpServerManager = class {
24589
24723
  );
24590
24724
  }
24591
24725
  }
24726
+ const byLowercase = /* @__PURE__ */ new Map();
24727
+ for (const key of this.definitions.keys()) {
24728
+ const lower = key.toLowerCase();
24729
+ byLowercase.set(lower, [...byLowercase.get(lower) ?? [], key]);
24730
+ }
24731
+ const caseClashes = [...byLowercase.entries()].filter(([, keys]) => keys.length > 1).map(([, keys]) => keys.map((key) => `"${key}"`).join(" and "));
24732
+ if (caseClashes.length > 0) {
24733
+ throw new Error(
24734
+ `MCP server names collide case-insensitively (their cache shard files share one name on macOS/Windows): ${caseClashes.join("; ")}. Rename one of them.`
24735
+ );
24736
+ }
24592
24737
  this.cacheDir = cacheDir;
24593
24738
  this.log = log;
24594
24739
  }
@@ -7,9 +7,9 @@ import {
7
7
  isPlanModeSafeCommand,
8
8
  markCompletedPlanSteps,
9
9
  restoreExecutionState
10
- } from "../../../chunks/chunk-3AOMWS2Z.js";
11
- import "../../../chunks/chunk-YEUBQBOB.js";
12
- import "../../../chunks/chunk-OCEYHWVX.js";
10
+ } from "../../../chunks/chunk-H6BAM2N4.js";
11
+ import "../../../chunks/chunk-KEARZX6F.js";
12
+ import "../../../chunks/chunk-G5B6D35K.js";
13
13
  import {
14
14
  Type
15
15
  } from "../../../chunks/chunk-XXGYUYXI.js";
@@ -1,13 +1,13 @@
1
1
  import { createRequire as __createRequire } from "node:module"; const require = __createRequire(import.meta.url);
2
2
  import {
3
3
  createCliHostCapabilities
4
- } from "../../../chunks/chunk-6QEDYZUR.js";
4
+ } from "../../../chunks/chunk-OWSXALCG.js";
5
5
  import {
6
6
  createSharedFileSystemTools,
7
7
  createSubagentCapability
8
- } from "../../../chunks/chunk-3AOMWS2Z.js";
9
- import "../../../chunks/chunk-YEUBQBOB.js";
10
- import "../../../chunks/chunk-OCEYHWVX.js";
8
+ } from "../../../chunks/chunk-H6BAM2N4.js";
9
+ import "../../../chunks/chunk-KEARZX6F.js";
10
+ import "../../../chunks/chunk-G5B6D35K.js";
11
11
  import "../../../chunks/chunk-5353DEEC.js";
12
12
  import "../../../chunks/chunk-XXGYUYXI.js";
13
13
  import "../../../chunks/chunk-LZQXQOPJ.js";
@@ -8,9 +8,9 @@ import {
8
8
  executeManageTodoList,
9
9
  executionStateToTodos,
10
10
  restoreExecutionState
11
- } from "../../../chunks/chunk-3AOMWS2Z.js";
12
- import "../../../chunks/chunk-YEUBQBOB.js";
13
- import "../../../chunks/chunk-OCEYHWVX.js";
11
+ } from "../../../chunks/chunk-H6BAM2N4.js";
12
+ import "../../../chunks/chunk-KEARZX6F.js";
13
+ import "../../../chunks/chunk-G5B6D35K.js";
14
14
  import "../../../chunks/chunk-XXGYUYXI.js";
15
15
  import "../../../chunks/chunk-LZQXQOPJ.js";
16
16
 
@@ -4,10 +4,11 @@ import {
4
4
  BRIDGE_PORT_IN_USE_EXIT_CODE,
5
5
  PAGE_IDENTITY_CHANGED_ERROR,
6
6
  clearSelectedPageId,
7
+ readSelfStartTime,
7
8
  resolveSessionName,
8
9
  resolveSessionPidFile,
9
10
  resolveSessionPort
10
- } from "./chunk-UPATXALW.js";
11
+ } from "./chunk-U75F4DPF.js";
11
12
  import {
12
13
  __commonJS,
13
14
  __export,
@@ -19599,7 +19600,7 @@ async function isBridgeTargetReachable(client) {
19599
19600
  function writePidFile(port) {
19600
19601
  const pidFile = resolveSessionPidFile();
19601
19602
  mkdirSync(dirname(pidFile), { recursive: true });
19602
- writeFileSync(pidFile, JSON.stringify({ pid: process.pid, port }));
19603
+ writeFileSync(pidFile, JSON.stringify({ pid: process.pid, port, startTime: readSelfStartTime() }));
19603
19604
  }
19604
19605
  function removePidFile(pidFile = resolveSessionPidFile(), ownerPid = process.pid) {
19605
19606
  try {
@@ -21,6 +21,6 @@ function tryFastPath(argv, options) {
21
21
 
22
22
  // vendor/chrome-devtools-axi/bin/chrome-devtools-axi.ts
23
23
  if (!tryFastPath(process.argv.slice(2), { version: VERSION })) {
24
- const { main } = await import("./cli-GB37NZHQ.js");
24
+ const { main } = await import("./cli-QAILYGLM.js");
25
25
  await main(process.argv.slice(2));
26
26
  }
@@ -200,10 +200,70 @@ function resolveBridgeScript(importMetaDir) {
200
200
  var BRIDGE_PORT_IN_USE_EXIT_CODE = 48;
201
201
  var PAGE_IDENTITY_CHANGED_ERROR = "The browser reconnected and every page id changed, so this call did not target the page you selected";
202
202
 
203
+ // vendor/chrome-devtools-axi/src/process-identity.ts
204
+ import { execFileSync } from "node:child_process";
205
+ function inspectProcessIdentity(pid) {
206
+ if (!Number.isInteger(pid) || pid <= 0) return {};
207
+ if (process.platform === "win32") {
208
+ try {
209
+ const output = execFileSync(
210
+ "powershell",
211
+ [
212
+ "-NoProfile",
213
+ "-NonInteractive",
214
+ "-Command",
215
+ `$p = Get-CimInstance Win32_Process -Filter "ProcessId=<pid>"; if ($p) { $p.CommandLine; $p.CreationDate.ToString('o') }`.replace("<pid>", String(pid))
216
+ ],
217
+ { encoding: "utf-8", timeout: 4e3 }
218
+ );
219
+ const lines = output.split(/\r?\n/).filter((line) => line.length > 0);
220
+ if (lines.length === 0) return {};
221
+ const startTime = lines.length >= 2 ? lines[lines.length - 1] : void 0;
222
+ return { command: lines.slice(0, lines.length - 1).join("\n"), startTime };
223
+ } catch {
224
+ return {};
225
+ }
226
+ }
227
+ try {
228
+ const psEnv = { ...process.env, LC_ALL: "C", LANG: "C" };
229
+ const output = execFileSync(
230
+ "ps",
231
+ ["-p", String(pid), "-o", "lstart=", "-o", "command="],
232
+ { encoding: "utf-8", timeout: 1e3, env: psEnv }
233
+ );
234
+ const firstNewline = output.indexOf("\n");
235
+ const firstLine = firstNewline === -1 ? output : output.slice(0, firstNewline);
236
+ const lstart = /^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s+(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+\d{1,2}\s+\d{1,2}:\d{2}:\d{2}\s+\d{4}\s+/.exec(firstLine);
237
+ if (lstart) {
238
+ return { startTime: lstart[0].trimEnd(), command: output.slice(lstart[0].length) };
239
+ }
240
+ try {
241
+ const command = execFileSync("ps", ["-p", String(pid), "-o", "command="], {
242
+ encoding: "utf-8",
243
+ timeout: 1e3,
244
+ env: psEnv
245
+ });
246
+ return { command };
247
+ } catch {
248
+ return {};
249
+ }
250
+ } catch {
251
+ return {};
252
+ }
253
+ }
254
+ function readProcessStartTime(pid) {
255
+ return inspectProcessIdentity(pid).startTime;
256
+ }
257
+ function readSelfStartTime() {
258
+ return readProcessStartTime(process.pid);
259
+ }
260
+
203
261
  export {
204
262
  resolveBridgeScript,
205
263
  BRIDGE_PORT_IN_USE_EXIT_CODE,
206
264
  PAGE_IDENTITY_CHANGED_ERROR,
265
+ inspectProcessIdentity,
266
+ readSelfStartTime,
207
267
  resolveSessionName,
208
268
  resolveSessionPort,
209
269
  resolveSessionStateDir,
@@ -7,6 +7,7 @@ import {
7
7
  PAGE_IDENTITY_CHANGED_ERROR,
8
8
  clearSelectedPageId,
9
9
  getSelectedPageId,
10
+ inspectProcessIdentity,
10
11
  overlaySessionSelected,
11
12
  rememberToolRouting,
12
13
  resolveBridgeScript,
@@ -14,7 +15,7 @@ import {
14
15
  resolveSessionPidFile,
15
16
  resolveSessionPort,
16
17
  resolveSessionStateDir
17
- } from "./chunk-UPATXALW.js";
18
+ } from "./chunk-U75F4DPF.js";
18
19
  import "./chunk-OJ6N23UZ.js";
19
20
 
20
21
  // ../../node_modules/chrome-devtools-axi/node_modules/@toon-format/toon/dist/index.mjs
@@ -1934,33 +1935,80 @@ async function waitForProcessExit(pid, timeoutMs) {
1934
1935
  }
1935
1936
  return !isProcessAlive(pid);
1936
1937
  }
1937
- function isBridgeProcess(pid) {
1938
- if (process.platform === "win32") {
1939
- try {
1940
- const command = execFileSync(
1941
- "powershell",
1942
- [
1943
- "-NoProfile",
1944
- "-NonInteractive",
1945
- "-Command",
1946
- `(Get-CimInstance Win32_Process -Filter "ProcessId=${pid}").CommandLine`
1947
- ],
1948
- { encoding: "utf-8", timeout: 4e3 }
1949
- );
1950
- return command.includes("chrome-devtools-axi-bridge");
1951
- } catch {
1952
- return false;
1938
+ function splitCommandLine(command) {
1939
+ const tokens = [];
1940
+ let current = "";
1941
+ let quote = null;
1942
+ for (const char of command.trim()) {
1943
+ if (quote !== null) {
1944
+ if (char === quote) quote = null;
1945
+ else current += char;
1946
+ continue;
1947
+ }
1948
+ if (char === '"' || char === "'") {
1949
+ quote = char;
1950
+ continue;
1953
1951
  }
1952
+ if (/\s/.test(char)) {
1953
+ if (current.length > 0) {
1954
+ tokens.push(current);
1955
+ current = "";
1956
+ }
1957
+ continue;
1958
+ }
1959
+ current += char;
1960
+ }
1961
+ if (current.length > 0) tokens.push(current);
1962
+ return tokens;
1963
+ }
1964
+ function commandLineLooksLikeBridge(command) {
1965
+ const tokens = splitCommandLine(command);
1966
+ if (tokens.length === 0) return false;
1967
+ if (!/(?:^|[\\/])(node|npx|tsx)(?:\.(?:exe|cmd))?$/i.test(tokens[0])) return false;
1968
+ for (let index = 1; index < tokens.length; index++) {
1969
+ const token = tokens[index];
1970
+ if (token.startsWith("-")) continue;
1971
+ if (/(?:^|[\\/])tsx(?:\.(?:exe|cmd))?$/i.test(token)) continue;
1972
+ const base = token.split(/[\\/]/).pop() ?? token;
1973
+ return /^chrome-devtools-axi-bridge\.(?:js|mjs|cjs|ts)$/i.test(base);
1954
1974
  }
1955
- try {
1956
- const command = execFileSync("ps", ["-p", String(pid), "-o", "command="], {
1957
- encoding: "utf-8",
1958
- timeout: 1e3
1959
- });
1960
- return command.includes("chrome-devtools-axi-bridge");
1961
- } catch {
1962
- return false;
1975
+ return false;
1976
+ }
1977
+ function isBridgeProcess(pid, expectedStartTime) {
1978
+ const identity = inspectProcessIdentity(pid);
1979
+ if (identity.command !== void 0) {
1980
+ if (!commandLineLooksLikeBridge(identity.command)) return false;
1981
+ } else {
1982
+ if (process.platform === "win32") {
1983
+ try {
1984
+ const command = execFileSync(
1985
+ "powershell",
1986
+ [
1987
+ "-NoProfile",
1988
+ "-NonInteractive",
1989
+ "-Command",
1990
+ `(Get-CimInstance Win32_Process -Filter "ProcessId=${pid}").CommandLine`
1991
+ ],
1992
+ { encoding: "utf-8", timeout: 4e3 }
1993
+ );
1994
+ if (!commandLineLooksLikeBridge(command)) return false;
1995
+ } catch {
1996
+ return false;
1997
+ }
1998
+ } else {
1999
+ try {
2000
+ const command = execFileSync("ps", ["-p", String(pid), "-o", "command="], {
2001
+ encoding: "utf-8",
2002
+ timeout: 1e3
2003
+ });
2004
+ if (!commandLineLooksLikeBridge(command)) return false;
2005
+ } catch {
2006
+ return false;
2007
+ }
2008
+ }
1963
2009
  }
2010
+ if (expectedStartTime === void 0) return true;
2011
+ return identity.startTime !== void 0 && identity.startTime === expectedStartTime;
1964
2012
  }
1965
2013
  async function probeBridgeIdentity(port, sessionName) {
1966
2014
  try {
@@ -1983,26 +2031,29 @@ async function judgeRecordedBridge(pidInfo, sessionName) {
1983
2031
  if (probe.identity === "own") {
1984
2032
  if (probe.reportedPid !== void 0) {
1985
2033
  if (probe.reportedPid === pidInfo.pid) {
2034
+ const verified = isBridgeProcess(pidInfo.pid, pidInfo.startTime);
1986
2035
  return {
1987
- terminate: true,
1988
- killProcessGroup: isBridgeProcess(pidInfo.pid),
1989
- targetPid: pidInfo.pid
2036
+ terminate: verified,
2037
+ killProcessGroup: verified,
2038
+ targetPid: pidInfo.pid,
2039
+ expectedStartTime: pidInfo.startTime
1990
2040
  };
1991
2041
  }
2042
+ const reportedIsBridge = isBridgeProcess(probe.reportedPid);
1992
2043
  return {
1993
- terminate: true,
1994
- killProcessGroup: isBridgeProcess(probe.reportedPid),
2044
+ terminate: reportedIsBridge,
2045
+ killProcessGroup: reportedIsBridge,
1995
2046
  targetPid: probe.reportedPid
1996
2047
  };
1997
2048
  }
1998
- const bridge2 = isBridgeProcess(pidInfo.pid);
1999
- return { terminate: bridge2, killProcessGroup: bridge2, targetPid: pidInfo.pid };
2049
+ const bridge2 = isBridgeProcess(pidInfo.pid, pidInfo.startTime);
2050
+ return { terminate: bridge2, killProcessGroup: bridge2, targetPid: pidInfo.pid, expectedStartTime: pidInfo.startTime };
2000
2051
  }
2001
2052
  if (probe.identity === "other") {
2002
2053
  return { terminate: false, killProcessGroup: false };
2003
2054
  }
2004
- const bridge = isBridgeProcess(pidInfo.pid);
2005
- return { terminate: bridge, killProcessGroup: bridge, targetPid: pidInfo.pid };
2055
+ const bridge = isBridgeProcess(pidInfo.pid, pidInfo.startTime);
2056
+ return { terminate: bridge, killProcessGroup: bridge, targetPid: pidInfo.pid, expectedStartTime: pidInfo.startTime };
2006
2057
  }
2007
2058
  function removePidFile(pidFile) {
2008
2059
  try {
@@ -2024,9 +2075,13 @@ function taskkillTree(pid) {
2024
2075
  async function terminateBridgeProcess(pid, opts = {}) {
2025
2076
  if (!isProcessAlive(pid)) return true;
2026
2077
  const killProcessGroup = opts.killProcessGroup === true;
2078
+ const identityHolds = () => !opts.requireBridgeIdentity || isBridgeProcess(pid, opts.expectedStartTime);
2079
+ const refuseIfUnverified = async () => !await isProcessAlive(pid);
2027
2080
  if (process.platform === "win32") {
2081
+ if (!identityHolds()) return refuseIfUnverified();
2028
2082
  return taskkillTree(pid) && !await isProcessAlive(pid);
2029
2083
  }
2084
+ if (!identityHolds()) return refuseIfUnverified();
2030
2085
  try {
2031
2086
  process.kill(pid, "SIGTERM");
2032
2087
  } catch (error) {
@@ -2041,10 +2096,12 @@ async function terminateBridgeProcess(pid, opts = {}) {
2041
2096
  }
2042
2097
  return true;
2043
2098
  }
2099
+ if (!identityHolds()) return refuseIfUnverified();
2044
2100
  if (killProcessGroup) {
2045
2101
  try {
2046
2102
  process.kill(-pid, "SIGKILL");
2047
2103
  } catch {
2104
+ if (!identityHolds()) return refuseIfUnverified();
2048
2105
  try {
2049
2106
  process.kill(pid, "SIGKILL");
2050
2107
  } catch {
@@ -2147,7 +2204,9 @@ async function ensureBridge(spawnBridge = spawnBridgeProcess, notice) {
2147
2204
  const verdict = await judgeRecordedBridge(pidInfo, sessionName);
2148
2205
  if (verdict.terminate) {
2149
2206
  await terminateBridgeProcess(verdict.targetPid ?? pidInfo.pid, {
2150
- killProcessGroup: verdict.killProcessGroup
2207
+ killProcessGroup: verdict.killProcessGroup,
2208
+ requireBridgeIdentity: true,
2209
+ expectedStartTime: verdict.expectedStartTime
2151
2210
  });
2152
2211
  } else {
2153
2212
  removePidFile(pidFile);
@@ -2425,7 +2484,9 @@ async function stopBridge() {
2425
2484
  const verdict = await judgeRecordedBridge(pidInfo, resolveSessionName());
2426
2485
  if (!verdict.terminate) return "noop";
2427
2486
  const terminated = await terminateBridgeProcess(verdict.targetPid ?? pidInfo.pid, {
2428
- killProcessGroup: verdict.killProcessGroup
2487
+ killProcessGroup: verdict.killProcessGroup,
2488
+ requireBridgeIdentity: true,
2489
+ expectedStartTime: verdict.expectedStartTime
2429
2490
  });
2430
2491
  clearSelectedPageId();
2431
2492
  return terminated ? "stopped" : "failed";
@@ -166,9 +166,9 @@ This section is one continuous sequence — don't stop partway through. Do NOT u
166
166
 
167
167
  Put results in `<skill-name>-workspace/` as a sibling to the skill directory. Within the workspace, organize results by iteration (`iteration-1/`, `iteration-2/`, etc.) and within that, each test case gets a directory (`eval-0/`, `eval-1/`, etc.). Don't create all of this upfront — just create directories as you go.
168
168
 
169
- ### Step 1: Spawn all runs (with-skill AND baseline) in the same turn
169
+ ### Step 1: Schedule paired runs (with-skill AND baseline)
170
170
 
171
- For each test case, spawn two subagents in the same turn — one with the skill, one without. This is important: don't spawn the with-skill runs first and then come back for baselines later. Launch everything at once so it all finishes around the same time.
171
+ For each test case, prepare paired runs — one with the skill, one baseline. Read-only runs may be batched; Pie uses bounded concurrency (default 4), not unlimited simultaneous execution. Default `mode: "read"` tasks cannot write source files: ask them to return outputs and grading evidence, then have the parent persist artifacts, metadata, timing and grades. When evaluation genuinely requires file edits, use explicit `mode: "edit"` with exactly one task per call, wait for it to finish before any parent edit or next edit task, and review its changes. The workspace is shared, not isolated. Use parent-owned persistence or serialized single edit tasks for all evaluation writes; a save-path instruction does not grant a read task write permission.
172
172
 
173
173
  **With-skill run:**
174
174
 
@@ -177,7 +177,7 @@ Execute this task:
177
177
  - Skill path: <path-to-skill>
178
178
  - Task: <eval prompt>
179
179
  - Input files: <eval files if any, or "none">
180
- - Save outputs to: <workspace>/iteration-<N>/eval-<ID>/with_skill/outputs/
180
+ - Return outputs for parent persistence to: <workspace>/iteration-<N>/eval-<ID>/with_skill/outputs/ (or use a serialized single edit task)
181
181
  - Outputs to save: <what the user cares about — e.g., "the .docx file", "the final CSV">
182
182
  ```
183
183
 
@@ -206,29 +206,28 @@ Update the `eval_metadata.json` files and `evals/evals.json` with the assertions
206
206
 
207
207
  ### Step 3: As runs complete, capture timing data
208
208
 
209
- When each subagent task completes, you receive a notification containing `total_tokens` and `duration_ms`. Save this data immediately to `timing.json` in the run directory:
209
+ Pie exposes per-task timing as `task.details.durationMs` when available and aggregate wall time as result `details.totalDuration` (milliseconds). It does not supply `total_tokens` or `duration_ms` notification fields. Have the parent persist available per-run timing to `timing.json`, converting units for the viewer:
210
210
 
211
211
  ```json
212
212
  {
213
- "total_tokens": 84852,
214
213
  "duration_ms": 23332,
215
214
  "total_duration_seconds": 23.3
216
215
  }
217
216
  ```
218
217
 
219
- This is the only opportunity to capture this data — it comes through the task notification and isn't persisted elsewhere. Process each notification as it arrives rather than trying to batch them.
218
+ Here `duration_ms` is an artifact field derived from `task.details.durationMs`, not a native notification field. Keep aggregate `totalDuration` separate from per-run duration; concurrency means it is not the sum of task times. If timing is absent, mark it unavailable rather than inventing a value. Do not report token counts unless separate instrumentation actually provides them; omit `total_tokens` otherwise. Viewer/aggregator default zeros are not measured token usage.
220
219
 
221
220
  ### Step 4: Grade, aggregate, and launch the viewer
222
221
 
223
222
  Once all runs are done:
224
223
 
225
- 1. **Grade each run** — spawn a grader subagent (or grade inline) that reads `agents/grader.md` and evaluates each assertion against the outputs. Save results to `grading.json` in each run directory. The grading.json expectations array must use the fields `text`, `passed`, and `evidence` (not `name`/`met`/`details` or other variants) — the viewer depends on these exact field names. For assertions that can be checked programmatically, write and run a script rather than eyeballing it — scripts are faster, more reliable, and can be reused across iterations.
224
+ 1. **Grade each run** — spawn a grader subagent (or grade inline) that reads `agents/grader.md` and evaluates each assertion against the outputs. Have the parent save returned grades to `grading.json` in each run directory (or use a serialized single edit task); read graders must not write files. The grading.json expectations array must use the fields `text`, `passed`, and `evidence` (not `name`/`met`/`details` or other variants) — the viewer depends on these exact field names. For assertions that can be checked programmatically, write and run a script rather than eyeballing it — scripts are faster, more reliable, and can be reused across iterations.
226
225
 
227
226
  2. **Aggregate into benchmark** — run the aggregation script from the skill-creator directory:
228
227
  ```bash
229
228
  node <skill-creator-path>/scripts/aggregate_benchmark.mjs <workspace>/iteration-N --skill-name <name>
230
229
  ```
231
- This produces `benchmark.json` and `benchmark.md` with pass_rate, time, and tokens for each configuration, with mean ± stddev and the delta. If generating benchmark.json manually, see `references/schemas.md` for the exact schema the viewer expects.
230
+ This produces `benchmark.json` and `benchmark.md` with pass_rate and available timing for each configuration, with mean ± stddev and the delta. Token fields are meaningful only with separate instrumentation; label them unavailable otherwise. If generating benchmark.json manually, see `references/schemas.md` for the exact schema the viewer expects.
232
231
  Put each with_skill version before its baseline counterpart.
233
232
 
234
233
  3. **Do an analyst pass** — read the benchmark data and surface patterns the aggregate stats might hide. See `agents/analyzer.md` (the "Analyzing Benchmark Results" section) for what to look for — things like assertions that always pass regardless of skill (non-discriminating), high-variance evals (possibly flaky), and time/token tradeoffs.