@irythill/orq 0.1.0 → 0.3.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/LICENSE +21 -21
- package/README.md +241 -240
- package/README.pt-BR.md +242 -241
- package/dist/agents.js +12 -0
- package/dist/cli.js +24 -22
- package/dist/commands/agent.js +212 -26
- package/dist/commands/block.js +2 -2
- package/dist/commands/board.js +1 -1
- package/dist/commands/claim.js +1 -1
- package/dist/commands/completion.js +43 -43
- package/dist/commands/config.js +2 -2
- package/dist/commands/gc.js +164 -0
- package/dist/commands/instructions.js +7 -7
- package/dist/commands/lock.js +1 -1
- package/dist/commands/pick.js +23 -3
- package/dist/commands/quota.js +32 -2
- package/dist/commands/setup.js +3 -3
- package/dist/config.js +1 -1
- package/dist/pick.js +11 -0
- package/dist/process.js +19 -11
- package/dist/quota.js +67 -9
- package/dist/terminal.js +26 -0
- package/dist/testrun.js +9 -5
- package/package.json +48 -27
- package/skills/issue/SKILL.md +229 -197
- package/skills/orq-setup/SKILL.md +114 -114
- package/skills/orq-status/SKILL.md +37 -37
- package/templates/workspace.CLAUDE.md +49 -49
|
@@ -15,13 +15,13 @@ const END = "<!-- orq:issue end -->";
|
|
|
15
15
|
/** The playbook for harnesses without skills: no frontmatter, a trigger line, and how to wait without background tasks. */
|
|
16
16
|
export function agentsBlock(skill) {
|
|
17
17
|
const body = skill.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, "").trim();
|
|
18
|
-
return `${BEGIN}
|
|
19
|
-
When the user says "/issue <ID>", "take issue X" or "run ABC-123", follow this playbook.
|
|
20
|
-
If your harness cannot run a command in the background, wait for children with \`orq wait\` in the foreground
|
|
21
|
-
(\`orq wait --any <prefix>...\`): it is the one allowed blocking wait.
|
|
22
|
-
|
|
23
|
-
${body}
|
|
24
|
-
${END}
|
|
18
|
+
return `${BEGIN}
|
|
19
|
+
When the user says "/issue <ID>", "take issue X" or "run ABC-123", follow this playbook.
|
|
20
|
+
If your harness cannot run a command in the background, wait for children with \`orq wait\` in the foreground
|
|
21
|
+
(\`orq wait --any <prefix>...\`): it is the one allowed blocking wait.
|
|
22
|
+
|
|
23
|
+
${body}
|
|
24
|
+
${END}
|
|
25
25
|
`;
|
|
26
26
|
}
|
|
27
27
|
/** Put the block into an existing AGENTS.md text: replace ours, or append it. */
|
package/dist/commands/lock.js
CHANGED
|
@@ -7,7 +7,7 @@ import { spawnSync } from "node:child_process";
|
|
|
7
7
|
import { getPath, loadConfig } from "../config.js";
|
|
8
8
|
import { m } from "../i18n.js";
|
|
9
9
|
import { acquire, release, status } from "../lock.js";
|
|
10
|
-
export const USAGE = `orq lock wait|release <name> [--pid P] [--label text] test queue (slots = machine.test_slots)
|
|
10
|
+
export const USAGE = `orq lock wait|release <name> [--pid P] [--label text] test queue (slots = machine.test_slots)
|
|
11
11
|
orq lock run <name> [--label text] -- <cmd>... · orq lock status [<name>] [--json]`;
|
|
12
12
|
const slots = () => Number(getPath(loadConfig().cfg, "machine.test_slots") ?? 1);
|
|
13
13
|
const since = (at) => `${Math.round((Date.now() - at) / 1000)}s`;
|
package/dist/commands/pick.js
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
// orq pick — agent, model, effort and account for a role, from [roles] (fixed or auto), the history, quota and blocks.
|
|
2
2
|
// Prints the runner arguments (e.g. `-a codex -m gpt-6-sol -e low -c work`); each pick is recorded in
|
|
3
3
|
// <state dir>/picks.jsonl (that feeds rotation). Exit 3 = no candidate: ask the user.
|
|
4
|
+
// `orq pick fix --same <file>`: the implementer's saved pick, effort one level up (no choice, nothing recorded).
|
|
4
5
|
import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync } from "node:fs";
|
|
5
6
|
import { tmpdir } from "node:os";
|
|
6
7
|
import { join } from "node:path";
|
|
7
8
|
import { loadConfig } from "../config.js";
|
|
8
9
|
import { m } from "../i18n.js";
|
|
9
|
-
import { asArgs, parseQuota, pick as choose } from "../pick.js";
|
|
10
|
+
import { asArgs, escalate, parseQuota, pick as choose } from "../pick.js";
|
|
10
11
|
import { expand, stateDir } from "../paths.js";
|
|
11
12
|
import { collect } from "../quota.js";
|
|
12
13
|
import { PICK_ROLES, vendorOf } from "../roles.js";
|
|
13
14
|
import { collectRuns, rows } from "../stats.js";
|
|
14
15
|
import { load as loadBlocks } from "./block.js";
|
|
15
|
-
export const USAGE = `orq pick <impl|fix|worker|review|opinion> [--level simple|medium|heavy] [--impl-vendor <v>|--impl-agent <a>]
|
|
16
|
-
[--exclude-agent <a>]... [-w <workspace>] [--explain] [--no-record]
|
|
16
|
+
export const USAGE = `orq pick <impl|fix|worker|review|opinion> [--level simple|medium|heavy] [--impl-vendor <v>|--impl-agent <a>]
|
|
17
|
+
[--exclude-agent <a>]... [-w <workspace>] [--explain] [--no-record]
|
|
18
|
+
orq pick fix --same <impl pick file> same agent/model/account as the implementer, effort one level up`;
|
|
17
19
|
// The board refreshes this file; fresher than 10 min → no new quota requests.
|
|
18
20
|
const QCACHE = join(process.env.XDG_RUNTIME_DIR ?? tmpdir(), "herdr-board.quota");
|
|
19
21
|
async function quotaText(cfg) {
|
|
@@ -27,6 +29,24 @@ export async function pick(argv) {
|
|
|
27
29
|
console.error(USAGE);
|
|
28
30
|
return 2;
|
|
29
31
|
}
|
|
32
|
+
if (role === "fix" && argv[1] === "--same") {
|
|
33
|
+
const f = argv[2];
|
|
34
|
+
if (!f || argv.length > 3) {
|
|
35
|
+
console.error(USAGE);
|
|
36
|
+
return 2;
|
|
37
|
+
}
|
|
38
|
+
if (!existsSync(f)) {
|
|
39
|
+
console.error(`orq pick: ${m("no such file: ", "arquivo não existe: ")}${f}`);
|
|
40
|
+
return 2;
|
|
41
|
+
}
|
|
42
|
+
const prev = readFileSync(f, "utf8").trim();
|
|
43
|
+
if (!/(^|\s)-a \S/.test(prev)) {
|
|
44
|
+
console.error(`orq pick: ${m("not a pick: ", "não é um pick: ")}${f}`);
|
|
45
|
+
return 2;
|
|
46
|
+
}
|
|
47
|
+
console.log(escalate(prev));
|
|
48
|
+
return 0;
|
|
49
|
+
}
|
|
30
50
|
let level = "medium", implVendor, implAgent, ws;
|
|
31
51
|
let explain = false, record = true;
|
|
32
52
|
const exclude = new Set();
|
package/dist/commands/quota.js
CHANGED
|
@@ -1,9 +1,39 @@
|
|
|
1
1
|
// orq quota [name...] quota of each source in [quota] (default: all) — lines "<name> 5h=NN% week=NN% [note]"
|
|
2
|
+
// orq quota --line one line for a status bar (herdr's tab_bar_right command entry): the board's cache, never
|
|
3
|
+
// the network — a stale cache starts the board's background collection, like the board does.
|
|
2
4
|
import { getPath, loadConfig } from "../config.js";
|
|
5
|
+
import { m } from "../i18n.js";
|
|
3
6
|
import { collect } from "../quota.js";
|
|
4
|
-
|
|
7
|
+
import { quota as cached } from "./board.js";
|
|
8
|
+
import { load as loadBlocks } from "./block.js";
|
|
9
|
+
export const USAGE = `orq quota [name...] quota per agent/account ([quota] in the config)
|
|
10
|
+
orq quota --line one line for a status bar, from the board's cache (e.g. herdr tab_bar_right)`;
|
|
11
|
+
const pct = (p) => (/^\d+%$/.test(p) ? Number(p.slice(0, -1)) : undefined);
|
|
12
|
+
/** "work 64 · home 93! · codex 81 · minimax ?": the tighter of the two windows per source; ! at or over the pick
|
|
13
|
+
* limit, ⊘ blocked (orq block), ? no data; sources with no window at all ("-" and "-") are left out. */
|
|
14
|
+
export function quotaLine(lines, limit, blocked, age) {
|
|
15
|
+
const parts = lines.filter((q) => q.five !== "-" || q.week !== "-").map((q) => {
|
|
16
|
+
if (blocked.has(q.name))
|
|
17
|
+
return `${q.name} ⊘`;
|
|
18
|
+
const ns = [pct(q.five), pct(q.week)].filter((n) => n !== undefined);
|
|
19
|
+
if (!ns.length)
|
|
20
|
+
return `${q.name} ?`;
|
|
21
|
+
const top = Math.max(...ns);
|
|
22
|
+
return `${q.name} ${top}${top >= limit ? "!" : ""}`;
|
|
23
|
+
});
|
|
24
|
+
if (!parts.length)
|
|
25
|
+
return age === undefined ? m("quota: collecting...", "cota: coletando...") : "";
|
|
26
|
+
const old = age !== undefined && age > 15 * 60_000 ? ` (${Math.round(age / 60_000)} min)` : "";
|
|
27
|
+
return parts.join(" · ") + old;
|
|
28
|
+
}
|
|
5
29
|
export async function quota(argv) {
|
|
6
|
-
const
|
|
30
|
+
const { cfg } = loadConfig();
|
|
31
|
+
if (argv[0] === "--line") {
|
|
32
|
+
const q = cached(Date.now());
|
|
33
|
+
console.log(quotaLine(q.lines, Number(getPath(cfg, "policy.quota_limit") ?? 90), new Set(Object.keys(loadBlocks())), q.age));
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
36
|
+
const sources = (getPath(cfg, "quota") ?? {});
|
|
7
37
|
for (const l of await collect(sources, argv))
|
|
8
38
|
console.log(l);
|
|
9
39
|
return 0;
|
package/dist/commands/setup.js
CHANGED
|
@@ -18,7 +18,7 @@ import { m } from "../i18n.js";
|
|
|
18
18
|
import { environment, installSkills, prepareWorkspace } from "../install.js";
|
|
19
19
|
import { expand, userConfigFile } from "../paths.js";
|
|
20
20
|
import { suggestRoles, toToml } from "../suggest.js";
|
|
21
|
-
export const USAGE = `orq setup [--preset economy|quality|single-agent] guided first configuration (roles, weights, memory)
|
|
21
|
+
export const USAGE = `orq setup [--preset economy|quality|single-agent] guided first configuration (roles, weights, memory)
|
|
22
22
|
orq setup --detect | --answers <file|-> [--write] | --install | --workspace <dir> [--conductor <agent>]`;
|
|
23
23
|
/** Agent name → its CLI and a cheap headless smoke test. */
|
|
24
24
|
const CLIS = {
|
|
@@ -139,8 +139,8 @@ function workspace(dir, conductor) {
|
|
|
139
139
|
for (const l of prepareWorkspace(d, c === "claude" ? "claude" : "agents"))
|
|
140
140
|
console.log(l);
|
|
141
141
|
}
|
|
142
|
-
const next = () => console.log(m(`Next: fill in the workspace CLAUDE.md, open your conductor in that folder and say "/issue <ID>".
|
|
143
|
-
Board: orq board · quota: orq quota · completion: orq completion <shell>`, `Próximo: preencha o CLAUDE.md do workspace, abra o seu conductor nessa pasta e diga "/issue <ID>".
|
|
142
|
+
const next = () => console.log(m(`Next: fill in the workspace CLAUDE.md, open your conductor in that folder and say "/issue <ID>".
|
|
143
|
+
Board: orq board · quota: orq quota · completion: orq completion <shell>`, `Próximo: preencha o CLAUDE.md do workspace, abra o seu conductor nessa pasta e diga "/issue <ID>".
|
|
144
144
|
Painel: orq board · cota: orq quota · autocompletar: orq completion <shell>`));
|
|
145
145
|
export async function setup(argv) {
|
|
146
146
|
let preset, answers, doWrite = false, ws, conductor;
|
package/dist/config.js
CHANGED
|
@@ -27,7 +27,7 @@ export const DEFAULTS = {
|
|
|
27
27
|
ui: { lang: "auto", theme: "gunmetal" }, // lang: auto | en | pt · theme: gunmetal | chrome | lcd | terminal
|
|
28
28
|
// Where quota comes from, per agent: "builtin" | "none" | false | { command = "..." } (see src/quota.ts).
|
|
29
29
|
// "claude" covers every account in claude-accounts.
|
|
30
|
-
quota: { claude: "builtin", codex: "builtin", opencode: "
|
|
30
|
+
quota: { claude: "builtin", codex: "builtin", opencode: "builtin", antigravity: "builtin", copilot: "builtin" },
|
|
31
31
|
tracker: { kind: "none" }, // plane | none
|
|
32
32
|
repos: {}, // defined by the workspace
|
|
33
33
|
};
|
package/dist/pick.js
CHANGED
|
@@ -111,5 +111,16 @@ export function pick(inp) {
|
|
|
111
111
|
}).sort((x, y) => y.score - x.score);
|
|
112
112
|
return { mode: "auto", best: cands[0], cands, notes };
|
|
113
113
|
}
|
|
114
|
+
const EFFORTS = ["low", "medium", "high"];
|
|
115
|
+
/** A fix continues the implementer's session: the same runner arguments (agent, model, account) with the effort one
|
|
116
|
+
* level up, capped at high. Keeping the model keeps the session's prompt cache; an unknown or absent effort stays. */
|
|
117
|
+
export function escalate(args) {
|
|
118
|
+
const p = args.trim().split(/\s+/);
|
|
119
|
+
const i = p.indexOf("-e");
|
|
120
|
+
const at = i >= 0 ? EFFORTS.indexOf(p[i + 1]) : -1;
|
|
121
|
+
if (at >= 0)
|
|
122
|
+
p[i + 1] = EFFORTS[Math.min(at + 1, EFFORTS.length - 1)];
|
|
123
|
+
return p.join(" ");
|
|
124
|
+
}
|
|
114
125
|
/** The arguments for the agent runner, e.g. "-a codex -m gpt-6-sol -e low -c work". */
|
|
115
126
|
export const asArgs = (c) => `-a ${c.agent}` + (c.model ? ` -m ${c.model}` : "") + (c.effort ? ` -e ${c.effort}` : "") + (c.acct ? ` -c ${c.acct}` : "");
|
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) => {
|
package/dist/quota.js
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
// codex: the rate_limits Codex writes into its latest session file
|
|
4
4
|
// antigravity: per-model quota from Cloud Code (fetchAvailableModels), with the login agy keeps
|
|
5
5
|
// in the OS keyring (src/keyring.ts: Secret Service, Keychain, Credential Manager)
|
|
6
|
+
// opencode: the MiniMax Token Plan (/v1/token_plan/remains) with $MINIMAX_API_KEY or the key
|
|
7
|
+
// OpenCode keeps in its auth.json; $MINIMAX_API_HOST for the China region
|
|
6
8
|
// copilot: premium requests left this month (`gh api /copilot_internal/user`; a monthly
|
|
7
9
|
// allowance, shown in the week slot)
|
|
8
10
|
// false left out of the list entirely
|
|
@@ -117,7 +119,8 @@ function latestCodexSession() {
|
|
|
117
119
|
walk(root, 0);
|
|
118
120
|
return best?.f;
|
|
119
121
|
}
|
|
120
|
-
/** The last rate_limits in a Codex session (.jsonl):
|
|
122
|
+
/** The last rate_limits in a Codex session (.jsonl): a window up to 6 h = 5h; the longest one over that (10080 = week,
|
|
123
|
+
* 43200 on the free plan = 30 days) fills the week slot, with `span` (minutes) when it isn't a week; reset → 0. */
|
|
121
124
|
export function codexFromSession(text, nowSec) {
|
|
122
125
|
let last;
|
|
123
126
|
for (const line of text.split("\n")) {
|
|
@@ -139,13 +142,11 @@ export function codexFromSession(text, nowSec) {
|
|
|
139
142
|
if (!last)
|
|
140
143
|
return undefined;
|
|
141
144
|
const ws = [last.primary, last.secondary].filter((w) => !!w);
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
};
|
|
148
|
-
return { five: win(300), week: win(10080) };
|
|
145
|
+
const pct = (w) => !w ? "-" : (w.resets_at ?? 0) < nowSec ? 0 : Math.floor(w.used_percent ?? 0);
|
|
146
|
+
const short = ws.find((x) => (x.window_minutes ?? 0) <= 360);
|
|
147
|
+
const long = ws.filter((x) => (x.window_minutes ?? 0) > 360).sort((a, b) => b.window_minutes - a.window_minutes)[0];
|
|
148
|
+
const span = long && long.window_minutes !== 10080 ? long.window_minutes : undefined;
|
|
149
|
+
return { five: pct(short), week: pct(long), ...(span ? { span } : {}) };
|
|
149
150
|
}
|
|
150
151
|
export function codexQuota() {
|
|
151
152
|
const f = latestCodexSession();
|
|
@@ -155,7 +156,10 @@ export function codexQuota() {
|
|
|
155
156
|
if (!q)
|
|
156
157
|
return noData("codex", m("no limit data", "sem dados de limite"));
|
|
157
158
|
const age = Math.round((Date.now() - statSync(f).mtimeMs) / 60_000);
|
|
158
|
-
|
|
159
|
+
const { span, ...pcts } = q;
|
|
160
|
+
const d = span && Math.round(span / 1440);
|
|
161
|
+
return { name: "codex", ...pcts, note: (d ? m(`${d}-day window in the week slot; `, `janela de ${d} dias no lugar da semana; `) : "")
|
|
162
|
+
+ m(`from the last session, ${age} min ago`, `da última sessão, há ${age} min`) };
|
|
159
163
|
}
|
|
160
164
|
/** Worst usage per window across models: a reset within 6 h belongs to the 5h window, a later one to the week. */
|
|
161
165
|
export function antigravityFromModels(models, now = Date.now()) {
|
|
@@ -243,6 +247,58 @@ export function copilotQuota() {
|
|
|
243
247
|
return noData("copilot", m("collector failed", "coletor falhou"));
|
|
244
248
|
}
|
|
245
249
|
}
|
|
250
|
+
/** Worst usage per window across the plan's models (remaining % → used %); resets from the windows' end times. */
|
|
251
|
+
export function minimaxFromRemains(j) {
|
|
252
|
+
const ms = j?.model_remains ?? [];
|
|
253
|
+
if (!ms.length)
|
|
254
|
+
return undefined;
|
|
255
|
+
const worst = (pct, end) => {
|
|
256
|
+
let w = { pct: "-" };
|
|
257
|
+
for (const x of ms) {
|
|
258
|
+
if (typeof x[pct] !== "number")
|
|
259
|
+
continue;
|
|
260
|
+
const used = 100 - x[pct];
|
|
261
|
+
if (w.pct === "-" || used > w.pct)
|
|
262
|
+
w = { pct: used, ...(typeof x[end] === "number" ? { resetsAt: new Date(x[end]).toISOString() } : {}) };
|
|
263
|
+
}
|
|
264
|
+
return w;
|
|
265
|
+
};
|
|
266
|
+
return { five: worst("current_interval_remaining_percent", "end_time"),
|
|
267
|
+
week: worst("current_weekly_remaining_percent", "weekly_end_time") };
|
|
268
|
+
}
|
|
269
|
+
function minimaxKey() {
|
|
270
|
+
if (process.env.MINIMAX_API_KEY)
|
|
271
|
+
return process.env.MINIMAX_API_KEY;
|
|
272
|
+
const data = process.env.XDG_DATA_HOME ?? join(homedir(), ".local", "share");
|
|
273
|
+
try {
|
|
274
|
+
return JSON.parse(readFileSync(join(data, "opencode", "auth.json"), "utf8")).minimax?.key;
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
return undefined;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
export async function minimaxQuota(name) {
|
|
281
|
+
const key = minimaxKey();
|
|
282
|
+
// OpenCode may run another provider: without a MiniMax key there is nothing to limit.
|
|
283
|
+
if (!key)
|
|
284
|
+
return { name, five: "-", week: "-", note: m("no MiniMax key", "sem chave da MiniMax") };
|
|
285
|
+
try {
|
|
286
|
+
const host = process.env.MINIMAX_API_HOST ?? "api.minimax.io";
|
|
287
|
+
const r = await fetch(`https://${host}/v1/token_plan/remains`, {
|
|
288
|
+
headers: { Authorization: `Bearer ${key}` }, signal: AbortSignal.timeout(15_000)
|
|
289
|
+
});
|
|
290
|
+
if (!r.ok)
|
|
291
|
+
return fromCache(name, `HTTP ${r.status}`);
|
|
292
|
+
const j = await r.json();
|
|
293
|
+
if (j.base_resp?.status_code)
|
|
294
|
+
return fromCache(name, `MiniMax ${j.base_resp.status_code}: ${j.base_resp.status_msg ?? ""}`.trim());
|
|
295
|
+
const q = minimaxFromRemains(j);
|
|
296
|
+
return q ? remember(name, q.five, q.week) : noData(name, m("no plan data", "sem dados do plano"));
|
|
297
|
+
}
|
|
298
|
+
catch (e) {
|
|
299
|
+
return fromCache(name, m("collector failed", "coletor falhou") + `: ${e.message}`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
246
302
|
// --- command ----------------------------------------------------------------------------------------------------------
|
|
247
303
|
function commandQuota(name, cmd) {
|
|
248
304
|
const r = spawnSync(cmd, { shell: true, encoding: "utf8", timeout: 30_000 });
|
|
@@ -270,6 +326,8 @@ export async function collect(sources, want = []) {
|
|
|
270
326
|
return wanted(name) ? [format(await antigravityQuota())] : [];
|
|
271
327
|
if (name === "copilot")
|
|
272
328
|
return wanted(name) ? [format(copilotQuota())] : [];
|
|
329
|
+
if (name === "opencode" || name === "minimax")
|
|
330
|
+
return wanted(name) ? [format(await minimaxQuota(name))] : [];
|
|
273
331
|
return wanted(name) ? [format(noData(name, m("no builtin collector: use \"none\" or a command", "sem coletor embutido: use \"none\" ou um comando")))] : [];
|
|
274
332
|
});
|
|
275
333
|
return (await Promise.all(jobs)).flat();
|
package/dist/terminal.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// tmux a background window in the current tmux session
|
|
4
4
|
// plain a detached background process; its screen output goes to <state dir>/agents/<run>.log
|
|
5
5
|
// auto = herdr inside a herdr pane (HERDR_PANE_ID), else tmux inside tmux (TMUX), else plain.
|
|
6
|
+
// The agent's own screen (-T) needs herdr or tmux; follow-ups are typed into it while it is open.
|
|
6
7
|
import { spawn, spawnSync } from "node:child_process";
|
|
7
8
|
import { closeSync, mkdirSync, openSync } from "node:fs";
|
|
8
9
|
import { join } from "node:path";
|
|
@@ -69,6 +70,31 @@ export function launch(host, l) {
|
|
|
69
70
|
closeSync(fd);
|
|
70
71
|
return `pid:${c.pid}`;
|
|
71
72
|
}
|
|
73
|
+
/** Is the screen `launch` returned still open? (herdr pane or tmux pane; a background pid has no screen) */
|
|
74
|
+
export function screenOpen(where) {
|
|
75
|
+
if (where.startsWith("tmux:"))
|
|
76
|
+
return spawnSync("tmux", ["display-message", "-p", "-t", where.slice(5), "#{pane_id}"], { stdio: "ignore" }).status === 0;
|
|
77
|
+
if (!where || where.startsWith("pid:"))
|
|
78
|
+
return false;
|
|
79
|
+
return spawnSync("herdr", ["pane", "get", where], { stdio: "ignore" }).status === 0;
|
|
80
|
+
}
|
|
81
|
+
/** Type `text` into an open screen and submit it (a follow-up to an agent's own TUI). */
|
|
82
|
+
export function typeInto(where, text) {
|
|
83
|
+
if (where.startsWith("tmux:")) {
|
|
84
|
+
// A bracketed paste keeps a multi-line prompt as one message; then Enter sends it.
|
|
85
|
+
const t = where.slice(5), run = (a, input) => {
|
|
86
|
+
const r = spawnSync("tmux", a, { input, encoding: "utf8" });
|
|
87
|
+
if (r.status !== 0)
|
|
88
|
+
throw new Error(`tmux ${a[0]}: ${(r.stderr || "").trim()}`);
|
|
89
|
+
};
|
|
90
|
+
run(["load-buffer", "-b", "orq-prompt", "-"], text);
|
|
91
|
+
run(["paste-buffer", "-p", "-d", "-b", "orq-prompt", "-t", t]);
|
|
92
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 300); // let the TUI take the paste before Enter
|
|
93
|
+
run(["send-keys", "-t", t, "Enter"]);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
herdr(["agent", "prompt", where, text]);
|
|
97
|
+
}
|
|
72
98
|
/** After a run: close our own herdr tab (the result stays in the files). tmux windows close when the runner exits. */
|
|
73
99
|
export function closeOwnTab(host) {
|
|
74
100
|
const p = process.env.HERDR_PANE_ID;
|
package/dist/testrun.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// memory cap (src/process.ts) and read its JSON report into "<file relative to the repo> :: <full test name>" lines.
|
|
3
3
|
// The runner's own bin (node_modules/<runner>/package.json "bin") runs with this node, so Windows needs no shell;
|
|
4
4
|
// without it, npx as before. Also the background job both share (launch on a TerminalHost, queue, .pid/.done, notify).
|
|
5
|
-
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
6
6
|
import { createWriteStream, existsSync, lstatSync, readFileSync, realpathSync, rmdirSync, rmSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
|
|
7
7
|
import { dirname, isAbsolute, join, relative } from "node:path";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
@@ -196,8 +196,12 @@ export async function inJob(j, label, slots, body, done) {
|
|
|
196
196
|
}
|
|
197
197
|
return st === "ok" ? 0 : 1;
|
|
198
198
|
}
|
|
199
|
-
/** The conductor is a herdr pane: type the line into it (herdr-notify waits for it to leave a dialog).
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
199
|
+
/** The conductor is a herdr pane: type the line into it (herdr-notify waits for it to leave a dialog). In the
|
|
200
|
+
* background when the caller must not wait for that (an agent's end-of-turn hook holds its screen meanwhile). */
|
|
201
|
+
export function notify(pane, line, background = false) {
|
|
202
|
+
const local = join(dirname(CLI), "..", "setup", "herdr-notify"), cmd = existsSync(local) ? local : "herdr-notify";
|
|
203
|
+
if (background)
|
|
204
|
+
spawn(cmd, [pane, line], { detached: true, stdio: "ignore" }).unref();
|
|
205
|
+
else
|
|
206
|
+
spawnSync(cmd, [pane, line], { stdio: "inherit" });
|
|
203
207
|
}
|
package/package.json
CHANGED
|
@@ -1,27 +1,48 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@irythill/orq",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Orchestrate coding agents (Claude Code, Codex, OpenCode, Antigravity, Copilot) from one conductor.",
|
|
5
|
-
"license": "MIT",
|
|
6
|
-
"type": "module",
|
|
7
|
-
"bin": {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
"
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"
|
|
18
|
-
},
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
},
|
|
23
|
-
"
|
|
24
|
-
|
|
25
|
-
"
|
|
26
|
-
|
|
27
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@irythill/orq",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Orchestrate coding agents (Claude Code, Codex, OpenCode, Antigravity, Copilot) from one conductor.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"orq": "dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"!dist/**/*.test.js",
|
|
13
|
+
"skills",
|
|
14
|
+
"templates/workspace.CLAUDE.md"
|
|
15
|
+
],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=20"
|
|
18
|
+
},
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/irythill/orq.git"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://github.com/irythill/orq#readme",
|
|
24
|
+
"keywords": [
|
|
25
|
+
"agents",
|
|
26
|
+
"orchestration",
|
|
27
|
+
"claude-code",
|
|
28
|
+
"codex",
|
|
29
|
+
"opencode",
|
|
30
|
+
"cli"
|
|
31
|
+
],
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsc",
|
|
37
|
+
"test": "tsc && node --test dist/orq.test.js",
|
|
38
|
+
"prepack": "tsc"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@napi-rs/keyring": "^2.1.1",
|
|
42
|
+
"smol-toml": "^1.9.1"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/node": "^24.0.0",
|
|
46
|
+
"typescript": "^7.0.2"
|
|
47
|
+
}
|
|
48
|
+
}
|