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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +32 -0
- package/PRIVACY.md +28 -12
- package/README.md +158 -79
- package/THIRD_PARTY_NOTICES.md +19 -0
- package/dist/lib.mjs +39 -3
- package/dist/pitroom.mjs +334 -83
- package/package.json +4 -2
- package/skills/pitroom-crew/SKILL.md +3 -3
- package/skills/pitroom-driven-development/SKILL.md +1 -1
- package/skills/pitroom-implement/SKILL.md +1 -1
- package/skills/pitroom-writing-plans/SKILL.md +1 -1
- package/skills/using-pitroom/SKILL.md +6 -0
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
|
|
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
|
-
|
|
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))
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
351
|
+
usage2.steps++;
|
|
336
352
|
if (TOOLS.has(it.type)) {
|
|
337
|
-
|
|
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 ?? "")))
|
|
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
|
|
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
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
581
|
+
usage2.toolCalls++;
|
|
548
582
|
tools[name] = (tools[name] ?? 0) + 1;
|
|
549
|
-
if (st.status === "error" && DENIED2.test(String(st.error ?? "")))
|
|
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
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
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
|
|
815
|
+
let base2 = {};
|
|
782
816
|
if (existing?.trim()) {
|
|
783
817
|
try {
|
|
784
|
-
|
|
818
|
+
base2 = JSON.parse(existing);
|
|
785
819
|
} catch {
|
|
786
|
-
|
|
820
|
+
base2 = {};
|
|
787
821
|
}
|
|
788
822
|
}
|
|
789
|
-
return JSON.stringify(deepMerge(
|
|
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
|
|
984
|
-
return path5.join(
|
|
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),
|
|
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(
|
|
1371
|
-
const wouldCost = (
|
|
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 - (
|
|
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
|
|
1623
|
-
if (!prev) emit({ event: m.state === "queued" ? "queued" : "started", ...
|
|
1624
|
-
else if (prev.state === "queued" && m.state === "running") emit({ event: "started", ...
|
|
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
|
|
1814
|
+
for (const base2 of skillTargets()) {
|
|
1748
1815
|
for (const legacy of LEGACY) {
|
|
1749
|
-
const l = path9.join(
|
|
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(
|
|
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
|
|
1769
|
-
if (!fs11.existsSync(
|
|
1770
|
-
for (const name of fs11.readdirSync(
|
|
1771
|
-
const l = path9.join(
|
|
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((
|
|
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) =>
|
|
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.
|
|
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
|
-
|
|
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)
|
|
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 ${
|
|
3467
|
-
else if (names.length) checks.push({ level: "warn", message: `skills in ${
|
|
3468
|
-
else if (!viaPlugin) checks.push({ level: "warn", message: `no Pitroom skills in ${
|
|
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
|
|
3508
|
-
const dir = path19.join(
|
|
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]
|
|
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
|
|
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")),
|