@irythill/orq 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -12
- package/README.pt-BR.md +41 -12
- package/dist/agents.js +12 -0
- package/dist/ci.js +239 -0
- package/dist/cli.js +7 -1
- package/dist/commands/agent.js +270 -29
- package/dist/commands/block.js +20 -3
- package/dist/commands/board.js +87 -8
- package/dist/commands/ci.js +48 -0
- package/dist/commands/claim.js +148 -12
- package/dist/commands/completion.js +3 -2
- package/dist/commands/gc.js +1 -1
- package/dist/commands/handoff.js +259 -0
- package/dist/commands/pick.js +18 -3
- package/dist/commands/quota.js +34 -9
- package/dist/commands/rounds.js +58 -0
- package/dist/config.js +4 -1
- package/dist/identity.js +55 -0
- package/dist/pick.js +106 -9
- package/dist/process.js +19 -11
- package/dist/quota.js +83 -21
- package/dist/rounds.js +167 -0
- package/dist/terminal.js +26 -0
- package/dist/testrun.js +26 -5
- package/package.json +1 -1
- package/skills/issue/SKILL.md +47 -10
- package/skills/orq-setup/SKILL.md +1 -1
- package/templates/workspace.CLAUDE.md +7 -0
package/dist/commands/quota.js
CHANGED
|
@@ -1,25 +1,49 @@
|
|
|
1
|
-
// orq quota [name...] quota of each source in [quota] (default: all) — lines "<name> 5h=NN% week=NN% [note]"
|
|
1
|
+
// orq quota [name...] quota of each source in [quota] (default: all) — lines "<name> 5h=NN% week=NN% [note]", with
|
|
2
|
+
// reset5h=/resetweek= epochs shown as human times at the end (e.g. "5h=92% week=40% (5h resets 14:30)")
|
|
2
3
|
// orq quota --line one line for a status bar (herdr's tab_bar_right command entry): the board's cache, never
|
|
3
4
|
// the network — a stale cache starts the board's background collection, like the board does.
|
|
4
5
|
import { getPath, loadConfig } from "../config.js";
|
|
5
6
|
import { m } from "../i18n.js";
|
|
6
|
-
import {
|
|
7
|
+
import { limitsFor } from "../pick.js";
|
|
8
|
+
import { collect, humanReset } from "../quota.js";
|
|
7
9
|
import { quota as cached } from "./board.js";
|
|
8
10
|
import { load as loadBlocks } from "./block.js";
|
|
9
11
|
export const USAGE = `orq quota [name...] quota per agent/account ([quota] in the config)
|
|
10
12
|
orq quota --line one line for a status bar, from the board's cache (e.g. herdr tab_bar_right)`;
|
|
11
13
|
const pct = (p) => (/^\d+%$/.test(p) ? Number(p.slice(0, -1)) : undefined);
|
|
12
|
-
/** "
|
|
13
|
-
*
|
|
14
|
-
|
|
14
|
+
/** A "name 5h=.. reset5h=<epoch> week=.. resetweek=<epoch> [note]" line → the same "name 5h=.. week=.." kept intact,
|
|
15
|
+
* with the raw epoch fields dropped and human reset times ("resets 14:30") moved to the end of the line, before
|
|
16
|
+
* any [note]. */
|
|
17
|
+
export function humanizeQuotaLine(line, now = Date.now()) {
|
|
18
|
+
const r5 = /reset5h=(\d+)/.exec(line), rw = /resetweek=(\d+)/.exec(line);
|
|
19
|
+
if (!r5 && !rw)
|
|
20
|
+
return line.replace(/ span5h=\d+/, "").replace(/ spanweek=\d+/, "");
|
|
21
|
+
let out = line.replace(/ reset5h=\d+/, "").replace(/ resetweek=\d+/, "")
|
|
22
|
+
.replace(/ span5h=\d+/, "").replace(/ spanweek=\d+/, "");
|
|
23
|
+
const parts = [];
|
|
24
|
+
if (r5)
|
|
25
|
+
parts.push(`${m("5h", "5h")} ${m("resets", "renova")} ${humanReset(Number(r5[1]), now)}`);
|
|
26
|
+
if (rw)
|
|
27
|
+
parts.push(`${m("week", "semana")} ${m("resets", "renova")} ${humanReset(Number(rw[1]), now)}`);
|
|
28
|
+
const reset = `(${parts.join(", ")})`;
|
|
29
|
+
const note = /(\[.*\])$/.exec(out);
|
|
30
|
+
return note ? `${out.slice(0, note.index).trimEnd()} ${reset} ${note[1]}` : `${out.trimEnd()} ${reset}`;
|
|
31
|
+
}
|
|
32
|
+
/** "work 64 · home 93! · codex 81 · minimax ?": the tighter of the two windows per source; ! when either window is
|
|
33
|
+
* at or over the medium-task pick limit for that window (5h: `policy.quota_5h.medium`/`quota_limit`, week:
|
|
34
|
+
* `policy.quota_week`) — the same thresholds `orq pick` applies for an unspecified task level; ⊘ blocked
|
|
35
|
+
* (orq block), ? no data; sources with no window at all ("-" and "-") are left out. */
|
|
36
|
+
export function quotaLine(lines, limit5, limitWeek, blocked, age) {
|
|
15
37
|
const parts = lines.filter((q) => q.five !== "-" || q.week !== "-").map((q) => {
|
|
16
38
|
if (blocked.has(q.name))
|
|
17
39
|
return `${q.name} ⊘`;
|
|
18
|
-
const
|
|
40
|
+
const five = pct(q.five), week = pct(q.week);
|
|
41
|
+
const ns = [five, week].filter((n) => n !== undefined);
|
|
19
42
|
if (!ns.length)
|
|
20
43
|
return `${q.name} ?`;
|
|
21
44
|
const top = Math.max(...ns);
|
|
22
|
-
|
|
45
|
+
const over = (five !== undefined && five >= limit5) || (week !== undefined && week >= limitWeek);
|
|
46
|
+
return `${q.name} ${top}${over ? "!" : ""}`;
|
|
23
47
|
});
|
|
24
48
|
if (!parts.length)
|
|
25
49
|
return age === undefined ? m("quota: collecting...", "cota: coletando...") : "";
|
|
@@ -30,11 +54,12 @@ export async function quota(argv) {
|
|
|
30
54
|
const { cfg } = loadConfig();
|
|
31
55
|
if (argv[0] === "--line") {
|
|
32
56
|
const q = cached(Date.now());
|
|
33
|
-
|
|
57
|
+
const [limit5, limitWeek] = limitsFor((getPath(cfg, "policy") ?? {}), "medium");
|
|
58
|
+
console.log(quotaLine(q.lines, limit5, limitWeek, new Set(Object.keys(loadBlocks())), q.age));
|
|
34
59
|
return 0;
|
|
35
60
|
}
|
|
36
61
|
const sources = (getPath(cfg, "quota") ?? {});
|
|
37
62
|
for (const l of await collect(sources, argv))
|
|
38
|
-
console.log(l);
|
|
63
|
+
console.log(humanizeQuotaLine(l));
|
|
39
64
|
return 0;
|
|
40
65
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// orq rounds <ID> [-w <workspace>] [--json] per-issue round metrics from .orquestra/<ID>/ (src/rounds.ts), no LLM
|
|
2
|
+
import { getPath, loadConfig } from "../config.js";
|
|
3
|
+
import { m } from "../i18n.js";
|
|
4
|
+
import { expand } from "../paths.js";
|
|
5
|
+
import { collectRounds, mark } from "../rounds.js";
|
|
6
|
+
export const USAGE = "orq rounds <ID> [-w <workspace>] [--json] rounds vs max_rounds, time per round, repeated findings, alert level";
|
|
7
|
+
const dur = (s) => (s >= 3600 ? `${Math.floor(s / 3600)}h${String(Math.floor(s / 60) % 60).padStart(2, "0")}m` : s >= 60 ? `${Math.floor(s / 60)}m${String(s % 60).padStart(2, "0")}s` : `${s}s`);
|
|
8
|
+
const why = (r) => ({
|
|
9
|
+
over_max: m(`rounds ${r.n} > max_rounds ${r.max}`, `rodadas ${r.n} > max_rounds ${r.max}`),
|
|
10
|
+
at_max: m(`rounds ${r.n} = max_rounds ${r.max}`, `rodadas ${r.n} = max_rounds ${r.max}`),
|
|
11
|
+
same_finding: m(`same finding in ${r.n} reviews`, `mesmo achado em ${r.n} revisões`),
|
|
12
|
+
repeated: m(`${r.n} repeated finding(s)`, `${r.n} achado(s) repetido(s)`),
|
|
13
|
+
slow: m(`round ${r.rounds?.join(", ")} much slower than the others`, `rodada ${r.rounds?.join(", ")} bem mais lenta que as outras`),
|
|
14
|
+
})[r.code];
|
|
15
|
+
export function rounds(argv) {
|
|
16
|
+
let ws, id, json = false;
|
|
17
|
+
for (let i = 0; i < argv.length; i++) {
|
|
18
|
+
if (argv[i] === "-w") {
|
|
19
|
+
if (!argv[i + 1]) {
|
|
20
|
+
console.error(USAGE);
|
|
21
|
+
return 2;
|
|
22
|
+
}
|
|
23
|
+
ws = expand(argv[++i]);
|
|
24
|
+
}
|
|
25
|
+
else if (argv[i] === "--json")
|
|
26
|
+
json = true;
|
|
27
|
+
else if (!id && !argv[i].startsWith("-"))
|
|
28
|
+
id = argv[i];
|
|
29
|
+
else {
|
|
30
|
+
console.error(USAGE);
|
|
31
|
+
return 2;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (!id) {
|
|
35
|
+
console.error(USAGE);
|
|
36
|
+
return 2;
|
|
37
|
+
}
|
|
38
|
+
ws ??= getPath(loadConfig().cfg, "_workspace") ?? process.cwd();
|
|
39
|
+
const { cfg } = loadConfig(ws); // the workspace's own policy.max_rounds
|
|
40
|
+
const x = collectRounds(ws, id, Number(getPath(cfg, "policy.max_rounds") ?? 2));
|
|
41
|
+
if (!x) {
|
|
42
|
+
console.error(m(`no rounds recorded for ${id} in ${ws}/.orquestra`, `nenhuma rodada registrada para ${id} em ${ws}/.orquestra`));
|
|
43
|
+
return 1;
|
|
44
|
+
}
|
|
45
|
+
if (json) {
|
|
46
|
+
console.log(JSON.stringify(x, null, 2));
|
|
47
|
+
return 0;
|
|
48
|
+
}
|
|
49
|
+
console.log(`${id} ${mark(x)} ${m("alert", "alerta")}: ${x.level}${x.reasons.length ? ` (${x.reasons.map(why).join("; ")})` : ""}`);
|
|
50
|
+
console.log(m(`rounds ${x.rounds} of max ${x.max}; total time ${dur(x.secs)}; ${x.findings} finding(s) across reviews`, `rodadas ${x.rounds} de no máx. ${x.max}; tempo total ${dur(x.secs)}; ${x.findings} achado(s) nas revisões`));
|
|
51
|
+
for (const r of x.perRound)
|
|
52
|
+
console.log(` ${r.round === 0 ? m("impl + review", "impl + revisão") : m(`fix ${r.round} + review`, `fix ${r.round} + revisão`)}: ${dur(r.secs)}`);
|
|
53
|
+
if (x.repeats.length)
|
|
54
|
+
console.log(m("repeated findings:", "achados repetidos:"));
|
|
55
|
+
for (const r of x.repeats)
|
|
56
|
+
console.log(` [${m("rounds", "rodadas")} ${r.rounds.join(",")}] ${r.title.slice(0, 100)}`);
|
|
57
|
+
return 0;
|
|
58
|
+
}
|
package/dist/config.js
CHANGED
|
@@ -20,7 +20,10 @@ export const DEFAULTS = {
|
|
|
20
20
|
},
|
|
21
21
|
agents: {}, // only agents declared here are available; run templates: built in for 5 (src/agents.ts), or `run = ...`
|
|
22
22
|
policy: {
|
|
23
|
-
quota_limit: 90, //
|
|
23
|
+
quota_limit: 90, // also caps the 5h pick limit; marks ! in `orq quota --line`
|
|
24
|
+
pacing_penalty: 0.3, // score penalty when usage at the current pace would pass the pick limit by the window's reset (0 = off)
|
|
25
|
+
quota_week: 95, // weekly % from which an agent/account is skipped
|
|
26
|
+
quota_5h: { simple: 90, medium: 85, heavy: 80 }, // same for the 5h window, per task level
|
|
24
27
|
max_rounds: 2, // fix rounds before stopping and calling the user
|
|
25
28
|
review: "other-vendor", // other-vendor | any
|
|
26
29
|
},
|
package/dist/identity.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Who is logged in behind an agent or account name — only to tell that the login CHANGED. `orq block` records it, and a
|
|
2
|
+
// block made for one login is dropped once another is there (a new account has its own quota). Ids are short hashes of
|
|
3
|
+
// something stable per login (never the secret itself); undefined = cannot tell, so the block stays until its date.
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
import { readFileSync } from "node:fs";
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import { join, resolve } from "node:path";
|
|
9
|
+
import { claudeAccounts } from "./accounts.js";
|
|
10
|
+
import { agyToken, minimaxKey } from "./quota.js";
|
|
11
|
+
const hash = (s) => createHash("sha256").update(s).digest("hex").slice(0, 16);
|
|
12
|
+
const json = (f) => { try {
|
|
13
|
+
return JSON.parse(readFileSync(f, "utf8"));
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return undefined;
|
|
17
|
+
} };
|
|
18
|
+
/** Claude Code keeps the logged-in account in .claude.json: inside the config dir, or in ~ for the default ~/.claude. */
|
|
19
|
+
function claudeId(dir) {
|
|
20
|
+
const files = [join(dir, ".claude.json"), ...(resolve(dir) === join(homedir(), ".claude") ? [join(homedir(), ".claude.json")] : [])];
|
|
21
|
+
for (const f of files) {
|
|
22
|
+
const id = json(f)?.oauthAccount?.accountUuid;
|
|
23
|
+
if (id)
|
|
24
|
+
return String(id);
|
|
25
|
+
}
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
function copilotId() {
|
|
29
|
+
const r = spawnSync("gh", ["api", "/user", "-q", ".id"], { encoding: "utf8", timeout: 15_000 });
|
|
30
|
+
return r.status === 0 && r.stdout.trim() ? r.stdout.trim() : undefined;
|
|
31
|
+
}
|
|
32
|
+
/** The login id for a block name (an agent, or a Claude account), or undefined when it cannot be told. */
|
|
33
|
+
export function loginId(name) {
|
|
34
|
+
const accs = claudeAccounts();
|
|
35
|
+
const acc = accs.find((a) => a.name === name);
|
|
36
|
+
if (acc)
|
|
37
|
+
return claudeId(acc.dir);
|
|
38
|
+
if (name === "claude") { // the whole vendor: any account changing counts
|
|
39
|
+
const ids = accs.map((a) => claudeId(a.dir));
|
|
40
|
+
return ids.every(Boolean) ? hash(ids.sort().join(",")) : undefined;
|
|
41
|
+
}
|
|
42
|
+
if (name === "codex") {
|
|
43
|
+
const t = json(join(process.env.CODEX_HOME ?? join(homedir(), ".codex"), "auth.json"))?.tokens;
|
|
44
|
+
return t?.account_id ?? undefined;
|
|
45
|
+
}
|
|
46
|
+
if (name === "antigravity" || name === "agy")
|
|
47
|
+
return agyToken()?.acct;
|
|
48
|
+
if (name === "copilot")
|
|
49
|
+
return copilotId();
|
|
50
|
+
if (name === "opencode" || name === "minimax") {
|
|
51
|
+
const k = minimaxKey();
|
|
52
|
+
return k ? hash(k) : undefined;
|
|
53
|
+
}
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
package/dist/pick.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { m } from "./i18n.js";
|
|
2
|
+
import { humanReset } from "./quota.js";
|
|
2
3
|
import { legacyRole, resolveRole, vendorOf } from "./roles.js";
|
|
3
4
|
/** "<name> 5h=NN% week=NN% [note]" lines → QuotaMap (same reading as the old orq-pick). */
|
|
4
5
|
export function parseQuota(text) {
|
|
@@ -17,6 +18,45 @@ export function parseQuota(text) {
|
|
|
17
18
|
}
|
|
18
19
|
return out;
|
|
19
20
|
}
|
|
21
|
+
/** The same lines → each window's %, plus its reset time (epoch seconds) when the line carries one. */
|
|
22
|
+
export function parseWindows(text) {
|
|
23
|
+
const out = {};
|
|
24
|
+
const num = (v) => (v && /^\d+%$/.test(v) ? Number(v.slice(0, -1)) : undefined);
|
|
25
|
+
for (const l of text.split(/\r?\n/)) {
|
|
26
|
+
const r = /^(\S+) 5h=(\S+) week=(\S+)(?: reset5h=(\d+))?(?: resetweek=(\d+))?(?: span5h=(\d+))?(?: spanweek=(\d+))?/.exec(l.trim());
|
|
27
|
+
if (!r)
|
|
28
|
+
continue;
|
|
29
|
+
// "[login expired: run `agy models`; last reading, 5 min ago]" → the part before the first ";" (the fix to show)
|
|
30
|
+
const note = /\[([^\]]*)\]\s*$/.exec(l)?.[1].split(";")[0].trim();
|
|
31
|
+
out[r[1]] = { five: num(r[2]), week: num(r[3]), ...(r[4] ? { resetFive: Number(r[4]) } : {}),
|
|
32
|
+
...(r[5] ? { resetWeek: Number(r[5]) } : {}), ...(r[6] ? { spanFive: Number(r[6]) } : {}),
|
|
33
|
+
...(r[7] ? { spanWeek: Number(r[7]) } : {}), ...(note && EXPIRED.test(note) ? { expired: note } : note ? { note } : {}) };
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
// Both languages orq writes these notes in (the collectors' not-logged-in and expired-login texts).
|
|
38
|
+
/** agy's idle-token note (collector text, both languages). */
|
|
39
|
+
const IDLE = /token idle|token ocioso/i;
|
|
40
|
+
const EXPIRED = /login expired|login expirado|not logged in|no agy login|sem login/i;
|
|
41
|
+
/** Window lengths in seconds, unless the quota line publishes another (span5h/spanweek, minutes). */
|
|
42
|
+
const WINDOW_S = { five: 5 * 3600, week: 7 * 86400 };
|
|
43
|
+
/** Usage at the window's reset if the pace so far continues (used ÷ elapsed share); undefined when it can't be told:
|
|
44
|
+
* no figure or reset time, or under 10% of the window elapsed (too early to project). */
|
|
45
|
+
export function projected(pct, reset, size, now) {
|
|
46
|
+
if (pct === undefined || pct < 0 || reset === undefined)
|
|
47
|
+
return undefined;
|
|
48
|
+
const left = reset - now;
|
|
49
|
+
if (left <= 0 || left > size)
|
|
50
|
+
return undefined;
|
|
51
|
+
const elapsed = (size - left) / size;
|
|
52
|
+
return elapsed < 0.1 ? undefined : pct / elapsed;
|
|
53
|
+
}
|
|
54
|
+
const FIVE = { simple: 90, medium: 85, heavy: 80 };
|
|
55
|
+
/** [5h limit, weekly limit] for a task level; an unknown level counts as medium. */
|
|
56
|
+
export function limitsFor(pol, level) {
|
|
57
|
+
const five = { ...FIVE, ...(pol.quota_5h ?? {}) };
|
|
58
|
+
return [Math.min(Number(five[level] ?? five.medium), Number(pol.quota_limit ?? 90)), Number(pol.quota_week ?? 95)];
|
|
59
|
+
}
|
|
20
60
|
export function modelFor(a, role, level) {
|
|
21
61
|
if (a.models === undefined)
|
|
22
62
|
return ["", undefined]; // no models configured: the CLI's default
|
|
@@ -35,13 +75,18 @@ export function pick(inp) {
|
|
|
35
75
|
const { cfg, role, level } = inp;
|
|
36
76
|
const agents = (cfg.agents ?? {});
|
|
37
77
|
const pol = (cfg.policy ?? {});
|
|
38
|
-
const
|
|
78
|
+
const [base5, baseWeek] = limitsFor(pol, level);
|
|
39
79
|
const w = weights(cfg);
|
|
80
|
+
const now = inp.now ?? Date.now() / 1000;
|
|
81
|
+
const penalty = Number(pol.pacing_penalty ?? 0.3);
|
|
40
82
|
const res = resolveRole(cfg, role);
|
|
41
83
|
const names = res.mode === "fixed" ? res.order : res.pool;
|
|
42
84
|
const notes = [];
|
|
43
85
|
const out = (n, why) => { notes.push(`${n}: ${why}`); };
|
|
44
86
|
const pass = [];
|
|
87
|
+
// Only with someone else to ask: a single agent must still be able to review.
|
|
88
|
+
const someoneElse = names.some((n) => n !== inp.implAgent && agents[n]);
|
|
89
|
+
const otherVendor = names.some((n) => n !== inp.implAgent && agents[n] && vendorOf(n, agents[n]) !== inp.implVendor);
|
|
45
90
|
for (const name of names) {
|
|
46
91
|
const a = agents[name];
|
|
47
92
|
if (!a) {
|
|
@@ -56,7 +101,11 @@ export function pick(inp) {
|
|
|
56
101
|
out(name, m("blocked (orq block -l)", "bloqueado (orq block -l)"));
|
|
57
102
|
continue;
|
|
58
103
|
}
|
|
59
|
-
if ((role === "review" || role === "opinion") &&
|
|
104
|
+
if ((role === "review" || role === "opinion") && name === inp.implAgent && someoneElse) {
|
|
105
|
+
out(name, m("it implemented this code", "foi quem implementou"));
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if ((role === "review" || role === "opinion") && res.rule === "other-vendor" && inp.implVendor && otherVendor
|
|
60
109
|
&& vendorOf(name, a) === inp.implVendor) {
|
|
61
110
|
out(name, m("same vendor as the implementer", "mesmo fornecedor do implementador"));
|
|
62
111
|
continue;
|
|
@@ -66,22 +115,68 @@ export function pick(inp) {
|
|
|
66
115
|
out(name, m(`no model for ${role}/${level}`, `sem modelo para ${role}/${level}`));
|
|
67
116
|
continue;
|
|
68
117
|
}
|
|
69
|
-
// Quota: an agent with several accounts (claude)
|
|
118
|
+
// Quota: an agent with several accounts (claude) prefers one not projecting past the limit, then the least used.
|
|
70
119
|
const qn = a.quota ?? [];
|
|
120
|
+
const headroom = Math.max(0, Number(a.headroom ?? 0));
|
|
121
|
+
const limit5 = base5 - headroom, limitWeek = baseWeek - headroom;
|
|
71
122
|
const accs = qn.filter((n) => !inp.blocks.has(n)).map((n) => [n, n in inp.quota ? inp.quota[n] : -1]);
|
|
72
123
|
if (qn.length && !accs.length) {
|
|
73
124
|
out(name, m("all accounts blocked", "todas as contas bloqueadas"));
|
|
74
125
|
continue;
|
|
75
126
|
}
|
|
76
|
-
const
|
|
127
|
+
const fix = accs.map(([n]) => inp.windows?.[n]?.expired).filter((x) => Boolean(x));
|
|
128
|
+
if (accs.length && fix.length === accs.length) {
|
|
129
|
+
out(name, fix[0]);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
// An idle agy token with nothing cached: the fix is one agy run, not a quota number.
|
|
133
|
+
const noRead = m("no reading yet: run `agy models` once", "sem leitura ainda: rode `agy models` uma vez");
|
|
134
|
+
const idleNoData = ([n, v]) => v === null && IDLE.test(inp.windows?.[n]?.note ?? "");
|
|
135
|
+
if (accs.length && accs.every(idleNoData)) {
|
|
136
|
+
out(name, noRead);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
const under = ([n, v]) => {
|
|
140
|
+
const q = inp.windows?.[n];
|
|
141
|
+
return !q?.expired && v !== null && (q?.five ?? (q ? -1 : v)) < limit5 && (q?.week ?? (q ? -1 : v)) < limitWeek;
|
|
142
|
+
};
|
|
143
|
+
const usable = accs.filter(under);
|
|
77
144
|
if (accs.length && !usable.length) {
|
|
78
|
-
|
|
145
|
+
const detail = accs.map(([n, v]) => {
|
|
146
|
+
const q = inp.windows?.[n], vn = v ?? -1;
|
|
147
|
+
if (q?.expired)
|
|
148
|
+
return `${n}: ${q.expired}`;
|
|
149
|
+
if (idleNoData([n, v]))
|
|
150
|
+
return `${n}: ${noRead}`;
|
|
151
|
+
const five = q?.five ?? (q ? -1 : vn), week = q?.week ?? (q ? -1 : vn);
|
|
152
|
+
const fiveOver = five >= limit5, weekOver = week >= limitWeek;
|
|
153
|
+
// The account is usable again only once every over-limit window has reset: the latest of their reset times.
|
|
154
|
+
// If one of them has no reset time, that time can't be promised at all.
|
|
155
|
+
const known = [fiveOver ? q?.resetFive : undefined, weekOver ? q?.resetWeek : undefined]
|
|
156
|
+
.filter((t) => t !== undefined);
|
|
157
|
+
const allKnown = (fiveOver || weekOver) && (!fiveOver || q?.resetFive !== undefined) && (!weekOver || q?.resetWeek !== undefined);
|
|
158
|
+
return `${n}=${v}` + (q?.note ? ` (${q.note})` : "") + (allKnown ? ` (${m("back", "volta")} ${humanReset(Math.max(...known))})` : "");
|
|
159
|
+
}).join(", ");
|
|
160
|
+
out(name, `${m("quota", "cota")} >= 5h ${limit5}% / ${m("week", "semana")} ${limitWeek}% ${m("or no data", "ou sem dados")}`
|
|
161
|
+
+ (headroom ? ` (headroom ${headroom})` : "") + ` (${detail})`);
|
|
79
162
|
continue;
|
|
80
163
|
}
|
|
81
|
-
const
|
|
164
|
+
const paceOf = (n) => {
|
|
165
|
+
const q = inp.windows?.[n];
|
|
166
|
+
const p5 = projected(q?.five, q?.resetFive, (q?.spanFive ? q.spanFive * 60 : WINDOW_S.five), now);
|
|
167
|
+
const pw = projected(q?.week, q?.resetWeek, (q?.spanWeek ? q.spanWeek * 60 : WINDOW_S.week), now);
|
|
168
|
+
const paces = [p5, pw].filter((x) => x !== undefined);
|
|
169
|
+
// Hot: past the same per-window limit the filter uses (5h by level, week), headroom included, when pacing is on.
|
|
170
|
+
return { pace: paces.length ? Math.max(...paces) : undefined,
|
|
171
|
+
hot: penalty > 0 && ((p5 !== undefined && p5 > limit5) || (pw !== undefined && pw > limitWeek)) };
|
|
172
|
+
};
|
|
173
|
+
const ranked = usable.map(([n, v]) => ({ n, v: v, ...paceOf(n) }))
|
|
174
|
+
.sort((x, y) => Number(x.hot) - Number(y.hot) || x.v - y.v);
|
|
175
|
+
const chosen = ranked[0];
|
|
176
|
+
const acct = qn.length > 1 ? chosen?.n : undefined;
|
|
82
177
|
const statRole = role === "fix" || role === "worker" ? "impl" : role;
|
|
83
178
|
const st = inp.stats.find((r) => r.role === statRole && r.agent === name && r.model === (mf[0].split("/").pop() || "default"));
|
|
84
|
-
pass.push({ agent: name, model: mf[0], effort: mf[1], acct, a, st });
|
|
179
|
+
pass.push({ agent: name, model: mf[0], effort: mf[1], acct, a, st, pace: chosen?.pace, hot: Boolean(chosen?.hot) });
|
|
85
180
|
if (res.mode === "fixed")
|
|
86
181
|
break; // the first that passes wins
|
|
87
182
|
}
|
|
@@ -102,11 +197,13 @@ export function pick(inp) {
|
|
|
102
197
|
const t = few(c.st) || !c.st?.new || !toks.length ? prior : Math.min(...toks) / c.st.new;
|
|
103
198
|
const s = few(c.st) || !c.st?.min || !mins.length ? prior : Math.min(...mins) / Math.max(c.st.min, 0.1);
|
|
104
199
|
const used = inp.recent.filter((p) => p.agent === c.agent).length;
|
|
200
|
+
const hot = Boolean(c.hot) && penalty > 0;
|
|
105
201
|
const score = w.quality * d + w.tokens * t + w.speed * s - w.rotation * used
|
|
106
|
-
- (reserve ? w.reserve : 0) + (few(c.st) ? w.explore : 0);
|
|
202
|
+
- (reserve ? w.reserve : 0) + (few(c.st) ? w.explore : 0) - (hot ? penalty : 0);
|
|
107
203
|
const why = `${m("delivery", "entrega")}=${d.toFixed(2)} ${m("economy", "economia")}=${t.toFixed(2)} `
|
|
108
204
|
+ `${m("speed", "rapidez")}=${s.toFixed(2)} ${m("rotation", "rodízio")}=-${used} `
|
|
109
|
-
+ (reserve ? m("reserve ", "reserva ") : "")
|
|
205
|
+
+ (reserve ? m("reserve ", "reserva ") : "")
|
|
206
|
+
+ (hot ? `${m("pace", "ritmo")}=${Math.round(c.pace)}%→-${penalty} ` : "") + (few(c.st) ? `${m("few samples", "poucos dados")}(n=${n})` : `n=${n}`);
|
|
110
207
|
return { agent: c.agent, model: c.model, effort: c.effort, acct: c.acct, score, why };
|
|
111
208
|
}).sort((x, y) => y.score - x.score);
|
|
112
209
|
return { mode: "auto", best: cands[0], cands, notes };
|
package/dist/process.js
CHANGED
|
@@ -58,27 +58,29 @@ function parseTable(text) {
|
|
|
58
58
|
}
|
|
59
59
|
return t;
|
|
60
60
|
}
|
|
61
|
-
/**
|
|
62
|
-
export function
|
|
61
|
+
/** `root` and all its descendants. */
|
|
62
|
+
export function treePids(root, table = processTable()) {
|
|
63
63
|
const kids = new Map();
|
|
64
64
|
for (const [pid, [ppid]] of table)
|
|
65
65
|
kids.set(ppid, [...(kids.get(ppid) ?? []), pid]);
|
|
66
|
-
let total = 0;
|
|
67
66
|
const seen = new Set(), todo = [root];
|
|
68
67
|
while (todo.length) {
|
|
69
68
|
const p = todo.pop();
|
|
70
69
|
if (seen.has(p))
|
|
71
70
|
continue;
|
|
72
71
|
seen.add(p);
|
|
73
|
-
total += table.get(p)?.[1] ?? 0;
|
|
74
72
|
todo.push(...(kids.get(p) ?? []));
|
|
75
73
|
}
|
|
76
|
-
return
|
|
74
|
+
return [...seen];
|
|
75
|
+
}
|
|
76
|
+
/** Total memory of `root` and all its descendants. */
|
|
77
|
+
export function treeMem(root, table = processTable()) {
|
|
78
|
+
return treePids(root, table).reduce((t, p) => t + (table.get(p)?.[1] ?? 0), 0);
|
|
77
79
|
}
|
|
78
|
-
export function killTree(pid) {
|
|
80
|
+
export function killTree(pid, group = true) {
|
|
79
81
|
if (process.platform === "win32")
|
|
80
82
|
spawnSync("taskkill", ["/T", "/F", "/PID", String(pid)], { stdio: "ignore" });
|
|
81
|
-
else
|
|
83
|
+
else if (group)
|
|
82
84
|
try {
|
|
83
85
|
process.kill(-pid, "SIGKILL");
|
|
84
86
|
}
|
|
@@ -88,6 +90,12 @@ export function killTree(pid) {
|
|
|
88
90
|
}
|
|
89
91
|
catch { /* gone */ }
|
|
90
92
|
}
|
|
93
|
+
else
|
|
94
|
+
for (const p of treePids(pid))
|
|
95
|
+
try {
|
|
96
|
+
process.kill(p, "SIGKILL");
|
|
97
|
+
}
|
|
98
|
+
catch { /* gone */ } // no group of its own
|
|
91
99
|
}
|
|
92
100
|
/** cmd.exe quoting for one argument (only used when Windows needs a shell). */
|
|
93
101
|
export const winQuote = (a) => /[\s"&|<>^%()]/.test(a) || a === "" ? `"${a.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/, "$1$1")}"` : a;
|
|
@@ -103,10 +111,10 @@ export function runCapped(cmd, args, o = {}) {
|
|
|
103
111
|
return wait(c);
|
|
104
112
|
}
|
|
105
113
|
const win = process.platform === "win32";
|
|
106
|
-
// Own process group on Linux/macOS, so the whole tree can be killed at once.
|
|
114
|
+
// Own process group on Linux/macOS, so the whole tree can be killed at once (not for a screen: it needs the terminal).
|
|
107
115
|
// Windows: a shell only for .cmd/.bat shims (npx, claude…); an .exe runs directly (paths with spaces stay intact).
|
|
108
116
|
const shell = win && !/\.exe$/i.test(cmd), q = shell && o.quote;
|
|
109
|
-
const child = spawn(q ? winQuote(cmd) : cmd, q ? args.map(winQuote) : args, { stdio: o.stdio ?? "inherit", cwd: o.cwd, env: o.env, detached: !win, shell, windowsHide: true });
|
|
117
|
+
const child = spawn(q ? winQuote(cmd) : cmd, q ? args.map(winQuote) : args, { stdio: o.stdio ?? "inherit", cwd: o.cwd, env: o.env, detached: !win && !o.tty, shell, windowsHide: true });
|
|
110
118
|
o.onSpawn?.(child);
|
|
111
119
|
if (mode === "off" || !child.pid)
|
|
112
120
|
return wait(child);
|
|
@@ -117,7 +125,7 @@ export function runCapped(cmd, args, o = {}) {
|
|
|
117
125
|
child.kill(sig);
|
|
118
126
|
} };
|
|
119
127
|
const onInt = fwd("SIGINT"), onTerm = fwd("SIGTERM");
|
|
120
|
-
if (!win) {
|
|
128
|
+
if (!win && !o.tty) {
|
|
121
129
|
process.on("SIGINT", onInt);
|
|
122
130
|
process.on("SIGTERM", onTerm);
|
|
123
131
|
}
|
|
@@ -131,7 +139,7 @@ export function runCapped(cmd, args, o = {}) {
|
|
|
131
139
|
if (used > o.cap && !killed) {
|
|
132
140
|
killed = true;
|
|
133
141
|
console.error(m(`orq: memory ${gb(used)} over the cap (${gb(o.cap)}): killing the process tree`, `orq: memória ${gb(used)} acima do limite (${gb(o.cap)}): encerrando a árvore de processos`));
|
|
134
|
-
killTree(child.pid);
|
|
142
|
+
killTree(child.pid, !o.tty);
|
|
135
143
|
}
|
|
136
144
|
}, o.intervalMs ?? 1000);
|
|
137
145
|
return wait(child).then((code) => {
|