@irythill/orq 0.0.0-stage → 0.1.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.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +240 -3
  3. package/README.pt-BR.md +241 -0
  4. package/dist/accounts.js +18 -0
  5. package/dist/agents.js +202 -0
  6. package/dist/banner.js +87 -0
  7. package/dist/cli.js +90 -0
  8. package/dist/commands/agent.js +334 -0
  9. package/dist/commands/baseline.js +101 -0
  10. package/dist/commands/block.js +74 -0
  11. package/dist/commands/board.js +435 -0
  12. package/dist/commands/claim.js +88 -0
  13. package/dist/commands/completion.js +88 -0
  14. package/dist/commands/config.js +35 -0
  15. package/dist/commands/deps.js +82 -0
  16. package/dist/commands/instructions.js +66 -0
  17. package/dist/commands/lang.js +35 -0
  18. package/dist/commands/lock.js +69 -0
  19. package/dist/commands/pick.js +91 -0
  20. package/dist/commands/quota.js +10 -0
  21. package/dist/commands/run.js +46 -0
  22. package/dist/commands/set.js +27 -0
  23. package/dist/commands/setup.js +187 -0
  24. package/dist/commands/stats.js +41 -0
  25. package/dist/commands/verify.js +123 -0
  26. package/dist/commands/wait.js +81 -0
  27. package/dist/config.js +93 -0
  28. package/dist/fsutil.js +9 -0
  29. package/dist/i18n.js +21 -0
  30. package/dist/install.js +137 -0
  31. package/dist/keyring.js +51 -0
  32. package/dist/lock.js +81 -0
  33. package/dist/paths.js +34 -0
  34. package/dist/pick.js +115 -0
  35. package/dist/process.js +147 -0
  36. package/dist/quota.js +276 -0
  37. package/dist/roles.js +28 -0
  38. package/dist/stats.js +159 -0
  39. package/dist/suggest.js +121 -0
  40. package/dist/terminal.js +83 -0
  41. package/dist/testrun.js +203 -0
  42. package/dist/toml-set.js +46 -0
  43. package/package.json +27 -6
  44. package/skills/issue/SKILL.md +197 -0
  45. package/skills/orq-setup/SKILL.md +114 -0
  46. package/skills/orq-status/SKILL.md +37 -0
  47. package/templates/workspace.CLAUDE.md +49 -0
@@ -0,0 +1,82 @@
1
+ // orq deps — the worktree's node_modules is a LINK to the main repo's (never install in a worktree: ~1 GB and tens of
2
+ // thousands of files per copy). If the main repo's deps are behind origin/<base> and it is clean and on <base>:
3
+ // fast-forward + ONE install there. One install per repo at a time (orq lock); the second waits, then only links.
4
+ // Windows: a directory junction (no admin rights needed); elsewhere a symlink.
5
+ // orq deps -r <main repo> -b <base> -w <worktree> exit 0 = linked · 3 = main repo dirty or not on <base>: ASK THE USER
6
+ import { spawnSync } from "node:child_process";
7
+ import { appendFileSync, existsSync, lstatSync, readFileSync, rmSync, symlinkSync } from "node:fs";
8
+ import { basename, join, resolve } from "node:path";
9
+ import { m } from "../i18n.js";
10
+ import { acquire, release } from "../lock.js";
11
+ export const USAGE = "orq deps -r <main repo> -b <base> -w <worktree> link node_modules into a worktree (exit 3 = ask the user)";
12
+ const git = (cwd, ...a) => spawnSync("git", ["-C", cwd, ...a], { encoding: "utf8" });
13
+ export function installer(repo) {
14
+ const has = (f) => existsSync(join(repo, f));
15
+ if (has("bun.lock") || has("bun.lockb"))
16
+ return ["bun", "install"];
17
+ if (has("pnpm-lock.yaml"))
18
+ return ["pnpm", "install"];
19
+ if (has("yarn.lock"))
20
+ return ["yarn", "install"];
21
+ return ["npm", "install"];
22
+ }
23
+ export function link(target, at) {
24
+ try {
25
+ if (lstatSync(at))
26
+ rmSync(at, { recursive: true, force: true });
27
+ }
28
+ catch { /* not there */ }
29
+ symlinkSync(target, at, process.platform === "win32" ? "junction" : "dir");
30
+ }
31
+ export async function deps(argv) {
32
+ let R = "", B = "", WT = "";
33
+ for (let i = 0; i < argv.length; i++) {
34
+ if (argv[i] === "-r")
35
+ R = resolve(argv[++i]);
36
+ else if (argv[i] === "-b")
37
+ B = argv[++i];
38
+ else if (argv[i] === "-w")
39
+ WT = resolve(argv[++i]);
40
+ else {
41
+ console.error(USAGE);
42
+ return 2;
43
+ }
44
+ }
45
+ if (!R || !B || !WT) {
46
+ console.error(USAGE);
47
+ return 2;
48
+ }
49
+ const gd = git(R, "rev-parse", "--path-format=absolute", "--git-common-dir").stdout.trim();
50
+ const ex = join(gd, "info", "exclude");
51
+ if (!(existsSync(ex) ? readFileSync(ex, "utf8") : "").split(/\r?\n/).includes("node_modules"))
52
+ appendFileSync(ex, "node_modules\n");
53
+ const name = `deps-${basename(R)}`;
54
+ await acquire(name, process.pid, `deps ${basename(R)}`, 1, () => console.log(m(`another conductor is installing dependencies in ${basename(R)}; waiting...`, `outro conductor está instalando dependências em ${basename(R)}; aguardando...`)));
55
+ try {
56
+ git(R, "fetch", "-q", "origin", B);
57
+ const locks = ["package.json", "bun.lock", "bun.lockb", "package-lock.json", "yarn.lock", "pnpm-lock.yaml"]
58
+ .filter((f) => existsSync(join(R, f)));
59
+ const stale = git(R, "diff", "--quiet", "HEAD", `origin/${B}`, "--", ...locks).status !== 0;
60
+ if (stale || !existsSync(join(R, "node_modules"))) {
61
+ const br = git(R, "branch", "--show-current").stdout.trim();
62
+ if (br !== B || git(R, "status", "--porcelain", "--untracked-files=no").stdout.trim()) {
63
+ console.log(m(`deps are stale, but ${basename(R)} is on '${br}' or has changes: not touching it. Ask the user.`, `deps defasadas, mas ${basename(R)} está em '${br}' ou com alterações: não mexo. Pergunte ao usuário.`));
64
+ return 3;
65
+ }
66
+ git(R, "merge", "-q", "--ff-only", `origin/${B}`);
67
+ const cmd = installer(R);
68
+ console.log(`${m("installing dependencies in", "instalando dependências em")} ${basename(R)} (${cmd.join(" ")})...`);
69
+ const r = spawnSync(cmd[0], cmd.slice(1), { cwd: R, stdio: "ignore", shell: process.platform === "win32" });
70
+ if (r.status !== 0) {
71
+ console.error(`${cmd.join(" ")}: exit ${r.status}`);
72
+ return 1;
73
+ }
74
+ }
75
+ link(join(R, "node_modules"), join(WT, "node_modules"));
76
+ console.log(`ok: ${join(WT, "node_modules")} -> ${join(R, "node_modules")}`);
77
+ return 0;
78
+ }
79
+ finally {
80
+ release(name, process.pid);
81
+ }
82
+ }
@@ -0,0 +1,66 @@
1
+ // orq instructions — the /issue playbook (skills/issue/SKILL.md, one source) in the conductor's own format:
2
+ // claude <workspace>/.claude/skills/issue/SKILL.md (Claude Code loads it as the /issue skill)
3
+ // agents <workspace>/AGENTS.md (Codex, OpenCode and others that read AGENTS.md)
4
+ // AGENTS.md may hold the user's own text: orq only replaces its block between the orq:issue markers.
5
+ // orq instructions claude|agents [-w <workspace>] [--print]
6
+ import { existsSync, readFileSync } from "node:fs";
7
+ import { join, resolve } from "node:path";
8
+ import { fileURLToPath } from "node:url";
9
+ import { writeAtomic } from "../fsutil.js";
10
+ import { m } from "../i18n.js";
11
+ export const USAGE = "orq instructions claude|agents [-w <workspace>] [--print] write the /issue playbook for the conductor";
12
+ const SRC = fileURLToPath(new URL("../../skills/issue/SKILL.md", import.meta.url));
13
+ const BEGIN = "<!-- orq:issue begin (generated by `orq instructions agents`; edits inside are overwritten) -->";
14
+ const END = "<!-- orq:issue end -->";
15
+ /** The playbook for harnesses without skills: no frontmatter, a trigger line, and how to wait without background tasks. */
16
+ export function agentsBlock(skill) {
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}
25
+ `;
26
+ }
27
+ /** Put the block into an existing AGENTS.md text: replace ours, or append it. */
28
+ export function mergeBlock(cur, block) {
29
+ const a = cur.indexOf(BEGIN), b = cur.indexOf(END);
30
+ if (a >= 0 && b > a)
31
+ return cur.slice(0, a) + block.trimEnd() + cur.slice(b + END.length);
32
+ return cur ? `${cur.trimEnd()}\n\n${block}` : block;
33
+ }
34
+ export async function instructions(argv) {
35
+ let ws = process.cwd(), print = false, fmt;
36
+ for (let i = 0; i < argv.length; i++) {
37
+ if (argv[i] === "-w")
38
+ ws = resolve(argv[++i]);
39
+ else if (argv[i] === "--print")
40
+ print = true;
41
+ else if (argv[i] === "claude" || argv[i] === "agents")
42
+ fmt = argv[i];
43
+ else {
44
+ console.error(USAGE);
45
+ return 2;
46
+ }
47
+ }
48
+ if (!fmt) {
49
+ console.error(USAGE);
50
+ return 2;
51
+ }
52
+ if (!existsSync(SRC)) {
53
+ console.error(m(`playbook not found: ${SRC}`, `playbook não encontrado: ${SRC}`));
54
+ return 2;
55
+ }
56
+ const skill = readFileSync(SRC, "utf8");
57
+ const file = fmt === "claude" ? join(ws, ".claude", "skills", "issue", "SKILL.md") : join(ws, "AGENTS.md");
58
+ const text = fmt === "claude" ? skill : mergeBlock(existsSync(file) ? readFileSync(file, "utf8") : "", agentsBlock(skill));
59
+ if (print) {
60
+ process.stdout.write(text);
61
+ return 0;
62
+ }
63
+ writeAtomic(file, text);
64
+ console.log(m(`wrote ${file}`, `escrito ${file}`));
65
+ return 0;
66
+ }
@@ -0,0 +1,35 @@
1
+ // orq lang show the current language
2
+ // orq lang pt|en|auto set it (auto = system locale)
3
+ // orq lang toggle switch between en and pt
4
+ // --notify also show a herdr pop-up (used by the herdr key binding)
5
+ // Applies to whatever starts afterwards (board, notices, orq pick...); running agents keep their language.
6
+ import { spawnSync } from "node:child_process";
7
+ import { getPath, loadConfig } from "../config.js";
8
+ import { detectLang } from "../i18n.js";
9
+ import { setValue } from "./set.js";
10
+ export const USAGE = "orq lang [pt|en|auto|toggle] [--notify]";
11
+ const cfgLang = () => getPath(loadConfig().cfg, "ui.lang");
12
+ export function lang(argv) {
13
+ let notify = false, arg = "";
14
+ for (const a of argv) {
15
+ if (a === "--notify")
16
+ notify = true;
17
+ else if (["pt", "en", "auto", "toggle"].includes(a))
18
+ arg = a;
19
+ else {
20
+ console.error(`usage: ${USAGE}`);
21
+ return 2;
22
+ }
23
+ }
24
+ const cur = detectLang(cfgLang(), false); // effective language, ignoring $ORQ_LANG
25
+ if (!arg) {
26
+ console.log(`${cur} (ui.lang = ${cfgLang() ?? "auto"})`);
27
+ return 0;
28
+ }
29
+ setValue("ui", "lang", arg === "toggle" ? (cur === "pt" ? "en" : "pt") : arg);
30
+ const msg = detectLang(cfgLang(), false) === "pt" ? "Idioma do orq: Português" : "orq language: English";
31
+ console.log(msg);
32
+ if (notify)
33
+ spawnSync("herdr", ["notification", "show", msg], { stdio: "ignore" });
34
+ return 0;
35
+ }
@@ -0,0 +1,69 @@
1
+ // orq lock — the portable test queue (src/lock.ts). Scripts hold a slot for their own pid:
2
+ // orq lock wait verify --pid $$ --label "verify ABC-1 api" blocks until it holds a slot (slots: machine.test_slots)
3
+ // orq lock release verify --pid $$ (also freed by itself when that process dies)
4
+ // orq lock run verify [--label t] -- <cmd> [args] runs a command holding a slot
5
+ // orq lock status [verify] [--json] holders and waiters, in order
6
+ import { spawnSync } from "node:child_process";
7
+ import { getPath, loadConfig } from "../config.js";
8
+ import { m } from "../i18n.js";
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)
11
+ orq lock run <name> [--label text] -- <cmd>... · orq lock status [<name>] [--json]`;
12
+ const slots = () => Number(getPath(loadConfig().cfg, "machine.test_slots") ?? 1);
13
+ const since = (at) => `${Math.round((Date.now() - at) / 1000)}s`;
14
+ export async function lock(argv) {
15
+ const [sub, name = "verify", ...rest] = argv;
16
+ let pid = process.ppid, label = "", json = false;
17
+ const dd = rest.indexOf("--");
18
+ const opts = dd >= 0 ? rest.slice(0, dd) : rest, cmd = dd >= 0 ? rest.slice(dd + 1) : [];
19
+ for (let i = 0; i < opts.length; i++) {
20
+ if (opts[i] === "--pid")
21
+ pid = Number(opts[++i]);
22
+ else if (opts[i] === "--label")
23
+ label = opts[++i];
24
+ else if (opts[i] === "--json")
25
+ json = true;
26
+ else {
27
+ console.error(USAGE);
28
+ return 2;
29
+ }
30
+ }
31
+ const waitMsg = () => console.log(m("another verification is running; waiting for my turn...", "outra verificação em andamento; aguardando a vez..."));
32
+ switch (sub) {
33
+ case "wait":
34
+ await acquire(name, pid, label, slots(), waitMsg);
35
+ return 0;
36
+ case "release":
37
+ release(name, pid);
38
+ return 0;
39
+ case "run": {
40
+ if (!cmd.length) {
41
+ console.error(USAGE);
42
+ return 2;
43
+ }
44
+ await acquire(name, process.pid, label || cmd.join(" "), slots(), waitMsg);
45
+ try {
46
+ return spawnSync(cmd[0], cmd.slice(1), { stdio: "inherit", shell: process.platform === "win32" }).status ?? 1;
47
+ }
48
+ finally {
49
+ release(name, process.pid);
50
+ }
51
+ }
52
+ case "status": {
53
+ const s = status(name);
54
+ if (json) {
55
+ console.log(JSON.stringify(s));
56
+ return 0;
57
+ }
58
+ for (const e of s.holders)
59
+ console.log(`▶ ${e.pid} ${e.label} (${since(e.at)})`);
60
+ s.waiters.forEach((e, i) => console.log(`${i + 1}. ${e.pid} ${e.label} (${m("waiting", "aguardando")} ${since(e.at)})`));
61
+ if (!s.holders.length && !s.waiters.length)
62
+ console.log(m("queue empty", "fila vazia"));
63
+ return 0;
64
+ }
65
+ default:
66
+ console.error(USAGE);
67
+ return 2;
68
+ }
69
+ }
@@ -0,0 +1,91 @@
1
+ // orq pick — agent, model, effort and account for a role, from [roles] (fixed or auto), the history, quota and blocks.
2
+ // Prints the runner arguments (e.g. `-a codex -m gpt-6-sol -e low -c work`); each pick is recorded in
3
+ // <state dir>/picks.jsonl (that feeds rotation). Exit 3 = no candidate: ask the user.
4
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync } from "node:fs";
5
+ import { tmpdir } from "node:os";
6
+ import { join } from "node:path";
7
+ import { loadConfig } from "../config.js";
8
+ import { m } from "../i18n.js";
9
+ import { asArgs, parseQuota, pick as choose } from "../pick.js";
10
+ import { expand, stateDir } from "../paths.js";
11
+ import { collect } from "../quota.js";
12
+ import { PICK_ROLES, vendorOf } from "../roles.js";
13
+ import { collectRuns, rows } from "../stats.js";
14
+ 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]`;
17
+ // The board refreshes this file; fresher than 10 min → no new quota requests.
18
+ const QCACHE = join(process.env.XDG_RUNTIME_DIR ?? tmpdir(), "herdr-board.quota");
19
+ async function quotaText(cfg) {
20
+ if (existsSync(QCACHE) && Date.now() - statSync(QCACHE).mtimeMs < 600_000)
21
+ return readFileSync(QCACHE, "utf8");
22
+ return (await collect((cfg.quota ?? {}))).join("\n");
23
+ }
24
+ export async function pick(argv) {
25
+ const role = argv[0];
26
+ if (!PICK_ROLES.includes(role)) {
27
+ console.error(USAGE);
28
+ return 2;
29
+ }
30
+ let level = "medium", implVendor, implAgent, ws;
31
+ let explain = false, record = true;
32
+ const exclude = new Set();
33
+ for (let i = 1; i < argv.length; i++) {
34
+ const a = argv[i];
35
+ if (a === "--level")
36
+ level = argv[++i];
37
+ else if (a === "--impl-vendor")
38
+ implVendor = argv[++i];
39
+ else if (a === "--impl-agent")
40
+ implAgent = argv[++i];
41
+ else if (a === "--exclude-agent")
42
+ exclude.add(argv[++i]);
43
+ else if (a === "-w")
44
+ ws = expand(argv[++i]);
45
+ else if (a === "--explain")
46
+ explain = true;
47
+ else if (a === "--no-record")
48
+ record = false;
49
+ else {
50
+ console.error(USAGE);
51
+ return 2;
52
+ }
53
+ }
54
+ const { cfg } = loadConfig(ws);
55
+ const agents = (cfg.agents ?? {});
56
+ if (implAgent && !implVendor)
57
+ implVendor = vendorOf(implAgent, agents[implAgent]);
58
+ const w = (cfg.policy ?? {});
59
+ const stats = rows(collectRuns((ws ?? cfg._workspace ?? process.cwd()), Number(w.history_days ?? 30)));
60
+ const picksF = join(stateDir(), "picks.jsonl");
61
+ const window = Number(((w.weights ?? {}).window) ?? 6);
62
+ const recent = (existsSync(picksF) ? readFileSync(picksF, "utf8").split("\n").filter(Boolean).slice(-200) : [])
63
+ .map((l) => { try {
64
+ return JSON.parse(l);
65
+ }
66
+ catch {
67
+ return {};
68
+ } })
69
+ .filter((p) => p.role === role).slice(-window);
70
+ const res = choose({ cfg, role, level, implVendor, exclude, stats, quota: parseQuota(await quotaText(cfg)),
71
+ blocks: new Set(Object.keys(loadBlocks())), recent });
72
+ if (!res.best) {
73
+ console.error(`orq pick: ${m("no candidate for ", "nenhum candidato para ")}${role}`
74
+ + (res.notes.length ? `; ${res.notes.join("; ")}` : ""));
75
+ return 3;
76
+ }
77
+ if (explain) {
78
+ for (const c of res.cands)
79
+ console.error(`${c === res.best ? "→" : " "} ${res.mode === "fixed" ? " fixed" : (c.score >= 0 ? "+" : "") + c.score.toFixed(3)} `
80
+ + `${c.agent.padEnd(12)} ${c.model.padEnd(24)} ${c.why}`);
81
+ for (const n of res.notes)
82
+ console.error(` ${m("out", "fora")}: ${n}`);
83
+ }
84
+ if (record) {
85
+ mkdirSync(stateDir(), { recursive: true });
86
+ appendFileSync(picksF, JSON.stringify({ t: Math.floor(Date.now() / 1000), role, level, agent: res.best.agent,
87
+ model: res.best.model }) + "\n");
88
+ }
89
+ console.log(asArgs(res.best));
90
+ return 0;
91
+ }
@@ -0,0 +1,10 @@
1
+ // orq quota [name...] quota of each source in [quota] (default: all) — lines "<name> 5h=NN% week=NN% [note]"
2
+ import { getPath, loadConfig } from "../config.js";
3
+ import { collect } from "../quota.js";
4
+ export const USAGE = "orq quota [name...] quota per agent/account ([quota] in the config)";
5
+ export async function quota(argv) {
6
+ const sources = (getPath(loadConfig().cfg, "quota") ?? {});
7
+ for (const l of await collect(sources, argv))
8
+ console.log(l);
9
+ return 0;
10
+ }
@@ -0,0 +1,46 @@
1
+ // orq run — run a command under a memory cap on any OS (src/process.ts).
2
+ // orq run [--agent <name> | --test] [--mem 4G|off] [--swap 1G] [--mode auto|kernel|watchdog|off] [--unit name] -- <cmd>...
3
+ // Cap: --mem, else agents.<name>.mem_max (with --agent), else machine.test_mem_max (--test) or machine.agent_mem_max.
4
+ // Mode: --mode, else machine.mem_enforce. Exit code = the command's; 137 = killed for memory.
5
+ import { loadConfig } from "../config.js";
6
+ import { parseSize, resolveMode, runCapped } from "../process.js";
7
+ export const USAGE = "orq run [--agent <a> | --test] [--mem 4G|off] [--mode auto|kernel|watchdog|off] -- <cmd>... memory-capped run";
8
+ export async function run(argv) {
9
+ const dd = argv.indexOf("--");
10
+ if (dd < 0 || dd === argv.length - 1) {
11
+ console.error(USAGE);
12
+ return 2;
13
+ }
14
+ const opts = argv.slice(0, dd), cmd = argv.slice(dd + 1);
15
+ let agent, test = false, mem, swap;
16
+ let mode, unit, explain = false;
17
+ for (let i = 0; i < opts.length; i++) {
18
+ const a = opts[i];
19
+ if (a === "--agent")
20
+ agent = opts[++i];
21
+ else if (a === "--test")
22
+ test = true;
23
+ else if (a === "--mem")
24
+ mem = opts[++i];
25
+ else if (a === "--swap")
26
+ swap = opts[++i];
27
+ else if (a === "--mode")
28
+ mode = opts[++i];
29
+ else if (a === "--unit")
30
+ unit = opts[++i];
31
+ else if (a === "--explain")
32
+ explain = true;
33
+ else {
34
+ console.error(USAGE);
35
+ return 2;
36
+ }
37
+ }
38
+ const { cfg } = loadConfig();
39
+ const mach = (cfg.machine ?? {});
40
+ const agentCap = agent ? (cfg.agents ?? {})[agent]?.mem_max : undefined;
41
+ const cap = parseSize(mem ?? agentCap ?? String((test ? mach.test_mem_max : mach.agent_mem_max) ?? "off"));
42
+ const m = mode ?? mach.mem_enforce ?? "auto";
43
+ if (explain)
44
+ console.error(`orq run: cap=${cap ?? "off"} mode=${resolveMode(m, cap)}`);
45
+ return runCapped(cmd[0], cmd.slice(1), { cap, mode: m, unit, swap: parseSize(swap ?? mach.agent_swap_max) });
46
+ }
@@ -0,0 +1,27 @@
1
+ // orq set <section> <key> <value> — writes one string value into your orq config, atomically.
2
+ // TOML: replaces `key = ...` inside [section], adds the key or the section if missing; everything else is kept.
3
+ import { existsSync, readFileSync } from "node:fs";
4
+ import { writeAtomic } from "../fsutil.js";
5
+ import { userConfigFile } from "../paths.js";
6
+ import { setTomlString } from "../toml-set.js";
7
+ export const USAGE = "orq set <section> <key> <value> e.g. orq set ui theme lcd";
8
+ export function setValue(section, key, value) {
9
+ const f = userConfigFile();
10
+ const text = existsSync(f) ? readFileSync(f, "utf8") : "";
11
+ if (f.endsWith(".json")) {
12
+ const j = text.trim() ? JSON.parse(text) : {};
13
+ j[section] = { ...(j[section] ?? {}), [key]: value };
14
+ writeAtomic(f, JSON.stringify(j, null, 2) + "\n");
15
+ }
16
+ else {
17
+ writeAtomic(f, setTomlString(text, section, key, value));
18
+ }
19
+ }
20
+ export function set(argv) {
21
+ if (argv.length !== 3) {
22
+ console.error(USAGE);
23
+ return 2;
24
+ }
25
+ setValue(argv[0], argv[1], argv[2]);
26
+ return 0;
27
+ }
@@ -0,0 +1,187 @@
1
+ // orq setup — guided first configuration: detect agent CLIs, ask what the user knows, suggest roles (src/suggest.ts),
2
+ // and do what a fresh `npm i -g @irythill/orq` still needs (src/install.ts): skills for Claude Code, a conductor workspace.
3
+ // orq setup interactive, in any terminal
4
+ // orq setup --detect installed agent CLIs, as JSON (for a conductor driving the setup)
5
+ // orq setup --answers <file|-> [--write] proposal from answers JSON (see Answers in src/suggest.ts); --write saves it
6
+ // orq setup --install no questions: what orq found, and the skills linked for Claude Code
7
+ // orq setup --workspace <dir> [--conductor <agent>] .orquestra, CLAUDE.md map, /issue playbook in its format
8
+ // --preset economy|quality|single-agent
9
+ import { spawnSync } from "node:child_process";
10
+ import { copyFileSync, existsSync, readFileSync } from "node:fs";
11
+ import { homedir, totalmem } from "node:os";
12
+ import { delimiter, join } from "node:path";
13
+ import { createInterface } from "node:readline/promises";
14
+ import { writeAtomic } from "../fsutil.js";
15
+ import { banner, infoLines } from "../banner.js";
16
+ import { getPath, loadConfig } from "../config.js";
17
+ import { m } from "../i18n.js";
18
+ import { environment, installSkills, prepareWorkspace } from "../install.js";
19
+ import { expand, userConfigFile } from "../paths.js";
20
+ import { suggestRoles, toToml } from "../suggest.js";
21
+ export const USAGE = `orq setup [--preset economy|quality|single-agent] guided first configuration (roles, weights, memory)
22
+ orq setup --detect | --answers <file|-> [--write] | --install | --workspace <dir> [--conductor <agent>]`;
23
+ /** Agent name → its CLI and a cheap headless smoke test. */
24
+ const CLIS = {
25
+ claude: { bin: "claude", test: 'claude -p "reply ok"' },
26
+ codex: { bin: "codex", test: 'codex exec --skip-git-repo-check "reply ok"' },
27
+ opencode: { bin: "opencode", test: 'opencode run "reply ok"' },
28
+ antigravity: { bin: "agy", test: 'agy -p "reply ok"' },
29
+ copilot: { bin: "copilot", test: 'copilot -p "reply ok" -s' },
30
+ };
31
+ /** Is `bin` on PATH? (PATHEXT on Windows: claude.cmd, codex.exe…) */
32
+ export function onPath(bin) {
33
+ const exts = process.platform === "win32" ? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";") : [""];
34
+ return (process.env.PATH ?? "").split(delimiter).some((d) => d && exts.some((e) => existsSync(join(d, bin + e))));
35
+ }
36
+ export const detect = () => Object.keys(CLIS).filter((n) => onPath(CLIS[n].bin));
37
+ function write(toml) {
38
+ const f = userConfigFile();
39
+ if (existsSync(f)) {
40
+ const bak = `${f}.bak-${new Date().toISOString().replace(/\D/g, "").slice(0, 14)}`;
41
+ copyFileSync(f, bak);
42
+ console.log(m(`previous config saved as ${bak}`, `config anterior salvo em ${bak}`));
43
+ }
44
+ writeAtomic(f, toml);
45
+ return f;
46
+ }
47
+ function show(a) {
48
+ const p = suggestRoles(a), toml = toToml(p);
49
+ console.log(toml);
50
+ return toml;
51
+ }
52
+ async function interactive(preset) {
53
+ process.stdout.write(banner([...infoLines({}).slice(0, 3), m("setup — let's build your orchestra", "setup — vamos montar sua orquestra")]));
54
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
55
+ // Input closed mid-question (Ctrl+D, empty stdin): cancel cleanly instead of hanging.
56
+ const closed = new Promise((_, rej) => rl.once("close", () => rej(new Error("closed"))));
57
+ closed.catch(() => { });
58
+ const ask = async (q, def = "") => ((await Promise.race([rl.question(def ? `${q} [${def}] ` : `${q} `), closed])).trim() || def);
59
+ const yes = async (q, def = "n") => /^[ysYS]/.test(await ask(`${q} (y/n)`, def));
60
+ try {
61
+ env();
62
+ const found = detect();
63
+ if (!found.length) {
64
+ console.log(m("No agent CLI found (claude, codex, opencode, agy, copilot). Install one and run orq setup again.", "Nenhuma CLI de agente encontrada (claude, codex, opencode, agy, copilot). Instale uma e rode orq setup de novo."));
65
+ return 1;
66
+ }
67
+ console.log(m(`Found: ${found.join(", ")}`, `Encontrados: ${found.join(", ")}`));
68
+ const agents = {};
69
+ for (const n of found) {
70
+ if (await yes(m(`Smoke-test ${n}? (costs a few tokens)`, `Testar ${n}? (gasta alguns tokens)`))) {
71
+ const r = spawnSync(CLIS[n].test, { shell: true, encoding: "utf8", timeout: 120_000 });
72
+ const ok = r.status === 0;
73
+ console.log(ok ? " ok" : m(" failed — probably not logged in", " falhou — provavelmente sem login"));
74
+ if (!ok && !(await yes(m(`Keep ${n} anyway?`, `Manter ${n} mesmo assim?`))))
75
+ continue;
76
+ }
77
+ else if (!(await yes(m(`Use ${n}?`, `Usar ${n}?`), "y")))
78
+ continue;
79
+ const plan = await ask(m(`${n} plan: free | small (Pro, Copilot Pro) | large (Max, high tiers) | payg`, `Plano do ${n}: free | small (Pro, Copilot Pro) | large (Max, planos altos) | payg (pague-por-uso)`), "large");
80
+ agents[n] = { plan: (["free", "small", "large", "payg"].includes(plan) ? plan : "large") };
81
+ }
82
+ const names = Object.keys(agents);
83
+ if (!names.length)
84
+ return 1;
85
+ const trust = await ask(m(`Which do you trust most to write code? (${names.join(", ")}, or empty)`, `Em qual você mais confia para escrever código? (${names.join(", ")}, ou vazio)`));
86
+ if (agents[trust])
87
+ agents[trust].trust = true;
88
+ const conductor = await ask(m(`Where will you talk to orq (the conductor)? (${names.join(", ")}, other)`, `Onde você vai conversar com o orq (o conductor)? (${names.join(", ")}, other)`), names.includes("claude") ? "claude" : names[0]);
89
+ const parallel = Number(await ask(m("How many tasks do you usually run at once?", "Quantas tarefas você costuma rodar ao mesmo tempo?"), "2")) || 2;
90
+ const ramGB = Number(await ask(m("RAM agents may use, in GB", "RAM que os agentes podem usar, em GB"), String(Math.round(totalmem() / 2 ** 30)))) || 8;
91
+ const toml = show({ agents, conductor, parallel, ramGB, preset });
92
+ if (await yes(m(`Write this to ${userConfigFile()}?`, `Gravar em ${userConfigFile()}?`)))
93
+ console.log(m(`written: ${write(toml)}`, `gravado: ${write(toml)}`));
94
+ if (names.includes("claude") || existsSync(join(homedir(), ".claude")))
95
+ skills();
96
+ const ws = await ask(m("Folder that groups your repos, for the conductor (empty = later: orq setup --workspace <dir>)", "Pasta que agrupa seus repos, para o conductor (vazio = depois: orq setup --workspace <pasta>)"));
97
+ if (ws)
98
+ workspace(ws, conductor);
99
+ next();
100
+ return 0;
101
+ }
102
+ catch (e) {
103
+ if (e.message !== "closed")
104
+ throw e;
105
+ console.log("\n" + m("setup cancelled — nothing was written", "setup cancelado — nada foi gravado"));
106
+ return 1;
107
+ }
108
+ finally {
109
+ rl.close();
110
+ }
111
+ }
112
+ function env() {
113
+ console.log(m("Environment", "Ambiente"));
114
+ let cfg = {};
115
+ try {
116
+ cfg = loadConfig().cfg;
117
+ }
118
+ catch { /* a broken config is reported by orq config */ }
119
+ for (const l of environment(cfg))
120
+ console.log(l);
121
+ }
122
+ function skills() {
123
+ console.log(m("Skills for Claude Code (/issue, /orq-setup, /orq-status)", "Skills do Claude Code (/issue, /orq-setup, /orq-status)"));
124
+ for (const l of installSkills())
125
+ console.log(l);
126
+ }
127
+ /** claude → .claude/skills/issue/SKILL.md; any other conductor → AGENTS.md. */
128
+ function workspace(dir, conductor) {
129
+ let c = conductor;
130
+ if (!c) {
131
+ try {
132
+ c = getPath(loadConfig().cfg, "roles.conductor.agent");
133
+ }
134
+ catch { /* none */ }
135
+ }
136
+ c ??= onPath("claude") ? "claude" : "agents";
137
+ const d = expand(dir); // typed at the prompt: ~ is not expanded by any shell
138
+ console.log(`Workspace ${d}`);
139
+ for (const l of prepareWorkspace(d, c === "claude" ? "claude" : "agents"))
140
+ console.log(l);
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>".
144
+ Painel: orq board · cota: orq quota · autocompletar: orq completion <shell>`));
145
+ export async function setup(argv) {
146
+ let preset, answers, doWrite = false, ws, conductor;
147
+ let install = false;
148
+ for (let i = 0; i < argv.length; i++) {
149
+ const a = argv[i];
150
+ if (a === "--detect") {
151
+ console.log(JSON.stringify(detect()));
152
+ return 0;
153
+ }
154
+ if (a === "--preset")
155
+ preset = argv[++i];
156
+ else if (a === "--answers")
157
+ answers = argv[++i];
158
+ else if (a === "--write")
159
+ doWrite = true;
160
+ else if (a === "--install")
161
+ install = true;
162
+ else if (a === "--workspace" && argv[i + 1])
163
+ ws = argv[++i];
164
+ else if (a === "--conductor" && argv[i + 1])
165
+ conductor = argv[++i];
166
+ else {
167
+ console.error(USAGE);
168
+ return 2;
169
+ }
170
+ }
171
+ if (install || ws) {
172
+ if (install) {
173
+ env();
174
+ skills();
175
+ }
176
+ if (ws)
177
+ workspace(ws, conductor);
178
+ return 0;
179
+ }
180
+ if (!answers)
181
+ return interactive(preset);
182
+ const a = JSON.parse(readFileSync(answers === "-" ? 0 : answers, "utf8"));
183
+ const toml = show({ ...a, preset: preset ?? a.preset });
184
+ if (doWrite)
185
+ console.error(m(`written: ${write(toml)}`, `gravado: ${write(toml)}`));
186
+ return 0;
187
+ }
@@ -0,0 +1,41 @@
1
+ // orq stats [-w <workspace>] [--days N] [--json] agent scoreboard from the .orquestra/ history (src/stats.ts)
2
+ import { getPath, loadConfig } from "../config.js";
3
+ import { m } from "../i18n.js";
4
+ import { expand } from "../paths.js";
5
+ import { collectRuns, rows } from "../stats.js";
6
+ export const USAGE = "orq stats [-w <workspace>] [--days N] [--json] scoreboard: delivery, time and tokens per agent";
7
+ const k = (n) => (n === null ? "-" : n >= 1000 ? `${Math.round(n / 1000)}k` : String(n));
8
+ const ORDER = ["impl", "fix", "review", "other"];
9
+ export function stats(argv) {
10
+ let ws, days = 30, json = false;
11
+ for (let i = 0; i < argv.length; i++) {
12
+ if (argv[i] === "-w")
13
+ ws = expand(argv[++i]);
14
+ else if (argv[i] === "--days")
15
+ days = Number(argv[++i]);
16
+ else if (argv[i] === "--json")
17
+ json = true;
18
+ else {
19
+ console.error(USAGE);
20
+ return 2;
21
+ }
22
+ }
23
+ // Default workspace: the config's (nearest .orq/ upwards), else the current folder.
24
+ ws ??= getPath(loadConfig().cfg, "_workspace") ?? process.cwd();
25
+ const runs = collectRuns(ws, days), rs = rows(runs);
26
+ if (json) {
27
+ console.log(JSON.stringify(rs, null, 2));
28
+ return 0;
29
+ }
30
+ console.log(m(`Scoreboard for the last ${days} days in ${ws} (${runs.length} runs; medians per run)`, `Placar dos últimos ${days} dias em ${ws} (${runs.length} execuções; medianas por execução)`) + "\n");
31
+ const hdr = `${m("role", "papel").padEnd(7)} ${m("agent", "agente").padEnd(12)} ${m("model", "modelo").padEnd(22)} ${"n".padStart(4)} `
32
+ + `${"ok%".padStart(5)} ${m("approved%", "aprov%").padStart(12)} ${"min".padStart(6)} ${m("new tok", "tok novos").padStart(10)} `
33
+ + `${m("total tok", "tok total").padStart(10)}`;
34
+ console.log(hdr + "\n" + "-".repeat(hdr.length));
35
+ for (const x of [...rs].sort((a, b) => ORDER.indexOf(a.role) - ORDER.indexOf(b.role) || b.n - a.n)) {
36
+ const ap = x.approved !== null ? `${x.approved}% (${x.n_appr})` : "-";
37
+ console.log(`${x.role.padEnd(7)} ${x.agent.padEnd(12)} ${x.model.slice(0, 22).padEnd(22)} ${String(x.n).padStart(4)} `
38
+ + `${String(x.ok).padStart(5)} ${ap.padStart(12)} ${String(x.min).padStart(6)} ${k(x.new).padStart(10)} ${k(x.tot).padStart(10)}`);
39
+ }
40
+ return 0;
41
+ }