@tekmidian/pai 0.42.6 → 0.43.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.
Files changed (42) hide show
  1. package/dist/{chain-BbcfP77K.mjs → chain-CEC1a5mX.mjs} +2 -2
  2. package/dist/{chain-BbcfP77K.mjs.map → chain-CEC1a5mX.mjs.map} +1 -1
  3. package/dist/cli/index.mjs +5 -5
  4. package/dist/cli/program.mjs +5 -5
  5. package/dist/daemon/index.mjs +4 -4
  6. package/dist/{daemon-BppziPi6.mjs → daemon-BhldpFvO.mjs} +6 -6
  7. package/dist/{daemon-BppziPi6.mjs.map → daemon-BhldpFvO.mjs.map} +1 -1
  8. package/dist/{daemon-BNjbVsQU.mjs → daemon-QIvCXB5f.mjs} +4 -4
  9. package/dist/daemon-mcp/index.mjs +3 -3
  10. package/dist/{fallback-Cwt4hmh5.mjs → fallback-BQJ8PbCK.mjs} +6 -4
  11. package/dist/fallback-BQJ8PbCK.mjs.map +1 -0
  12. package/dist/hooks/load-project-context.mjs +51 -48
  13. package/dist/hooks/load-project-context.mjs.map +4 -4
  14. package/dist/hooks/whisper-reinject.mjs +8 -0
  15. package/dist/hooks/whisper-reinject.mjs.map +3 -3
  16. package/dist/hooks/worker-status-line.mjs +108 -41
  17. package/dist/hooks/worker-status-line.mjs.map +3 -3
  18. package/dist/{planner-pXRc-8yt.mjs → planner-BlKGgpWI.mjs} +3 -3
  19. package/dist/{planner-pXRc-8yt.mjs.map → planner-BlKGgpWI.mjs.map} +1 -1
  20. package/dist/{program-LLNPpxlj.mjs → program-C5jRQ1kg.mjs} +749 -25
  21. package/dist/program-C5jRQ1kg.mjs.map +1 -0
  22. package/dist/{run-DDdT9teG.mjs → run-ByBKowN5.mjs} +472 -59
  23. package/dist/run-ByBKowN5.mjs.map +1 -0
  24. package/dist/{tools-BradfQk7.mjs → tools-CAG2sUjf.mjs} +2 -2
  25. package/dist/{tools-UrSHIe29.mjs → tools-DiRC5glA.mjs} +2 -2
  26. package/dist/{tools-UrSHIe29.mjs.map → tools-DiRC5glA.mjs.map} +1 -1
  27. package/dist/{wakeup-TnGjJY3I.mjs → wakeup-BfElduEU.mjs} +21 -8
  28. package/dist/wakeup-BfElduEU.mjs.map +1 -0
  29. package/docs/commands/README.md +3 -1
  30. package/docs/commands/audit.md +27 -2
  31. package/docs/commands/worker.md +2 -0
  32. package/package.json +1 -1
  33. package/scripts/build-hooks.mjs +2 -0
  34. package/src/hooks/ts/post-tool-use/whisper-reinject.test.ts +55 -0
  35. package/src/hooks/ts/post-tool-use/whisper-reinject.ts +3 -0
  36. package/src/hooks/ztk-auto.sh +28 -0
  37. package/src/hooks/ztk-auto.test.ts +72 -0
  38. package/src/hooks/ztk-passthrough.mjs +71 -0
  39. package/dist/fallback-Cwt4hmh5.mjs.map +0 -1
  40. package/dist/program-LLNPpxlj.mjs.map +0 -1
  41. package/dist/run-DDdT9teG.mjs.map +0 -1
  42. package/dist/wakeup-TnGjJY3I.mjs.map +0 -1
@@ -12,16 +12,17 @@ import { B as claudeProjectsDir, C as findLatestNote, H as scanTranscriptFolders
12
12
  import { a as expandHome, c as paiConfigFilePath, i as ensureConfigDir, l as OWNER_LABEL_PREFIX, n as CONFIG_FILE$2, o as loadConfig, s as migrateConfigFile, t as CONFIG_DIR, u as UNROUTED } from "./config-DCu8THKi.mjs";
13
13
  import { a as schedulerLogPath, i as paiSocketPath, n as daemonLogPath, r as daemonPidPath } from "./runtime-paths-B0P1TvUr.mjs";
14
14
  import { A as DEFAULT_CONTEXT_WINDOW, C as inlineWorkersYamlKeys, D as relocateWorkersYaml, E as parseWorkersYamlDocument, M as stripModelVariant, N as readJsonStrict, O as workersYamlLegacyNotice, P as writeJsonAtomic, S as initWorkersYaml, T as needsWorkersYamlRelocation, d as getProviderOrNative, f as isModelCapability, h as nativeAnthropicProvider, i as PROVIDER_TAGS, j as contextWindowFromModelId, k as workersYamlPath, m as maskKey, o as WorkersConfigError, r as MODEL_CAPABILITIES, t as ANTHROPIC_NATIVE, u as expandHome$1, v as readWorkersSection, w as migrateWorkersToYaml } from "./config-DxMZjpvY.mjs";
15
- import { D as isChatPane, F as longInlinePromptHint, I as parseRunnerArgs, M as saveStatus, N as setWorkerLabel, O as loadStatus, P as waitForTerminalStatus, R as describeMcp, S as UNLABELED, b as appendLedger, c as handoffFromInside, f as checkPaneForWorker, i as parseWorkerReport, k as loadStatuses, m as openPaneForWorker, n as runWorker, o as discardWorker, p as openFollowPane, r as testProvider, s as mergeWorker, u as sayToWorker, z as expandMcpNames } from "./run-DDdT9teG.mjs";
15
+ import { A as loadStatuses, B as expandMcpNames, C as UNLABELED, F as waitForTerminalStatus, I as longInlinePromptHint, L as parseRunnerArgs, N as saveStatus, O as isChatPane, P as setWorkerLabel, a as parseWorkerReport, c as mergeWorker, d as sayToWorker, h as openPaneForWorker, i as usageContextTokens, k as loadStatus, l as handoffFromInside, m as openFollowPane, n as runWorker, p as checkPaneForWorker, r as testProvider, s as discardWorker, x as appendLedger, z as describeMcp } from "./run-ByBKowN5.mjs";
16
16
  import { t as PaiClient } from "./ipc-client-C6NryMMb.mjs";
17
17
  import { r as humanDuration, t as createStorageBackend } from "./factory-DNYAqjH0.mjs";
18
+ import { n as migrateIdentityFile } from "./wakeup-BfElduEU.mjs";
18
19
  import { a as renderDedupedSessions, c as probeResume, d as fmtAge, f as migrateSessionScanCache, h as scanSessions, i as normalizeName$2, l as restoreTopLevel, m as scanCacheFilePath, o as hasConversation, p as resolveSessionByNameOrId, r as buildDeduped, s as launchInDir, t as cmdMain, u as printExitDir } from "./main-resolver-DvYu9jbl.mjs";
19
20
  import { n as queriesDirPath, t as migrateQueriesDir } from "./query-feedback-CYyOGi3y.mjs";
20
21
  import { s as kgQuery } from "./kg-entity-BSjM5xZh.mjs";
21
- import { _ as workersLogDir, i as buildRunEnv, m as ledgerPath, n as ensureProxyRunning, p as eventsPath, r as stopProxy, t as DEFAULT_PROXY_PORT } from "./server-Ch99xt5K.mjs";
22
+ import { _ as workersLogDir, f as ensureNoMcpConfig, i as buildRunEnv, m as ledgerPath, n as ensureProxyRunning, p as eventsPath, r as stopProxy, t as DEFAULT_PROXY_PORT } from "./server-Ch99xt5K.mjs";
22
23
  import { i as sendToSession, n as fetchLiveSessions, r as revealItermSession, t as callAiBroker } from "./aibroker-client-Bn9SnGMa.mjs";
23
- import { t as runChain } from "./chain-BbcfP77K.mjs";
24
- import { C as readSpecPrompt, S as statusLineOutput, _ as updateProvider, a as addProvider, b as psOutput, c as describeProviders, d as resolveProviderName, f as setClass, g as unsetClass, h as setWorkersEnabled, i as fallbackStatusText, l as modelPrefsText, m as setProviderModel, n as fallbackOn, o as classTargetText, p as setProviderEnabled, r as fallbackStatus, s as describeModels, t as fallbackOff, u as removeProvider, v as useProvider, w as resolveSpecPath, x as replayOutput, y as followWorkers } from "./fallback-Cwt4hmh5.mjs";
24
+ import { t as runChain } from "./chain-CEC1a5mX.mjs";
25
+ import { C as readSpecPrompt, S as statusLineOutput, _ as updateProvider, a as addProvider, b as psOutput, c as describeProviders, d as resolveProviderName, f as setClass, g as unsetClass, h as setWorkersEnabled, i as fallbackStatusText, l as modelPrefsText, m as setProviderModel, n as fallbackOn, o as classTargetText, p as setProviderEnabled, r as fallbackStatus, s as describeModels, t as fallbackOff, u as removeProvider, v as useProvider, w as resolveSpecPath, x as replayOutput, y as followWorkers } from "./fallback-BQJ8PbCK.mjs";
25
26
  import { appendFileSync, chmodSync, copyFileSync, cpSync, createReadStream, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, realpathSync, renameSync, statSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
26
27
  import { homedir, platform, tmpdir } from "node:os";
27
28
  import { basename, dirname, join, relative, resolve, sep } from "node:path";
@@ -5905,7 +5906,7 @@ function cmdLogs(opts) {
5905
5906
  }
5906
5907
  function registerDaemonCommands(daemonCmd) {
5907
5908
  daemonCmd.command("serve").description("Start the PAI daemon in the foreground").action(async () => {
5908
- const { serve } = await import("./daemon-BNjbVsQU.mjs");
5909
+ const { serve } = await import("./daemon-QIvCXB5f.mjs");
5909
5910
  const { loadConfig: lc, ensureConfigDir } = await import("./config-DeqFqa1N.mjs");
5910
5911
  ensureConfigDir();
5911
5912
  await serve(lc());
@@ -10159,32 +10160,124 @@ function readWorkerStatus(logDir, id) {
10159
10160
  return null;
10160
10161
  }
10161
10162
  }
10162
- async function readOne(path) {
10163
- const report = await parseSessionUsage(path);
10163
+ /** Token count of the first `type:"user"` message's text, or null if none found. */
10164
+ async function firstUserPromptTokens(path) {
10165
+ const rl = createInterface({
10166
+ input: createReadStream(path, "utf8"),
10167
+ crlfDelay: Infinity
10168
+ });
10169
+ try {
10170
+ for await (const raw of rl) {
10171
+ if (!raw.trim()) continue;
10172
+ let obj;
10173
+ try {
10174
+ obj = JSON.parse(raw);
10175
+ } catch {
10176
+ continue;
10177
+ }
10178
+ if (obj.type !== "user") continue;
10179
+ const content = obj.message?.content;
10180
+ return countTokens(typeof content === "string" ? content : Array.isArray(content) ? content.map((part) => part?.text ?? "").join(" ") : "");
10181
+ }
10182
+ } finally {
10183
+ rl.close();
10184
+ }
10185
+ return null;
10186
+ }
10187
+ /** `session_id` off the first record that carries one, or null if none do. */
10188
+ async function firstSessionId(path) {
10189
+ const rl = createInterface({
10190
+ input: createReadStream(path, "utf8"),
10191
+ crlfDelay: Infinity
10192
+ });
10193
+ try {
10194
+ for await (const raw of rl) {
10195
+ if (!raw.trim()) continue;
10196
+ let obj;
10197
+ try {
10198
+ obj = JSON.parse(raw);
10199
+ } catch {
10200
+ continue;
10201
+ }
10202
+ if (obj.session_id) return obj.session_id;
10203
+ }
10204
+ } finally {
10205
+ rl.close();
10206
+ }
10207
+ return null;
10208
+ }
10209
+ /** `~/.claude/projects/**\/<sessionId>.jsonl`, searched only under the projects dir. */
10210
+ function findSessionTranscript(sessionId, projectsDir = join(homedir(), ".claude", "projects")) {
10211
+ const target = `${sessionId}.jsonl`;
10212
+ const stack = [projectsDir];
10213
+ while (stack.length > 0) {
10214
+ const dir = stack.pop();
10215
+ let entries;
10216
+ try {
10217
+ entries = readdirSync(dir, { withFileTypes: true });
10218
+ } catch {
10219
+ continue;
10220
+ }
10221
+ for (const entry of entries) {
10222
+ const full = join(dir, entry.name);
10223
+ if (entry.isDirectory()) stack.push(full);
10224
+ else if (entry.name === target) return full;
10225
+ }
10226
+ }
10227
+ return null;
10228
+ }
10229
+ /**
10230
+ * The mirror log has no `type:"user"` record (worker event mirrors carry
10231
+ * system/assistant/result records only, never the -p prompt itself), so
10232
+ * fall back to the session's own transcript under ~/.claude/projects,
10233
+ * located via whichever record in the mirror log carries a `session_id`.
10234
+ */
10235
+ async function resolvePromptTokens(mirrorPath) {
10236
+ const direct = await firstUserPromptTokens(mirrorPath);
10237
+ if (direct !== null) return direct;
10238
+ const sessionId = await firstSessionId(mirrorPath);
10239
+ if (!sessionId) return null;
10240
+ const transcript = findSessionTranscript(sessionId);
10241
+ if (!transcript) return null;
10242
+ return firstUserPromptTokens(transcript);
10243
+ }
10244
+ async function readOne(path, group) {
10245
+ const [report, promptTokens] = await Promise.all([parseSessionUsage(path), resolvePromptTokens(path)]);
10164
10246
  const model = Object.keys(report.models)[0] ?? null;
10247
+ const overhead = report.firstTurnContext !== null && promptTokens !== null ? report.firstTurnContext - promptTokens : null;
10165
10248
  return {
10166
10249
  path,
10167
10250
  firstTurnContext: report.firstTurnContext,
10168
- model
10251
+ model,
10252
+ promptTokens,
10253
+ overhead,
10254
+ group
10169
10255
  };
10170
10256
  }
10257
+ function median(values) {
10258
+ if (values.length === 0) return null;
10259
+ const sorted = [...values].sort((a, b) => a - b);
10260
+ const mid = Math.floor(sorted.length / 2);
10261
+ return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
10262
+ }
10171
10263
  function summarize(readings) {
10172
10264
  const contexts = readings.map((r) => r.firstTurnContext).filter((v) => v !== null).sort((a, b) => a - b);
10265
+ const overheads = readings.map((r) => r.overhead).filter((v) => v !== null);
10173
10266
  const models = [...new Set(readings.map((r) => r.model).filter((m) => !!m))];
10174
10267
  if (contexts.length === 0) return {
10175
10268
  count: readings.length,
10176
10269
  min: null,
10177
10270
  median: null,
10178
10271
  max: null,
10272
+ medianOverhead: median(overheads),
10179
10273
  models
10180
10274
  };
10181
- const mid = Math.floor(contexts.length / 2);
10182
- const median = contexts.length % 2 ? contexts[mid] : (contexts[mid - 1] + contexts[mid]) / 2;
10183
10275
  return {
10184
10276
  count: readings.length,
10185
10277
  min: contexts[0],
10186
- median,
10278
+ median: median(contexts),
10187
10279
  max: contexts[contexts.length - 1],
10280
+ medianOverhead: median(overheads),
10188
10281
  models
10189
10282
  };
10190
10283
  }
@@ -10192,17 +10285,25 @@ async function auditSpawn(n = 10) {
10192
10285
  const { workers } = readWorkersSection();
10193
10286
  const logDir = workersLogDir(workers);
10194
10287
  const agentLogs = newestAgentSubagentLogs(n);
10195
- const agentReadings = await Promise.all(agentLogs.map(readOne));
10288
+ const agentReadings = await Promise.all(agentLogs.map((path) => readOne(path, "agentSubagents")));
10196
10289
  const workerLogs = newestWorkerLogs(logDir, n);
10197
- const workerReadings = await Promise.all(workerLogs.map(readOne));
10290
+ const workerReadings = await Promise.all(workerLogs.map((path) => readOne(path, "workersInteractive")));
10198
10291
  const interactive = [];
10199
10292
  const headless = [];
10200
- for (const reading of workerReadings) if (readWorkerStatus(logDir, workerIdFromLogPath(reading.path))?.outputFormat) headless.push(reading);
10293
+ for (const reading of workerReadings) if (readWorkerStatus(logDir, workerIdFromLogPath(reading.path))?.outputFormat) headless.push({
10294
+ ...reading,
10295
+ group: "workersHeadless"
10296
+ });
10201
10297
  else interactive.push(reading);
10202
10298
  return {
10203
10299
  agentSubagents: summarize(agentReadings),
10204
10300
  workersInteractive: summarize(interactive),
10205
- workersHeadless: summarize(headless)
10301
+ workersHeadless: summarize(headless),
10302
+ readings: [
10303
+ ...agentReadings,
10304
+ ...interactive,
10305
+ ...headless
10306
+ ]
10206
10307
  };
10207
10308
  }
10208
10309
 
@@ -10332,6 +10433,45 @@ function extract(pattern, text) {
10332
10433
  const match = text.match(pattern);
10333
10434
  return match ? match[1] : null;
10334
10435
  }
10436
+ /**
10437
+ * `--tools A,B` and `--tools=A,B` are both valid on the argv this process was
10438
+ * launched with; an explicit but empty value (`--tools ""` / `--tools=`)
10439
+ * means "no tools at all", distinct from the flag being absent entirely.
10440
+ */
10441
+ function extractToolsArg(line) {
10442
+ const eq = line.match(/--tools=(?:"([^"]*)"|(\S*))/);
10443
+ if (eq) return eq[1] !== void 0 ? eq[1] : eq[2];
10444
+ const sp = line.match(/--tools\s+(?:"([^"]*)"|(\S+))/);
10445
+ if (sp) return sp[1] !== void 0 ? sp[1] : sp[2];
10446
+ return null;
10447
+ }
10448
+ function extractMcpConfigArg(line) {
10449
+ const eq = line.match(/--mcp-config=(?:"([^"]*)"|(\S*))/);
10450
+ if (eq) return eq[1] !== void 0 ? eq[1] : eq[2];
10451
+ const sp = line.match(/--mcp-config\s+(?:"([^"]*)"|(\S+))/);
10452
+ if (sp) return sp[1] !== void 0 ? sp[1] : sp[2];
10453
+ return null;
10454
+ }
10455
+ /**
10456
+ * No `--tools` flag: the CLI default tool set applies, which includes
10457
+ * ToolSearch, so deferral is on. An explicit but empty list means no tools
10458
+ * at all, so deferral doesn't apply. Otherwise deferral is on only if the
10459
+ * explicit list still names ToolSearch.
10460
+ */
10461
+ function classifyDeferral(toolsArg) {
10462
+ if (toolsArg === null) return "on";
10463
+ if (toolsArg.trim() === "") return "n/a";
10464
+ return toolsArg.split(",").map((t) => t.trim()).includes("ToolSearch") ? "on" : "OFF";
10465
+ }
10466
+ function countMcpServersInFile(path) {
10467
+ try {
10468
+ const resolved = path.startsWith("~") ? join(homedir(), path.slice(1)) : path;
10469
+ const parsed = JSON.parse(readFileSync(resolved, "utf8"));
10470
+ return Object.keys(parsed.mcpServers ?? {}).length;
10471
+ } catch {
10472
+ return 0;
10473
+ }
10474
+ }
10335
10475
  function readProcessEnvLine(pid) {
10336
10476
  try {
10337
10477
  return execFileSync("ps", [
@@ -10366,6 +10506,8 @@ function auditEnv() {
10366
10506
  const modelPattern = /ANTHROPIC_DEFAULT_(\w+?)_MODEL=(\S*)/g;
10367
10507
  let modelMatch;
10368
10508
  while (modelMatch = modelPattern.exec(line)) defaultModels[modelMatch[1]] = modelMatch[2];
10509
+ const toolsArg = extractToolsArg(line);
10510
+ const mcpConfigPath = extractMcpConfigArg(line);
10369
10511
  processes.push({
10370
10512
  pid,
10371
10513
  baseUrl: extract(/ANTHROPIC_BASE_URL=(\S*)/, line),
@@ -10373,7 +10515,10 @@ function auditEnv() {
10373
10515
  defaultModels,
10374
10516
  enableToolSearch: extract(/ENABLE_TOOL_SEARCH=(\S*)/, line),
10375
10517
  workerId: extract(/PAI_WORKER_ID=(\S*)/, line),
10376
- age: readProcessAge(pid)
10518
+ age: readProcessAge(pid),
10519
+ toolsArg,
10520
+ mcpServers: mcpConfigPath ? countMcpServersInFile(mcpConfigPath) : "default",
10521
+ deferral: classifyDeferral(toolsArg)
10377
10522
  });
10378
10523
  }
10379
10524
  let mcpServerCount = 0;
@@ -10467,6 +10612,264 @@ function auditSchedule(ttlSeconds = DEFAULT_TTL_SECONDS) {
10467
10612
  };
10468
10613
  }
10469
10614
 
10615
+ //#endregion
10616
+ //#region src/audit/skills.ts
10617
+ /**
10618
+ * `pai audit tokens skills` — token cost of the whole skill/command/plugin
10619
+ * catalogue: every `SKILL.md` under ~/.claude/skills and ~/.claude/plugins,
10620
+ * plus every command under ~/.claude/commands, rendered as the one-line
10621
+ * `- name: description` entry Claude Code loads into context for each.
10622
+ *
10623
+ * Two failure modes matter more than the raw total: the same real file
10624
+ * reachable through two paths (a plugin's marketplaces/ checkout and its
10625
+ * cache/ install both point at one file), and two different real files that
10626
+ * collide on name (a skill and a command both called "Sessions") — the first
10627
+ * wastes nothing but must not be double-counted, the second silently shadows
10628
+ * one of the two in whichever catalogue order Claude Code loads them.
10629
+ */
10630
+ /** Recursively collect every file under `dir` whose basename satisfies `matches`. */
10631
+ function findFilesRecursive(dir, matches) {
10632
+ let entries;
10633
+ try {
10634
+ entries = readdirSync(dir, { withFileTypes: true });
10635
+ } catch {
10636
+ return [];
10637
+ }
10638
+ const found = [];
10639
+ for (const entry of entries) {
10640
+ const full = join(dir, entry.name);
10641
+ if (entry.isDirectory()) found.push(...findFilesRecursive(full, matches));
10642
+ else if (entry.isFile() && matches(entry.name)) found.push(full);
10643
+ }
10644
+ return found;
10645
+ }
10646
+ /**
10647
+ * `<skillsDir>/*\/SKILL.md`, one level deep only — this is what Claude Code
10648
+ * actually loads. `readdirSync(withFileTypes)` reports a symlinked skill
10649
+ * directory as neither file nor directory, so entries are resolved with
10650
+ * `statSync` (which follows symlinks) instead of trusting `Dirent`.
10651
+ */
10652
+ function findTopLevelSkillFiles(skillsDir) {
10653
+ let entries;
10654
+ try {
10655
+ entries = readdirSync(skillsDir);
10656
+ } catch {
10657
+ return [];
10658
+ }
10659
+ const found = [];
10660
+ for (const name of entries) {
10661
+ const dir = join(skillsDir, name);
10662
+ let isDir = false;
10663
+ try {
10664
+ isDir = statSync(dir).isDirectory();
10665
+ } catch {
10666
+ continue;
10667
+ }
10668
+ if (!isDir) continue;
10669
+ const skillFile = join(dir, "SKILL.md");
10670
+ if (existsSync(skillFile)) found.push(skillFile);
10671
+ }
10672
+ return found;
10673
+ }
10674
+ /** Every SKILL.md/*.md the three catalogue roots contribute, source-tagged. */
10675
+ function findCatalogueFiles(claudeDir) {
10676
+ const found = [];
10677
+ for (const path of findTopLevelSkillFiles(join(claudeDir, "skills"))) found.push({
10678
+ path,
10679
+ source: "skills"
10680
+ });
10681
+ for (const path of findFilesRecursive(join(claudeDir, "commands"), (n) => n.endsWith(".md"))) found.push({
10682
+ path,
10683
+ source: "commands"
10684
+ });
10685
+ for (const path of findFilesRecursive(join(claudeDir, "plugins"), (n) => n === "SKILL.md")) found.push({
10686
+ path,
10687
+ source: "plugins"
10688
+ });
10689
+ return found;
10690
+ }
10691
+ /** Strip a single layer of matching quotes ("..." or '...') from a trimmed value. */
10692
+ function unquote(value) {
10693
+ if (value.length >= 2) {
10694
+ const first = value[0];
10695
+ const last = value[value.length - 1];
10696
+ if (first === "\"" && last === "\"" || first === "'" && last === "'") return value.slice(1, -1);
10697
+ }
10698
+ return value;
10699
+ }
10700
+ /** Parse the `name:`/`description:` frontmatter fields out of a skill/command file. */
10701
+ function parseFrontmatter(text) {
10702
+ const match = /^---\n([\s\S]*?)\n---/.exec(text);
10703
+ const fm = match ? match[1] : "";
10704
+ const nameMatch = /^name:\s*(.+)$/m.exec(fm);
10705
+ const descMatch = /(?:^|\n)description:\s*(.+?)(?=\n\w[\w-]*:|$)/s.exec(fm);
10706
+ return {
10707
+ name: nameMatch ? unquote(nameMatch[1].trim()) : null,
10708
+ description: descMatch ? unquote(descMatch[1].trim()) : ""
10709
+ };
10710
+ }
10711
+ function nameFallback(path, source) {
10712
+ if (source === "commands") return basename(path).replace(/\.md$/, "");
10713
+ return basename(dirname(path));
10714
+ }
10715
+ /** Build one catalogue entry from a file, deriving its name/description/tokens. */
10716
+ function readEntry(path, source) {
10717
+ let text = "";
10718
+ try {
10719
+ text = readFileSync(path, "utf8");
10720
+ } catch {}
10721
+ const fm = parseFrontmatter(text);
10722
+ const name = fm.name ?? nameFallback(path, source);
10723
+ const line = `- ${name}: ${fm.description}`;
10724
+ return {
10725
+ name,
10726
+ description: fm.description,
10727
+ source,
10728
+ path,
10729
+ tokens: countTokens(line),
10730
+ enabled: true
10731
+ };
10732
+ }
10733
+ /** `enabledPlugins` map from settings.json, or `{}` if the file is missing/unreadable. */
10734
+ function readEnabledPlugins(settingsPath) {
10735
+ try {
10736
+ return JSON.parse(readFileSync(settingsPath, "utf8")).enabledPlugins ?? {};
10737
+ } catch {
10738
+ return {};
10739
+ }
10740
+ }
10741
+ /** `<relative-to-root>` split on the platform separator, or null if `path` is not under `root`. */
10742
+ function pathUnder(root, path) {
10743
+ const rel = relative(root, path);
10744
+ if (rel.startsWith("..")) return null;
10745
+ return rel.split(sep);
10746
+ }
10747
+ /**
10748
+ * A cache install is `plugins/cache/<marketplace>/<plugin>/<version>/...`, so its id is exact.
10749
+ * A marketplace checkout is `plugins/marketplaces/<marketplace>/...` with no plugin segment, so
10750
+ * it can only be matched by marketplace name against whichever `enabledPlugins` key ends in
10751
+ * `@<marketplace>` — good enough for the single-plugin-per-marketplace installs this audits today.
10752
+ */
10753
+ function resolvePluginStatus(claudeDir, path, enabledPlugins) {
10754
+ const cacheParts = pathUnder(join(claudeDir, "plugins", "cache"), path);
10755
+ if (cacheParts && cacheParts.length >= 4) {
10756
+ const [marketplace, plugin, , ...suffix] = cacheParts;
10757
+ const id = `${plugin}@${marketplace}`;
10758
+ const known = id in enabledPlugins;
10759
+ return {
10760
+ id,
10761
+ enabled: known && enabledPlugins[id] === true,
10762
+ status: known ? void 0 : "unknown",
10763
+ dedupeKey: `${id}::${suffix.join("/")}`
10764
+ };
10765
+ }
10766
+ const marketplaceParts = pathUnder(join(claudeDir, "plugins", "marketplaces"), path);
10767
+ if (marketplaceParts && marketplaceParts.length >= 2) {
10768
+ const [marketplace, ...suffix] = marketplaceParts;
10769
+ const matchKey = Object.keys(enabledPlugins).find((k) => k.endsWith(`@${marketplace}`));
10770
+ if (matchKey) return {
10771
+ id: matchKey,
10772
+ enabled: enabledPlugins[matchKey] === true,
10773
+ dedupeKey: `${matchKey}::${suffix.join("/")}`
10774
+ };
10775
+ return {
10776
+ id: `unknown@${marketplace}`,
10777
+ enabled: false,
10778
+ status: "unknown",
10779
+ dedupeKey: `unknown@${marketplace}::${suffix.join("/")}`
10780
+ };
10781
+ }
10782
+ return {
10783
+ id: path,
10784
+ enabled: false,
10785
+ status: "unknown",
10786
+ dedupeKey: path
10787
+ };
10788
+ }
10789
+ /** Group entries by case-insensitive name; only groups with >1 real path. */
10790
+ function findDuplicates(entries) {
10791
+ const byName = /* @__PURE__ */ new Map();
10792
+ for (const entry of entries) {
10793
+ const key = entry.name.toLowerCase();
10794
+ if (!byName.has(key)) byName.set(key, /* @__PURE__ */ new Set());
10795
+ byName.get(key).add(entry.path);
10796
+ }
10797
+ const duplicates = [];
10798
+ for (const [name, paths] of byName) if (paths.size > 1) duplicates.push({
10799
+ name,
10800
+ paths: [...paths]
10801
+ });
10802
+ return duplicates.sort((a, b) => a.name.localeCompare(b.name));
10803
+ }
10804
+ function auditSkills(claudeDir = join(homedir(), ".claude"), settingsPath = join(claudeDir, "settings.json")) {
10805
+ const rawFiles = findCatalogueFiles(claudeDir);
10806
+ const seenReal = /* @__PURE__ */ new Set();
10807
+ const deduped = [];
10808
+ for (const file of rawFiles) {
10809
+ let real = file.path;
10810
+ try {
10811
+ real = realpathSync(file.path);
10812
+ } catch {}
10813
+ if (seenReal.has(real)) continue;
10814
+ seenReal.add(real);
10815
+ deduped.push(file);
10816
+ }
10817
+ const enabledPlugins = readEnabledPlugins(settingsPath);
10818
+ const entries = deduped.map((f) => readEntry(f.path, f.source));
10819
+ const dedupeKeys = /* @__PURE__ */ new Map();
10820
+ for (const entry of entries) {
10821
+ if (entry.source !== "plugins") continue;
10822
+ const status = resolvePluginStatus(claudeDir, entry.path, enabledPlugins);
10823
+ entry.enabled = status.enabled;
10824
+ entry.status = status.status;
10825
+ dedupeKeys.set(entry.path, status.dedupeKey);
10826
+ }
10827
+ const totalsBySource = {
10828
+ skills: 0,
10829
+ commands: 0,
10830
+ plugins: 0
10831
+ };
10832
+ const countsBySource = {
10833
+ skills: 0,
10834
+ commands: 0,
10835
+ plugins: 0
10836
+ };
10837
+ const enabledTotalsBySource = {
10838
+ skills: 0,
10839
+ commands: 0,
10840
+ plugins: 0
10841
+ };
10842
+ const seenEnabledPluginKeys = /* @__PURE__ */ new Set();
10843
+ for (const entry of entries) {
10844
+ totalsBySource[entry.source] += entry.tokens;
10845
+ countsBySource[entry.source] += 1;
10846
+ if (!entry.enabled) continue;
10847
+ if (entry.source !== "plugins") {
10848
+ enabledTotalsBySource[entry.source] += entry.tokens;
10849
+ continue;
10850
+ }
10851
+ const dedupeKey = dedupeKeys.get(entry.path);
10852
+ if (seenEnabledPluginKeys.has(dedupeKey)) continue;
10853
+ seenEnabledPluginKeys.add(dedupeKey);
10854
+ enabledTotalsBySource.plugins += entry.tokens;
10855
+ }
10856
+ return {
10857
+ encoding: TOKEN_ENCODING,
10858
+ entries,
10859
+ total: entries.reduce((sum, e) => sum + e.tokens, 0),
10860
+ totalsBySource,
10861
+ countsBySource,
10862
+ enabledTotal: enabledTotalsBySource.skills + enabledTotalsBySource.commands + enabledTotalsBySource.plugins,
10863
+ enabledTotalsBySource,
10864
+ duplicates: findDuplicates(entries)
10865
+ };
10866
+ }
10867
+ function topByTokens(report, n) {
10868
+ return [...report.entries].sort((a, b) => b.tokens - a.tokens).slice(0, n);
10869
+ }
10870
+ const SKILL_CATALOGUE_AMBER = 3e3;
10871
+ const SKILL_CATALOGUE_RED = 6e3;
10872
+
10470
10873
  //#endregion
10471
10874
  //#region src/audit/severity.ts
10472
10875
  const HOOK_TOKEN_LIMIT = 1e3;
@@ -10528,9 +10931,197 @@ function buildFindings(input) {
10528
10931
  severity: "GREEN",
10529
10932
  evidence: `${input.env.processes.length} claude process(es), none proxied`
10530
10933
  });
10934
+ const deferralOff = input.env.processes.filter((p) => {
10935
+ const mcpCount = p.mcpServers === "default" ? input.env.mcpServerCount : p.mcpServers;
10936
+ return p.deferral === "OFF" && mcpCount > 0;
10937
+ });
10938
+ findings.push({
10939
+ finding: "tool deferral on live processes",
10940
+ severity: deferralOff.length > 0 ? "RED" : "GREEN",
10941
+ evidence: deferralOff.length > 0 ? deferralOff.map((p) => `pid ${p.pid}: ${p.mcpServers === "default" ? input.env.mcpServerCount : p.mcpServers} mcp servers, tools=${p.toolsArg}`).join("; ") : `${input.env.processes.length} claude process(es) checked, none with an explicit --tools list missing ToolSearch and MCP servers registered`
10942
+ });
10943
+ const skillTokens = input.skills.enabledTotal;
10944
+ findings.push({
10945
+ finding: "skill catalogue tokens",
10946
+ severity: skillTokens > SKILL_CATALOGUE_RED ? "RED" : skillTokens > SKILL_CATALOGUE_AMBER ? "AMBER" : "GREEN",
10947
+ evidence: `${skillTokens} tokens loaded (${input.skills.total} on disk) across ${input.skills.entries.length} entries` + (input.skills.duplicates.length ? `, ${input.skills.duplicates.length} case-insensitive duplicate name(s)` : "")
10948
+ });
10531
10949
  return sortFindings(findings);
10532
10950
  }
10533
10951
 
10952
+ //#endregion
10953
+ //#region src/audit/ladder.ts
10954
+ /**
10955
+ * `pai audit tokens ladder` — LIVE: what each layer of headless-worker
10956
+ * scaffolding actually costs in first-turn context, measured by spawning the
10957
+ * real `claude -p` binary at increasing configuration and reading the
10958
+ * `usage` block of its `--output-format json` result. No section above this
10959
+ * one makes a network call; this is the one exception, gated behind `--live`.
10960
+ *
10961
+ * Rungs (each a superset of the last, except L3 which drops every flag):
10962
+ * L0 empty MCP config, no tool schemas at all
10963
+ * L1 same, plus the core file/shell tool schemas (`--tools`)
10964
+ * L2 same as L1, but the empty MCP config is swapped for a real one
10965
+ * L3 plain: none of --strict-mcp-config / --mcp-config / --tools
10966
+ */
10967
+ const PROBE_PROMPT = "Reply with exactly the single word OK and nothing else. Do not use any tool.";
10968
+ const LADDER_TOOLS = "Bash,Read,Edit,Write,Grep,Glob,Skill,ToolSearch";
10969
+ const LADDER_TIMEOUT_MS = 18e4;
10970
+ const DEFAULT_LADDER_MODEL = "haiku";
10971
+ /** Build the four rungs' argv. Pure — no spawning, so it's unit-testable. */
10972
+ function buildLadderRungs(model, emptyMcpPath, realMcpPath) {
10973
+ const base = [
10974
+ "-p",
10975
+ PROBE_PROMPT,
10976
+ "--model",
10977
+ model,
10978
+ "--output-format",
10979
+ "json"
10980
+ ];
10981
+ return [
10982
+ {
10983
+ id: "L0",
10984
+ description: "empty MCP config, no tools",
10985
+ args: [
10986
+ ...base,
10987
+ "--strict-mcp-config",
10988
+ "--mcp-config",
10989
+ emptyMcpPath,
10990
+ "--tools",
10991
+ ""
10992
+ ]
10993
+ },
10994
+ {
10995
+ id: "L1",
10996
+ description: `empty MCP config, --tools ${LADDER_TOOLS}`,
10997
+ args: [
10998
+ ...base,
10999
+ "--strict-mcp-config",
11000
+ "--mcp-config",
11001
+ emptyMcpPath,
11002
+ "--tools",
11003
+ LADDER_TOOLS
11004
+ ]
11005
+ },
11006
+ {
11007
+ id: "L2",
11008
+ description: `real MCP config, --tools ${LADDER_TOOLS}`,
11009
+ args: [
11010
+ ...base,
11011
+ "--strict-mcp-config",
11012
+ "--mcp-config",
11013
+ realMcpPath,
11014
+ "--tools",
11015
+ LADDER_TOOLS
11016
+ ]
11017
+ },
11018
+ {
11019
+ id: "L3",
11020
+ description: "plain (no --strict-mcp-config, no --mcp-config, no --tools)",
11021
+ args: [...base]
11022
+ }
11023
+ ];
11024
+ }
11025
+ /** Newest `*.mcp.json` in a worker log dir, or null if none exist. */
11026
+ function newestMcpConfig(logDir) {
11027
+ let files;
11028
+ try {
11029
+ files = readdirSync(logDir);
11030
+ } catch {
11031
+ return null;
11032
+ }
11033
+ const candidates = files.filter((f) => f.endsWith(".mcp.json")).map((f) => join(logDir, f));
11034
+ if (candidates.length === 0) return null;
11035
+ return candidates.map((path) => ({
11036
+ path,
11037
+ mtime: statSync(path).mtimeMs
11038
+ })).sort((a, b) => b.mtime - a.mtime)[0].path;
11039
+ }
11040
+ /** Spawn one rung of `claude -p ...` and read its usage-derived first-turn context. */
11041
+ function runRung(rung, cwd) {
11042
+ return new Promise((resolvePromise) => {
11043
+ const env = { ...process.env };
11044
+ delete env.PAI_WORKER;
11045
+ const proc = spawn("claude", rung.args, {
11046
+ cwd,
11047
+ env,
11048
+ stdio: [
11049
+ "ignore",
11050
+ "pipe",
11051
+ "pipe"
11052
+ ]
11053
+ });
11054
+ let out = "";
11055
+ let timedOut = false;
11056
+ const timer = setTimeout(() => {
11057
+ timedOut = true;
11058
+ try {
11059
+ proc.kill("SIGKILL");
11060
+ } catch {}
11061
+ }, LADDER_TIMEOUT_MS);
11062
+ proc.stdout.on("data", (chunk) => {
11063
+ out += chunk.toString("utf8");
11064
+ });
11065
+ proc.on("error", (e) => {
11066
+ clearTimeout(timer);
11067
+ resolvePromise({
11068
+ firstTurnContext: null,
11069
+ timedOut: false,
11070
+ error: e.message
11071
+ });
11072
+ });
11073
+ proc.on("close", () => {
11074
+ clearTimeout(timer);
11075
+ if (timedOut) {
11076
+ resolvePromise({
11077
+ firstTurnContext: null,
11078
+ timedOut: true
11079
+ });
11080
+ return;
11081
+ }
11082
+ try {
11083
+ resolvePromise({
11084
+ firstTurnContext: usageContextTokens(JSON.parse(out.trim()).usage),
11085
+ timedOut: false
11086
+ });
11087
+ } catch (e) {
11088
+ resolvePromise({
11089
+ firstTurnContext: null,
11090
+ timedOut: false,
11091
+ error: e.message
11092
+ });
11093
+ }
11094
+ });
11095
+ });
11096
+ }
11097
+ async function auditLadder(opts) {
11098
+ const model = opts.model ?? DEFAULT_LADDER_MODEL;
11099
+ const emptyMcpPath = ensureNoMcpConfig(opts.logDir);
11100
+ const realMcpPath = opts.mcpConfigPath ?? newestMcpConfig(opts.logDir) ?? emptyMcpPath;
11101
+ const cwd = opts.cwd ?? process.cwd();
11102
+ const rungs = buildLadderRungs(model, emptyMcpPath, realMcpPath);
11103
+ const readings = [];
11104
+ let previous = null;
11105
+ for (const rung of rungs) {
11106
+ const result = await runRung(rung, cwd);
11107
+ const delta = result.firstTurnContext !== null && previous !== null ? result.firstTurnContext - previous : null;
11108
+ readings.push({
11109
+ id: rung.id,
11110
+ description: rung.description,
11111
+ firstTurnContext: result.firstTurnContext,
11112
+ timedOut: result.timedOut,
11113
+ error: result.error,
11114
+ delta
11115
+ });
11116
+ if (result.firstTurnContext !== null) previous = result.firstTurnContext;
11117
+ }
11118
+ return {
11119
+ model,
11120
+ mcpConfigPath: realMcpPath,
11121
+ readings
11122
+ };
11123
+ }
11124
+
10534
11125
  //#endregion
10535
11126
  //#region src/cli/commands/audit.ts
10536
11127
  function severityColor(sev, text) {
@@ -10617,10 +11208,35 @@ function printSpawnGroup(name, stats) {
10617
11208
  stats.min === null ? "n/a" : String(stats.min),
10618
11209
  stats.median === null ? "n/a" : String(stats.median),
10619
11210
  stats.max === null ? "n/a" : String(stats.max),
11211
+ stats.medianOverhead === null ? "n/a" : String(stats.medianOverhead),
10620
11212
  stats.models.join(", ")
10621
11213
  ];
10622
11214
  }
10623
- function printSpawn(report) {
11215
+ const SPAWN_GROUP_LABELS = {
11216
+ agentSubagents: "Agent-tool subagents",
11217
+ workersInteractive: "pai workers (interactive/pane)",
11218
+ workersHeadless: "pai workers (headless -p)"
11219
+ };
11220
+ function printSpawn(report, detail) {
11221
+ if (detail) {
11222
+ console.log(header("Spawn overhead — per-log detail"));
11223
+ const rows = report.readings.map((r) => [
11224
+ SPAWN_GROUP_LABELS[r.group] ?? r.group,
11225
+ r.firstTurnContext === null ? "n/a" : String(r.firstTurnContext),
11226
+ r.promptTokens === null ? "n/a" : String(r.promptTokens),
11227
+ r.overhead === null ? "n/a" : String(r.overhead),
11228
+ r.model ?? "",
11229
+ shortenPath(r.path, 70)
11230
+ ]);
11231
+ console.log(renderTable([
11232
+ "group",
11233
+ "first-turn ctx",
11234
+ "prompt tok",
11235
+ "overhead",
11236
+ "model",
11237
+ "path"
11238
+ ], rows));
11239
+ }
10624
11240
  console.log(header("Spawn overhead (first-turn context tokens)"));
10625
11241
  const rows = [
10626
11242
  printSpawnGroup("Agent-tool subagents", report.agentSubagents),
@@ -10633,13 +11249,14 @@ function printSpawn(report) {
10633
11249
  "min",
10634
11250
  "median",
10635
11251
  "max",
11252
+ "median overhead",
10636
11253
  "models"
10637
11254
  ], rows));
10638
11255
  }
10639
11256
  async function cmdSpawn(opts) {
10640
11257
  const report = await auditSpawn(opts.n ? parseInt(opts.n, 10) : 10);
10641
11258
  if (opts.json) return printJson(report);
10642
- printSpawn(report);
11259
+ printSpawn(report, opts.detail);
10643
11260
  }
10644
11261
  function printDaemon(report) {
10645
11262
  console.log(header(`Daemon log: ${report.logPath}`));
@@ -10679,6 +11296,8 @@ function printEnv(report) {
10679
11296
  p.authTokenPresent ? "present" : "absent",
10680
11297
  JSON.stringify(p.defaultModels),
10681
11298
  p.enableToolSearch ?? "",
11299
+ String(p.mcpServers),
11300
+ p.deferral === "OFF" ? err(p.deferral) : p.deferral === "n/a" ? dim$1(p.deferral) : p.deferral,
10682
11301
  p.workerId ?? "",
10683
11302
  p.age ?? ""
10684
11303
  ]);
@@ -10688,10 +11307,16 @@ function printEnv(report) {
10688
11307
  "auth token",
10689
11308
  "default models",
10690
11309
  "tool search",
11310
+ "tools",
11311
+ "deferral",
10691
11312
  "worker id",
10692
11313
  "age"
10693
11314
  ], rows));
10694
- for (const p of report.processes) if (p.baseUrl) console.log(err(` RED: claude process ${p.pid} is routed through ${p.baseUrl}`));
11315
+ for (const p of report.processes) {
11316
+ if (p.baseUrl) console.log(err(` RED: claude process ${p.pid} is routed through ${p.baseUrl}`));
11317
+ const mcpCount = p.mcpServers === "default" ? report.mcpServerCount : p.mcpServers;
11318
+ if (p.deferral === "OFF" && mcpCount > 0) console.log(err(` RED: claude process ${p.pid} has --tools without ToolSearch (${mcpCount} MCP servers registered)`));
11319
+ }
10695
11320
  console.log(dim$1(`MCP servers registered: ${report.mcpServerCount}`));
10696
11321
  console.log(dim$1(`settings.json model: ${report.settingsModel ?? "unset"}, effortLevel: ${report.settingsEffortLevel ?? "unset"}`));
10697
11322
  }
@@ -10729,6 +11354,86 @@ async function cmdSchedule(opts) {
10729
11354
  if (opts.json) return printJson(report);
10730
11355
  printSchedule(report);
10731
11356
  }
11357
+ function printSkills(report, top) {
11358
+ console.log(header(`Skill/command/plugin catalogue (encoding: ${report.encoding})`));
11359
+ console.log(dim$1(`${report.entries.length} entries, ${report.enabledTotal} tokens loaded (${report.total} on disk)`));
11360
+ const subtotalRows = [
11361
+ "skills",
11362
+ "commands",
11363
+ "plugins"
11364
+ ].map((source) => [
11365
+ source,
11366
+ String(report.countsBySource[source]),
11367
+ String(report.totalsBySource[source]),
11368
+ String(report.enabledTotalsBySource[source])
11369
+ ]);
11370
+ console.log(renderTable([
11371
+ "source",
11372
+ "entries",
11373
+ "tokens",
11374
+ "enabled"
11375
+ ], subtotalRows));
11376
+ console.log(header(`Top ${top} by tokens`));
11377
+ const topRows = topByTokens(report, top).map((e) => [
11378
+ String(e.tokens),
11379
+ e.source,
11380
+ e.enabled ? "yes" : "no",
11381
+ e.name,
11382
+ e.path
11383
+ ]);
11384
+ console.log(renderTable([
11385
+ "tokens",
11386
+ "source",
11387
+ "on",
11388
+ "name",
11389
+ "path"
11390
+ ], topRows));
11391
+ console.log(header("Case-insensitive duplicate names"));
11392
+ if (report.duplicates.length === 0) console.log(dim$1("none"));
11393
+ else for (const dup of report.duplicates) {
11394
+ console.log(`${bold$1(dup.name)}:`);
11395
+ for (const path of dup.paths) console.log(` ${path}`);
11396
+ }
11397
+ }
11398
+ function cmdSkills(opts) {
11399
+ const top = opts.top ? parseInt(opts.top, 10) : 15;
11400
+ const report = auditSkills();
11401
+ if (opts.json) return printJson({
11402
+ ...report,
11403
+ top: topByTokens(report, top)
11404
+ });
11405
+ printSkills(report, top);
11406
+ }
11407
+ function printLadder(report) {
11408
+ console.log(header(`Context ladder (model: ${report.model}, mcp config: ${report.mcpConfigPath})`));
11409
+ const rows = report.readings.map((r) => [
11410
+ r.id,
11411
+ r.description,
11412
+ r.timedOut ? "TIMEOUT" : r.firstTurnContext === null ? `n/a${r.error ? ` (${r.error})` : ""}` : String(r.firstTurnContext),
11413
+ r.delta === null ? "n/a" : String(r.delta)
11414
+ ]);
11415
+ console.log(renderTable([
11416
+ "rung",
11417
+ "description",
11418
+ "first-turn tokens",
11419
+ "delta"
11420
+ ], rows));
11421
+ }
11422
+ async function cmdLadder(opts) {
11423
+ if (!opts.live) {
11424
+ console.log(warn$1("Refusing to run: `pai audit tokens ladder` spawns several real `claude -p` calls (billed API traffic). Pass --live to run it."));
11425
+ return;
11426
+ }
11427
+ const { workers } = readWorkersSection();
11428
+ const logDir = workersLogDir(workers);
11429
+ const report = await auditLadder({
11430
+ model: opts.model,
11431
+ mcpConfigPath: opts.mcpConfig,
11432
+ logDir
11433
+ });
11434
+ if (opts.json) return printJson(report);
11435
+ printLadder(report);
11436
+ }
10732
11437
  async function cmdCombined(opts) {
10733
11438
  const cwd = process.cwd();
10734
11439
  const files = auditFiles(defaultFileSet(cwd));
@@ -10738,12 +11443,14 @@ async function cmdCombined(opts) {
10738
11443
  const env = auditEnv();
10739
11444
  const spawn = await auditSpawn();
10740
11445
  const schedule = auditSchedule(cacheTtlSeconds(session?.cacheCreationSplit));
11446
+ const skills = auditSkills();
10741
11447
  const findings = buildFindings({
10742
11448
  files,
10743
11449
  hooks,
10744
11450
  session,
10745
11451
  daemon,
10746
- env
11452
+ env,
11453
+ skills
10747
11454
  });
10748
11455
  if (opts.json) return printJson({
10749
11456
  encoding: TOKEN_ENCODING,
@@ -10754,7 +11461,8 @@ async function cmdCombined(opts) {
10754
11461
  spawn,
10755
11462
  daemon,
10756
11463
  env,
10757
- schedule
11464
+ schedule,
11465
+ skills
10758
11466
  });
10759
11467
  console.log(header("pai audit tokens — combined report"));
10760
11468
  console.log(dim$1(`token encoding: ${TOKEN_ENCODING}`));
@@ -10778,7 +11486,7 @@ async function cmdCombined(opts) {
10778
11486
  * `--json` on `pai audit tokens files` silently produced the table output.
10779
11487
  */
10780
11488
  function registerAuditCommands(auditCmd) {
10781
- const tokensCmd = auditCmd.command("tokens").description("Token-waste audit: memory files, hooks, session usage, spawn overhead, daemon, env, schedule").option("--json", "Print JSON instead of a table").action(async () => {
11489
+ const tokensCmd = auditCmd.command("tokens").description("Token-waste audit: memory files, hooks, session usage, spawn overhead, daemon, env, schedule, skill catalogue").option("--json", "Print JSON instead of a table").action(async () => {
10782
11490
  await cmdCombined(jsonOpt());
10783
11491
  });
10784
11492
  const jsonOpt = () => tokensCmd.opts();
@@ -10789,7 +11497,7 @@ function registerAuditCommands(auditCmd) {
10789
11497
  tokensCmd.command("session").description("Cache/input/output token split for a session transcript (default: newest)").argument("[path]", "Session JSONL path (default: newest under ~/.claude/projects)").action(async (path) => {
10790
11498
  await cmdSession(path, jsonOpt());
10791
11499
  });
10792
- tokensCmd.command("spawn").description("Spawn-overhead comparison: Agent-tool subagents vs. pai workers").option("-n, --n <count>", "How many of the newest logs per group to read (default 10)").action(async (opts) => {
11500
+ tokensCmd.command("spawn").description("Spawn-overhead comparison: Agent-tool subagents vs. pai workers").option("-n, --n <count>", "How many of the newest logs per group to read (default 10)").option("--detail", "Print one row per log (first-turn ctx, prompt tokens, overhead, model, path)").action(async (opts) => {
10793
11501
  await cmdSpawn({
10794
11502
  ...jsonOpt(),
10795
11503
  ...opts
@@ -10800,6 +11508,18 @@ function registerAuditCommands(auditCmd) {
10800
11508
  tokensCmd.command("schedule").description("Launchd agents / crontab entries that outpace the measured prompt-cache TTL").action(async () => {
10801
11509
  await cmdSchedule(jsonOpt());
10802
11510
  });
11511
+ tokensCmd.command("skills").description("Token cost of the SKILL.md / command / plugin catalogue, top entries, duplicate names").option("--top <n>", "How many top-by-tokens entries to show (default 15)").action((opts) => {
11512
+ cmdSkills({
11513
+ ...jsonOpt(),
11514
+ ...opts
11515
+ });
11516
+ });
11517
+ tokensCmd.command("ladder").description("LIVE: first-turn context at increasing headless-worker configuration (spawns real claude -p calls)").option("--live", "Actually spawn claude -p (billed API traffic); refuses without this flag").option("--model <model>", "Model to probe with (default haiku)").option("--mcp-config <path>", "MCP config for the L2 rung (default: newest *.mcp.json in the workers log dir)").action(async (opts) => {
11518
+ await cmdLadder({
11519
+ ...jsonOpt(),
11520
+ ...opts
11521
+ });
11522
+ });
10803
11523
  }
10804
11524
 
10805
11525
  //#endregion
@@ -16005,9 +16725,11 @@ function fail(e) {
16005
16725
  process.exitCode = 1;
16006
16726
  }
16007
16727
  function registerWorkerCommands(workerCmd) {
16008
- workerCmd.command("run").description("Run one claude-code worker through the configured provider.\nUnknown options are passed to claude verbatim (e.g. -p, --allowedTools);\n--output-format/--verbose are handled here.\n--label \"<goal>\" is optional — it is the row shown in ps / follow /\nthe status line; when absent it is derived from the prompt's first line\n(--chain/--agent derive their own instead).\n--spec <file> (or --spec -) reads the prompt from a file/stdin instead of\nan inline -p '<prompt>', which breaks on shell quoting; mutually\nexclusive with -p.\nGrant MCP tools by naming mcp__server__tool in --allowedTools (the server loads automatically);\n--chain draft,implement[,review] runs a spec-first pipeline;\n--agent <name> runs an agent definition from ~/.claude/agents.").allowUnknownOption(true).option("--provider <name>", "Provider to run on (default: active, else routing order)").option("--class <name>", "Use the provider of this class (draft, implement, review, research, spotcheck, simple, complex, image)").option("--role <name>", "Alias of --class (roles were renamed to classes)").option("--chain <stages>", "Comma-separated stage classes, e.g. draft,implement or draft,implement,review").option("--agent <name>", "Run the agent definition ~/.claude/agents/<name>.md on a worker").option("--model <model>", "Override the model for this run. Headless (-p) workers default to the --class model; an interactive launch (no -p) with no --model uses the harness default model from settings.json.").option("--label <text>", "Short task label shown in ps / follow / status line (default: first line of the prompt)").option("--spec <path>", "Read the prompt from this file (or - for stdin) instead of -p; mutually exclusive with -p").option("--cwd <dir>", "Directory the worker runs in (default: this process's cwd)").option("--mcp <names>", "MCP servers/sets this worker may use (comma-separated; see `pai worker mcp`)").option("--no-pane", "Do not open a follow pane for this worker").option("--worktree", "Run in a git worktree on branch worker/<id> (default for implement/complex/plan in a git repo)").option("--no-worktree", "Run in place, no worktree").option("--print-cmd", "Print the assembled claude argv as JSON and exit, without spawning (audit tool)").argument("[args...]", "claude arguments, e.g. -p '<task>' --allowedTools 'Read,Edit,Bash'").action(async (args, opts) => {
16728
+ workerCmd.command("run").description("Run one claude-code worker through the configured provider.\nUnknown options are passed to claude verbatim (e.g. -p, --allowedTools);\n--output-format/--verbose are handled here.\n--label \"<goal>\" is optional — it is the row shown in ps / follow /\nthe status line; when absent it is derived from the prompt's first line\n(--chain/--agent derive their own instead).\n--spec <file> (or --spec -) reads the prompt from a file/stdin instead of\nan inline -p '<prompt>', which breaks on shell quoting; mutually\nexclusive with -p.\nGrant MCP tools by naming mcp__server__tool in --allowedTools (the server loads automatically);\n--chain draft,implement[,review] runs a spec-first pipeline;\n--agent <name> runs an agent definition from ~/.claude/agents.").allowUnknownOption(true).option("--provider <name>", "Provider to run on (default: active, else routing order)").option("--class <name>", "Use the provider of this class (draft, implement, review, research, spotcheck, simple, complex, image)").option("--role <name>", "Alias of --class (roles were renamed to classes)").option("--chain <stages>", "Comma-separated stage classes, e.g. draft,implement or draft,implement,review").option("--agent <name>", "Run the agent definition ~/.claude/agents/<name>.md on a worker").option("--model <model>", "Override the model for this run. Headless (-p) workers default to the --class model; an interactive launch (no -p) with no --model uses the harness default model from settings.json.").option("--label <text>", "Short task label shown in ps / follow / status line (default: first line of the prompt)").option("--spec <path>", "Read the prompt from this file (or - for stdin) instead of -p; mutually exclusive with -p").option("--cwd <dir>", "Directory the worker runs in (default: this process's cwd)").option("--mcp <names>", "MCP servers/sets this worker may use (comma-separated; see `pai worker mcp`)").option("--no-pane", "Do not open a follow pane for this worker").option("--worktree", "Run in a git worktree on branch worker/<id> (default for implement/complex/plan in a git repo)").option("--no-worktree", "Run in place, no worktree").option("--print-cmd", "Print the assembled claude argv as JSON and exit, without spawning (audit tool)").option("--report <format>", "Final-report contract/parser: json or ag2 (default: ag2, or PAI_WORKER_REPORT)").option("--no-report-retry", "Skip the one bounded re-ask when the final AG2 message fails validation").argument("[args...]", "claude arguments, e.g. -p '<task>' --allowedTools 'Read,Edit,Bash'").action(async (args, opts) => {
16009
16729
  try {
16010
16730
  const className = opts.class ?? opts.role;
16731
+ if (opts.report !== void 0 && opts.report !== "json" && opts.report !== "ag2") throw new Error(`--report: expected "json" or "ag2", got "${opts.report}"`);
16732
+ const reportFormatFlag = opts.report;
16011
16733
  if (opts.cwd !== void 0) {
16012
16734
  if (!existsSync(opts.cwd)) throw new Error(`--cwd: directory does not exist: ${opts.cwd}`);
16013
16735
  if (!statSync(opts.cwd).isDirectory()) throw new Error(`--cwd: not a directory: ${opts.cwd}`);
@@ -16072,6 +16794,8 @@ function registerWorkerCommands(workerCmd) {
16072
16794
  noPane: opts.pane === false,
16073
16795
  worktreeFlag: opts.worktree,
16074
16796
  printCmd: opts.printCmd,
16797
+ reportFormatFlag,
16798
+ noReportRetry: opts.reportRetry === false,
16075
16799
  claudeArgs
16076
16800
  });
16077
16801
  process.exitCode = rc;
@@ -18630,4 +19354,4 @@ claude() {
18630
19354
 
18631
19355
  //#endregion
18632
19356
  export { drainStdio as n, buildProgram as t };
18633
- //# sourceMappingURL=program-LLNPpxlj.mjs.map
19357
+ //# sourceMappingURL=program-C5jRQ1kg.mjs.map