@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
package/dist/agents.js ADDED
@@ -0,0 +1,202 @@
1
+ import { m } from "./i18n.js";
2
+ const NOMCP_CODEX = "-c mcp_servers.plane.enabled=false -c mcp_servers.shadcn.enabled=false";
3
+ const CLAUDE = "claude -p --strict-mcp-config --output-format stream-json --verbose [--model {model}] --effort {effort:low}";
4
+ const OPENCODE = "opencode run --standalone --format json [-m {model}]"; // v2: effort goes in the model, "provider/model#variant"
5
+ const AGY = "agy --output-format stream-json --sandbox [--model {model}] [--effort {effort}]";
6
+ const COPILOT = "-s -C {dir} --disable-builtin-mcps --disable-mcp-server plane --allow-tool 'shell(git diff:*)' " +
7
+ "--allow-tool 'shell(git log:*)' --allow-tool 'shell(git show:*)' --deny-tool write [--model {model}] " +
8
+ "[--reasoning-effort {effort}] -p {prompt_text}";
9
+ /** What herdr-agent did per agent, as templates. */
10
+ export const BUILTIN_AGENTS = {
11
+ claude: {
12
+ // stream-json shows live progress; a fixed --session-id lets the follow-up --resume. No MCP; the child must not
13
+ // report state on the conductor's herdr pane.
14
+ run: `${CLAUDE} --session-id {session} {mode}`, resume: `${CLAUDE} --resume {session} {mode}`,
15
+ readonly: "--permission-mode plan", write: "--permission-mode auto --add-dir {gitdir}",
16
+ stdin: true, format: "claude", session: "uuid", accounts: true, unset_env: ["HERDR_PANE_ID"],
17
+ },
18
+ codex: {
19
+ // Codex treats any .git inside a writable root as read only: the folders a commit needs are granted one by one.
20
+ // Read only: its read-only sandbox doesn't even let jest write its cache, so it runs on a throwaway worktree.
21
+ run: `codex exec [-m {model}] -c model_reasoning_effort={effort:low} -s workspace-write -C {dir} ` +
22
+ `--skip-git-repo-check {mode} ${NOMCP_CODEX} -o {out} -`,
23
+ resume: `codex exec resume {session} -c model_reasoning_effort={effort:low} ` +
24
+ `-c sandbox_workspace_write.writable_roots={gitroots_json} ${NOMCP_CODEX} -o {out} -`,
25
+ readonly: "", write: "[--add-dir {gitroots}]", stdin: true, format: "codex", answer: "file", throwaway: true,
26
+ },
27
+ opencode: {
28
+ // Permissions and MCPs off via OPENCODE_CONFIG_CONTENT. Headless has nobody to approve, so write = --auto (only
29
+ // ever in a worktree) and read only = throwaway worktree with the edit tool denied.
30
+ run: `${OPENCODE} {mode} {prompt_text}`, resume: `${OPENCODE} -s {session} {mode} {prompt_text}`, readonly: "", write: "--auto",
31
+ format: "opencode", throwaway: true, env: { OPENCODE_CONFIG_CONTENT: "{opencode_config}" },
32
+ },
33
+ antigravity: {
34
+ // --sandbox + accept-edits (no skip-permissions); headless denies commands outside agy's allowlist, so in practice
35
+ // it reviews. The prompt goes in a file in its folder (excluded from git) and -p only says "read it".
36
+ run: `${AGY} {mode} -p {prompt_hint}`, resume: `${AGY} --conversation {session} {mode} -p {prompt_hint}`,
37
+ readonly: "--mode plan", write: "--mode accept-edits --add-dir {gitdir}",
38
+ format: "antigravity", throwaway: true, prompt_in_dir: true,
39
+ },
40
+ copilot: {
41
+ // Second opinion, always read only: git reads allowed, anything else denied by -p without asking.
42
+ run: `copilot --session-id={session} ${COPILOT}`, resume: `copilot --resume={session} ${COPILOT}`,
43
+ readonly: "", write: false, session: "uuid",
44
+ },
45
+ };
46
+ export const ALIASES = { agy: "antigravity" };
47
+ /** The definition of `name`: built-in defaults, then [agents.<name>] from the config. */
48
+ export function agentDef(cfg, name) {
49
+ const user = (cfg.agents ?? {})[name] ?? {};
50
+ const def = { ...(BUILTIN_AGENTS[name] ?? {}), ...user };
51
+ if (typeof def.run !== "string")
52
+ throw new Error(m(`unknown agent: ${name} (built in: ${Object.keys(BUILTIN_AGENTS).join(", ")}; others need agents.${name}.run in the config)`, `agente desconhecido: ${name} (embutidos: ${Object.keys(BUILTIN_AGENTS).join(", ")}; outros precisam de agents.${name}.run no config)`));
53
+ return def;
54
+ }
55
+ /** Split a template into words, remembering which [group] each belongs to. */
56
+ export function tokenize(t) {
57
+ const out = [];
58
+ let cur = "", q = "", has = false, g = -1, gid = 0, close = false;
59
+ const push = () => {
60
+ if (has)
61
+ out.push({ text: cur, group: g });
62
+ if (close) {
63
+ g = -1;
64
+ close = false;
65
+ }
66
+ cur = "";
67
+ has = false;
68
+ };
69
+ for (const c of t) {
70
+ if (q) {
71
+ if (c === q)
72
+ q = "";
73
+ else
74
+ cur += c;
75
+ continue;
76
+ }
77
+ if (c === "'" || c === '"') {
78
+ q = c;
79
+ has = true;
80
+ continue;
81
+ }
82
+ if (/\s/.test(c)) {
83
+ push();
84
+ continue;
85
+ }
86
+ if (c === "[" && !has && g < 0) {
87
+ g = gid++;
88
+ continue;
89
+ }
90
+ if (c === "]" && g >= 0) {
91
+ close = true;
92
+ push();
93
+ continue;
94
+ }
95
+ cur += c;
96
+ has = true;
97
+ }
98
+ push();
99
+ return out;
100
+ }
101
+ const PH = /\{(\w+)(?::([^}]*))?\}/g;
102
+ function expandWords(ws, vars, optional) {
103
+ const names = ws.flatMap((w) => [...w.matchAll(PH)].map((x) => x[1]));
104
+ for (const n of names)
105
+ if (!(n in vars))
106
+ throw new Error(m(`unknown placeholder {${n}}`, `placeholder desconhecido {${n}}`));
107
+ const list = optional ? names.find((n) => Array.isArray(vars[n])) : undefined;
108
+ if (list)
109
+ return vars[list].flatMap((v) => expandWords(ws, { ...vars, [list]: v }, true));
110
+ const out = [];
111
+ for (const w of ws) {
112
+ const whole = /^\{(\w+)(?::([^}]*))?\}$/.exec(w);
113
+ const wv = whole ? vars[whole[1]] : undefined;
114
+ if (Array.isArray(wv)) {
115
+ out.push(...wv);
116
+ continue;
117
+ }
118
+ let empty = false;
119
+ const s = w.replace(PH, (_, n, d) => {
120
+ const v = vars[n], x = (Array.isArray(v) ? v.join(" ") : v) || d || "";
121
+ if (!x)
122
+ empty = true;
123
+ return x;
124
+ });
125
+ if (empty && optional)
126
+ return [];
127
+ if (!(whole && !s))
128
+ out.push(s);
129
+ }
130
+ return out;
131
+ }
132
+ /** Template → argv. */
133
+ export function expandTemplate(t, vars) {
134
+ const words = tokenize(t), out = [];
135
+ for (let i = 0; i < words.length;) {
136
+ const g = words[i].group;
137
+ let j = i + 1;
138
+ if (g >= 0)
139
+ while (j < words.length && words[j].group === g)
140
+ j++;
141
+ out.push(...expandWords(words.slice(i, j).map((w) => w.text), vars, g >= 0));
142
+ i = j;
143
+ }
144
+ return out;
145
+ }
146
+ /** Placeholders inside a single string (env values): no splitting. */
147
+ export const expandString = (s, vars) => s.replace(PH, (_, n, d) => { const v = vars[n]; return (Array.isArray(v) ? v.join(" ") : v) || d || ""; });
148
+ const json = (l) => { try {
149
+ const v = JSON.parse(l);
150
+ return v && typeof v === "object" ? v : undefined;
151
+ }
152
+ catch {
153
+ return undefined;
154
+ } };
155
+ const jsonLines = (log) => log.split(/\r?\n/).filter((l) => l.startsWith("{")).map(json).filter((x) => !!x);
156
+ const lastOf = (xs) => xs.length ? xs[xs.length - 1] : undefined;
157
+ export const FORMATS = {
158
+ text: { show: (l) => `${l}\n`, finish: (_log, stdout) => ({ answer: stdout }) },
159
+ codex: { show: (l) => `${l}\n`, finish: (log) => ({ session: /session id:?\s*(\S+)/.exec(log)?.[1] }) },
160
+ claude: {
161
+ show(l) {
162
+ const j = json(l);
163
+ if (j?.type !== "assistant")
164
+ return undefined;
165
+ const parts = (j.message?.content ?? []).map((c) => c.type === "text" ? c.text : c.type === "tool_use" ? `· ${c.name}` : undefined)
166
+ .filter((x) => x !== undefined);
167
+ return parts.length ? parts.join("\n") + "\n" : undefined;
168
+ },
169
+ finish: (log) => ({ answer: lastOf(jsonLines(log).filter((j) => j.type === "result").map((j) => String(j.result ?? ""))) ?? "" }),
170
+ },
171
+ opencode: {
172
+ show(l) {
173
+ const j = json(l);
174
+ return j?.type === "text" ? `${j.part?.text ?? ""}\n` : j?.type === "tool_use" ? `· ${j.part?.tool ?? "tool"}\n` : undefined;
175
+ },
176
+ finish(log) {
177
+ const js = jsonLines(log);
178
+ return { session: lastOf(js.filter((j) => j.sessionID).map((j) => String(j.sessionID))),
179
+ answer: lastOf(js.filter((j) => j.type === "text").map((j) => String(j.part?.text ?? ""))) ?? "" };
180
+ },
181
+ idle: (last) => { const j = json(last); return j?.type === "step_finish" && j.part?.reason === "stop"; },
182
+ },
183
+ antigravity: {
184
+ show(l) {
185
+ const j = json(l), s = j?.event === "step_update" ? j.step_update : undefined;
186
+ if (!s)
187
+ return undefined;
188
+ if (s.step_type === "agent_response")
189
+ return s.text_delta || undefined;
190
+ return s.step_type === "tool" && s.state === "DONE" ? "\n· tool\n" : undefined;
191
+ },
192
+ finish(log) {
193
+ const res = jsonLines(log).filter((j) => j.event === "result").map((j) => j.result ?? {});
194
+ let answer = lastOf(res.map((r) => String(r.response ?? ""))) ?? "";
195
+ // An agy error (e.g. a denied command) is a failure, even with an answer.
196
+ const errs = log.split(/\r?\n/).filter((l) => l.startsWith("jetski: "));
197
+ if (errs.length)
198
+ answer += (answer ? "\n" : "") + errs.join("\n") + "\n";
199
+ return { session: lastOf(res.map((r) => r.conversation_id).filter(Boolean)), answer, rc: errs.length ? 3 : undefined };
200
+ },
201
+ },
202
+ };
package/dist/banner.js ADDED
@@ -0,0 +1,87 @@
1
+ // The orc conductor: ASCII art shown when orq starts in a real terminal (`orq`, `orq --version`, `orq setup`).
2
+ // Never printed when stdout is not a TTY, so conductors and scripts reading orq's output never see it.
3
+ // Colors are painted over the ASCII (24-bit when COLORTERM says so, else the 16 basic colors); NO_COLOR turns them off.
4
+ import { readFileSync } from "node:fs";
5
+ import { m } from "./i18n.js";
6
+ export const ORC = [
7
+ " (@) *",
8
+ " .-'''''''-. /",
9
+ " / /==^==\\ \\ /",
10
+ " /\\ | [ (o) (o) ] | /\\ _/",
11
+ " \\ '-| (_) |-' / (_)",
12
+ " '-.| ^ ^ |.-' | |",
13
+ " \\ '-._____.-' / / /",
14
+ " _.--'-._______.-'--.______/ /",
15
+ " / /| \\>o</ |\\ \\ /",
16
+ " | / | \\|/ | \\ |",
17
+ ];
18
+ const RGB = {
19
+ skin: [155, 209, 74], brow: [106, 165, 49], eye: [214, 52, 40], white: [240, 236, 224],
20
+ coat: [150, 146, 170], hair: [120, 116, 140], wood: [190, 120, 60], tip: [226, 178, 70],
21
+ };
22
+ const BASIC = { skin: 92, brow: 32, eye: 91, white: 97, coat: 37, hair: 90, wood: 33, tip: 93 };
23
+ /** Which part of the orc a character belongs to (row, column). */
24
+ export function tone(y, x, ch) {
25
+ if (ch === " ")
26
+ return undefined;
27
+ if (y === 0 && x < 20)
28
+ return "hair";
29
+ if (x >= 29)
30
+ return y <= 3 ? (ch === "*" ? "tip" : "wood") : y >= 8 ? "coat" : "skin";
31
+ if (y === 3 && ch === "o")
32
+ return "eye";
33
+ if (y === 5 && ch === "^")
34
+ return "white";
35
+ if (y === 2 && (ch === "=" || ch === "^"))
36
+ return "brow";
37
+ if (y >= 7)
38
+ return y >= 8 && x >= 10 && x <= 16 ? (">o<".includes(ch) ? "eye" : "white") : "coat";
39
+ return "skin";
40
+ }
41
+ export function colorMode(env = process.env, tty = Boolean(process.stdout.isTTY)) {
42
+ if (!tty || env.NO_COLOR)
43
+ return "none";
44
+ return /truecolor|24bit/i.test(env.COLORTERM ?? "") || env.WT_SESSION ? "true" : "basic";
45
+ }
46
+ const paint = (t, s, mode) => mode === "none" ? s
47
+ : mode === "true" ? `\x1b[38;2;${RGB[t].join(";")}m${s}\x1b[0m` : `\x1b[${BASIC[t]}m${s}\x1b[0m`;
48
+ export function version() {
49
+ try {
50
+ return JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
51
+ }
52
+ catch {
53
+ return "?";
54
+ }
55
+ }
56
+ /** Lines shown next to the orc: version and who plays which role. */
57
+ export function infoLines(cfg) {
58
+ const roles = (cfg.roles ?? {});
59
+ const agents = Object.keys((cfg.agents ?? {}));
60
+ const who = (r) => r?.agent ? String(r.agent) : r?.pool ? `auto (${r.pool.join(", ")})` : undefined;
61
+ const lines = [`orq ${version()}`, m("orchestrate coding agents", "orquestrando agentes de código"), ""];
62
+ if (!agents.length)
63
+ return [...lines, m("no agents yet — run `orq setup`", "nenhum agente ainda — rode `orq setup`")];
64
+ const conductor = who(roles.conductor), primary = who(roles.primary), reviewer = who(roles.reviewer);
65
+ if (conductor)
66
+ lines.push(`${m("conductor", "conductor")} ${conductor}`);
67
+ if (primary)
68
+ lines.push(`${m("primary", "principal")} ${primary}`);
69
+ if (reviewer)
70
+ lines.push(`${m("reviewer", "revisor")} ${reviewer}`);
71
+ if (!primary && !reviewer)
72
+ lines.push(`${m("agents", "agentes")} ${agents.join(", ")}`);
73
+ return [...lines, "", "orq setup · orq pick · orq --help"];
74
+ }
75
+ /** The orc with `info` beside it, or "" when stdout is not a terminal. */
76
+ export function banner(info, mode = colorMode(), tty = Boolean(process.stdout.isTTY)) {
77
+ if (!tty)
78
+ return "";
79
+ const w = Math.max(...ORC.map((l) => l.length)) + 4;
80
+ const top = Math.max(0, Math.floor((ORC.length - info.length) / 2));
81
+ return ORC.map((line, y) => {
82
+ const art = [...line].map((ch, x) => { const t = tone(y, x, ch); return t ? paint(t, ch, mode) : ch; }).join("");
83
+ const i = info[y - top];
84
+ const text = i === undefined ? "" : y === top && mode !== "none" ? `\x1b[1m${i}\x1b[0m` : i;
85
+ return (art + " ".repeat(w - line.length) + text).trimEnd();
86
+ }).join("\n") + "\n";
87
+ }
package/dist/cli.js ADDED
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env node
2
+ // orq — one CLI for the orchestration. Subcommands are being ported from setup/ one by one
3
+ // (docs/portability.md); the old names (orq-config, orq-set, orq-lang, orq-block) call into this.
4
+ import { ConfigError, getPath, loadConfig } from "./config.js";
5
+ import { banner, infoLines, version } from "./banner.js";
6
+ import { detectLang, m, setLang } from "./i18n.js";
7
+ import { agent, USAGE as AGENT } from "./commands/agent.js";
8
+ import { board, USAGE as BOARD } from "./commands/board.js";
9
+ import { baseline, USAGE as BASELINE } from "./commands/baseline.js";
10
+ import { block, USAGE as BLOCK } from "./commands/block.js";
11
+ import { claim, USAGE as CLAIM } from "./commands/claim.js";
12
+ import { completion, USAGE as COMPLETION } from "./commands/completion.js";
13
+ import { config, USAGE as CONFIG } from "./commands/config.js";
14
+ import { deps, USAGE as DEPS } from "./commands/deps.js";
15
+ import { instructions, USAGE as INSTRUCTIONS } from "./commands/instructions.js";
16
+ import { lang, USAGE as LANG } from "./commands/lang.js";
17
+ import { lock, USAGE as LOCK } from "./commands/lock.js";
18
+ import { pick, USAGE as PICK } from "./commands/pick.js";
19
+ import { quota, USAGE as QUOTA } from "./commands/quota.js";
20
+ import { run, USAGE as RUN } from "./commands/run.js";
21
+ import { set, USAGE as SET } from "./commands/set.js";
22
+ import { setup, USAGE as SETUP } from "./commands/setup.js";
23
+ import { stats, USAGE as STATS } from "./commands/stats.js";
24
+ import { verify, USAGE as VERIFY } from "./commands/verify.js";
25
+ import { wait, USAGE as WAIT } from "./commands/wait.js";
26
+ const COMMANDS = { config, set, board, lang, block, quota, pick, stats, setup, lock, claim, deps, run, agent, wait, instructions, verify, baseline,
27
+ completion: (a) => completion(a, Object.keys(COMMANDS)) };
28
+ const usage = () => `orq <command> [args]
29
+
30
+ ${SETUP}
31
+ ${CONFIG}
32
+ ${SET}
33
+ ${LANG}
34
+ ${QUOTA}
35
+ ${PICK}
36
+ ${STATS}
37
+ ${BOARD}
38
+ ${LOCK}
39
+ ${RUN}
40
+ ${AGENT}
41
+ ${WAIT}
42
+ ${VERIFY}
43
+ ${BASELINE}
44
+ ${INSTRUCTIONS}
45
+ ${CLAIM}
46
+ ${DEPS}
47
+ ${COMPLETION}
48
+ ${BLOCK.split("\n")[0]}
49
+ (orq block --help ${m("for more", "para mais")})`;
50
+ async function main(argv) {
51
+ try {
52
+ setLang(detectLang(getPath(loadConfig().cfg, "ui.lang")));
53
+ }
54
+ catch { /* a broken config is reported by the command itself */ }
55
+ const [cmd, ...rest] = argv;
56
+ // The orc greets only a person at a terminal; scripts and conductors (no TTY) get the plain output.
57
+ const hello = () => { try {
58
+ process.stdout.write(banner(infoLines(loadConfig().cfg)) + (process.stdout.isTTY ? "\n" : ""));
59
+ }
60
+ catch { /* broken config: skip */ } };
61
+ if (cmd === "-v" || cmd === "--version") {
62
+ if (process.stdout.isTTY)
63
+ hello();
64
+ else
65
+ console.log(version());
66
+ return 0;
67
+ }
68
+ if (!cmd && process.stdout.isTTY) {
69
+ hello();
70
+ console.log(usage());
71
+ return 0;
72
+ }
73
+ if (!cmd || cmd === "-h" || cmd === "--help") {
74
+ console.log(usage());
75
+ return cmd ? 0 : 2;
76
+ }
77
+ const run = COMMANDS[cmd];
78
+ if (!run) {
79
+ console.error(`${m("unknown command", "comando desconhecido")}: ${cmd}\n\n${usage()}`);
80
+ return 2;
81
+ }
82
+ try {
83
+ return await run(rest);
84
+ }
85
+ catch (e) {
86
+ console.error(e instanceof ConfigError ? e.message : `orq ${cmd}: ${e.message}`);
87
+ return 2;
88
+ }
89
+ }
90
+ process.exitCode = await main(process.argv.slice(2));