pitroom 0.6.3 → 0.6.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/pitroom.mjs CHANGED
@@ -7,6 +7,12 @@ var UserError = class extends Error {
7
7
  this.code = code;
8
8
  }
9
9
  };
10
+ var DeletionRefused = class extends UserError {
11
+ constructor(message, files) {
12
+ super(message, 3);
13
+ this.files = files;
14
+ }
15
+ };
10
16
 
11
17
  // src/backends/claude/index.ts
12
18
  import { spawnSync } from "node:child_process";
@@ -55,7 +61,7 @@ function resolveCommand(bin) {
55
61
  var EDIT_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
56
62
  var DENIED = /permission|not allowed|denied|blocked|disallowed/i;
57
63
  function parseEvents(jsonl) {
58
- const usage = {
64
+ const usage2 = {
59
65
  input: 0,
60
66
  output: 0,
61
67
  reasoning: 0,
@@ -100,7 +106,7 @@ function parseEvents(jsonl) {
100
106
  lastText = String(c.text).trim();
101
107
  lastActivity = `says: ${oneLine(lastText)}`;
102
108
  } else if (c.type === "tool_use") {
103
- usage.toolCalls++;
109
+ usage2.toolCalls++;
104
110
  tools[c.name] = (tools[c.name] ?? 0) + 1;
105
111
  const target = c.input?.file_path ?? c.input?.path ?? c.input?.pattern ?? c.input?.command ?? c.input?.url;
106
112
  lastActivity = `${c.name} ${target ? oneLine(String(target), 60) : ""}`.trim();
@@ -112,21 +118,21 @@ function parseEvents(jsonl) {
112
118
  if (c?.type !== "tool_result") continue;
113
119
  const text = typeof c.content === "string" ? c.content : JSON.stringify(c.content ?? "");
114
120
  if (c.is_error) {
115
- if (DENIED.test(text)) usage.denied++;
121
+ if (DENIED.test(text)) usage2.denied++;
116
122
  } else if (pendingEdits.has(c.tool_use_id)) {
117
123
  edits.push(pendingEdits.get(c.tool_use_id));
118
124
  }
119
125
  }
120
126
  } else if (e.type === "result") {
121
127
  const u = e.usage ?? {};
122
- usage.input += num(u.input_tokens);
123
- usage.cacheRead += num(u.cache_read_input_tokens);
124
- usage.cacheWrite += num(u.cache_creation_input_tokens);
125
- usage.reasoning += num(u.output_tokens_details?.thinking_tokens);
126
- usage.output += Math.max(0, num(u.output_tokens) - num(u.output_tokens_details?.thinking_tokens));
127
- usage.total += num(u.input_tokens) + num(u.cache_read_input_tokens) + num(u.cache_creation_input_tokens) + num(u.output_tokens);
128
- usage.cost = (usage.cost ?? 0) + num(e.total_cost_usd);
129
- usage.denied = Math.max(usage.denied, Array.isArray(e.permission_denials) ? e.permission_denials.length : 0);
128
+ usage2.input += num(u.input_tokens);
129
+ usage2.cacheRead += num(u.cache_read_input_tokens);
130
+ usage2.cacheWrite += num(u.cache_creation_input_tokens);
131
+ usage2.reasoning += num(u.output_tokens_details?.thinking_tokens);
132
+ usage2.output += Math.max(0, num(u.output_tokens) - num(u.output_tokens_details?.thinking_tokens));
133
+ usage2.total += num(u.input_tokens) + num(u.cache_read_input_tokens) + num(u.cache_creation_input_tokens) + num(u.output_tokens);
134
+ usage2.cost = (usage2.cost ?? 0) + num(e.total_cost_usd);
135
+ usage2.denied = Math.max(usage2.denied, Array.isArray(e.permission_denials) ? e.permission_denials.length : 0);
130
136
  if (e.is_error) {
131
137
  const tags = [e.terminal_reason, e.api_error_status && `HTTP ${e.api_error_status}`].filter(Boolean);
132
138
  error ??= `${String(e.result ?? e.subtype ?? "Claude Code error")}${tags.length ? ` [${tags.join(", ")}]` : ""}`;
@@ -135,8 +141,8 @@ function parseEvents(jsonl) {
135
141
  }
136
142
  }
137
143
  }
138
- usage.steps = steps.size;
139
- return { sessionId, model, finalText: finalText ?? (error ? "" : lastText), usage, tools, edits, lastActivity, error };
144
+ usage2.steps = steps.size;
145
+ return { sessionId, model, finalText: finalText ?? (error ? "" : lastText), usage: usage2, tools, edits, lastActivity, error };
140
146
  }
141
147
  function num(v) {
142
148
  return typeof v === "number" && Number.isFinite(v) ? v : 0;
@@ -225,7 +231,9 @@ function invocation(req) {
225
231
  "stream-json",
226
232
  "--verbose"
227
233
  );
228
- if (req.model) args.push("--model", req.model);
234
+ const [model, effort] = (req.model ?? "").split("#");
235
+ if (model) args.push("--model", model);
236
+ if (effort) args.push("--effort", effort);
229
237
  if (req.sessionId) args.push("--resume", req.sessionId);
230
238
  args.push("--", req.prompt);
231
239
  const { command, prefix } = resolveCommand(binary());
@@ -278,11 +286,19 @@ function doctor({ models, hasFallback }) {
278
286
  if (!hasFallback) checks.push({ level: "warn", message: "no fallback workers configured for Claude Code runs" });
279
287
  return checks;
280
288
  }
289
+ var CLAUDE_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
290
+ function catalog() {
291
+ const def = defaultModel();
292
+ const models = ["haiku", "sonnet", "opus"].map((id) => ({ id, efforts: CLAUDE_EFFORTS }));
293
+ if (def && !models.some((m) => m.id === def)) models.push({ id: def, efforts: CLAUDE_EFFORTS });
294
+ return { models, source: "Claude Code aliases (latest of each size) and --effort levels from `claude --help`" };
295
+ }
281
296
  var claude = {
282
297
  id: "claude",
283
298
  name: "Claude Code",
284
299
  capabilities: { readOnly: "tool-allowlist", resume: "by-id", reportsCost: true, attachFiles: false },
285
300
  binary,
301
+ catalog,
286
302
  invocation,
287
303
  parse: parseEvents,
288
304
  failure,
@@ -300,7 +316,7 @@ import path3 from "node:path";
300
316
  var WORK = /* @__PURE__ */ new Set(["agent_message", "reasoning", "command_execution", "file_change", "mcp_tool_call", "web_search", "todo_list"]);
301
317
  var TOOLS = /* @__PURE__ */ new Set(["command_execution", "file_change", "mcp_tool_call", "web_search"]);
302
318
  function parseEvents2(jsonl) {
303
- const usage = {
319
+ const usage2 = {
304
320
  input: 0,
305
321
  output: 0,
306
322
  reasoning: 0,
@@ -332,21 +348,21 @@ function parseEvents2(jsonl) {
332
348
  case "item.completed": {
333
349
  const it = e.item ?? {};
334
350
  if (!WORK.has(it.type)) break;
335
- usage.steps++;
351
+ usage2.steps++;
336
352
  if (TOOLS.has(it.type)) {
337
- usage.toolCalls++;
353
+ usage2.toolCalls++;
338
354
  tools[it.type] = (tools[it.type] ?? 0) + 1;
339
355
  }
340
356
  if (it.type === "agent_message" && String(it.text ?? "").trim()) {
341
357
  finalText = String(it.text).trim();
342
358
  lastActivity = `says: ${oneLine2(finalText)}`;
343
359
  } else if (it.type === "command_execution") {
344
- if (it.status === "declined" || /operation not permitted|sandbox/i.test(String(it.aggregated_output ?? ""))) usage.denied++;
360
+ if (it.status === "declined" || /operation not permitted|sandbox/i.test(String(it.aggregated_output ?? ""))) usage2.denied++;
345
361
  lastActivity = `shell ${oneLine2(String(it.command ?? ""), 60)}`;
346
362
  } else if (it.type === "file_change") {
347
363
  const paths = (it.changes ?? []).map((c) => String(c.path));
348
364
  if (it.status !== "failed" && it.status !== "declined") edits.push(...paths);
349
- else usage.denied++;
365
+ else usage2.denied++;
350
366
  lastActivity = `edit ${oneLine2(paths.join(", "), 60)}`;
351
367
  }
352
368
  break;
@@ -355,12 +371,12 @@ function parseEvents2(jsonl) {
355
371
  const u = e.usage ?? {};
356
372
  const cached2 = num2(u.cached_input_tokens);
357
373
  const reasoning = num2(u.reasoning_output_tokens);
358
- usage.input += Math.max(0, num2(u.input_tokens) - cached2);
359
- usage.cacheRead += cached2;
360
- usage.cacheWrite += num2(u.cache_write_input_tokens);
361
- usage.output += Math.max(0, num2(u.output_tokens) - reasoning);
362
- usage.reasoning += reasoning;
363
- usage.total += num2(u.input_tokens) + num2(u.output_tokens);
374
+ usage2.input += Math.max(0, num2(u.input_tokens) - cached2);
375
+ usage2.cacheRead += cached2;
376
+ usage2.cacheWrite += num2(u.cache_write_input_tokens);
377
+ usage2.output += Math.max(0, num2(u.output_tokens) - reasoning);
378
+ usage2.reasoning += reasoning;
379
+ usage2.total += num2(u.input_tokens) + num2(u.output_tokens);
364
380
  break;
365
381
  }
366
382
  case "error":
@@ -371,7 +387,7 @@ function parseEvents2(jsonl) {
371
387
  break;
372
388
  }
373
389
  }
374
- return { sessionId, finalText, usage, tools, edits, lastActivity, error };
390
+ return { sessionId, finalText, usage: usage2, tools, edits, lastActivity, error };
375
391
  }
376
392
  function describe(raw) {
377
393
  const s = String(raw ?? "unknown Codex error");
@@ -476,17 +492,35 @@ function doctor2({ models, hasFallback }) {
476
492
  login.ok && /logged in/i.test(login.out) ? { level: "ok", message: `Codex: ${login.out.trim().split("\n")[0]}` } : { level: "fail", message: "Codex is not logged in: run `codex login`" }
477
493
  );
478
494
  for (const m of models) {
495
+ const pinned = m?.replace(/#.*$/, "");
479
496
  checks.push({
480
- level: "ok",
481
- message: m ? `Codex model: ${m} (checked on first use; unsupported models fail over)` : "Codex model: Codex's built-in default (your ~/.codex/config.toml is ignored for workers; pick one with -W codex:<model>)"
497
+ level: pinned ? "ok" : "warn",
498
+ message: pinned ? `Codex model: ${pinned} (checked on first use; unsupported models fail over)` : 'Codex model: none pinned, so Codex runs its own default (it can change and cost more); set "models": {"codex": "<model>"} or use -W codex:<model>'
482
499
  });
483
500
  }
484
501
  if (!hasFallback) checks.push({ level: "warn", message: "no fallback workers configured for Codex runs" });
485
502
  return checks;
486
503
  }
504
+ function catalog2() {
505
+ const file = path3.join(codexHome(), "models_cache.json");
506
+ try {
507
+ const raw = JSON.parse(fs3.readFileSync(file, "utf8"));
508
+ const models = (raw.models ?? []).filter((m) => m.visibility !== "hide" && typeof m.slug === "string").map((m) => ({
509
+ id: m.slug,
510
+ efforts: (m.supported_reasoning_levels ?? []).map(
511
+ (l) => typeof l === "string" ? l : l.effort ?? ""
512
+ ).filter(Boolean),
513
+ defaultEffort: typeof m.default_reasoning_level === "string" ? m.default_reasoning_level : void 0
514
+ }));
515
+ return { models, source: `Codex model cache${raw.fetched_at ? `, fetched ${raw.fetched_at.slice(0, 10)}` : ""}` };
516
+ } catch {
517
+ return { models: [], source: "no Codex model cache found (run codex once)" };
518
+ }
519
+ }
487
520
  var codex = {
488
521
  id: "codex",
489
522
  name: "Codex",
523
+ catalog: catalog2,
490
524
  capabilities: { readOnly: "os-sandbox", resume: "by-id", reportsCost: false, attachFiles: false },
491
525
  binary: binary2,
492
526
  invocation: invocation2,
@@ -503,7 +537,7 @@ import { spawnSync as spawnSync3 } from "node:child_process";
503
537
  var EDIT_TOOLS2 = /* @__PURE__ */ new Set(["edit", "write", "patch", "multiedit", "apply_patch"]);
504
538
  var DENIED2 = /rule which prevents you|permission denied|permission\.rejected/i;
505
539
  function parseEvents3(ndjson) {
506
- const usage = {
540
+ const usage2 = {
507
541
  input: 0,
508
542
  output: 0,
509
543
  reasoning: 0,
@@ -544,9 +578,9 @@ function parseEvents3(ndjson) {
544
578
  case "tool_use": {
545
579
  const name = String(p.tool ?? "tool");
546
580
  const st = p.state ?? {};
547
- usage.toolCalls++;
581
+ usage2.toolCalls++;
548
582
  tools[name] = (tools[name] ?? 0) + 1;
549
- if (st.status === "error" && DENIED2.test(String(st.error ?? ""))) usage.denied++;
583
+ if (st.status === "error" && DENIED2.test(String(st.error ?? ""))) usage2.denied++;
550
584
  if (st.status === "completed" && EDIT_TOOLS2.has(name)) {
551
585
  edits.push(String(st.input?.filePath ?? st.input?.path ?? name));
552
586
  }
@@ -555,14 +589,14 @@ function parseEvents3(ndjson) {
555
589
  }
556
590
  case "step_finish": {
557
591
  const t = p.tokens ?? {};
558
- usage.steps++;
559
- usage.input += num3(t.input);
560
- usage.output += num3(t.output);
561
- usage.reasoning += num3(t.reasoning);
562
- usage.cacheRead += num3(t.cache?.read);
563
- usage.cacheWrite += num3(t.cache?.write);
564
- usage.total += num3(t.total) || num3(t.input) + num3(t.output) + num3(t.reasoning) + num3(t.cache?.read);
565
- usage.cost = (usage.cost ?? 0) + num3(p.cost);
592
+ usage2.steps++;
593
+ usage2.input += num3(t.input);
594
+ usage2.output += num3(t.output);
595
+ usage2.reasoning += num3(t.reasoning);
596
+ usage2.cacheRead += num3(t.cache?.read);
597
+ usage2.cacheWrite += num3(t.cache?.write);
598
+ usage2.total += num3(t.total) || num3(t.input) + num3(t.output) + num3(t.reasoning) + num3(t.cache?.read);
599
+ usage2.cost = (usage2.cost ?? 0) + num3(p.cost);
566
600
  break;
567
601
  }
568
602
  case "error": {
@@ -573,7 +607,7 @@ function parseEvents3(ndjson) {
573
607
  }
574
608
  const groups = [...textByMessage.values()];
575
609
  const finalText = (groups[groups.length - 1] ?? []).join("\n").trim();
576
- return { sessionId, finalText, usage, tools, edits, lastActivity, error };
610
+ return { sessionId, finalText, usage: usage2, tools, edits, lastActivity, error };
577
611
  }
578
612
  function describeError(err) {
579
613
  if (!err || typeof err !== "object") return "unknown OpenCode error";
@@ -778,15 +812,15 @@ function configContent(existing, allowWeb = false) {
778
812
  }
779
813
  }
780
814
  };
781
- let base = {};
815
+ let base2 = {};
782
816
  if (existing?.trim()) {
783
817
  try {
784
- base = JSON.parse(existing);
818
+ base2 = JSON.parse(existing);
785
819
  } catch {
786
- base = {};
820
+ base2 = {};
787
821
  }
788
822
  }
789
- return JSON.stringify(deepMerge(base, ours));
823
+ return JSON.stringify(deepMerge(base2, ours));
790
824
  }
791
825
  function deepMerge(a, b) {
792
826
  if (!isObject(a) || !isObject(b)) return b;
@@ -891,6 +925,13 @@ function resolveModel2(sessionId) {
891
925
  return void 0;
892
926
  }
893
927
  }
928
+ function catalog3() {
929
+ const ids = listModels();
930
+ return {
931
+ models: (ids.length ? ids : listModels()).map((id) => ({ id })),
932
+ source: "`opencode models` (reasoning variants are provider-specific: provider/model#variant)"
933
+ };
934
+ }
894
935
  function listModels() {
895
936
  return oc(["models"]).out.split("\n").map((s) => s.trim()).filter(Boolean);
896
937
  }
@@ -940,6 +981,7 @@ var opencode = {
940
981
  defaultModel: defaultModel2,
941
982
  resolveModel: resolveModel2,
942
983
  listModels,
984
+ catalog: catalog3,
943
985
  doctor: doctor3
944
986
  };
945
987
 
@@ -948,6 +990,7 @@ var REGISTRY = new Map([opencode, codex, claude].map((b) => [b.id, b]));
948
990
  var DEFAULT_BACKEND = opencode.id;
949
991
  var PLANNED = ["gemini"];
950
992
  var backendIds = () => [...REGISTRY.keys()];
993
+ var allBackends = () => [...REGISTRY.values()];
951
994
  var isBackendId = (id) => REGISTRY.has(id) || PLANNED.includes(id);
952
995
  function getBackend(id) {
953
996
  const backend = REGISTRY.get(id);
@@ -976,12 +1019,13 @@ var SCHEMA = {
976
1019
  web: "boolean",
977
1020
  maxParallel: "number",
978
1021
  models: "record",
979
- tiers: "record"
1022
+ tiers: "record",
1023
+ costs: "numbers"
980
1024
  };
981
1025
  function configPath() {
982
1026
  if (process.env.PITROOM_CONFIG) return path5.resolve(process.env.PITROOM_CONFIG);
983
- const base = process.platform === "win32" ? process.env.APPDATA ?? path5.join(os5.homedir(), "AppData", "Roaming") : process.env.XDG_CONFIG_HOME ?? path5.join(os5.homedir(), ".config");
984
- return path5.join(base, "pitroom", "config.json");
1027
+ const base2 = process.platform === "win32" ? process.env.APPDATA ?? path5.join(os5.homedir(), "AppData", "Roaming") : process.env.XDG_CONFIG_HOME ?? path5.join(os5.homedir(), ".config");
1028
+ return path5.join(base2, "pitroom", "config.json");
985
1029
  }
986
1030
  var cached;
987
1031
  function loadConfig() {
@@ -1017,8 +1061,14 @@ function matches(v, type) {
1017
1061
  if (type === "record") {
1018
1062
  return typeof v === "object" && v !== null && !Array.isArray(v) && Object.values(v).every((s) => typeof s === "string");
1019
1063
  }
1064
+ if (type === "numbers") {
1065
+ return typeof v === "object" && v !== null && !Array.isArray(v) && Object.values(v).every((n) => typeof n === "number" && Number.isFinite(n) && n >= 0);
1066
+ }
1020
1067
  return typeof v === type;
1021
1068
  }
1069
+ var DEFAULT_PARALLEL = 20;
1070
+ var MAX_PARALLEL_LIMIT = 30;
1071
+ var clampParallel = (s) => ({ ...s, value: Math.min(s.value, MAX_PARALLEL_LIMIT) });
1022
1072
  var positiveInt = (v) => {
1023
1073
  const n = Number(v);
1024
1074
  return v !== void 0 && v !== "" && Number.isInteger(n) && n > 0 ? n : void 0;
@@ -1043,9 +1093,10 @@ function effective(flags = {}) {
1043
1093
  price: setting(void 0, e.PITROOM_PRICE, c.price, void 0),
1044
1094
  link: setting(void 0, void 0, c.link, []),
1045
1095
  web: setting(void 0, void 0, c.web, false),
1046
- maxParallel: setting(void 0, positiveInt(e.PITROOM_MAX_PARALLEL), positiveInt(c.maxParallel), 4),
1096
+ maxParallel: clampParallel(setting(void 0, positiveInt(e.PITROOM_MAX_PARALLEL), positiveInt(c.maxParallel), DEFAULT_PARALLEL)),
1047
1097
  models: setting(void 0, void 0, c.models, {}),
1048
- tiers: setting(void 0, void 0, c.tiers, {})
1098
+ tiers: setting(void 0, void 0, c.tiers, {}),
1099
+ costs: setting(void 0, void 0, c.costs, {})
1049
1100
  };
1050
1101
  }
1051
1102
 
@@ -1137,6 +1188,7 @@ var VALUE_FLAGS = {
1137
1188
  "-W": "worker",
1138
1189
  "--worker": "worker",
1139
1190
  "--tier": "tier",
1191
+ "--effort": "effort",
1140
1192
  "-g": "group",
1141
1193
  "--group": "group",
1142
1194
  "-t": "timeout",
@@ -1173,7 +1225,10 @@ var BOOL_FLAGS = {
1173
1225
  "--badge": "badge",
1174
1226
  "--probe": "probe",
1175
1227
  "--copy": "copy",
1228
+ "--all": "all",
1229
+ "--models": "models",
1176
1230
  "--force": "force",
1231
+ "--allow-delete": "allow-delete",
1177
1232
  "--yes": "yes",
1178
1233
  "--any": "any",
1179
1234
  "--brief": "brief",
@@ -1251,6 +1306,7 @@ function runOptions(p, task) {
1251
1306
  worker: flag(p, "worker"),
1252
1307
  model: flag(p, "model"),
1253
1308
  tier: flag(p, "tier"),
1309
+ effort: effortFlag(p),
1254
1310
  timeoutSec: parseDuration(effective({ timeout: flag(p, "timeout") }).timeout.value),
1255
1311
  verify: flag(p, "verify"),
1256
1312
  continueFrom: cont ? resolveRun(cont) : void 0,
@@ -1268,6 +1324,11 @@ function planStep(p) {
1268
1324
  if (!/^\d+$/.test(step)) throw new UserError(`--step takes a task number, not "${step}"`);
1269
1325
  return { file, step: Number(step) };
1270
1326
  }
1327
+ function effortFlag(p) {
1328
+ const v = flag(p, "effort");
1329
+ if (v !== void 0 && !/^[a-z][a-z0-9-]*$/i.test(v)) throw new UserError(`--effort takes a level such as low, medium, high or xhigh, not "${v}"`);
1330
+ return v?.toLowerCase();
1331
+ }
1271
1332
 
1272
1333
  // src/cli/commands.ts
1273
1334
  import { spawnSync as spawnSync6 } from "node:child_process";
@@ -1367,10 +1428,10 @@ function primaryPrice() {
1367
1428
  return PRESETS[eff.primary.value.toLowerCase()] ?? PRESETS.sonnet;
1368
1429
  }
1369
1430
  var estimateTokens = (text) => Math.ceil(text.length / 4);
1370
- function savedUsd(usage, returnedTokens, price = primaryPrice()) {
1371
- const wouldCost = (usage.input * price.input + usage.cacheRead * price.cachedInput + (usage.output + usage.reasoning) * price.output) / 1e6;
1431
+ function savedUsd(usage2, returnedTokens, price = primaryPrice()) {
1432
+ const wouldCost = (usage2.input * price.input + usage2.cacheRead * price.cachedInput + (usage2.output + usage2.reasoning) * price.output) / 1e6;
1372
1433
  const readingTheReport = returnedTokens * price.input / 1e6;
1373
- return Math.max(0, wouldCost - (usage.cost ?? 0) - readingTheReport);
1434
+ return Math.max(0, wouldCost - (usage2.cost ?? 0) - readingTheReport);
1374
1435
  }
1375
1436
  function record(meta) {
1376
1437
  if (!meta.usage?.steps) return;
@@ -1514,6 +1575,12 @@ function formatReport(meta, finalText = readSummary(meta), maxLines = 400) {
1514
1575
  );
1515
1576
  for (const c of meta.changes.slice(0, 50)) out.push(` ${c.status} ${c.path}`);
1516
1577
  if (meta.changes.length > 50) out.push(` \u2026 ${meta.changes.length - 50} more`);
1578
+ const deleted = meta.changes.filter((c) => c.status === "D").length;
1579
+ if (deleted && !meta.applied && !meta.reverted && !meta.discarded) {
1580
+ out.push(
1581
+ meta.mode === "isolate" ? ` \u26A0 deletes ${deleted} file${deleted === 1 ? "" : "s"}: check they are wanted before applying (apply refuses without --allow-delete)` : ` \u26A0 deleted ${deleted} file${deleted === 1 ? "" : "s"} in your tree: check they are wanted (undo: pitroom revert ${meta.id})`
1582
+ );
1583
+ }
1517
1584
  if (meta.changes.length) {
1518
1585
  out.push(` diff: pitroom show ${meta.id} --patch`);
1519
1586
  out.push(` review: pitroom review ${meta.id}`);
@@ -1619,9 +1686,9 @@ async function watch(select, opts) {
1619
1686
  const prev = seen.get(m.id);
1620
1687
  const l = m.state === "running" ? live(m) : { steps: m.usage?.steps ?? 0, toolCalls: m.usage?.toolCalls ?? 0 };
1621
1688
  const attempts = m.attempts?.length ?? 0;
1622
- const base = { run: m.id, mode: m.mode, task: oneLine4(m.task, 60) };
1623
- if (!prev) emit({ event: m.state === "queued" ? "queued" : "started", ...base });
1624
- else if (prev.state === "queued" && m.state === "running") emit({ event: "started", ...base });
1689
+ const base2 = { run: m.id, mode: m.mode, task: oneLine4(m.task, 60) };
1690
+ if (!prev) emit({ event: m.state === "queued" ? "queued" : "started", ...base2 });
1691
+ else if (prev.state === "queued" && m.state === "running") emit({ event: "started", ...base2 });
1625
1692
  if (attempts > (prev?.attempts ?? 0)) {
1626
1693
  const a = m.attempts[attempts - 1];
1627
1694
  emit({ event: "fallback", run: m.id, failed: a.target, reason: oneLine4(a.error, 120) });
@@ -1744,15 +1811,15 @@ function install(opts) {
1744
1811
  const skills = skillNames(root);
1745
1812
  if (!skills.length) throw new Error(`no skills found under ${path9.join(root, "skills")}`);
1746
1813
  const out = [];
1747
- for (const base of skillTargets()) {
1814
+ for (const base2 of skillTargets()) {
1748
1815
  for (const legacy of LEGACY) {
1749
- const l = path9.join(base, legacy);
1816
+ const l = path9.join(base2, legacy);
1750
1817
  if (!skills.includes(legacy) && linksInto(l, root)) {
1751
1818
  fs11.unlinkSync(l);
1752
1819
  out.push(`\u2714 removed old link ${l}`);
1753
1820
  }
1754
1821
  }
1755
- for (const name of skills) out.push(place(path9.join(root, "skills", name), path9.join(base, name), opts));
1822
+ for (const name of skills) out.push(place(path9.join(root, "skills", name), path9.join(base2, name), opts));
1756
1823
  }
1757
1824
  const bundle = path9.join(root, "dist", "pitroom.mjs");
1758
1825
  if (fs11.existsSync(bundle)) {
@@ -1765,10 +1832,10 @@ function install(opts) {
1765
1832
  function uninstall() {
1766
1833
  const root = packageRoot();
1767
1834
  const out = [];
1768
- for (const base of skillTargets()) {
1769
- if (!fs11.existsSync(base)) continue;
1770
- for (const name of fs11.readdirSync(base)) {
1771
- const l = path9.join(base, name);
1835
+ for (const base2 of skillTargets()) {
1836
+ if (!fs11.existsSync(base2)) continue;
1837
+ for (const name of fs11.readdirSync(base2)) {
1838
+ const l = path9.join(base2, name);
1772
1839
  if (linksInto(l, root)) {
1773
1840
  fs11.unlinkSync(l);
1774
1841
  out.push(`\u2714 removed ${l}`);
@@ -1783,7 +1850,7 @@ function uninstall() {
1783
1850
  }
1784
1851
  function installedSkills() {
1785
1852
  const names = skillNames();
1786
- return skillTargets().map((base) => ({ base, names: names.filter((n) => fs11.existsSync(path9.join(base, n, "SKILL.md"))) }));
1853
+ return skillTargets().map((base2) => ({ base: base2, names: names.filter((n) => fs11.existsSync(path9.join(base2, n, "SKILL.md"))) }));
1787
1854
  }
1788
1855
 
1789
1856
  // src/core/plan-status.ts
@@ -2121,6 +2188,7 @@ function pickReviewer(job) {
2121
2188
  const eff = effective();
2122
2189
  const tiers = eff.tiers.value;
2123
2190
  if (job.kind === "range") return tiers.capable;
2191
+ if (tiers.review) return tiers.review;
2124
2192
  const implementer = job.implementer;
2125
2193
  if (!implementer) return void 0;
2126
2194
  const def = parseTarget(eff.worker.value, DEFAULT_BACKEND).backend;
@@ -2156,10 +2224,108 @@ function fill(template, values) {
2156
2224
  return template.replace(/\{\{([A-Z_]+)\}\}/g, (_, key) => values[key]);
2157
2225
  }
2158
2226
 
2227
+ // src/core/models.ts
2228
+ var base = (model) => (model ?? "").split("#")[0];
2229
+ function usage() {
2230
+ const eff = effective();
2231
+ const out = /* @__PURE__ */ new Map();
2232
+ const add = (spec, label) => {
2233
+ let t;
2234
+ try {
2235
+ t = parseTarget(spec, "opencode");
2236
+ } catch {
2237
+ return;
2238
+ }
2239
+ const model = base(t.model) || base(eff.models.value[t.backend]);
2240
+ if (!model) return;
2241
+ const key = `${t.backend}:${model}`;
2242
+ out.set(key, [...out.get(key) ?? [], label]);
2243
+ };
2244
+ add(eff.worker.value, "default worker");
2245
+ for (const [name, spec] of Object.entries(eff.tiers.value)) add(spec, `tier ${name}`);
2246
+ eff.fallback.value.forEach((spec) => add(spec, "fallback"));
2247
+ for (const [backend, model] of Object.entries(eff.models.value)) add(`${backend}:${model}`, "models");
2248
+ return out;
2249
+ }
2250
+ function modelTable(opts = {}) {
2251
+ const costs = effective().costs.value;
2252
+ const used = usage();
2253
+ const seen = /* @__PURE__ */ new Map();
2254
+ for (const e of readLedger()) {
2255
+ if (!e.backend || !e.model) continue;
2256
+ const key = `${e.backend}:${base(e.model)}`;
2257
+ const a = seen.get(key) ?? { runs: 0, tokens: 0, usd: 0 };
2258
+ seen.set(key, { runs: a.runs + 1, tokens: a.tokens + e.tokens, usd: a.usd + e.workerCost });
2259
+ }
2260
+ const rows = [];
2261
+ const sources = [];
2262
+ let hiddenOpenCode = 0;
2263
+ const known = /* @__PURE__ */ new Set();
2264
+ for (const b of allBackends()) {
2265
+ if (opts.backend && b.id !== opts.backend) continue;
2266
+ const catalog4 = b.catalog?.() ?? { models: [], source: "no model list for this worker" };
2267
+ sources.push(`${b.id}: ${catalog4.source}`);
2268
+ for (const m of catalog4.models) {
2269
+ const key = `${b.id}:${m.id}`;
2270
+ known.add(key);
2271
+ const s = seen.get(key);
2272
+ const row = {
2273
+ worker: b.id,
2274
+ model: m.id,
2275
+ efforts: m.efforts?.length ? m.efforts : void 0,
2276
+ defaultEffort: m.defaultEffort,
2277
+ cost: costs[key],
2278
+ runs: s?.runs ?? 0,
2279
+ avgTokens: s ? Math.round(s.tokens / s.runs) : void 0,
2280
+ reportedUsdPerRun: s && s.usd > 0 ? s.usd / s.runs : void 0,
2281
+ inUse: used.get(key) ?? []
2282
+ };
2283
+ const relevant = row.inUse.length || row.runs || row.cost !== void 0;
2284
+ if (b.id === "opencode" && !opts.all && !relevant) hiddenOpenCode++;
2285
+ else rows.push(row);
2286
+ }
2287
+ }
2288
+ for (const [key, labels] of used) {
2289
+ const [worker, ...rest] = key.split(":");
2290
+ if (known.has(key) || opts.backend && worker !== opts.backend) continue;
2291
+ const s = seen.get(key);
2292
+ rows.push({ worker, model: rest.join(":"), cost: costs[key], runs: s?.runs ?? 0, avgTokens: s ? Math.round(s.tokens / s.runs) : void 0, inUse: labels });
2293
+ }
2294
+ rows.sort(
2295
+ (a, b) => a.worker.localeCompare(b.worker) || Number(b.inUse.length > 0) - Number(a.inUse.length > 0) || (a.cost ?? Infinity) - (b.cost ?? Infinity) || b.runs - a.runs || a.model.localeCompare(b.model)
2296
+ );
2297
+ return { rows, sources, hiddenOpenCode };
2298
+ }
2299
+ var compact2 = (n) => n >= 1e6 ? `${(n / 1e6).toFixed(1)}M` : n >= 1e3 ? `${Math.round(n / 1e3)}k` : String(n);
2300
+ function formatModels(t) {
2301
+ const efforts = (r) => r.efforts ? r.efforts.map((e) => e === r.defaultEffort ? `${e}*` : e).join("/") : "-";
2302
+ const cells = t.rows.map((r) => [
2303
+ r.worker,
2304
+ r.model,
2305
+ efforts(r),
2306
+ r.cost !== void 0 ? String(r.cost) : "?",
2307
+ r.runs ? String(r.runs) : "-",
2308
+ r.avgTokens !== void 0 ? compact2(r.avgTokens) : "-",
2309
+ r.reportedUsdPerRun !== void 0 ? `$${r.reportedUsdPerRun.toFixed(3)}` : "-",
2310
+ r.inUse.join(", ") || "-"
2311
+ ]);
2312
+ const head = ["worker", "model", "effort (* default)", "cost", "runs", "avg tokens", "$/run", "in use"];
2313
+ const widths = head.map((h, i) => Math.max(h.length, ...cells.map((c) => c[i].length)));
2314
+ const line = (c) => c.map((x, i) => x.padEnd(widths[i])).join(" ").trimEnd();
2315
+ const out = [line(head), ...cells.map(line), ""];
2316
+ out.push('cost: your relative cost from the config ("costs": {"codex:gpt-6-sol": 1, \u2026}); ? = not set. Pitroom cannot know vendor prices.');
2317
+ out.push("runs, avg tokens, $/run: from your own runs (only Claude Code and OpenCode report dollars). Choose one with -W worker:model --effort LEVEL.");
2318
+ if (t.hiddenOpenCode) out.push(`${t.hiddenOpenCode} more OpenCode models not shown (use --all).`);
2319
+ out.push(...t.sources.map((s) => `source ${s}`));
2320
+ return out.join("\n");
2321
+ }
2322
+
2159
2323
  // src/core/ui.ts
2160
2324
  import fs16 from "node:fs";
2161
2325
  var WEEK_MS = 7 * 24 * 3600 * 1e3;
2162
2326
  var RECENT = 40;
2327
+ var RECENT_HOURS = 1;
2328
+ var MAX_CARDS = 5;
2163
2329
  function activeRuns() {
2164
2330
  const out = [];
2165
2331
  for (const id of listRunIds().slice(-RECENT)) {
@@ -2258,8 +2424,17 @@ function hookCards(input) {
2258
2424
  const output = typeof response === "string" ? response : [response?.stdout, response?.stderr].filter(Boolean).join("\n");
2259
2425
  const ids = [...new Set(`${command}
2260
2426
  ${output}`.match(RUN_ID) ?? [])].filter((id) => fs16.existsSync(`${runsDir()}/${id}`));
2427
+ const since = Date.now() - RECENT_HOURS * 36e5;
2428
+ for (const id of listRunIds().slice(-RECENT)) {
2429
+ if (ids.includes(id) || fs16.existsSync(runFile(id, "card-ended"))) continue;
2430
+ try {
2431
+ const m = readMeta(id);
2432
+ if (!isActive(m.state) && Date.parse(m.endedAt ?? m.startedAt) > since) ids.push(id);
2433
+ } catch {
2434
+ }
2435
+ }
2261
2436
  const cards = [];
2262
- for (const id of ids) {
2437
+ for (const id of ids.slice(0, MAX_CARDS)) {
2263
2438
  try {
2264
2439
  cards.push(...cardsFor(readMeta(id)));
2265
2440
  } catch {
@@ -2284,10 +2459,28 @@ function resolveChain(flags = {}) {
2284
2459
  }
2285
2460
  const eff = effective({ worker: spec, model: flags.model });
2286
2461
  const models = eff.models.value;
2287
- const withDefault = (t) => t.model || !models[t.backend] ? t : { ...t, model: models[t.backend] };
2462
+ const withDefault = (t) => {
2463
+ const pinned = models[t.backend];
2464
+ if (!pinned) return t;
2465
+ if (!t.model) return { ...t, model: pinned };
2466
+ return t.model.startsWith("#") ? { ...t, model: `${pinned}${t.model}` } : t;
2467
+ };
2288
2468
  let worker = parseTarget(eff.worker.value, DEFAULT_BACKEND);
2289
2469
  if (eff.model.value) worker = { ...worker, model: eff.model.value };
2290
2470
  worker = withDefault(worker);
2471
+ if (flags.effort) {
2472
+ const base2 = (worker.model ?? "").split("#")[0];
2473
+ if (worker.backend === "opencode" && !base2) {
2474
+ warnings.push("--effort needs a model for OpenCode (provider/model#variant): set one in the config or pass -m; ignored");
2475
+ } else {
2476
+ worker = { ...worker, model: `${base2}#${flags.effort}` };
2477
+ }
2478
+ }
2479
+ if ((worker.backend === "codex" || worker.backend === "claude") && !(worker.model ?? "").replace(/#.*$/, "")) {
2480
+ warnings.push(
2481
+ `${worker.backend} has no pinned model, so it runs its own default (which can change and cost more): set "models": {"${worker.backend}": "<model>"} in the pitroom config, or pass -W ${worker.backend}:<model>`
2482
+ );
2483
+ }
2291
2484
  const fallback = [];
2292
2485
  if (!flags.noFallback) {
2293
2486
  for (const s of eff.fallback.value) {
@@ -2598,6 +2791,7 @@ Rules:
2598
2791
  - Stay inside the task scope and the project directory.
2599
2792
  - Never commit, push, reset, checkout, restore, stash, clean, rebase or merge, and never discard or overwrite uncommitted work you did not create.
2600
2793
  - Never read or reveal secrets (.env files, keys, tokens).
2794
+ - Delete a file only when the task explicitly asks for it, and name every file you delete under FILES CHANGED. Whether anything else should be deleted or overwritten is the primary agent's decision: propose it under OPEN ISSUES and leave the file alone.
2601
2795
  - Do not ask questions. If something is ambiguous, choose the safest reasonable interpretation and state the assumption.
2602
2796
  - Be economical: open only what you need.
2603
2797
 
@@ -2720,7 +2914,7 @@ function inside(file, roots) {
2720
2914
  var escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2721
2915
 
2722
2916
  // src/core/run.ts
2723
- var VERSION2 = true ? "0.6.3" : "0.0.0-dev";
2917
+ var VERSION2 = true ? "0.6.5" : "0.0.0-dev";
2724
2918
  function planWork(o) {
2725
2919
  if (!o.plan) return { task: o.task.trim(), tier: o.tier };
2726
2920
  if (o.continueFrom) throw new UserError("a follow-up continues its parent's task; drop --plan/--step");
@@ -2750,7 +2944,7 @@ function prepareRun(o) {
2750
2944
  let fallback;
2751
2945
  if (o.continueFrom) {
2752
2946
  parent = readMeta(o.continueFrom);
2753
- if (o.worker || o.tier) throw new UserError("a follow-up runs on the same worker as its parent; drop --worker/--tier");
2947
+ if (o.worker || o.tier || o.effort) throw new UserError("a follow-up runs on the same worker as its parent; drop --worker/--tier/--effort");
2754
2948
  if (!parent.sessionId) throw new UserError(`run ${parent.id} has no worker session to continue`, 3);
2755
2949
  if (parent.mode === "isolate" && (!parent.worktree || !fs21.existsSync(parent.worktree))) {
2756
2950
  throw new UserError(`the isolated copy of run ${parent.id} is gone (applied or discarded)`, 3);
@@ -2759,7 +2953,7 @@ function prepareRun(o) {
2759
2953
  worker = o.model ? { ...ran, model: o.model } : ran;
2760
2954
  fallback = o.noFallback ? [] : parent.fallback.filter((t) => t.backend === ran.backend);
2761
2955
  } else {
2762
- const chain = resolveChain({ worker: o.worker, model: o.model, tier: work.tier, noFallback: o.noFallback });
2956
+ const chain = resolveChain({ worker: o.worker, model: o.model, tier: work.tier, effort: o.effort, noFallback: o.noFallback });
2763
2957
  ({ worker, fallback } = chain);
2764
2958
  warnings.push(...chain.warnings);
2765
2959
  }
@@ -3020,10 +3214,21 @@ function runVerify(meta) {
3020
3214
  fs21.writeFileSync(runFile(meta.id, "verify.log"), output);
3021
3215
  return { ok: r.status === 0, code: r.status, tail: output.trimEnd().split("\n").slice(-25).join("\n") };
3022
3216
  }
3023
- function applyRun(meta) {
3217
+ function applyRun(meta, allowDelete = false) {
3024
3218
  if (meta.mode !== "isolate") throw new UserError(`run ${meta.id} edited your tree directly (${meta.mode}); nothing to apply`);
3025
3219
  if (meta.applied) throw new UserError(`run ${meta.id} was already applied`);
3026
3220
  if (!meta.changes?.length) throw new UserError(`run ${meta.id} has no changes`);
3221
+ const deleted = meta.changes.filter((c) => c.status === "D").map((c) => c.path);
3222
+ if (deleted.length && !allowDelete) {
3223
+ const list2 = deleted.slice(0, 10).map((f) => ` ${f}`).join("\n") + (deleted.length > 10 ? `
3224
+ \u2026 ${deleted.length - 10} more` : "");
3225
+ throw new DeletionRefused(
3226
+ `run ${meta.id} deletes ${deleted.length} file${deleted.length === 1 ? "" : "s"}; nothing was applied:
3227
+ ${list2}
3228
+ Check that the deletion is what the user asked for: if so apply with --allow-delete, otherwise ask the user or discard the run.`,
3229
+ deleted
3230
+ );
3231
+ }
3027
3232
  const res = applyPatch(meta.repoRoot, runFile(meta.id, "changes.patch"), false);
3028
3233
  if (!res.ok) throw new UserError(`patch does not apply cleanly (your tree changed since the snapshot):
3029
3234
  ${res.message}`, 1);
@@ -3092,7 +3297,7 @@ async function cmdReview(p) {
3092
3297
  group: flag(p, "group") ?? job.group,
3093
3298
  review: { of: job.of, kind: job.kind, packageFile, plan: job.plan }
3094
3299
  });
3095
- const automatic = !range && !flag(p, "worker") && !flag(p, "tier");
3300
+ const automatic = !range && !flag(p, "worker") && !flag(p, "tier") && !effective().tiers.value.review;
3096
3301
  if (automatic && job.implementer && meta.worker.backend === job.implementer.backend) {
3097
3302
  meta.warnings.push(
3098
3303
  `reviewer runs on the same backend as the implementer (${job.implementer.backend}); configure tiers "standard" or "capable" for a second model`
@@ -3229,19 +3434,21 @@ function cmdLs(p) {
3229
3434
  function cmdApply(p) {
3230
3435
  const group = flag(p, "group");
3231
3436
  if (!group) {
3232
- console.log(applyRun(freshMeta(resolveRun(p.positional[0]))));
3437
+ console.log(applyRun(freshMeta(resolveRun(p.positional[0])), has(p, "allow-delete")));
3233
3438
  return 0;
3234
3439
  }
3235
3440
  const pending = groupIds(group).map((id) => freshMeta(id)).filter((m) => m.mode === "isolate" && m.state === "done" && m.changes?.length && !m.applied && !m.discarded);
3236
3441
  if (!pending.length) throw new UserError(`group "${group}" has no finished isolate patches to apply`);
3237
3442
  for (const [i, m] of pending.entries()) {
3238
3443
  try {
3239
- console.log(applyRun(m));
3444
+ console.log(applyRun(m, has(p, "allow-delete")));
3240
3445
  } catch (e) {
3241
3446
  const rest = pending.slice(i + 1).map((r) => r.id);
3242
3447
  console.log(`\u2718 ${m.id}: ${e.message}`);
3243
3448
  if (rest.length) console.log(` not applied yet: ${rest.join(" ")}`);
3244
- console.log(` resolve it (e.g. pitroom run --continue ${m.id} "rebase your change on the current tree"), then apply the rest`);
3449
+ if (!(e instanceof DeletionRefused)) {
3450
+ console.log(` resolve it (e.g. pitroom run --continue ${m.id} "rebase your change on the current tree"), then apply the rest`);
3451
+ }
3245
3452
  return 1;
3246
3453
  }
3247
3454
  }
@@ -3286,14 +3493,42 @@ function cmdStatusline(p) {
3286
3493
  function cmdHookCard() {
3287
3494
  try {
3288
3495
  const cards = hookCards(readStdin());
3289
- if (cards) console.log(JSON.stringify({ systemMessage: cards }));
3496
+ if (cards) {
3497
+ console.log(
3498
+ JSON.stringify({
3499
+ systemMessage: cards,
3500
+ hookSpecificOutput: { hookEventName: "PostToolUse", additionalContext: `Pitroom, for the user:
3501
+ ${cards}` }
3502
+ })
3503
+ );
3504
+ }
3290
3505
  } catch {
3291
3506
  }
3292
3507
  return 0;
3293
3508
  }
3509
+ function cmdModels(p) {
3510
+ const backend = p.positional[0];
3511
+ const table2 = modelTable({ backend, all: has(p, "all") });
3512
+ console.log(has(p, "json") ? JSON.stringify(table2, null, 2) : formatModels(table2));
3513
+ return 0;
3514
+ }
3294
3515
  function cmdSavings(p) {
3295
3516
  const since = flag(p, "since") ?? "all";
3296
3517
  const t = totals(readLedger(sinceMs(since)));
3518
+ if (has(p, "models")) {
3519
+ const by = /* @__PURE__ */ new Map();
3520
+ for (const e of readLedger(sinceMs(since))) {
3521
+ const key = `${e.backend ?? "?"} ${e.model ?? "(default model)"}`;
3522
+ const a = by.get(key) ?? { runs: 0, tokens: 0, returned: 0, cost: 0, saved: 0 };
3523
+ by.set(key, { runs: a.runs + 1, tokens: a.tokens + e.tokens, returned: a.returned + e.returned, cost: a.cost + e.workerCost, saved: a.saved + e.saved });
3524
+ }
3525
+ const width = Math.max(12, ...[...by.keys()].map((k) => k.length));
3526
+ console.log(`${"worker model".padEnd(width)} ${"runs".padStart(5)} ${"processed".padStart(10)} ${"returned".padStart(9)} ${"cost".padStart(8)} ${"saved".padStart(8)}`);
3527
+ for (const [key, a] of [...by].sort((x, y) => y[1].runs - x[1].runs)) {
3528
+ console.log(`${key.padEnd(width)} ${String(a.runs).padStart(5)} ${compact(a.tokens).padStart(10)} ${compact(a.returned).padStart(9)} ${usd(a.cost).padStart(8)} ${usd(a.saved).padStart(8)}`);
3529
+ }
3530
+ return 0;
3531
+ }
3297
3532
  const period = since === "all" ? "all time" : `last ${since.replace("d", " days")}`;
3298
3533
  if (has(p, "json")) {
3299
3534
  console.log(JSON.stringify({ period, ...t, price: primaryPrice() }, null, 2));
@@ -3403,6 +3638,16 @@ function doctor4(probe) {
3403
3638
  }
3404
3639
  }
3405
3640
  if (tierNames.length) add("ok", `tiers: ${tierNames.map((name, i) => `${name}=${describeTarget(tierTargets[i])}`).join(", ")}`);
3641
+ const costs = effective().costs.value;
3642
+ if (Object.keys(costs).length) {
3643
+ for (const t of [...chain, ...tierTargets]) {
3644
+ const key = `${t.backend}:${(t.model ?? "").split("#")[0]}`;
3645
+ const mine = costs[key];
3646
+ if (mine === void 0) continue;
3647
+ const cheaper = Object.entries(costs).filter(([k, v]) => k.startsWith(`${t.backend}:`) && v < mine).sort((a, b) => a[1] - b[1])[0];
3648
+ add("ok", `cost: ${key} = ${mine}${cheaper ? `; you priced ${cheaper[0]} cheaper (${cheaper[1]}): is the dearer one needed?` : ""}`);
3649
+ }
3650
+ }
3406
3651
  const byBackend = /* @__PURE__ */ new Map();
3407
3652
  for (const t of [...chain, ...tierTargets]) byBackend.set(t.backend, [...byBackend.get(t.backend) ?? [], t.model]);
3408
3653
  for (const [id, models] of byBackend) {
@@ -3462,10 +3707,10 @@ function skillChecks() {
3462
3707
  message: `superpowers is installed too (${superpowers.join(", ")}): two bootstraps compete for the same work; keep one (Pitroom includes the superpowers workflow)`
3463
3708
  });
3464
3709
  }
3465
- for (const { base, names } of installedSkills()) {
3466
- if (names.length === all.length) checks.push({ level: "ok", message: `skills in ${base}: ${names.join(", ")}` });
3467
- else if (names.length) checks.push({ level: "warn", message: `skills in ${base}: only ${names.join(", ")} of ${all.length}; run \`pitroom install\`` });
3468
- else if (!viaPlugin) checks.push({ level: "warn", message: `no Pitroom skills in ${base}; run \`pitroom install\`` });
3710
+ for (const { base: base2, names } of installedSkills()) {
3711
+ if (names.length === all.length) checks.push({ level: "ok", message: `skills in ${base2}: ${names.join(", ")}` });
3712
+ else if (names.length) checks.push({ level: "warn", message: `skills in ${base2}: only ${names.join(", ")} of ${all.length}; run \`pitroom install\`` });
3713
+ else if (!viaPlugin) checks.push({ level: "warn", message: `no Pitroom skills in ${base2}; run \`pitroom install\`` });
3469
3714
  }
3470
3715
  if (viaPlugin) {
3471
3716
  const linked = installedSkills().some((i) => i.base.includes(`${path19.sep}.claude${path19.sep}`) && i.names.length);
@@ -3504,8 +3749,8 @@ function superpowersActive() {
3504
3749
  for (const p of openCodePlugins()) {
3505
3750
  if (/superpowers/i.test(p)) found.push(`OpenCode plugin ${p}`);
3506
3751
  }
3507
- for (const base of [path19.join(os9.homedir(), ".agents", "skills"), path19.join(os9.homedir(), ".claude", "skills")]) {
3508
- const dir = path19.join(base, "using-superpowers");
3752
+ for (const base2 of [path19.join(os9.homedir(), ".agents", "skills"), path19.join(os9.homedir(), ".claude", "skills")]) {
3753
+ const dir = path19.join(base2, "using-superpowers");
3509
3754
  if (fs23.existsSync(path19.join(dir, "SKILL.md"))) found.push(dir);
3510
3755
  }
3511
3756
  return found;
@@ -3588,11 +3833,15 @@ Usage
3588
3833
  live table (TTY) or one JSON line per change, until done
3589
3834
  pitroom show [run] [--patch|--events|--full|--json]
3590
3835
  pitroom ls [--running] [-g NAME] recent runs
3591
- pitroom apply [run | -g NAME] apply --isolate patch(es) to your tree (checked first)
3836
+ pitroom apply [run | -g NAME] [--allow-delete]
3837
+ apply --isolate patch(es) to your tree (checked first); a patch
3838
+ that deletes files is refused unless --allow-delete
3592
3839
  pitroom discard [run] drop an --isolate run's copy (the patch is kept)
3593
3840
  pitroom revert [run] undo the changes of a --write run (checked first)
3594
3841
  pitroom stop [run | -g NAME] stop running or queued workers
3595
- pitroom savings [--since 7d|30d|all] [--card file.svg] [--badge]
3842
+ pitroom savings [--since 7d|30d|all] [--models] [--card file.svg] [--badge]
3843
+ pitroom models [worker] [--all] [--json]
3844
+ models each worker offers, with effort levels, your costs and usage
3596
3845
  pitroom statusline [--then CMD] status-bar line: running workers, savings this week (after CMD's)
3597
3846
  pitroom hook-card PostToolUse hook: a card after each Bash \`pitroom\` command
3598
3847
  pitroom doctor [--probe] check workers, models, permissions, skills
@@ -3611,6 +3860,7 @@ Run options
3611
3860
  -W, --worker T worker target "backend[:model]" (default: config "worker", else opencode)
3612
3861
  -m, --model M model for that worker (default: the worker CLI's own default)
3613
3862
  --tier NAME a worker from the config's "tiers" (e.g. cheap, standard, capable); -W wins
3863
+ --effort LEVEL reasoning effort for the worker: low, medium, high, xhigh, \u2026 (model#level)
3614
3864
  --plan PLAN with --step N: implement Task N of a plan (-i or -w); the task text is your notes
3615
3865
  --step N the plan task for --plan
3616
3866
  -t, --timeout DUR e.g. 900, 20m, 1h (default 30m, or PITROOM_TIMEOUT)
@@ -3625,7 +3875,7 @@ Run options
3625
3875
  --task-file PATH read the task from a file (crew: tasks separated by --- lines)
3626
3876
  --allow-non-git allow --write outside a git repository (no tracking/revert)
3627
3877
 
3628
- Parallel: at most maxParallel workers (default 4, PITROOM_MAX_PARALLEL) run at once; others queue.
3878
+ Parallel: at most maxParallel workers (default 20, up to 30; PITROOM_MAX_PARALLEL) run at once; others queue.
3629
3879
  One --write run per repository; use --isolate for parallel changes.
3630
3880
  Exit codes: 0 ok \xB7 1 worker failed \xB7 2 usage \xB7 3 refused/setup \xB7 4 timeout
3631
3881
  5 read-only violation \xB7 6 verify failed \xB7 75 still running (wait again)
@@ -3650,6 +3900,7 @@ var COMMANDS = {
3650
3900
  discard: cmdDiscard,
3651
3901
  stop: cmdStop,
3652
3902
  savings: cmdSavings,
3903
+ models: cmdModels,
3653
3904
  statusline: cmdStatusline,
3654
3905
  "hook-card": cmdHookCard,
3655
3906
  doctor: (p) => doctor4(has(p, "probe")),