@irythill/orq 0.1.0 → 0.2.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.
@@ -11,7 +11,7 @@ const DESC = {
11
11
  quota: "quota per agent/account", pick: "pick agent, model and account for a role", stats: "scoreboard per agent",
12
12
  board: "live board", lock: "test queue", run: "run a command under a memory cap", agent: "run a child agent",
13
13
  wait: "wait for child agents", verify: "tests of a diff", baseline: "tests already failing on the base",
14
- instructions: "write the /issue playbook", claim: "issue owner among conductors", deps: "link node_modules into a worktree",
14
+ instructions: "write the /issue playbook", claim: "issue owner among conductors", deps: "link node_modules into a worktree", gc: "remove merged issue worktrees",
15
15
  block: "block an agent or account", completion: "shell completion script",
16
16
  };
17
17
  /** Fixed words after a subcommand. */
@@ -27,52 +27,52 @@ export function script(shell, cmds) {
27
27
  const all = cmds.join(" ");
28
28
  const cases = (fmt) => Object.entries(WORDS).filter(([c]) => cmds.includes(c)).map(([c, w]) => fmt(c, w.join(" "))).join("\n");
29
29
  switch (shell) {
30
- case "bash": return `# orq completion (bash): eval "$(orq completion bash)" in ~/.bashrc
31
- _orq() {
32
- local cur=\${COMP_WORDS[COMP_CWORD]}
33
- if [ "$COMP_CWORD" -eq 1 ]; then COMPREPLY=($(compgen -W "${all}" -- "$cur")); return; fi
34
- [ "$COMP_CWORD" -eq 2 ] || return
35
- case \${COMP_WORDS[1]} in
36
- ${cases((c, w) => ` ${c}) COMPREPLY=($(compgen -W "${w}" -- "$cur")) ;;`)}
37
- esac
38
- }
39
- complete -o default -F _orq orq
30
+ case "bash": return `# orq completion (bash): eval "$(orq completion bash)" in ~/.bashrc
31
+ _orq() {
32
+ local cur=\${COMP_WORDS[COMP_CWORD]}
33
+ if [ "$COMP_CWORD" -eq 1 ]; then COMPREPLY=($(compgen -W "${all}" -- "$cur")); return; fi
34
+ [ "$COMP_CWORD" -eq 2 ] || return
35
+ case \${COMP_WORDS[1]} in
36
+ ${cases((c, w) => ` ${c}) COMPREPLY=($(compgen -W "${w}" -- "$cur")) ;;`)}
37
+ esac
38
+ }
39
+ complete -o default -F _orq orq
40
40
  `;
41
- case "zsh": return `# orq completion (zsh): eval "$(orq completion zsh)" in ~/.zshrc, after compinit
42
- _orq() {
43
- if (( CURRENT == 2 )); then compadd -- ${all}; return; fi
44
- if (( CURRENT == 3 )); then
45
- case $words[2] in
46
- ${cases((c, w) => ` ${c}) compadd -- ${w}; return ;;`)}
47
- esac
48
- fi
49
- _files
50
- }
51
- compdef _orq orq
41
+ case "zsh": return `# orq completion (zsh): eval "$(orq completion zsh)" in ~/.zshrc, after compinit
42
+ _orq() {
43
+ if (( CURRENT == 2 )); then compadd -- ${all}; return; fi
44
+ if (( CURRENT == 3 )); then
45
+ case $words[2] in
46
+ ${cases((c, w) => ` ${c}) compadd -- ${w}; return ;;`)}
47
+ esac
48
+ fi
49
+ _files
50
+ }
51
+ compdef _orq orq
52
52
  `;
53
- case "fish": return `# orq completion (fish): orq completion fish > ~/.config/fish/completions/orq.fish
54
- complete -c orq -f -n __fish_use_subcommand
55
- ${cmds.map((c) => `complete -c orq -n __fish_use_subcommand -a ${c}${DESC[c] ? ` -d ${qf(DESC[c])}` : ""}`).join("\n")}
56
- ${cases((c, w) => `complete -c orq -n "__fish_seen_subcommand_from ${c}" -a ${qf(w)}`)}
53
+ case "fish": return `# orq completion (fish): orq completion fish > ~/.config/fish/completions/orq.fish
54
+ complete -c orq -f -n __fish_use_subcommand
55
+ ${cmds.map((c) => `complete -c orq -n __fish_use_subcommand -a ${c}${DESC[c] ? ` -d ${qf(DESC[c])}` : ""}`).join("\n")}
56
+ ${cases((c, w) => `complete -c orq -n "__fish_seen_subcommand_from ${c}" -a ${qf(w)}`)}
57
57
  `;
58
58
  case "powershell":
59
- case "pwsh": return `# orq completion (PowerShell): add to $PROFILE: orq completion powershell | Out-String | Invoke-Expression
60
- Register-ArgumentCompleter -Native -CommandName orq -ScriptBlock {
61
- param($word, $ast, $cursor)
62
- $w = @($ast.CommandElements | ForEach-Object { $_.ToString() })
63
- $n = $w.Count; if ($word -ne '') { $n-- }
64
- $words = @{
65
- ${Object.entries(WORDS).filter(([c]) => cmds.includes(c)).map(([c, w]) => ` ${q(c)} = @(${w.map(q).join(", ")})`).join("\n")}
66
- }
67
- $desc = @{
68
- ${cmds.map((c) => ` ${q(c)} = ${q(DESC[c] ?? c)}`).join("\n")}
69
- }
70
- if ($n -eq 1) { $list = @(${cmds.map(q).join(", ")}) } elseif ($n -eq 2 -and $words.ContainsKey($w[1])) { $list = $words[$w[1]] } else { return }
71
- $list | Where-Object { $_ -like "$word*" } | ForEach-Object {
72
- $tip = if ($n -eq 1) { $desc[$_] } else { $_ }
73
- [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $tip)
74
- }
75
- }
59
+ case "pwsh": return `# orq completion (PowerShell): add to $PROFILE: orq completion powershell | Out-String | Invoke-Expression
60
+ Register-ArgumentCompleter -Native -CommandName orq -ScriptBlock {
61
+ param($word, $ast, $cursor)
62
+ $w = @($ast.CommandElements | ForEach-Object { $_.ToString() })
63
+ $n = $w.Count; if ($word -ne '') { $n-- }
64
+ $words = @{
65
+ ${Object.entries(WORDS).filter(([c]) => cmds.includes(c)).map(([c, w]) => ` ${q(c)} = @(${w.map(q).join(", ")})`).join("\n")}
66
+ }
67
+ $desc = @{
68
+ ${cmds.map((c) => ` ${q(c)} = ${q(DESC[c] ?? c)}`).join("\n")}
69
+ }
70
+ if ($n -eq 1) { $list = @(${cmds.map(q).join(", ")}) } elseif ($n -eq 2 -and $words.ContainsKey($w[1])) { $list = $words[$w[1]] } else { return }
71
+ $list | Where-Object { $_ -like "$word*" } | ForEach-Object {
72
+ $tip = if ($n -eq 1) { $desc[$_] } else { $_ }
73
+ [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $tip)
74
+ }
75
+ }
76
76
  `;
77
77
  default: return undefined;
78
78
  }
@@ -5,8 +5,8 @@
5
5
  import { getPath, loadConfig } from "../config.js";
6
6
  import { expand } from "../paths.js";
7
7
  import { m } from "../i18n.js";
8
- export const USAGE = `orq config [-w <workspace>] everything, as JSON
9
- orq config [-w <workspace>] get <a.b.c> one value (plain text if scalar; JSON if object/list)
8
+ export const USAGE = `orq config [-w <workspace>] everything, as JSON
9
+ orq config [-w <workspace>] get <a.b.c> one value (plain text if scalar; JSON if object/list)
10
10
  orq config [-w <workspace>] files which files were read`;
11
11
  export function config(argv) {
12
12
  let ws;
@@ -0,0 +1,164 @@
1
+ // orq gc — find issue worktrees whose branch is already safe to remove.
2
+ import { spawnSync } from "node:child_process";
3
+ import { existsSync, readdirSync, readFileSync, realpathSync } from "node:fs";
4
+ import { basename, isAbsolute, join, relative, resolve } from "node:path";
5
+ import { loadConfig } from "../config.js";
6
+ import { m } from "../i18n.js";
7
+ import { aliveOwner } from "./claim.js";
8
+ export const USAGE = "orq gc [-w <workspace>] [--yes] [--days N] [--json] remove merged issue worktrees";
9
+ const git = (cwd, ...args) => spawnSync("git", ["-C", cwd, ...args], { encoding: "utf8" });
10
+ // Paths as git prints them can differ from ours (macOS /var → /private/var, Windows C:/ vs C:\): compare real paths.
11
+ const real = (p) => { try {
12
+ return realpathSync.native(p);
13
+ }
14
+ catch {
15
+ return resolve(p);
16
+ } };
17
+ const inside = (dir, p) => { const r = relative(dir, p); return !!r && !r.startsWith("..") && !isAbsolute(r); };
18
+ // ORQ_GH may be a .js file (tests: a fake gh that also runs on Windows).
19
+ const gh = (cwd, args) => {
20
+ const bin = process.env.ORQ_GH || "gh";
21
+ return bin.endsWith(".js") ? spawnSync(process.execPath, [bin, ...args], { cwd, encoding: "utf8" })
22
+ : spawnSync(bin, args, { cwd, encoding: "utf8" });
23
+ };
24
+ const output = (cwd, ...args) => { const r = git(cwd, ...args); return r.status === 0 ? r.stdout.trim() : undefined; };
25
+ export async function gc(argv, clock = Date.now) {
26
+ let ws = process.cwd(), yes = false, json = false, days = 14;
27
+ for (let i = 0; i < argv.length; i++) {
28
+ if (argv[i] === "-w" && argv[i + 1])
29
+ ws = resolve(argv[++i]);
30
+ else if (argv[i] === "--yes")
31
+ yes = true;
32
+ else if (argv[i] === "--json")
33
+ json = true;
34
+ else if (argv[i] === "--days" && argv[i + 1] && /^\d+$/.test(argv[i + 1]))
35
+ days = Number(argv[++i]);
36
+ else {
37
+ console.error(USAGE);
38
+ return 2;
39
+ }
40
+ }
41
+ const { cfg } = loadConfig(ws), rows = [];
42
+ let ghUnavailable = false, fetchWarned = false;
43
+ const root = join(ws, ".worktrees");
44
+ const orq = join(ws, ".orquestra");
45
+ const issues = existsSync(orq) ? readdirSync(orq) : [];
46
+ for (const repoName of readdirSync(ws, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name)) {
47
+ const repo = join(ws, repoName);
48
+ if (!existsSync(join(repo, ".git")))
49
+ continue;
50
+ const listings = output(repo, "worktree", "list", "--porcelain");
51
+ if (listings === undefined)
52
+ continue;
53
+ const worktrees = listings.split(/\n\n/).map((block) => Object.fromEntries(block.split("\n").map((line) => {
54
+ const i = line.indexOf(" ");
55
+ return [i < 0 ? line : line.slice(0, i), i < 0 ? "" : line.slice(i + 1)];
56
+ })));
57
+ const main = real(worktrees[0]?.worktree ?? repo);
58
+ const conf = cfg.repos?.[repoName];
59
+ const head = output(repo, "symbolic-ref", "refs/remotes/origin/HEAD")?.replace("refs/remotes/", "");
60
+ const prod = conf?.prod_ref || head || "origin/main";
61
+ const fetchRef = prod.replace(/^origin\//, "");
62
+ if (git(repo, "fetch", "origin", fetchRef).status !== 0 && !fetchWarned) {
63
+ console.error(m("WARNING: git fetch failed; using local refs", "AVISO: git fetch falhou; usando as refs locais"));
64
+ fetchWarned = true;
65
+ }
66
+ const prodTree = output(repo, "rev-parse", `${prod}^{tree}`);
67
+ for (const wt of worktrees) {
68
+ const path = real(wt.worktree ?? "");
69
+ if (!wt.worktree || path === main || !inside(real(root), path))
70
+ continue;
71
+ const branch = wt.branch?.replace(/^refs\/heads\//, "") ?? "";
72
+ if (!branch)
73
+ continue;
74
+ // The branch carries the ID in lowercase (plane-abc-123); .orquestra/ keeps it as written (ABC-123).
75
+ const id = /^.*?plane-(.+)$/.exec(branch)?.[1];
76
+ const issue = id && issues.find((d) => d.toLowerCase() === id.toLowerCase());
77
+ const status = output(path, "status", "--porcelain");
78
+ const unpushed = output(repo, "rev-list", branch, "--not", "--remotes");
79
+ let reason = "", verdict = "REMOVABLE";
80
+ if (status) {
81
+ verdict = "KEEP";
82
+ reason = m("uncommitted changes", "alterações não commitadas");
83
+ }
84
+ const ownerFile = issue && join(ws, ".orquestra", issue, "owner");
85
+ if (verdict !== "KEEP" && ownerFile && existsSync(ownerFile) && aliveOwner(readFileSync(ownerFile, "utf8"))) {
86
+ verdict = "KEEP";
87
+ reason = m("issue has a live owner", "issue tem um responsável ativo");
88
+ }
89
+ // A .pane/.id without a same-stem .done is treated as a running child.
90
+ const issueDir = issue && join(ws, ".orquestra", issue);
91
+ if (verdict !== "KEEP" && issueDir && existsSync(issueDir)) {
92
+ const files = readdirSync(issueDir), running = files.some((f) => /\.(pane|id)$/.test(f) && !existsSync(join(issueDir, `${f.replace(/\.(pane|id)$/, "")}.done`)));
93
+ if (running) {
94
+ verdict = "KEEP";
95
+ reason = m("child agent may still be running", "um agente filho pode estar em execução");
96
+ }
97
+ }
98
+ let merged = false;
99
+ if (verdict !== "KEEP") {
100
+ // Merged counts only if the PR's head is the local head: a commit made after the merge must not be lost.
101
+ const pr = gh(repo, ["pr", "list", "--head", branch, "--state", "merged", "--json", "number,headRefOid", "--limit", "1"]);
102
+ const local = output(repo, "rev-parse", branch);
103
+ if (pr.status === 0) {
104
+ try {
105
+ const p = JSON.parse(pr.stdout)[0];
106
+ merged = !!p && (!p.headRefOid || p.headRefOid === local);
107
+ }
108
+ catch {
109
+ ghUnavailable = true;
110
+ }
111
+ }
112
+ else
113
+ ghUnavailable = true;
114
+ }
115
+ const inBase = verdict !== "KEEP" && !!prodTree && output(repo, "merge-tree", "--write-tree", prod, branch) === prodTree;
116
+ if (verdict !== "KEEP" && unpushed && !merged && !inBase) {
117
+ verdict = "KEEP";
118
+ reason = m("commits not contained in remote refs", "commits fora das refs remotas");
119
+ }
120
+ else if (verdict !== "KEEP" && (merged || inBase)) {
121
+ verdict = "REMOVABLE";
122
+ reason = merged ? "PR merged" : m("content is in base", "conteúdo já está na base");
123
+ }
124
+ else if (verdict !== "KEEP") {
125
+ const closed = gh(repo, ["pr", "list", "--head", branch, "--state", "closed", "--json", "number", "--limit", "1"]);
126
+ const isClosed = closed?.status === 0 && (() => { try {
127
+ return JSON.parse(closed.stdout).length > 0;
128
+ }
129
+ catch {
130
+ return false;
131
+ } })();
132
+ const commit = output(repo, "log", "-1", "--format=%ct", branch), old = commit !== undefined && clock() - Number(commit) * 1000 > days * 86400_000;
133
+ verdict = "STALE";
134
+ reason = isClosed ? m("PR closed without merge", "PR fechada sem merge") : old ? m(`last commit is older than ${days} days`, `último commit tem mais de ${days} dias`) : m("not merged", "não integrado");
135
+ }
136
+ rows.push({ repo: repoName, worktree: basename(path), branch, verdict, reason, path });
137
+ }
138
+ }
139
+ if (ghUnavailable)
140
+ console.error(m("NOTE: gh unavailable or failed; PR signals were skipped", "NOTA: gh indisponível ou falhou; sinais de PR ignorados"));
141
+ let failed = false;
142
+ if (yes)
143
+ for (const row of rows.filter((r) => r.verdict === "REMOVABLE")) {
144
+ const removed = git(join(ws, row.repo), "worktree", "remove", row.path);
145
+ if (removed.status !== 0) {
146
+ failed = true;
147
+ row.reason += `; ${m("worktree removal failed", "falha ao remover worktree")}`;
148
+ continue;
149
+ }
150
+ if (git(join(ws, row.repo), "branch", "-D", row.branch).status !== 0) {
151
+ failed = true;
152
+ row.reason += `; ${m("branch deletion failed", "falha ao excluir branch")}`;
153
+ }
154
+ git(join(ws, row.repo), "worktree", "prune");
155
+ }
156
+ if (json)
157
+ console.log(JSON.stringify(rows));
158
+ else {
159
+ const table = [["REPO", "WORKTREE", "BRANCH", "VERDICT", "REASON"], ...rows.map((r) => [r.repo, r.worktree, r.branch, r.verdict, r.reason])];
160
+ const w = table[0].map((_, i) => Math.max(...table.map((t) => t[i].length)));
161
+ console.log(table.map((t) => t.map((c, i) => i < t.length - 1 ? c.padEnd(w[i]) : c).join(" ")).join("\n"));
162
+ }
163
+ return failed ? 1 : 0;
164
+ }
@@ -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. */
@@ -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`;
@@ -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();
@@ -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
- export const USAGE = "orq quota [name...] quota per agent/account ([quota] in the config)";
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 sources = (getPath(loadConfig().cfg, "quota") ?? {});
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;
@@ -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: "none", antigravity: "builtin", copilot: "builtin" },
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/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): 300-min window = 5h, 10080 = week; a window already reset → 0. */
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 win = (mins) => {
143
- const w = ws.find((x) => x.window_minutes === mins);
144
- if (!w)
145
- return "-";
146
- return (w.resets_at ?? 0) < nowSec ? 0 : Math.floor(w.used_percent ?? 0);
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
- return { name: "codex", ...q, note: m(`from the last session, ${age} min ago`, `da última sessão, há ${age} min`) };
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();