@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,123 @@
1
+ // orq verify — tests related to what changed in a worktree (port of herdr-verify), run by the conductor and not by the
2
+ // child (the child hits its 4 GB cap with jest/vitest). One suite at a time on the machine (the "verify" queue,
3
+ // machine.test_slots) under its own memory cap (machine.test_mem_max, or $VERIFY_MEM_MAX). Returns at once: the run goes
4
+ // to a herdr tab, a tmux window or the background (src/terminal.ts); -x runs it here, in the foreground.
5
+ // output: <prefix>.txt — NEW failures and failures that already exist on the base; <prefix>.done "status=ok|fail new=N".
6
+ // status=ok only with no new failure. On-demand base check: only if some test fails, the SAME test files run in a
7
+ // temporary worktree at origin/<base> (fetched right then); whatever fails there is pre-existing.
8
+ // Same protocol as orq agent: `orq wait <prefix>`, or "[HERDR-AGENT DONE] verify <repo> ..." in the conductor's herdr pane.
9
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
10
+ import { tmpdir } from "node:os";
11
+ import { basename, dirname, join, resolve } from "node:path";
12
+ import { getPath, loadConfig } from "../config.js";
13
+ import { m } from "../i18n.js";
14
+ import { dropWorktree, failures, git, inJob, JS_EXT, launchJob, linkModules, runnerArgs, runnerOf, runTests, testCap, today, unsupported } from "../testrun.js";
15
+ export const USAGE = "orq verify -w <workspace> -r <repo> -d <worktree> -b <base> -o <prefix> [-g group] [-x] tests of the diff, new vs pre-existing failures";
16
+ export async function verify(argv) {
17
+ if (argv[0] === "--run")
18
+ return run(JSON.parse(readFileSync(argv[1], "utf8")));
19
+ const o = {};
20
+ let here = false;
21
+ for (let i = 0; i < argv.length; i++) {
22
+ const a = argv[i];
23
+ if (a === "-x")
24
+ here = true;
25
+ else if (/^-[wrdbog]$/.test(a) && i + 1 < argv.length)
26
+ o[a[1]] = argv[++i];
27
+ else {
28
+ console.error(USAGE);
29
+ return 2;
30
+ }
31
+ }
32
+ for (const f of "wrdbo")
33
+ if (!o[f]) {
34
+ console.error(`${m("missing", "faltou")} -${f}\n${USAGE}`);
35
+ return 2;
36
+ }
37
+ const out = resolve(o.o);
38
+ mkdirSync(dirname(out), { recursive: true });
39
+ rmSync(`${out}.done`, { force: true });
40
+ rmSync(`${out}.txt`, { force: true });
41
+ // Title "<ID> verify <repo>" (ID = the prefix's folder, .orquestra/<ID>/) so closing the issue finds the tab.
42
+ const j = { cmd: "verify", title: `${basename(dirname(out))} verify ${o.r}`, ws: resolve(o.w), repo: o.r, wt: resolve(o.d), base: o.b, out };
43
+ const { cfg } = loadConfig(j.ws);
44
+ if (here)
45
+ return run({ ...j, parent: process.env.HERDR_NOTIFY_PANE ?? "", host: "plain", closeAfter: 0, keep: true });
46
+ console.log(`[orq verify] ${j.title} → ${launchJob(cfg, j, o.g || "agents")}`);
47
+ return 0;
48
+ }
49
+ async function run(j) {
50
+ const O = j.out, R = j.repo, B = j.base, WT = j.wt;
51
+ const { cfg } = loadConfig(j.ws);
52
+ let NEW = 0;
53
+ return inJob(j, `verify ${basename(dirname(O))} ${R}`, Number(getPath(cfg, "machine.test_slots") ?? 1), async (c) => {
54
+ const diff = git(WT, "diff", "--name-only", "--diff-filter=d", `origin/${B}...HEAD`);
55
+ if (diff === undefined)
56
+ throw new Error(m(`git diff against origin/${B} failed in ${WT}`, `o git diff contra origin/${B} falhou em ${WT}`));
57
+ const files = diff.split(/\r?\n/).filter((f) => JS_EXT.test(f));
58
+ const head = git(WT, "rev-parse", "--short", "HEAD") ?? "?";
59
+ if (!files.length) {
60
+ writeFileSync(`${O}.txt`, `# verify ${R} ${head}: no JS/TS file changed relative to origin/${B}\n`);
61
+ return true;
62
+ }
63
+ const { runner, script } = runnerOf(WT);
64
+ if (!runner)
65
+ throw new Error(unsupported(script));
66
+ const cap = testCap(cfg);
67
+ // One run → "<file> :: <test>" for each failure (undefined = the runner left no JSON report).
68
+ const tests = async (dir, mode, list) => {
69
+ const json = join(tmpdir(), `orq-verify-${process.pid}-${Date.now()}.json`);
70
+ try {
71
+ await runTests(dir, runner, runnerArgs(runner, mode, list, json), cap, c);
72
+ const r = failures(json, dir);
73
+ if (!r)
74
+ c.say(m(`no JSON result in ${dir} (did the runner die? see ${O}.log)`, `sem resultado JSON em ${dir} (o runner morreu? veja ${O}.log)`));
75
+ return r;
76
+ }
77
+ finally {
78
+ rmSync(json, { force: true });
79
+ }
80
+ };
81
+ const mine = await tests(WT, "related", files);
82
+ if (!mine)
83
+ throw new Error(m("the test run failed", "a execução dos testes falhou"));
84
+ const failed = mine.failed;
85
+ let baseFailed = [], baseInfo = "no failure on the branch: base not checked";
86
+ if (failed.length)
87
+ baseInfo = await onBase(j, c, failed, async (dir, list) => (await tests(dir, "files", list))?.failed, (f) => { baseFailed = f; });
88
+ const newList = failed.filter((f) => !baseFailed.includes(f)), oldList = failed.filter((f) => baseFailed.includes(f));
89
+ NEW = newList.length;
90
+ const text = [`# verify ${R} ${head} ${today()}: ${mine.total} tests related to ${files.length} changed files`, `# base: ${baseInfo}`,
91
+ "", `## New failures (${NEW})`, ...(newList.length ? newList : ["none"]),
92
+ "", `## Already failing on origin/${B}`, ...(oldList.length ? oldList : ["none"])].join("\n") + "\n";
93
+ writeFileSync(`${O}.txt`, text);
94
+ process.stdout.write(text);
95
+ return NEW === 0;
96
+ }, (st) => ({ fields: `status=${st} new=${NEW}`, notice: `verify ${R} status=${st} new=${NEW} answer: ${O}.txt` }));
97
+ }
98
+ /** The failing test files again, on a fresh origin/<base> in a temporary worktree (only those that exist there). */
99
+ async function onBase(j, c, failed, tests, set) {
100
+ const REPO = join(j.ws, j.repo), B = j.base, BWT = join(j.ws, ".worktrees", `verifybase-${j.repo}-${process.pid}`);
101
+ if (git(REPO, "fetch", "-q", "origin", B) === undefined)
102
+ c.say(m(`WARNING: fetching origin/${B} failed; using the local origin/${B}`, `AVISO: o fetch de origin/${B} falhou; usando o origin/${B} local`));
103
+ if (git(REPO, "worktree", "add", "-q", "--detach", BWT, `origin/${B}`) === undefined)
104
+ return `WARNING: could not create a worktree at origin/${B}; every failure counts as new`;
105
+ const drop = () => dropWorktree(REPO, BWT);
106
+ c.cleanup.push(drop);
107
+ try {
108
+ linkModules(REPO, BWT);
109
+ const files = [...new Set(failed.map((f) => f.replace(/ :: .*/, "")))].filter((f) => existsSync(join(BWT, f)));
110
+ const info = `${files.length} failing file(s) run on origin/${B} ${git(REPO, "rev-parse", "--short", `origin/${B}`) ?? "?"}`;
111
+ if (!files.length)
112
+ return info;
113
+ const base = await tests(BWT, files);
114
+ if (!base)
115
+ return `WARNING: the base run failed (see ${j.out}.log); every failure counts as new`;
116
+ set(base);
117
+ return info;
118
+ }
119
+ finally {
120
+ c.cleanup.splice(c.cleanup.indexOf(drop), 1);
121
+ drop();
122
+ }
123
+ }
@@ -0,0 +1,81 @@
1
+ // orq wait — Notifier for any conductor: block until children started by `orq agent` (or orq verify/baseline) write
2
+ // <prefix>.done, then print one line each: "[ORQ AGENT DONE] <title> status=ok rc=0 ... answer: <file>".
3
+ // Exit 0 = all ok, 1 = some failed (or its runner died without .done), 124 = timeout. With --any, returns at the first.
4
+ // Claude Code: run it as a background task; it wakes the conductor when it exits.
5
+ import { existsSync, readFileSync } from "node:fs";
6
+ import { basename } from "node:path";
7
+ import { m } from "../i18n.js";
8
+ import { alive } from "../lock.js";
9
+ export const USAGE = "orq wait [--any] [--timeout <s>] [--answer] <prefix>... block until child agents finish (exit 1 = one failed)";
10
+ const read = (f) => { try {
11
+ return readFileSync(f, "utf8");
12
+ }
13
+ catch {
14
+ return undefined;
15
+ } };
16
+ /** Finished? → its summary line and whether it went well. */
17
+ export function doneLine(prefix) {
18
+ let done = read(`${prefix}.done`)?.trim();
19
+ if (done === undefined) {
20
+ const pid = Number(read(`${prefix}.pid`));
21
+ if (!pid || alive(pid) || existsSync(`${prefix}.done`))
22
+ return undefined;
23
+ done = `status=died ${m("(the runner exited without writing .done)", "(o runner saiu sem escrever .done)")}`;
24
+ }
25
+ let title = basename(prefix);
26
+ try {
27
+ title = JSON.parse(read(`${prefix}.job.json`) ?? "").title || title;
28
+ }
29
+ catch { /* verify/baseline: no job */ }
30
+ const answer = [`${prefix}.out.md`, `${prefix}.txt`].find(existsSync) ?? prefix;
31
+ return { line: `[ORQ AGENT DONE] ${title} ${done} answer: ${answer}`, ok: /\bstatus=ok\b/.test(done) };
32
+ }
33
+ export async function wait(argv) {
34
+ let any = false, timeout = 0, answer = false;
35
+ const pending = [];
36
+ for (let i = 0; i < argv.length; i++) {
37
+ const a = argv[i];
38
+ if (a === "--any")
39
+ any = true;
40
+ else if (a === "--answer")
41
+ answer = true;
42
+ else if (a === "--timeout")
43
+ timeout = Number(argv[++i]);
44
+ else if (!a.startsWith("-"))
45
+ pending.push(a.replace(/\.(done|out\.md)$/, ""));
46
+ else {
47
+ console.error(USAGE);
48
+ return 2;
49
+ }
50
+ }
51
+ if (!pending.length || Number.isNaN(timeout)) {
52
+ console.error(USAGE);
53
+ return 2;
54
+ }
55
+ const end = timeout > 0 ? Date.now() + timeout * 1000 : Infinity;
56
+ let failed = false;
57
+ for (;;) {
58
+ for (const p of [...pending]) {
59
+ const d = doneLine(p);
60
+ if (!d)
61
+ continue;
62
+ pending.splice(pending.indexOf(p), 1);
63
+ console.log(d.line);
64
+ if (answer) {
65
+ const t = read(`${p}.out.md`);
66
+ if (t)
67
+ console.log(t.trimEnd());
68
+ }
69
+ failed ||= !d.ok;
70
+ if (any)
71
+ return d.ok ? 0 : 1;
72
+ }
73
+ if (!pending.length)
74
+ return failed ? 1 : 0;
75
+ if (Date.now() > end) {
76
+ console.log(m(`still running: ${pending.join(" ")}`, `ainda rodando: ${pending.join(" ")}`));
77
+ return 124;
78
+ }
79
+ await new Promise((r) => setTimeout(r, 1000));
80
+ }
81
+ }
package/dist/config.js ADDED
@@ -0,0 +1,93 @@
1
+ // The orq configuration, merged in layers (each overrides the previous, key by key):
2
+ // 1. built-in defaults (DEFAULTS, below)
3
+ // 2. personal: $ORQ_CONFIG, or <config dir>/config.toml | config.json
4
+ // 3. workspace: <workspace>/.orq/workspace.toml | workspace.json (searched from -w or the current folder, upwards)
5
+ import { existsSync, readFileSync } from "node:fs";
6
+ import { dirname, join, resolve } from "node:path";
7
+ import { parse as parseToml } from "smol-toml";
8
+ import { configDir, expand } from "./paths.js";
9
+ import { m } from "./i18n.js";
10
+ export const DEFAULTS = {
11
+ machine: {
12
+ agent_mem_max: "4G", // memory cap per agent
13
+ agent_swap_max: "1G",
14
+ test_mem_max: "6G", // cap per test suite (orq verify, orq baseline)
15
+ mem_enforce: "auto", // auto (kernel cap where possible, else watchdog) | kernel | watchdog | off
16
+ test_slots: 1, // test suites in parallel on the machine
17
+ close_tab_after: 20, // seconds until a finished child tab closes (0 = never)
18
+ tui: false, // children in the agent's own screen (-T) by default
19
+ terminal: "auto", // where children show their output: auto (herdr, else tmux, else plain) | herdr | tmux | plain
20
+ },
21
+ agents: {}, // only agents declared here are available; run templates: built in for 5 (src/agents.ts), or `run = ...`
22
+ policy: {
23
+ quota_limit: 90, // quota % from which an agent/account is skipped
24
+ max_rounds: 2, // fix rounds before stopping and calling the user
25
+ review: "other-vendor", // other-vendor | any
26
+ },
27
+ ui: { lang: "auto", theme: "gunmetal" }, // lang: auto | en | pt · theme: gunmetal | chrome | lcd | terminal
28
+ // Where quota comes from, per agent: "builtin" | "none" | false | { command = "..." } (see src/quota.ts).
29
+ // "claude" covers every account in claude-accounts.
30
+ quota: { claude: "builtin", codex: "builtin", opencode: "none", antigravity: "builtin", copilot: "builtin" },
31
+ tracker: { kind: "none" }, // plane | none
32
+ repos: {}, // defined by the workspace
33
+ };
34
+ export class ConfigError extends Error {
35
+ }
36
+ const isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
37
+ export function merge(base, over) {
38
+ const out = { ...base };
39
+ for (const [k, v] of Object.entries(over))
40
+ out[k] = isObj(v) && isObj(out[k]) ? merge(out[k], v) : v;
41
+ return out;
42
+ }
43
+ function expandAll(v) {
44
+ if (Array.isArray(v))
45
+ return v.map(expandAll);
46
+ if (isObj(v))
47
+ return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, expandAll(x)]));
48
+ return typeof v === "string" ? expand(v) : v;
49
+ }
50
+ export function loadFile(path) {
51
+ const text = readFileSync(path, "utf8");
52
+ try {
53
+ return (path.endsWith(".json") ? JSON.parse(text) : parseToml(text));
54
+ }
55
+ catch (e) {
56
+ throw new ConfigError(m(`orq config: error reading ${path}: ${e.message}`, `orq config: erro lendo ${path}: ${e.message}`));
57
+ }
58
+ }
59
+ const firstFile = (...c) => c.find((f) => f && existsSync(f));
60
+ export function findWorkspace(start) {
61
+ for (let d = resolve(start);; d = dirname(d)) {
62
+ const file = firstFile(join(d, ".orq", "workspace.toml"), join(d, ".orq", "workspace.json"));
63
+ if (file)
64
+ return { root: d, file };
65
+ if (dirname(d) === d)
66
+ return {};
67
+ }
68
+ }
69
+ export function loadConfig(workspace) {
70
+ const user = firstFile(process.env.ORQ_CONFIG && expand(process.env.ORQ_CONFIG), join(configDir(), "config.toml"), join(configDir(), "config.json"));
71
+ const ws = findWorkspace(workspace ?? process.cwd());
72
+ let cfg = DEFAULTS;
73
+ const files = [];
74
+ for (const f of [user, ws.file]) {
75
+ if (!f)
76
+ continue;
77
+ cfg = merge(cfg, loadFile(f));
78
+ files.push(f);
79
+ }
80
+ cfg = expandAll(cfg);
81
+ cfg._workspace = ws.root ?? null;
82
+ return { cfg, files };
83
+ }
84
+ /** Value at a dotted path (a.b.c), or undefined. */
85
+ export function getPath(cfg, path) {
86
+ let v = cfg;
87
+ for (const part of path.split(".")) {
88
+ if (!isObj(v) || !(part in v))
89
+ return undefined;
90
+ v = v[part];
91
+ }
92
+ return v;
93
+ }
package/dist/fsutil.js ADDED
@@ -0,0 +1,9 @@
1
+ import { mkdirSync, renameSync, writeFileSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ /** Write via a temp file + rename, so a reader never sees a half-written file. */
4
+ export function writeAtomic(path, text) {
5
+ mkdirSync(dirname(path), { recursive: true });
6
+ const tmp = `${path}.${process.pid}.tmp`;
7
+ writeFileSync(tmp, text, "utf8");
8
+ renameSync(tmp, path);
9
+ }
package/dist/i18n.js ADDED
@@ -0,0 +1,21 @@
1
+ export function systemLang() {
2
+ const e = process.env;
3
+ const loc = e.LC_ALL || e.LC_MESSAGES || e.LANG || Intl.DateTimeFormat().resolvedOptions().locale || "";
4
+ return loc.toLowerCase().startsWith("pt") ? "pt" : "en";
5
+ }
6
+ export function detectLang(cfgLang, useEnv = true) {
7
+ const v = useEnv ? process.env.ORQ_LANG : undefined;
8
+ if (v === "en" || v === "pt")
9
+ return v;
10
+ if (cfgLang === "en" || cfgLang === "pt")
11
+ return cfgLang;
12
+ return systemLang();
13
+ }
14
+ let current;
15
+ export function setLang(l) {
16
+ current = l;
17
+ }
18
+ export function m(en, pt) {
19
+ current ??= detectLang();
20
+ return current === "pt" && pt !== undefined ? pt : en;
21
+ }
@@ -0,0 +1,137 @@
1
+ // What a fresh `npm i -g @irythill/orq` still needs, done by `orq setup` on any OS (install.sh is a Linux extra):
2
+ // skills orq's skills (skills/*/SKILL.md, shipped in the package) linked into ~/.claude/skills and each Claude
3
+ // account's skills folder — a junction on Windows (no admin rights), a copy if linking fails. A real
4
+ // folder, or a link to another live copy (a clone of the repo), is never touched.
5
+ // workspace <folder>/.orquestra, .worktrees, CLAUDE.md from the template (only if missing) and the /issue playbook
6
+ // in the conductor's format (orq instructions)
7
+ // environment what orq found: git, terminal host, memory cap, keyring, the shell's completion line
8
+ import { spawnSync } from "node:child_process";
9
+ import { cpSync, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, readlinkSync, realpathSync, rmSync, symlinkSync } from "node:fs";
10
+ import { homedir } from "node:os";
11
+ import { basename, join, resolve } from "node:path";
12
+ import { fileURLToPath } from "node:url";
13
+ import { claudeAccounts } from "./accounts.js";
14
+ import { writeAtomic } from "./fsutil.js";
15
+ import { m } from "./i18n.js";
16
+ import { backend } from "./keyring.js";
17
+ import { hasKernelCap } from "./process.js";
18
+ import { detectHost } from "./terminal.js";
19
+ export const PKG = fileURLToPath(new URL("..", import.meta.url));
20
+ const SKILLS = join(PKG, "skills");
21
+ const real = (p) => { try {
22
+ return realpathSync(p);
23
+ }
24
+ catch {
25
+ return undefined;
26
+ } };
27
+ /** Skill folders of every Claude account (~/.claude always, like install.sh). */
28
+ export function skillRoots() {
29
+ return [...new Set([join(homedir(), ".claude"), ...claudeAccounts().map((a) => a.dir)].map((d) => join(d, "skills")))];
30
+ }
31
+ /** Link each of orq's skills into each root; one line per skill and root. */
32
+ export function installSkills(roots = skillRoots()) {
33
+ const out = [];
34
+ const skills = readdirSync(SKILLS).filter((s) => existsSync(join(SKILLS, s, "SKILL.md")));
35
+ for (const root of roots)
36
+ for (const s of skills) {
37
+ const src = join(SKILLS, s), dst = join(root, s);
38
+ let st;
39
+ try {
40
+ st = lstatSync(dst);
41
+ }
42
+ catch { /* missing */ }
43
+ if (st && !st.isSymbolicLink()) {
44
+ // A copy made by an earlier `orq setup` (no links on this system) is refreshed; anything else is the user's.
45
+ if (existsSync(join(dst, ".orq-copy"))) {
46
+ rmSync(dst, { recursive: true, force: true });
47
+ st = undefined;
48
+ }
49
+ else {
50
+ out.push(` ${m("skip", "pulei")} ${dst} ${m("(a real folder, not a link: move it away to install)", "(uma pasta de verdade, não um link: tire-a do caminho para instalar)")}`);
51
+ continue;
52
+ }
53
+ }
54
+ if (st) {
55
+ const to = real(dst);
56
+ if (to === real(src)) {
57
+ out.push(` ok ${dst}`);
58
+ continue;
59
+ }
60
+ if (to && existsSync(join(to, "SKILL.md"))) {
61
+ out.push(` ${m("keep", "mantive")} ${dst} → ${readlinkSync(dst)}`);
62
+ continue;
63
+ }
64
+ rmSync(dst, { force: true }); // dangling (e.g. an old node version's global folder)
65
+ }
66
+ mkdirSync(root, { recursive: true });
67
+ try {
68
+ symlinkSync(src, dst, process.platform === "win32" ? "junction" : "dir");
69
+ out.push(` link ${dst} → ${src}`);
70
+ }
71
+ catch {
72
+ cpSync(src, dst, { recursive: true });
73
+ writeAtomic(join(dst, ".orq-copy"), m("copied by orq setup; refreshed on every run\n", "copiado pelo orq setup; atualizado a cada execução\n"));
74
+ out.push(` ${m("copy", "cópia")} ${dst}`);
75
+ }
76
+ }
77
+ return out;
78
+ }
79
+ /** Prepare a conductor's workspace folder; `fmt` = claude (SKILL.md) | agents (AGENTS.md). */
80
+ export function prepareWorkspace(dir, fmt, name = basename(resolve(dir))) {
81
+ const ws = resolve(dir), out = [];
82
+ for (const d of [".orquestra", ".worktrees"])
83
+ mkdirSync(join(ws, d), { recursive: true });
84
+ const map = join(ws, "CLAUDE.md");
85
+ if (!existsSync(map)) {
86
+ const t = readFileSync(join(PKG, "templates", "workspace.CLAUDE.md"), "utf8");
87
+ writeAtomic(map, t.split("{{WORKSPACE}}").join(ws).split("{{NAME}}").join(name));
88
+ out.push(m(` created ${map} — fill in repos, group, base branch, Plane and issue standard`, ` criado ${map} — preencha repos, grupo, branch base, Plane e padrão de issue`));
89
+ }
90
+ else
91
+ out.push(m(` ok ${map} (kept)`, ` ok ${map} (mantido)`));
92
+ const r = spawnSync(process.execPath, [join(PKG, "dist", "cli.js"), "instructions", fmt, "-w", ws], { encoding: "utf8" });
93
+ out.push(` ${(r.stdout || r.stderr).trim()}`);
94
+ return out;
95
+ }
96
+ const SHELLS = ["bash", "zsh", "fish", "powershell"];
97
+ /** The user's shell, from $SHELL (PowerShell on Windows without one). */
98
+ export function userShell() {
99
+ const s = basename(process.env.SHELL ?? "").replace(/\.exe$/i, "");
100
+ if (s === "pwsh")
101
+ return "powershell";
102
+ if (SHELLS.includes(s))
103
+ return s;
104
+ return process.platform === "win32" ? "powershell" : undefined;
105
+ }
106
+ /** The line that turns completion on, per shell. */
107
+ export const completionLine = (s) => ({
108
+ bash: `echo 'eval "$(orq completion bash)"' >> ~/.bashrc`,
109
+ zsh: `echo 'eval "$(orq completion zsh)"' >> ~/.zshrc # ${m("after compinit", "depois do compinit")}`,
110
+ fish: "orq completion fish > ~/.config/fish/completions/orq.fish",
111
+ powershell: "Add-Content $PROFILE 'orq completion powershell | Out-String | Invoke-Expression'",
112
+ }[s]);
113
+ /** What orq found on this machine, one line each (required things first). */
114
+ export function environment(cfg) {
115
+ const has = (c, a = ["--version"]) => spawnSync(c, a, { stdio: "ignore", timeout: 10_000 }).status === 0;
116
+ const row = (k, v) => ` ${k.padEnd(10)} ${v}`;
117
+ let host;
118
+ try {
119
+ host = detectHost(cfg);
120
+ }
121
+ catch (e) {
122
+ host = e.message;
123
+ }
124
+ const mode = String((cfg.machine ?? {}).mem_enforce ?? "auto");
125
+ const kb = backend(), sh = userShell();
126
+ return [
127
+ row("node", process.version),
128
+ row("git", has("git") ? "ok" : m("MISSING — required (worktrees, diffs, tests): install git", "FALTANDO — obrigatório (worktrees, diffs, testes): instale o git")),
129
+ row("terminal", `${host} ${m("(children: herdr tab, tmux window or background; machine.terminal)", "(filhos: aba do herdr, janela do tmux ou segundo plano; machine.terminal)")}`),
130
+ row(m("memory", "memória"), mode === "off" ? "off" : mode === "watchdog" || !hasKernelCap()
131
+ ? m("watchdog (samples the process tree; machine.mem_enforce)", "vigia (amostra a árvore de processos; machine.mem_enforce)")
132
+ : m("kernel (systemd scope)", "kernel (scope do systemd)")),
133
+ row("keyring", kb === "native" ? "ok" : kb === "cli" ? m("OS command (native module did not load)", "comando do SO (o módulo nativo não carregou)")
134
+ : m("none — Antigravity/macOS Claude quota shows ?", "nenhum — a cota do Antigravity/Claude no macOS mostra ?")),
135
+ row("shell", sh ? `${sh} — ${m("completion", "autocompletar")}: ${completionLine(sh)}` : m("unknown — orq completion bash|zsh|fish|powershell", "desconhecido — orq completion bash|zsh|fish|powershell")),
136
+ ];
137
+ }
@@ -0,0 +1,51 @@
1
+ // Keyring — read a secret another tool keeps in the OS keyring (Secret Service on Linux, Keychain on macOS, Credential
2
+ // Manager on Windows) through @napi-rs/keyring. orq only reads; it never writes or refreshes someone else's login.
3
+ // The native module may not load (a platform without a prebuilt binary, a broken install): then the OS's own CLI where
4
+ // there is one (Linux secret-tool, macOS security), else nothing. $ORQ_KEYRING = native | cli | off forces a backend.
5
+ import { spawnSync } from "node:child_process";
6
+ import { createRequire } from "node:module";
7
+ let native;
8
+ function load() {
9
+ if (native === undefined) {
10
+ try {
11
+ native = createRequire(import.meta.url)("@napi-rs/keyring");
12
+ }
13
+ catch {
14
+ native = null;
15
+ }
16
+ }
17
+ return native;
18
+ }
19
+ const cli = () => process.platform === "linux" ? "secret-tool" : process.platform === "darwin" ? "security" : undefined;
20
+ /** Which backend reads secrets here. */
21
+ export function backend() {
22
+ const want = process.env.ORQ_KEYRING;
23
+ if (want === "off")
24
+ return "none";
25
+ if (want !== "cli" && load())
26
+ return "native";
27
+ return cli() && spawnSync(cli(), ["--help"], { stdio: "ignore", timeout: 5000 }).error === undefined ? "cli" : "none";
28
+ }
29
+ /**
30
+ * The secret stored for (service, account), or undefined. `target` is the Windows credential name when the tool that
31
+ * wrote it doesn't use the keyring-rs default ("<account>.<service>"), e.g. go-keyring's "<service>:<account>".
32
+ */
33
+ export function readSecret(service, account, target) {
34
+ const b = backend();
35
+ if (b === "native") {
36
+ try {
37
+ const e = target && process.platform === "win32" ? load().Entry.withTarget(target, service, account) : new (load().Entry)(service, account);
38
+ return e.getPassword() ?? undefined;
39
+ }
40
+ catch {
41
+ return undefined;
42
+ } // no keyring service (headless Linux without D-Bus), locked, denied
43
+ }
44
+ if (b === "cli") {
45
+ const args = process.platform === "linux" ? ["lookup", "service", service, "username", account]
46
+ : ["find-generic-password", "-s", service, "-a", account, "-w"];
47
+ const r = spawnSync(cli(), args, { encoding: "utf8", timeout: 10_000, stdio: ["ignore", "pipe", "ignore"] });
48
+ return r.status === 0 && r.stdout.trim() ? r.stdout.trim() : undefined;
49
+ }
50
+ return undefined;
51
+ }
package/dist/lock.js ADDED
@@ -0,0 +1,81 @@
1
+ // Portable FIFO lock with N slots (the test queue), made of plain files — no flock, no /proc/locks.
2
+ // <state dir>/locks/<name>/<ms>-<pid>.json one entry per process that asked: {pid, label, at, held}
3
+ // Holders are the entries with held = true whose process is alive. A waiter takes a slot when fewer than `slots` are
4
+ // held and it is the oldest live non-held entry, after its file has existed for SETTLE_MS (so an older entry still
5
+ // being written is seen first). Dead processes' entries are swept by whoever looks, so a crash never leaves the queue
6
+ // stuck. Works the same on Linux, macOS and Windows (process.kill(pid, 0) checks liveness everywhere).
7
+ import { mkdirSync, readdirSync, readFileSync, rmSync, statSync } from "node:fs";
8
+ import { join } from "node:path";
9
+ import { writeAtomic } from "./fsutil.js";
10
+ import { stateDir } from "./paths.js";
11
+ const SETTLE_MS = 250;
12
+ const dir = (name) => join(stateDir(), "locks", name);
13
+ export function alive(pid) {
14
+ if (!(pid > 0))
15
+ return false; // kill(0, …) would signal our own process group and "succeed"
16
+ try {
17
+ process.kill(pid, 0);
18
+ return true;
19
+ }
20
+ catch (e) {
21
+ return e.code === "EPERM";
22
+ }
23
+ }
24
+ /** Live entries in queue order (dead ones are removed). */
25
+ export function entries(name) {
26
+ let files;
27
+ try {
28
+ files = readdirSync(dir(name)).filter((f) => f.endsWith(".json"));
29
+ }
30
+ catch {
31
+ return [];
32
+ }
33
+ const out = [];
34
+ for (const f of files) {
35
+ const file = join(dir(name), f);
36
+ try {
37
+ const e = { ...JSON.parse(readFileSync(file, "utf8")), file };
38
+ if (alive(e.pid))
39
+ out.push(e);
40
+ else
41
+ rmSync(file, { force: true });
42
+ }
43
+ catch { /* being written or removed: skip this round */ }
44
+ }
45
+ return out.sort((a, b) => a.at - b.at || a.pid - b.pid);
46
+ }
47
+ export const status = (name) => {
48
+ const all = entries(name);
49
+ return { holders: all.filter((e) => e.held), waiters: all.filter((e) => !e.held) };
50
+ };
51
+ /** Queue `pid` and resolve once it holds a slot. `onWait` is called once if it has to wait. */
52
+ export async function acquire(name, pid, label, slots = 1, onWait, pollMs = 500) {
53
+ mkdirSync(dir(name), { recursive: true });
54
+ const mine = entries(name).find((e) => e.pid === pid);
55
+ if (mine?.held)
56
+ return; // re-entrant for the same process
57
+ const at = mine?.at ?? Date.now();
58
+ const file = mine?.file ?? join(dir(name), `${at}-${pid}.json`);
59
+ writeAtomic(file, JSON.stringify({ pid, label, at, held: false }));
60
+ let told = false;
61
+ for (;;) {
62
+ const all = entries(name), held = all.filter((e) => e.held).length;
63
+ const first = all.find((e) => !e.held);
64
+ const settled = Date.now() - statSync(file).mtimeMs >= SETTLE_MS || Date.now() - at >= SETTLE_MS;
65
+ if (held < Math.max(1, slots) && first?.pid === pid && settled) {
66
+ writeAtomic(file, JSON.stringify({ pid, label, at, held: true }));
67
+ return;
68
+ }
69
+ if (!told && (held >= slots || first?.pid !== pid)) {
70
+ onWait?.(all.findIndex((e) => e.pid === pid) - held + 1);
71
+ told = true;
72
+ }
73
+ await new Promise((r) => setTimeout(r, pollMs));
74
+ }
75
+ }
76
+ export function release(name, pid) {
77
+ const e = entries(name).find((x) => x.pid === pid);
78
+ if (e)
79
+ rmSync(e.file, { force: true });
80
+ return Boolean(e);
81
+ }
package/dist/paths.js ADDED
@@ -0,0 +1,34 @@
1
+ // Where orq keeps its files, per OS. Linux/macOS keep the XDG paths the bash/Python scripts always used, so an
2
+ // existing ~/.config/orq and ~/.local/state/orq keep working; Windows uses %APPDATA% / %LOCALAPPDATA%.
3
+ import { existsSync } from "node:fs";
4
+ import { homedir } from "node:os";
5
+ import { join } from "node:path";
6
+ const env = process.env;
7
+ const win = process.platform === "win32";
8
+ export function configDir() {
9
+ if (env.XDG_CONFIG_HOME)
10
+ return join(env.XDG_CONFIG_HOME, "orq");
11
+ if (win)
12
+ return join(env.APPDATA ?? join(homedir(), "AppData", "Roaming"), "orq");
13
+ return join(homedir(), ".config", "orq");
14
+ }
15
+ export function stateDir() {
16
+ if (env.XDG_STATE_HOME)
17
+ return join(env.XDG_STATE_HOME, "orq");
18
+ if (win)
19
+ return join(env.LOCALAPPDATA ?? join(homedir(), "AppData", "Local"), "orq", "state");
20
+ return join(homedir(), ".local", "state", "orq");
21
+ }
22
+ /** The personal config file to write: $ORQ_CONFIG, else config.json if only that exists, else config.toml. */
23
+ export function userConfigFile() {
24
+ if (env.ORQ_CONFIG)
25
+ return env.ORQ_CONFIG;
26
+ const toml = join(configDir(), "config.toml"), json = join(configDir(), "config.json");
27
+ return existsSync(json) && !existsSync(toml) ? json : toml;
28
+ }
29
+ /** Expand a leading ~ and $VAR / ${VAR} (unknown variables are left as they are, like Python's expandvars). */
30
+ export function expand(s) {
31
+ if (s === "~" || s.startsWith("~/") || s.startsWith("~\\"))
32
+ s = homedir() + s.slice(1);
33
+ return s.replace(/\$(\w+)|\$\{(\w+)\}/g, (all, a, b) => env[a ?? b] ?? all);
34
+ }