@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.
- package/LICENSE +21 -21
- package/README.md +241 -240
- package/README.pt-BR.md +242 -241
- package/dist/cli.js +24 -22
- 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/quota.js +67 -9
- 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
|
@@ -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
|
}
|
package/dist/commands/config.js
CHANGED
|
@@ -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. */
|
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/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();
|